[
  {
    "path": ".github/.cspell/dart_dictionary.txt",
    "content": "# keywords/terms specific to the Dart/Flutter ecosystem\ncupertino # Flutter module containing iOS-style widgets\ndartdoc # documentation tool for dart\ndartdocs # plural of dartdoc\nendtemplate # Use @endtemplate to close a @template block in dartdoc\npubspec # dependency and configuration file of every Dart project\n"
  },
  {
    "path": ".github/.cspell/flame_dictionary.txt",
    "content": "# dictionary file for Flame-related words, including companies, tools, and libraries (and their associated concepts) mentioned on our codebase\nArtboard # What a project file is called within Rive\nAseprite # Animated sprite editor and pixel art tool https://www.aseprite.org/\nAudioplayers # A Flutter plugin to play multiple simultaneously audio files https://github.com/bluefireteam/audioplayers\nBGUG # Break Guns Using Gems, a game by BlueFire https://github.com/bluefireteam/bgug\nBodymovin # An After Effects extension to export Lottie animations https://aescripts.com/bodymovin/\nDashbook # UI development tool for Flutter https://github.com/bluefireteam/dashbook\nemberquest # Ember Quest, our platformer tutorial game\nHermione # A character from the book Harry Potter\nKawabunga # Word expressing exhilaration, of unclear origins but popularized by the show Teenage Mutant Ninja Turtles\nKenobi # Eminent Jedi Master, General of the Republic Army, Obi-Wan Kenobi\nNakama # An open-source server designed to power modern games and apps https://github.com/Allan-Nava/nakama-flutter\nOvermind # A character in the game StarCraft\npadracing # A pad racing game by BlueFire https://github.com/flame-engine/flame/tree/main/examples/games/padracing\nProsser # A character from the book The Hitchhiker's Guide to the Galaxy\nriverpod # A state management library for Flutter https://github.com/rrousselGit/riverpod\nspineboy # Name of a famous character used as an example for Spine https://en.esotericsoftware.com/spine-examples-spineboy\nspineboys # Plural of spineboy\nSpritecow # A handy tool for locating sprites within a spritesheet http://www.spritecow.com/\nSupabase # Supabase, one of our sponsors https://supabase.com/\nterminui # A terminal UI library for Flutter\ntexturepacker # a packed spritesheet format\nTilemap # What tile maps are called within Tiled\nvantablack # brand name for a famous super-black ink known as the darkest ever made\nWeasley # Ron Weasley, a character from the book Harry Potter\nWyrmsun # An open-source real-time strategy game https://www.indiedb.com/games/wyrmsun\nyarnspinner # A tool for building branching narrative and dialogue in games # https://yarnspinner.dev/\n"
  },
  {
    "path": ".github/.cspell/gamedev_dictionary.txt",
    "content": "# general development-adjacent terms and expressions\nAABB # axis aligned bounding box\nabelian # Abelian Group, also known as commutative group\nARGB # alpha red green blue\narities # plural of arity\nbackgrounded # moving the app to the background\nbackgrounding # moving the app to the background\nbackpressure # strategy to deal with excess flow of data\nBGRA # blue green red alpha\nbimedian # line segment joining the midpoints of opposite sides of a shape\nbitfield # data structure consisting of adjacent bits\nbroadphase # common division of collision detection between broad and narrow phases\ncathetus # the non-hypotenuse sides of a right triangle\nclusterized # past tense of clusterize\ngles # OpenGL for Embedded Systems\nglsl # OpenGL Shading Language\ngltf # OpenGL Transmission Format, a file format for 3D models\ngoldens # test files used as reference for Golden Tests\nhighp # high float precission setting on glsl fragment shaders\nhitbox # the collision box around objects for the purposes of collision detection\nhitboxes # plural of hitbox\nIDAT # PNG data chunk\nIEND # PNG end chunk\nIHDR # PNG header chunk\nints # short for integers\njank # stutter or inconsistent gap or timing\nlerp # short for linear interpolation\nLTRBR # left top right bottom radius\nLTWH # left top width height\nmediump # medium GLSL float precision\nmetalness # a measure of how much a surface reflects light for the purposes of physically based rendering\nMinkowski # Minkowski sum, a sum of two sets of vectors, A and B, where the result is the sum of each vector pair\nmultitap # support from a device to recognize many taps at the same time\norientable # can be oriented\npathfinding # computer algorithm to find the best path through a world or maze\nperlin # Perlin Noise, a type of noise generating algorithm\nplatformers # plural of platformer, a genre of video game\nquadtree # a tree-based data structure where each node has exactly 4 children\nrasterizing\nrespawn # when the player character dies and is brought back after some time and penalties\nrespawned # past tense of respawn\nretarget # to direct (something) toward a different target\nRGBA # red green blue alpha\nRGBO # red green blue opacity\nscos # cosine of a rotation multiplied by the scale factor\nscrollers # plural of scroller, a genre of video game\nshaderbundle # a file extension used to bundle shaders for GLSL\nslerp # short for spherical linear interpolation, a method to interpolate quaternions\nspritesheet # a single image packing multiple sprites, normally in a grid\nssin # sine of a rotation multiplied by the scale factor\nsubfolders # plural of subfolders\nsublists # plural of sublist\nsubrange # a range entirely contained on a given range\nSVGs # plural of SVG\ntexel # texture pixel (unit of texture map)\ntexels # plural of texel\ntileset # image with a collection of tiles. in games, tiles are small square sprites laid out in a grid to form the game map\ntilesets # plural of tileset\ntruecolor # truecolor rendering\ntweening # the process of tween\nviewports # plural of viewport\nWASD # movement keys on a keyboard\nWBMP # wireless bitmap image format\nWebP # WebP image format\n"
  },
  {
    "path": ".github/.cspell/people_usernames.txt",
    "content": "# specific people's names and/or usernames\nakida # github.com/akida \nbdero # github.com/bdero\nbluefireteam # github.com/bluefireteam\nerayzesen # erayzesen.itch.io\nerickzanardo # github.com/erickzanardo \nferoult # github.com/feroult \nfröber # github.com/Brixto\ngnarhard # github.com/gnarhard\nHoodead # github.com/kornellapu\nkenney # kenney.nl\nKlingsbo # github.com/spydon\nKornél # github.com/kornellapu\nlapu # Artist of reference art (basic shader tutorial)\nLapu # github.com/kornellapu\nluan # github.com/luanpotter\nluanpotter # github.com/luanpotter\nLukas # github.com/spydon\npgainullin # github.com/pgainullin \nSchnurber # github.com/schnurber \nspydon # github.com/spydon\nstpasha # github.com/stpasha\nsubosito # github.com/subosito\ntavian # tavianator.com\nTellinghuisen # Author of Statistical Error Propagation (2001)\nvideon # github.com/markvideon\nwolfenrain # github.com/wolfenrain\nxaha # github.com/xvrh\n"
  },
  {
    "path": ".github/.cspell/sphinx_dictionary.txt",
    "content": "# keywords used on the sphinx language\ninfobox\nlinkcheck\nseealso\ntoctree\ntoctrees"
  },
  {
    "path": ".github/.cspell/words_dictionary.txt",
    "content": "# actual english words (or common abbreviations) missing from CSpell\nbloodlust\ncollidable\ncollidables\ngamepads\ngrayscale\nhoverable\nHoverables\ninactives\nlayouting\nNTSC\norientable\nplatformer\npositionable\npostmultiply\npreorder\nprerender\nprerendered\npressable\nptero # short for pterodactyl\nrasterizes\nrefreshable\nrenderable\nrerasterize\nrescan\nRoboto\ntappable\nunderutilize\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "open_collective: blue-fire\ngithub: bluefireteam\npatreon: bluefireoss"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: You are creating a Game with Flame but you are noticing some strange behavior, that it throws an unexpected exception, or that it is not working according to the specifications.\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        When reporting a bug, please read this complete template and fill in all the questions in order to get a better response!\n  - type: textarea\n    id: what-happened\n    attributes:\n      label: What happened?\n      description: Tell us, what happened?\n    validations:\n      required: true\n      \n  - type: textarea\n    id: expectation\n    attributes:\n      label: What do you expect?\n      description: Also tell us, what behavior did you expect? \n    validations:\n      required: true\n\n  - type: textarea\n    id: reproduction-steps\n    attributes:\n      label: How can we reproduce this?\n      description: Thoroughly explain how to reproduce the bug.\n      \n  - type: textarea\n    id: steps-to-fix\n    attributes:\n      label: What steps should take to fix this?\n      description: If possible please report steps based on the example from this plugin! \n      \n  - type: textarea\n    id: example-changes\n    attributes:\n      label: Do have an example of where the bug occurs?\n      description: If you can make a minimal reproducible example it is incredibly helpful, the simplest way is to share a link from https://zapp.run, you can start from https://zapp.run/edit/flame where all dependencies are already set up.\n\n  - type: textarea\n    id: logs\n    attributes:\n      label: Relevant log output\n      description: If you have any debug / error logging, please fill it here within the code block below\n      render: shell\n\n  - type: textarea\n    attributes:\n      label: Execute in a terminal and put output into the code block below\n      value: 'Output of: flutter doctor -v'\n\n  - type: dropdown\n    id: affected-platforms\n    attributes:\n      label: Affected platforms\n      multiple: true\n      options:\n        - All\n        - Android\n        - iOS\n        - Linux\n        - macOS\n        - Windows\n        - Web\n    validations:\n      required: true\n\n  - type: textarea\n    id: other-information\n    attributes:\n      label: Other information\n      description: Do you have any other useful information about this bug report? Please write it down here\n\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: Are you interested in working on a PR for this?\n      options:\n        - label: I want to work on this\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: I have some questions about Flame.\n    url: https://stackoverflow.com/tags/flame\n    about: Ask your questions on StackOverflow! The community is always willing to help out.\n  - name: Join us on Discord!\n    url: https://discord.gg/pxrBmy4\n    about: Ask your questions in our community!\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature request\ndescription: Suggest a new feature for Flame.\nlabels: [\"enhancement\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        When suggesting a feature, please read this complete form and fill in all the questions in order to get a better response!\n  - type: textarea\n    id: problem-to-solve\n    attributes:\n      label: Problem to solve\n      description: Which problem would be solved with this feature?\n    validations:\n      required: true\n  - type: textarea\n    id: proposal\n    attributes:\n      label: Proposal\n      description: What do you propose as a solution? Add as much information as you can!\n    validations:\n      required: true\n  - type: textarea\n    id: more-information\n    attributes:\n      label: More information\n      description: Do you have any other useful information about this feature report? Please write it down here. Possible helpful information are references to other sites/repositories.\n  - type: checkboxes\n    id: other\n    attributes:\n      label: Other\n      options:\n        - label: Are you interested in working on a PR for this? \n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/improvement_suggestion.yml",
    "content": "name: Improvement suggestion\ndescription: Something in Flame can be improved.\nlabels: [\"enhancement\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        When suggesting an improvement, please read this complete form and fill in all the questions in order to get a better response!\n  - type: textarea\n    id: area-to-improve\n    attributes:\n      label: What could be improved\n      description: What part of the code/functionality could be improved?\n    validations:\n      required: true\n  - type: textarea\n    id: why\n    attributes:\n      label: Why should this be improved\n      description: Why is this necessary to be improved?\n    validations:\n      required: true\n  - type: textarea\n    id: risks\n    attributes:\n      label: Risks\n      description: Are there any risks in improving this? Will the API change? Will other functionality change?\n  - type: textarea\n    id: more-information\n    attributes:\n      label: More information\n      description: Do you have any other useful information about this feature report? Please write it down here. Possible helpful information are references to other sites/repositories.\n  - type: checkboxes\n    id: other\n    attributes:\n      label: Other\n      options:\n        - label: Are you interested in working on a PR for this? \n"
  },
  {
    "path": ".github/cspell.json",
    "content": "{\n  \"version\": \"0.2\",\n  \"language\": \"en\",\n  \"flagWords\": [\n    \"teh\",\n    \"hte\"\n  ],\n  \"ignorePaths\": [\n    \"**/media/**\",\n    \"**/assets/**\",\n    \"**/CHANGELOG.md\"\n  ],\n  \"ignoreRegExpList\": [\n    \"\\\\#[\\\\w\\\\-]+\"\n  ],\n  \"dictionaries\": [\n    \"en_US\",\n    \"softwareTerms\",\n    \"dart_dictionary\",\n    \"flame_dictionary\",\n    \"gamedev_dictionary\",\n    \"sphinx_dictionary\",\n    \"people_usernames\",\n    \"words_dictionary\"\n  ],\n  \"dictionaryDefinitions\": [\n    {\n      \"name\": \"dart_dictionary\",\n      \"path\": \"./.cspell/dart_dictionary.txt\",\n      \"addWords\": true\n    },\n    {\n      \"name\": \"flame_dictionary\",\n      \"path\": \"./.cspell/flame_dictionary.txt\",\n      \"addWords\": true\n    },\n    {\n      \"name\": \"gamedev_dictionary\",\n      \"path\": \"./.cspell/gamedev_dictionary.txt\",\n      \"addWords\": true\n    },\n    {\n      \"name\": \"sphinx_dictionary\",\n      \"path\": \"./.cspell/sphinx_dictionary.txt\",\n      \"addWords\": true\n    },\n    {\n      \"name\": \"people_usernames\",\n      \"path\": \"./.cspell/people_usernames.txt\",\n      \"addWords\": true\n    },\n    {\n      \"name\": \"words_dictionary\",\n      \"path\": \"./.cspell/words_dictionary.txt\",\n      \"addWords\": true\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!-- Exclude from commit message -->\n<!--\nThe title of your PR on the line above should start with a [Conventional Commit] prefix\n(`fix:`, `feat:`, `docs:`, `test:`, `chore:`, `refactor:`, `perf:`, `build:`, `ci:`,\n`style:`, `revert:`). This title will later become an entry in the [CHANGELOG], so please\nmake sure that it summarizes the PR adequately.\n\nDon't remove the \"exclude from commit message\" comments below. They are used to prevent\nthe PR description template from being included in the git log.\nOnly change the \"Replace this text\" parts.\n-->\n\n# Description\n<!--\nProvide a description of what this PR is doing.\nIf you're modifying existing behavior, describe the existing behavior, how this PR is changing it,\nand what motivated the change. If this is a breaking change, specify explicitly which APIs were\nchanged.\n-->\n<!-- End of exclude from commit message -->\nReplace this text.\n\n<!-- Exclude from commit message -->\n## Checklist\n<!--\nBefore you create this PR confirm that it meets all requirements listed below by checking the\nrelevant checkboxes with `[x]`. If some checkbox is not applicable, mark it as `[-]`.\n-->\n\n- [ ] I have followed the [Contributor Guide] when preparing my PR.\n- [ ] I have updated/added tests for ALL new/updated/fixed functionality.\n- [ ] I have updated/added relevant documentation in `docs` and added dartdoc comments with `///`.\n- [ ] I have updated/added relevant examples in `examples` or `docs`.\n\n\n## Breaking Change?\n<!--\nWould your PR require Flame users to update their apps following your change?\n\nIf yes, then the title of the PR should include \"!\" (for example, `feat!:`, `fix!:`). See\n[Conventional Commit] for details. Also, for a breaking PR uncomment and fill in the \"Migration\ninstructions\" section below.\n-->\n\n- [ ] Yes, this PR is a breaking change.\n- [ ] No, this PR is not a breaking change.\n\n<!--\n### Migration instructions\n\nIf the PR is breaking, uncomment this header and add instructions for how to migrate from the\ncurrently released version in-between the two following tags:\n-->\n<!-- End of exclude from commit message -->\n<!-- Exclude from commit message -->\n\n## Related Issues\n<!--\nIndicate which issues this PR resolves, if any. For example:\n\nCloses #1234\n!-->\n\n<!-- Links -->\n[Contributor Guide]: https://github.com/flame-engine/flame/blob/main/CONTRIBUTING.md\n[Conventional Commit]: https://conventionalcommits.org\n[CHANGELOG]: https://github.com/flame-engine/flame/blob/main/CHANGELOG.md\n<!-- End of exclude from commit message -->\n"
  },
  {
    "path": ".github/workflows/cicd.yml",
    "content": "name: cicd\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    types: [opened, reopened, synchronize]\n\nenv:\n  FLUTTER_MIN_VERSION: '3.41.0'\n\njobs:\n  # BEGIN LINTING STAGE\n  analyze:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: subosito/flutter-action@v2\n        with:\n          flutter-version: ${{env.FLUTTER_MIN_VERSION}}\n      - uses: bluefireteam/melos-action@v3\n      - name: \"Analyze with lowest supported version\"\n        uses: invertase/github-action-dart-analyzer@v3\n        with:\n          fatal-infos: true\n\n  markdown-lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 18\n      - run: npm install -g markdownlint-cli\n      - run: markdownlint . -p .markdownlintignore -c .markdownlint.yaml\n\n  dcm:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: subosito/flutter-action@v2\n      - uses: bluefireteam/melos-action@v3\n      - name: Install DCM\n        uses: CQLabs/setup-dcm@v2\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Run DCM\n        run: dcm analyze .\n  # END LINTING STAGE\n"
  },
  {
    "path": ".github/workflows/gh-pages.yml",
    "content": "name: Gh-Pages\n\non:\n  push:\n    branches: [main]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n      - uses: subosito/flutter-action@v2\n      - uses: bluefireteam/melos-action@v2\n      - uses: bluefireteam/flutter-gh-pages@v9\n        with:\n          workingDir: examples\n\n  build-docs:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Trigger building the docs\n        run: |\n          curl -XPOST -u \"${{ secrets.DOCS_BUILD_DISPATCH }}\" \\\n          -H \"Accept:application/vnd.github\" \\\n          -H \"Content-Type:application/json\" \\\n          https://api.github.com/repos/flame-engine/flame-docs-site/actions/workflows/13757924/dispatches \\\n          --data '{\"ref\": \"main\" }'\n"
  },
  {
    "path": ".github/workflows/release-prepare.yml",
    "content": "name: Prepare release\non:\n  workflow_dispatch:\n    inputs:\n      prerelease:\n        description: 'Version as prerelease'\n        required: false\n        default: false\n        type: boolean\n\njobs:\n  prepare-release:\n    name: Prepare release\n    permissions:\n      contents: write\n      pull-requests: write\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - uses: subosito/flutter-action@v2\n      - uses: bluefireteam/melos-action@v3\n        with:\n          run-versioning: ${{ inputs.prerelease == false }}\n          run-versioning-prerelease: ${{ inputs.prerelease == true }}\n          publish-dry-run: true\n          create-pr: true\n"
  },
  {
    "path": ".github/workflows/release-publish.yml",
    "content": "name: Publish packages\non:\n  # Enable to also publish, when pushing a tag\n  #push:\n  #  tags:\n  #    - '*'\n  workflow_dispatch:\n\njobs:\n  publish-packages:\n    name: Publish packages\n    permissions:\n      contents: write\n      id-token: write # Required for authentication using OIDC\n    runs-on: [ ubuntu-latest ]\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - uses: subosito/flutter-action@v2\n      - uses: bluefireteam/melos-action@v3\n        with:\n          publish: true\n\n"
  },
  {
    "path": ".github/workflows/release-tag.yml",
    "content": "name: Tag release\non:\n  push:\n    branches: [main]\n\njobs:\n  publish-packages:\n    name: Create tags for release\n    permissions:\n      actions: write\n      contents: write\n    runs-on: [ ubuntu-latest ]\n    if: contains(github.event.head_commit.message, 'chore(release)')\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - uses: subosito/flutter-action@v2\n      - uses: bluefireteam/melos-action@v3\n        with:\n          tag: true\n      - run: |\n          melos exec -c1 --no-published --no-private --order-dependents -- \\\n          gh workflow run release-publish.yml \\\n          --ref \\$MELOS_PACKAGE_NAME-v\\$MELOS_PACKAGE_VERSION\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n"
  },
  {
    "path": ".github/workflows/spell_checker.yml",
    "content": "name: spell_checker\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    types: [opened, reopened, synchronize]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - run: npm -g install cspell@9.2.1\n      # spell check\n      - run: ./scripts/cspell-run.sh\n      # verify dictionary words are sorted and not orphan\n      - run: ./scripts/cspell-verify.sh"
  },
  {
    "path": ".github/workflows/title-validation.yml",
    "content": "# See https://github.com/amannn/action-semantic-pull-request\nname: 'PR Title is Conventional'\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - edited\n      - synchronize\n\njobs:\n  main:\n    name: Validate PR title\n    runs-on: ubuntu-latest\n    steps:\n      - uses: amannn/action-semantic-pull-request@v6\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          types: |\n            build\n            chore\n            ci\n            docs\n            feat\n            fix\n            perf\n            refactor\n            revert\n            style\n            test\n          subjectPattern: ^[^a-z].+$\n          subjectPatternError: |\n            The subject of the PR can't begin with a lowercase letter.\n"
  },
  {
    "path": ".gitignore",
    "content": "# Miscellaneous\n*.class\n*.bak\n*.fvm\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.vscode/\n.venv/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/failures\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\nandroid/\nios/\nmacos/\nwindows/\nlinux/\ndesktop/\n**/build/\ncoverage/\npubspec.lock\npubspec_overrides.yaml\ndevtools_options.yaml\n\n# Sphinx related\n__pycache__/\n_build/\n\n# Custom\nlib/generated_plugin_registrant.dart\nexamples/**/.gitignore\n\n**/example/android\n**/example/ios\n**/example/linux\n**/example/macos\n**/example/windows\n**/example/web\n\n.metadata\n\n# FVM\n.fvm/\n.fvmrc\n\n# LLM\n.claude/\n.cursor/\n"
  },
  {
    "path": ".markdownlint.yaml",
    "content": "# Default state for all rules\ndefault: true\n\n# MD001/heading-increment/header-increment - Heading levels should only increment by one level at a time\nMD001: true\n\n# MD002/first-heading-h1/first-header-h1 - First heading should be a top-level heading\nMD002:\n  # Heading level\n  level: 1\n\n# MD003/heading-style/header-style - Heading style\nMD003:\n  # Heading style\n  style: \"atx\"\n\n# MD004/ul-style - Unordered list style\nMD004:\n  # List style\n  style: \"dash\"\n\n# MD005/list-indent - Inconsistent indentation for list items at the same level\nMD005: true\n\n# MD006/ul-start-left - Consider starting bulleted lists at the beginning of the line\nMD006: true\n\n# MD007/ul-indent - Unordered list indentation\nMD007:\n  # Spaces for indent\n  indent: 2\n  # Whether to indent the first level of the list\n  start_indented: false\n  # Spaces for first level indent (when start_indented is set)\n  start_indent: 2\n\n# MD009/no-trailing-spaces - Trailing spaces\nMD009:\n  # Spaces for line break\n  br_spaces: 2\n  # Allow spaces for empty lines in list items\n  list_item_empty_lines: false\n  # Include unnecessary breaks\n  strict: false\n\n# MD010/no-hard-tabs - Hard tabs\nMD010:\n  # Include code blocks\n  code_blocks: true\n  # Fenced code languages to ignore\n  ignore_code_languages: []\n  # Number of spaces for each hard tab\n  spaces_per_tab: 1\n\n# MD011/no-reversed-links - Reversed link syntax\nMD011: true\n\n# MD012/no-multiple-blanks - Multiple consecutive blank lines\nMD012:\n  # Consecutive blank lines\n  maximum: 2\n\n# MD013/line-length - Line length\nMD013:\n  # Number of characters\n  line_length: 100\n  # Number of characters for headings\n  heading_line_length: 80\n  # Number of characters for code blocks\n  code_block_line_length: 80\n  # Include code blocks\n  code_blocks: true\n  # Include tables\n  tables: true\n  # Include headings\n  headings: true\n  # Include headings\n  headers: true\n  # Strict length checking\n  strict: false\n  # Stern length checking\n  stern: false\n\n# MD014/commands-show-output - Dollar signs used before commands without showing output\nMD014: true\n\n# MD018/no-missing-space-atx - No space after hash on atx style heading\nMD018: true\n\n# MD019/no-multiple-space-atx - Multiple spaces after hash on atx style heading\nMD019: true\n\n# MD020/no-missing-space-closed-atx - No space inside hashes on closed atx style heading\nMD020: true\n\n# MD021/no-multiple-space-closed-atx - Multiple spaces inside hashes on closed atx style heading\nMD021: true\n\n# MD022/blanks-around-headings/blanks-around-headers - Headings should be surrounded by blank lines\nMD022:\n  # Blank lines above heading\n  lines_above: 2\n  # Blank lines below heading\n  lines_below: 1\n\n# MD023/heading-start-left/header-start-left - Headings must start at the beginning of the line\nMD023: true\n\n# MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content\nMD024:\n  # Only check sibling headings\n  allow_different_nesting: false\n  # Only check sibling headings\n  siblings_only: false\n\n# MD025/single-title/single-h1 - Multiple top-level headings in the same document\nMD025:\n  # Heading level\n  level: 1\n  # RegExp for matching title in front matter\n  front_matter_title: \"^\\\\s*title\\\\s*[:=]\"\n\n# MD026/no-trailing-punctuation - Trailing punctuation in heading\nMD026:\n  # Punctuation characters\n  punctuation: \".,;:!。，；：！\"\n\n# MD027/no-multiple-space-blockquote - Multiple spaces after blockquote symbol\nMD027: true\n\n# MD028/no-blanks-blockquote - Blank line inside blockquote\nMD028: true\n\n# MD029/ol-prefix - Ordered list item prefix\nMD029:\n  # List style\n  style: \"one_or_ordered\"\n\n# MD030/list-marker-space - Spaces after list markers\nMD030:\n  # Spaces for single-line unordered list items\n  ul_single: 1\n  # Spaces for single-line ordered list items\n  ol_single: 1\n  # Spaces for multi-line unordered list items\n  ul_multi: 1\n  # Spaces for multi-line ordered list items\n  ol_multi: 1\n\n# MD031/blanks-around-fences - Fenced code blocks should be surrounded by blank lines\nMD031:\n  # Include list items\n  list_items: true\n\n# MD032/blanks-around-lists - Lists should be surrounded by blank lines\nMD032: true\n\n# MD033/no-inline-html - Inline HTML\nMD033:\n  # Allowed elements\n  allowed_elements: [p, a, img, h2]\n\n# MD034/no-bare-urls - Bare URL used\nMD034: true\n\n# MD035/hr-style - Horizontal rule style\nMD035:\n  # Horizontal rule style\n  style: \"---\"\n\n# MD036/no-emphasis-as-heading/no-emphasis-as-header - Emphasis used instead of a heading\nMD036:\n  # Punctuation characters\n  punctuation: \".,;:!?。，；：！？\"\n\n# MD037/no-space-in-emphasis - Spaces inside emphasis markers\nMD037: true\n\n# MD038/no-space-in-code - Spaces inside code span elements\nMD038: true\n\n# MD039/no-space-in-links - Spaces inside link text\nMD039: true\n\n# MD040/fenced-code-language - Fenced code blocks should have a language specified\nMD040: true\n\n# MD041/first-line-heading - First line in a file should be a top-level heading\nMD041: false\n\n# MD042/no-empty-links - No empty links\nMD042: true\n\n# MD044/proper-names - Proper names should have the correct capitalization\nMD044:\n  # List of proper names\n  names: []\n  # Include code blocks\n  code_blocks: true\n  # Include HTML elements\n  html_elements: true\n\n# MD045/no-alt-text - Images should have alternate text (alt text)\n# TODO: Activate this with #2913 \nMD045: false\n\n# MD046/code-block-style - Code block style\nMD046:\n  # Block style\n  style: \"fenced\"\n\n# MD047/single-trailing-newline - Files should end with a single newline character\nMD047: true\n\n# MD048/code-fence-style - Code fence style\nMD048:\n  # Code fence style\n  style: \"backtick\"\n\n# MD049/emphasis-style - Emphasis style should be consistent\nMD049:\n  # Emphasis style should be consistent\n  style: \"asterisk\"\n\n# MD050/strong-style - Strong style should be consistent\nMD050:\n  # Strong style should be consistent\n  style: \"asterisk\"\n\n# MD051/link-fragments - Link fragments should be valid\nMD051: true\n\n# MD052/reference-links-images - Reference links and images should use a label that is defined\nMD052: true\n\n# MD053/link-image-reference-definitions - Link and image reference definitions should be needed\nMD053:\n  # Ignored definitions\n  ignored_definitions: [\"//\"]\n"
  },
  {
    "path": ".markdownlintignore",
    "content": "**/CHANGELOG.md\n**/ios/**\n**/doc/api/static-assets/**\n**/macos/**\n**/windows/**\n**/linux/**\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "# [Read the Docs] configuration file\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\nversion: 2\n\nsphinx:\n  builder: html\n  configuration: doc/_sphinx/conf.py\n  fail_on_warning: false\n\npython:\n  version: \"3.8\"\n  install:\n    - requirements: doc/_sphinx/requirements.txt\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Change Log\n\nAll notable changes to this project will be documented in this file.\nSee [Conventional Commits](https://conventionalcommits.org) for commit guidelines.\n\n## 2026-03-06\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`behavior_tree` - `v0.1.5+1`](#behavior_tree---v0151)\n - [`flame` - `v1.36.0`](#flame---v1360)\n - [`flame_3d` - `v0.1.1+7`](#flame_3d---v0117)\n - [`flame_audio` - `v2.12.0`](#flame_audio---v2120)\n - [`flame_behavior_tree` - `v0.1.4+3`](#flame_behavior_tree---v0143)\n - [`flame_behaviors` - `v1.3.4`](#flame_behaviors---v134)\n - [`flame_bloc` - `v1.12.22`](#flame_bloc---v11222)\n - [`flame_console` - `v0.1.2+17`](#flame_console---v01217)\n - [`flame_fire_atlas` - `v1.8.16`](#flame_fire_atlas---v1816)\n - [`flame_forge2d` - `v0.19.2+5`](#flame_forge2d---v01925)\n - [`flame_isolate` - `v0.6.2+21`](#flame_isolate---v06221)\n - [`flame_kenney_xml` - `v0.1.2`](#flame_kenney_xml---v012)\n - [`flame_lint` - `v1.4.3`](#flame_lint---v143)\n - [`flame_lottie` - `v0.4.2+21`](#flame_lottie---v04221)\n - [`flame_markdown` - `v0.2.4+14`](#flame_markdown---v02414)\n - [`flame_network_assets` - `v0.3.3+21`](#flame_network_assets---v03321)\n - [`flame_noise` - `v0.3.2+21`](#flame_noise---v03221)\n - [`flame_oxygen` - `v0.2.3+21`](#flame_oxygen---v02321)\n - [`flame_rive` - `v1.11.0`](#flame_rive---v1110)\n - [`flame_riverpod` - `v5.5.3`](#flame_riverpod---v553)\n - [`flame_spine` - `v0.3.0+4`](#flame_spine---v0304)\n - [`flame_splash_screen` - `v0.3.1+3`](#flame_splash_screen---v0313)\n - [`flame_sprite_fusion` - `v0.2.3`](#flame_sprite_fusion---v023)\n - [`flame_steering_behaviors` - `v0.2.1+4`](#flame_steering_behaviors---v0214)\n - [`flame_svg` - `v1.12.0`](#flame_svg---v1120)\n - [`flame_test` - `v2.2.3`](#flame_test---v223)\n - [`flame_texturepacker` - `v5.1.0`](#flame_texturepacker---v510)\n - [`flame_tiled` - `v3.1.0`](#flame_tiled---v310)\n - [`jenny` - `v1.5.1`](#jenny---v151)\n\n---\n\n#### `behavior_tree` - `v0.1.5+1`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame` - `v1.36.0`\n\n - **FIX**: Ray direction normalization drift issues ([#3841](https://github.com/flame-engine/flame/issues/3841)). ([b8e2bab5](https://github.com/flame-engine/flame/commit/b8e2bab58fbfb1b817dd294db3d2389097d31a2d))\n - **FIX**: Initialize center offset in `CircleComponent` ctor ([#3842](https://github.com/flame-engine/flame/issues/3842)). ([a0d2a5f3](https://github.com/flame-engine/flame/commit/a0d2a5f3a2db6ca2b0c25c93ca48fe12a4bdbc11))\n - **FIX**: Resolve `fontPackage` in `IconComponent` (`_rasterizeIcon`) ([#3838](https://github.com/flame-engine/flame/issues/3838)). ([cdb2a0dd](https://github.com/flame-engine/flame/commit/cdb2a0ddf8ec6db733c904696a17ceeaed2e7b5f))\n - **FIX**: Hitboxes now correctly account for parent scale and rotation ([#3834](https://github.com/flame-engine/flame/issues/3834)). ([57adcd60](https://github.com/flame-engine/flame/commit/57adcd60970efb2dbf4f52e396a4e78a51e47be8))\n - **FIX**: Make `buildContext` available during `onLoad` and `onMount` ([#3833](https://github.com/flame-engine/flame/issues/3833)). ([60bfcb30](https://github.com/flame-engine/flame/commit/60bfcb30e7751ee65caa6425d79bbd7bb82ae3ed))\n - **FIX**: Add CJK wrapping for `TextBoxComponent` ([#3830](https://github.com/flame-engine/flame/issues/3830)). ([7f41c261](https://github.com/flame-engine/flame/commit/7f41c2614e1669398bb08858df30270d75dced68))\n - **FIX**: Prevent removed children from being re-added when parent is moved ([#3824](https://github.com/flame-engine/flame/issues/3824)). ([8e77bc2d](https://github.com/flame-engine/flame/commit/8e77bc2d87300a52c03177fb187c4e35b810acc4))\n - **FIX**: Make `removeAll(children)` work in `onRemove` ([#3823](https://github.com/flame-engine/flame/issues/3823)). ([ff760230](https://github.com/flame-engine/flame/commit/ff760230881f2a27f9d3a9462fc831c460d7ffc3))\n - **FIX**: End active collisions in ShapeHitbox.onRemove to fix inconsistent isColliding ([#3821](https://github.com/flame-engine/flame/issues/3821)). ([bc81e7fd](https://github.com/flame-engine/flame/commit/bc81e7fd96c1ec5ffad210d6d027894f6faef77b))\n - **FIX**: Use scaledRadius for CircleHitbox collision detection ([#3808](https://github.com/flame-engine/flame/issues/3808)). ([3498c1e5](https://github.com/flame-engine/flame/commit/3498c1e565985ce7d12212e6a625f538da98c362))\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n - **FEAT**: Add `clampDouble` to `Vector2Extension` and its tests ([#3840](https://github.com/flame-engine/flame/issues/3840)). ([6c6ccf31](https://github.com/flame-engine/flame/commit/6c6ccf317f3f397edf30a61c90ed00d087a96e9a))\n - **FEAT**: Add `transformMatrix` setter to `Transform2D` ([#3836](https://github.com/flame-engine/flame/issues/3836)). ([957ff2e0](https://github.com/flame-engine/flame/commit/957ff2e085f03ab43f40f4213d94730b52e2cfc6))\n - **FEAT**: Support package argument in asset loading methods and widgets ([#3835](https://github.com/flame-engine/flame/issues/3835)). ([3f6f95b0](https://github.com/flame-engine/flame/commit/3f6f95b0225df9e4035c74de6cfad501e8c45167))\n - **FEAT**: Propagate Flutter hot reload through the component tree ([#3828](https://github.com/flame-engine/flame/issues/3828)). ([c44643f1](https://github.com/flame-engine/flame/commit/c44643f1afdf9890d9890d6653f7429366e7f03b))\n - **FEAT**: Add an IconComponent that renders IconData ([#3820](https://github.com/flame-engine/flame/issues/3820)). ([97931a59](https://github.com/flame-engine/flame/commit/97931a59361c51c433d366fecb30b7867813c521))\n - **FEAT**: Add `dispose()` method to `FlameGame` ([#3825](https://github.com/flame-engine/flame/issues/3825)). ([aa5a27b7](https://github.com/flame-engine/flame/commit/aa5a27b791c4bd5a3ab64ee73b34feefedb8b210))\n - **FEAT**: Add object pooling support with `ComponentPool` ([#3816](https://github.com/flame-engine/flame/issues/3816)). ([46802fab](https://github.com/flame-engine/flame/commit/46802fab4705ae8d1ff2a61660b2d80450f6075e))\n - **FEAT**: Add opposite method to Anchor ([#3817](https://github.com/flame-engine/flame/issues/3817)). ([1ffd59f0](https://github.com/flame-engine/flame/commit/1ffd59f09f90d17980b70e494ce585d676888b57))\n - **FEAT**: HitTestBehavior for GameWidget ([#3815](https://github.com/flame-engine/flame/issues/3815)). ([b888d4e2](https://github.com/flame-engine/flame/commit/b888d4e2d7ea95694a0c5f83e2fa68f8cee67069))\n - **DOCS**: Document FlameGame<W extends World> generic type parameter ([#3822](https://github.com/flame-engine/flame/issues/3822)). ([00a66123](https://github.com/flame-engine/flame/commit/00a66123d84d305e66953f1a85e66c46c8e0c4fc))\n\n#### `flame_3d` - `v0.1.1+7`\n\n - **FIX**(flame_3d): Use float for numLights uniform to fix GLES crash ([#3810](https://github.com/flame-engine/flame/issues/3810)). ([f241ebd6](https://github.com/flame-engine/flame/commit/f241ebd6d73caa43387972cffeb609af2909ac2f))\n - **FIX**(flame_3d): Fix alpha blend factors for transparent background compositing ([#3812](https://github.com/flame-engine/flame/issues/3812)). ([8d7212a1](https://github.com/flame-engine/flame/commit/8d7212a1c79d561330131ea794a0ed318c3945f8))\n - **FIX**(flame_3d): Remove duplicate baseColor multiply in ambient calculation ([#3814](https://github.com/flame-engine/flame/issues/3814)). ([080bde7f](https://github.com/flame-engine/flame/commit/080bde7f4c14c9f4f91e09d9bf6e737f75449351))\n - **FIX**: Use scaledRadius for CircleHitbox collision detection ([#3808](https://github.com/flame-engine/flame/issues/3808)). ([3498c1e5](https://github.com/flame-engine/flame/commit/3498c1e565985ce7d12212e6a625f538da98c362))\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_audio` - `v2.12.0`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n - **FEAT**: Support package argument in asset loading methods and widgets ([#3835](https://github.com/flame-engine/flame/issues/3835)). ([3f6f95b0](https://github.com/flame-engine/flame/commit/3f6f95b0225df9e4035c74de6cfad501e8c45167))\n\n#### `flame_behavior_tree` - `v0.1.4+3`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_behaviors` - `v1.3.4`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_bloc` - `v1.12.22`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_console` - `v0.1.2+17`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_fire_atlas` - `v1.8.16`\n\n - **FIX**: Fix warnings on flame_fire_atlas.dart ([#3818](https://github.com/flame-engine/flame/issues/3818)). ([458a79a9](https://github.com/flame-engine/flame/commit/458a79a97d63d128ab127fc15616192114dc9448))\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_forge2d` - `v0.19.2+5`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_isolate` - `v0.6.2+21`\n\n - **FIX**: Use scaledRadius for CircleHitbox collision detection ([#3808](https://github.com/flame-engine/flame/issues/3808)). ([3498c1e5](https://github.com/flame-engine/flame/commit/3498c1e565985ce7d12212e6a625f538da98c362))\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_kenney_xml` - `v0.1.2`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n - **FEAT**: Support package argument in asset loading methods and widgets ([#3835](https://github.com/flame-engine/flame/issues/3835)). ([3f6f95b0](https://github.com/flame-engine/flame/commit/3f6f95b0225df9e4035c74de6cfad501e8c45167))\n\n#### `flame_lint` - `v1.4.3`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_lottie` - `v0.4.2+21`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_markdown` - `v0.2.4+14`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_network_assets` - `v0.3.3+21`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_noise` - `v0.3.2+21`\n\n - **FIX**: `NoiseEffectController` producing zero progress on some platforms ([#3831](https://github.com/flame-engine/flame/issues/3831)). ([5d88832f](https://github.com/flame-engine/flame/commit/5d88832fa522a6880beeecb617b565aebcb703e7))\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_oxygen` - `v0.2.3+21`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_rive` - `v1.11.0`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n - **FEAT**: Rive 0.14 support ([#3839](https://github.com/flame-engine/flame/issues/3839)). ([8b245181](https://github.com/flame-engine/flame/commit/8b2451813672c25d1783447b966fd2f739492b65))\n\n#### `flame_riverpod` - `v5.5.3`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_spine` - `v0.3.0+4`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_splash_screen` - `v0.3.1+3`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_sprite_fusion` - `v0.2.3`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n - **FEAT**: Support package argument in asset loading methods and widgets ([#3835](https://github.com/flame-engine/flame/issues/3835)). ([3f6f95b0](https://github.com/flame-engine/flame/commit/3f6f95b0225df9e4035c74de6cfad501e8c45167))\n\n#### `flame_steering_behaviors` - `v0.2.1+4`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_svg` - `v1.12.0`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n - **FEAT**: Support package argument in asset loading methods and widgets ([#3835](https://github.com/flame-engine/flame/issues/3835)). ([3f6f95b0](https://github.com/flame-engine/flame/commit/3f6f95b0225df9e4035c74de6cfad501e8c45167))\n\n#### `flame_test` - `v2.2.3`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n#### `flame_texturepacker` - `v5.1.0`\n\n - **REFACTOR**: Asset path resolution/loading in TexturePackerAtlas to correctly handle prefixes and different asset types. ([17fac08d](https://github.com/flame-engine/flame/commit/17fac08d8964991a421cde8f5dd1ace4dc4e9063))\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n - **FEAT**: Support package argument in asset loading methods and widgets ([#3835](https://github.com/flame-engine/flame/issues/3835)). ([3f6f95b0](https://github.com/flame-engine/flame/commit/3f6f95b0225df9e4035c74de6cfad501e8c45167))\n\n#### `flame_tiled` - `v3.1.0`\n\n - **FIX**: Use resolved path when loading single-image tilesets ([#3826](https://github.com/flame-engine/flame/issues/3826)). ([bbdff923](https://github.com/flame-engine/flame/commit/bbdff9230f96ab18ee83a1a0ffab5b4d7f4a43bf))\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n - **FEAT**: Add dynamic layer opacity support to flame_tiled ([#3843](https://github.com/flame-engine/flame/issues/3843)). ([b1702997](https://github.com/flame-engine/flame/commit/b17029977b3c5890fe6794942a0553eb3f21ff8d))\n - **FEAT**: Support package argument in asset loading methods and widgets ([#3835](https://github.com/flame-engine/flame/issues/3835)). ([3f6f95b0](https://github.com/flame-engine/flame/commit/3f6f95b0225df9e4035c74de6cfad501e8c45167))\n\n#### `jenny` - `v1.5.1`\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n\n## 2026-02-11\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.35.1`](#flame---v1351)\n - [`flame_behaviors` - `v1.3.3`](#flame_behaviors---v133)\n - [`flame_behavior_tree` - `v0.1.4+2`](#flame_behavior_tree---v0142)\n - [`flame_test` - `v2.2.2`](#flame_test---v222)\n - [`flame_tiled` - `v3.0.11`](#flame_tiled---v3011)\n - [`flame_oxygen` - `v0.2.3+20`](#flame_oxygen---v02320)\n - [`flame_isolate` - `v0.6.2+20`](#flame_isolate---v06220)\n - [`flame_texturepacker` - `v5.0.5`](#flame_texturepacker---v505)\n - [`flame_sprite_fusion` - `v0.2.2+3`](#flame_sprite_fusion---v0223)\n - [`flame_steering_behaviors` - `v0.2.1+3`](#flame_steering_behaviors---v0213)\n - [`flame_fire_atlas` - `v1.8.15`](#flame_fire_atlas---v1815)\n - [`flame_audio` - `v2.11.14`](#flame_audio---v21114)\n - [`flame_spine` - `v0.3.0+3`](#flame_spine---v0303)\n - [`flame_bloc` - `v1.12.21`](#flame_bloc---v11221)\n - [`flame_kenney_xml` - `v0.1.1+20`](#flame_kenney_xml---v01120)\n - [`flame_lottie` - `v0.4.2+20`](#flame_lottie---v04220)\n - [`flame_markdown` - `v0.2.4+13`](#flame_markdown---v02413)\n - [`flame_console` - `v0.1.2+16`](#flame_console---v01216)\n - [`flame_rive` - `v1.10.23`](#flame_rive---v11023)\n - [`flame_forge2d` - `v0.19.2+4`](#flame_forge2d---v01924)\n - [`flame_noise` - `v0.3.2+20`](#flame_noise---v03220)\n - [`flame_riverpod` - `v5.5.2`](#flame_riverpod---v552)\n - [`flame_svg` - `v1.11.20`](#flame_svg---v11120)\n - [`flame_network_assets` - `v0.3.3+20`](#flame_network_assets---v03320)\n - [`flame_3d` - `v0.1.1+6`](#flame_3d---v0116)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_behaviors` - `v1.3.3`\n - `flame_behavior_tree` - `v0.1.4+2`\n - `flame_test` - `v2.2.2`\n - `flame_tiled` - `v3.0.11`\n - `flame_oxygen` - `v0.2.3+20`\n - `flame_isolate` - `v0.6.2+20`\n - `flame_texturepacker` - `v5.0.5`\n - `flame_sprite_fusion` - `v0.2.2+3`\n - `flame_steering_behaviors` - `v0.2.1+3`\n - `flame_fire_atlas` - `v1.8.15`\n - `flame_audio` - `v2.11.14`\n - `flame_spine` - `v0.3.0+3`\n - `flame_bloc` - `v1.12.21`\n - `flame_kenney_xml` - `v0.1.1+20`\n - `flame_lottie` - `v0.4.2+20`\n - `flame_markdown` - `v0.2.4+13`\n - `flame_console` - `v0.1.2+16`\n - `flame_rive` - `v1.10.23`\n - `flame_forge2d` - `v0.19.2+4`\n - `flame_noise` - `v0.3.2+20`\n - `flame_riverpod` - `v5.5.2`\n - `flame_svg` - `v1.11.20`\n - `flame_network_assets` - `v0.3.3+20`\n - `flame_3d` - `v0.1.1+6`\n\n---\n\n#### `flame` - `v1.35.1`\n\n - **FIX**: Cancel taps that start inside the component and end outside ([#3805](https://github.com/flame-engine/flame/issues/3805)). ([ebcdb81c](https://github.com/flame-engine/flame/commit/ebcdb81c83b0aa23906d1c652bad2bfadd5add71))\n\n\n## 2026-02-01\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.35.0`](#flame---v1350)\n - [`flame_tiled` - `v3.0.10`](#flame_tiled---v3010)\n - [`flame_behaviors` - `v1.3.2`](#flame_behaviors---v132)\n - [`flame_behavior_tree` - `v0.1.4+1`](#flame_behavior_tree---v0141)\n - [`flame_test` - `v2.2.1`](#flame_test---v221)\n - [`flame_oxygen` - `v0.2.3+19`](#flame_oxygen---v02319)\n - [`flame_isolate` - `v0.6.2+19`](#flame_isolate---v06219)\n - [`flame_texturepacker` - `v5.0.4`](#flame_texturepacker---v504)\n - [`flame_sprite_fusion` - `v0.2.2+2`](#flame_sprite_fusion---v0222)\n - [`flame_steering_behaviors` - `v0.2.1+2`](#flame_steering_behaviors---v0212)\n - [`flame_fire_atlas` - `v1.8.14`](#flame_fire_atlas---v1814)\n - [`flame_audio` - `v2.11.13`](#flame_audio---v21113)\n - [`flame_spine` - `v0.3.0+2`](#flame_spine---v0302)\n - [`flame_bloc` - `v1.12.20`](#flame_bloc---v11220)\n - [`flame_kenney_xml` - `v0.1.1+19`](#flame_kenney_xml---v01119)\n - [`flame_lottie` - `v0.4.2+19`](#flame_lottie---v04219)\n - [`flame_markdown` - `v0.2.4+12`](#flame_markdown---v02412)\n - [`flame_console` - `v0.1.2+15`](#flame_console---v01215)\n - [`flame_rive` - `v1.10.22`](#flame_rive---v11022)\n - [`flame_forge2d` - `v0.19.2+3`](#flame_forge2d---v01923)\n - [`flame_noise` - `v0.3.2+19`](#flame_noise---v03219)\n - [`flame_riverpod` - `v5.5.1`](#flame_riverpod---v551)\n - [`flame_svg` - `v1.11.19`](#flame_svg---v11119)\n - [`flame_network_assets` - `v0.3.3+19`](#flame_network_assets---v03319)\n - [`flame_3d` - `v0.1.1+5`](#flame_3d---v0115)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_behaviors` - `v1.3.2`\n - `flame_behavior_tree` - `v0.1.4+1`\n - `flame_test` - `v2.2.1`\n - `flame_oxygen` - `v0.2.3+19`\n - `flame_isolate` - `v0.6.2+19`\n - `flame_texturepacker` - `v5.0.4`\n - `flame_sprite_fusion` - `v0.2.2+2`\n - `flame_steering_behaviors` - `v0.2.1+2`\n - `flame_fire_atlas` - `v1.8.14`\n - `flame_audio` - `v2.11.13`\n - `flame_spine` - `v0.3.0+2`\n - `flame_bloc` - `v1.12.20`\n - `flame_kenney_xml` - `v0.1.1+19`\n - `flame_lottie` - `v0.4.2+19`\n - `flame_markdown` - `v0.2.4+12`\n - `flame_console` - `v0.1.2+15`\n - `flame_rive` - `v1.10.22`\n - `flame_forge2d` - `v0.19.2+3`\n - `flame_noise` - `v0.3.2+19`\n - `flame_riverpod` - `v5.5.1`\n - `flame_svg` - `v1.11.19`\n - `flame_network_assets` - `v0.3.3+19`\n - `flame_3d` - `v0.1.1+5`\n\n---\n\n#### `flame` - `v1.35.0`\n\n - **FIX**: Loading page should always be possible to add to route ([#3800](https://github.com/flame-engine/flame/issues/3800)). ([a2f5df11](https://github.com/flame-engine/flame/commit/a2f5df113293525d3c5cc6626c5fea05a02350c2))\n - **FIX**: Reimplement setLayoutSize to only notify once ([#3796](https://github.com/flame-engine/flame/issues/3796)). ([97f8bebe](https://github.com/flame-engine/flame/commit/97f8bebecaf5bb9a8018c85220609b2d9d67524a))\n - **FEAT**: Use a Free List Strategy on BatchItem indexes within SpriteBatch and return index from .add() ([#3650](https://github.com/flame-engine/flame/issues/3650)). ([8d77c84e](https://github.com/flame-engine/flame/commit/8d77c84e0c05d0b5b6ca57187bd0ee39e94c752f))\n - **FEAT**: Add TextBoxComponent.resetAnimation ([#3787](https://github.com/flame-engine/flame/issues/3787)). ([33fb10c0](https://github.com/flame-engine/flame/commit/33fb10c02a83354030fe0a278c16869f5940941a))\n - **FEAT**: Implement padding component inflateChild ([#3785](https://github.com/flame-engine/flame/issues/3785)). ([9ac53a69](https://github.com/flame-engine/flame/commit/9ac53a69e468a3e3ff073db94c1c5df57997c4f1))\n\n#### `flame_tiled` - `v3.0.10`\n\n - **FIX**: Image layer paint area fix ([#3783](https://github.com/flame-engine/flame/issues/3783)). ([437d4bec](https://github.com/flame-engine/flame/commit/437d4becb5752e8429f67297b08ceeb2b971388c))\n\n\n## 2025-12-17\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_riverpod` - `v5.5.0`](#flame_riverpod---v550)\n\n---\n\n#### `flame_riverpod` - `v5.5.0`\n\n - **FEAT**: Riverpod 3.0 in flame_riverpod ([#3789](https://github.com/flame-engine/flame/issues/3789)). ([57f7f9d8](https://github.com/flame-engine/flame/commit/57f7f9d8b30632717fdf894321758a26d4985907))\n\n\n## 2025-11-23\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`behavior_tree` - `v0.1.5`](#behavior_tree---v015)\n - [`flame` - `v1.34.0`](#flame---v1340)\n - [`flame_behavior_tree` - `v0.1.4`](#flame_behavior_tree---v014)\n - [`flame_network_assets` - `v0.3.3+18`](#flame_network_assets---v03318)\n - [`flame_spine` - `v0.3.0+1`](#flame_spine---v0301)\n - [`flame_test` - `v2.2.0`](#flame_test---v220)\n - [`flame_behaviors` - `v1.3.1`](#flame_behaviors---v131)\n - [`flame_tiled` - `v3.0.9`](#flame_tiled---v309)\n - [`flame_oxygen` - `v0.2.3+18`](#flame_oxygen---v02318)\n - [`flame_isolate` - `v0.6.2+18`](#flame_isolate---v06218)\n - [`flame_texturepacker` - `v5.0.3`](#flame_texturepacker---v503)\n - [`flame_sprite_fusion` - `v0.2.2+1`](#flame_sprite_fusion---v0221)\n - [`flame_steering_behaviors` - `v0.2.1+1`](#flame_steering_behaviors---v0211)\n - [`flame_fire_atlas` - `v1.8.13`](#flame_fire_atlas---v1813)\n - [`flame_audio` - `v2.11.12`](#flame_audio---v21112)\n - [`flame_bloc` - `v1.12.19`](#flame_bloc---v11219)\n - [`flame_kenney_xml` - `v0.1.1+18`](#flame_kenney_xml---v01118)\n - [`flame_lottie` - `v0.4.2+18`](#flame_lottie---v04218)\n - [`flame_markdown` - `v0.2.4+11`](#flame_markdown---v02411)\n - [`flame_console` - `v0.1.2+14`](#flame_console---v01214)\n - [`flame_rive` - `v1.10.21`](#flame_rive---v11021)\n - [`flame_forge2d` - `v0.19.2+2`](#flame_forge2d---v01922)\n - [`flame_noise` - `v0.3.2+18`](#flame_noise---v03218)\n - [`flame_riverpod` - `v5.4.21`](#flame_riverpod---v5421)\n - [`flame_svg` - `v1.11.18`](#flame_svg---v11118)\n - [`flame_3d` - `v0.1.1+4`](#flame_3d---v0114)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_behaviors` - `v1.3.1`\n - `flame_tiled` - `v3.0.9`\n - `flame_oxygen` - `v0.2.3+18`\n - `flame_isolate` - `v0.6.2+18`\n - `flame_texturepacker` - `v5.0.3`\n - `flame_sprite_fusion` - `v0.2.2+1`\n - `flame_steering_behaviors` - `v0.2.1+1`\n - `flame_fire_atlas` - `v1.8.13`\n - `flame_audio` - `v2.11.12`\n - `flame_bloc` - `v1.12.19`\n - `flame_kenney_xml` - `v0.1.1+18`\n - `flame_lottie` - `v0.4.2+18`\n - `flame_markdown` - `v0.2.4+11`\n - `flame_console` - `v0.1.2+14`\n - `flame_rive` - `v1.10.21`\n - `flame_forge2d` - `v0.19.2+2`\n - `flame_noise` - `v0.3.2+18`\n - `flame_riverpod` - `v5.4.21`\n - `flame_svg` - `v1.11.18`\n - `flame_3d` - `v0.1.1+4`\n\n---\n\n#### `behavior_tree` - `v0.1.5`\n\n - **FEAT**: Add `Blackboard` support for behavior trees ([#3756](https://github.com/flame-engine/flame/issues/3756)). ([955411d3](https://github.com/flame-engine/flame/commit/955411d34a6e85e25524b52938146578fa4aa3e3))\n\n#### `flame` - `v1.34.0`\n\n - **FEAT**: Add scaling gesture for components ([#3770](https://github.com/flame-engine/flame/issues/3770)). ([f413eddb](https://github.com/flame-engine/flame/commit/f413eddbf1581f30087ba53f9516e22e035bda7a))\n - **FEAT**: Linear layout component text box component ([#3779](https://github.com/flame-engine/flame/issues/3779)). ([476680d7](https://github.com/flame-engine/flame/commit/476680d7b699be7ffd4e3c3421e6f27659a66420))\n - **FEAT**: TextBoxComponent updateBounds on set boxConfig ([#3777](https://github.com/flame-engine/flame/issues/3777)). ([eefb2f9e](https://github.com/flame-engine/flame/commit/eefb2f9e966a4ead5bcf0d3985314609166609f1))\n - **FEAT**: Add the `CombinedEffect` that bundles many effects together in one ([#3776](https://github.com/flame-engine/flame/issues/3776)). ([77869f57](https://github.com/flame-engine/flame/commit/77869f573df8b0eb1ce02de8f2b896286768bc6f))\n - **DOCS**: Fix documentation typos ([#3769](https://github.com/flame-engine/flame/issues/3769)). ([db74da15](https://github.com/flame-engine/flame/commit/db74da154b309e7544f5242c49d56717e385c5fc))\n\n#### `flame_behavior_tree` - `v0.1.4`\n\n - **FEAT**: Add `Blackboard` support for behavior trees ([#3756](https://github.com/flame-engine/flame/issues/3756)). ([955411d3](https://github.com/flame-engine/flame/commit/955411d34a6e85e25524b52938146578fa4aa3e3))\n\n#### `flame_network_assets` - `v0.3.3+18`\n\n - **FIX**: Not re-encode image when saving ([#3780](https://github.com/flame-engine/flame/issues/3780)). ([30a344cf](https://github.com/flame-engine/flame/commit/30a344cfae78c6608713d4b194f3112e47068adf))\n\n#### `flame_spine` - `v0.3.0+1`\n\n - **DOCS**: Updated flame_spine documentation ([#3763](https://github.com/flame-engine/flame/issues/3763)). ([3c721d91](https://github.com/flame-engine/flame/commit/3c721d91f3e3514c79e77b8e2e37a641305a2e04))\n\n#### `flame_test` - `v2.2.0`\n\n - **FEAT**: Add scaling gesture for components ([#3770](https://github.com/flame-engine/flame/issues/3770)). ([f413eddb](https://github.com/flame-engine/flame/commit/f413eddbf1581f30087ba53f9516e22e035bda7a))\n\n\n## 2025-11-03\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame_spine` - `v0.3.0`](#flame_spine---v030)\n\nPackages with other changes:\n\n - There are no other changes in this release.\n\n---\n\n#### `flame_spine` - `v0.3.0`\n\n - **BREAKING** **FEAT**: Support Spine 4.3 ([#3760](https://github.com/flame-engine/flame/issues/3760)). ([17bc40c3](https://github.com/flame-engine/flame/commit/17bc40c361471fde35b360671cccef115ed0b4bc))\n\n\n## 2025-10-29\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`behavior_tree` - `v0.1.4`](#behavior_tree---v014)\n - [`flame_sprite_fusion` - `v0.2.2`](#flame_sprite_fusion---v022)\n\n---\n\n#### `behavior_tree` - `v0.1.4`\n\n - **CHORE**: Bump dependencies.\n\n#### `flame_sprite_fusion` - `v0.2.2`\n\n - **FEAT**: Add `getLayerByName` to `SpriteFusionTilemapData` ([#3755](https://github.com/flame-engine/flame/issues/3755)). ([9a6ca27b](https://github.com/flame-engine/flame/commit/9a6ca27b42715c0b9ae52885ab8c94bed3819c11))\n\n\n## 2025-10-22\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.33.0`](#flame---v1330)\n - [`flame_test` - `v2.1.0`](#flame_test---v210)\n\nPackages with other changes:\n\n - [`flame_audio` - `v2.11.11`](#flame_audio---v21111)\n - [`flame_behaviors` - `v1.3.0`](#flame_behaviors---v130)\n - [`flame_console` - `v0.1.2+13`](#flame_console---v01213)\n - [`flame_fire_atlas` - `v1.8.12`](#flame_fire_atlas---v1812)\n - [`flame_network_assets` - `v0.3.3+17`](#flame_network_assets---v03317)\n - [`flame_spine` - `v0.2.2+17`](#flame_spine---v02217)\n - [`flame_sprite_fusion` - `v0.2.1`](#flame_sprite_fusion---v021)\n - [`flame_steering_behaviors` - `v0.2.1`](#flame_steering_behaviors---v021)\n - [`flame_tiled` - `v3.0.8`](#flame_tiled---v308)\n - [`flame_behavior_tree` - `v0.1.3+17`](#flame_behavior_tree---v01317)\n - [`flame_oxygen` - `v0.2.3+17`](#flame_oxygen---v02317)\n - [`flame_isolate` - `v0.6.2+17`](#flame_isolate---v06217)\n - [`flame_texturepacker` - `v5.0.2`](#flame_texturepacker---v502)\n - [`flame_bloc` - `v1.12.18`](#flame_bloc---v11218)\n - [`flame_kenney_xml` - `v0.1.1+17`](#flame_kenney_xml---v01117)\n - [`flame_lottie` - `v0.4.2+17`](#flame_lottie---v04217)\n - [`flame_markdown` - `v0.2.4+10`](#flame_markdown---v02410)\n - [`flame_rive` - `v1.10.20`](#flame_rive---v11020)\n - [`flame_forge2d` - `v0.19.2+1`](#flame_forge2d---v01921)\n - [`flame_noise` - `v0.3.2+17`](#flame_noise---v03217)\n - [`flame_riverpod` - `v5.4.20`](#flame_riverpod---v5420)\n - [`flame_svg` - `v1.11.17`](#flame_svg---v11117)\n - [`flame_3d` - `v0.1.1+3`](#flame_3d---v0113)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_behavior_tree` - `v0.1.3+17`\n - `flame_oxygen` - `v0.2.3+17`\n - `flame_isolate` - `v0.6.2+17`\n - `flame_texturepacker` - `v5.0.2`\n - `flame_bloc` - `v1.12.18`\n - `flame_kenney_xml` - `v0.1.1+17`\n - `flame_lottie` - `v0.4.2+17`\n - `flame_markdown` - `v0.2.4+10`\n - `flame_rive` - `v1.10.20`\n - `flame_forge2d` - `v0.19.2+1`\n - `flame_noise` - `v0.3.2+17`\n - `flame_riverpod` - `v5.4.20`\n - `flame_svg` - `v1.11.17`\n - `flame_3d` - `v0.1.1+3`\n\n---\n\n#### `flame` - `v1.33.0`\n\n - **REFACTOR**: Re-organize internal event imports ([#3742](https://github.com/flame-engine/flame/issues/3742)). ([7523e014](https://github.com/flame-engine/flame/commit/7523e014706a2a368eebf4843379d470c5924c68))\n - **PERF**: `addAll` shouldn't create unnecessary growing lists ([#3737](https://github.com/flame-engine/flame/issues/3737)). ([d1fa9d0d](https://github.com/flame-engine/flame/commit/d1fa9d0d5491264fbf4bb2b7e0c731597e8c0fb5))\n - **FIX**: Store json maps directly in AssetsCache ([#3746](https://github.com/flame-engine/flame/issues/3746)). ([8a9f493f](https://github.com/flame-engine/flame/commit/8a9f493fdddf68ba2889f19be71eb9e91071190f))\n - **FIX**: Unique `ComponentKey` toString ([#3739](https://github.com/flame-engine/flame/issues/3739)). ([9a4a8f20](https://github.com/flame-engine/flame/commit/9a4a8f20ff0ad20625ae7db7aca0ca683e4db417))\n - **FIX**: Depreciated AssetManifest.json switched to AssetManifest API ([#3734](https://github.com/flame-engine/flame/issues/3734)). ([a2bd9827](https://github.com/flame-engine/flame/commit/a2bd982764b8e59830e86ba8a07239cedc1bad1c))\n - **FEAT**: Dummy commit. ([8f7437d3](https://github.com/flame-engine/flame/commit/8f7437d3a468a9708f47da8863454e843bbbf72c))\n - **FEAT**: Added fromCache method to AssetsCache ([#3740](https://github.com/flame-engine/flame/issues/3740)). ([33a7123f](https://github.com/flame-engine/flame/commit/33a7123ff9538f222534379a35d9a1074102a3fd))\n - **FEAT**: Add a \"raw\" field to access the underlying Flutter event in the new event system ([#3731](https://github.com/flame-engine/flame/issues/3731)). ([36eb3929](https://github.com/flame-engine/flame/commit/36eb3929aa44451c1d6aa986a305c436cfa93349))\n - **DOCS**: Layout components ([#3752](https://github.com/flame-engine/flame/issues/3752)). ([0aa145bb](https://github.com/flame-engine/flame/commit/0aa145bbbd6decfa121080b1cf223e9e799c1ac4))\n - **DOCS**: Deprecate TapDetector in favour of TapCallbacks ([#2886](https://github.com/flame-engine/flame/issues/2886)). ([b173697b](https://github.com/flame-engine/flame/commit/b173697bfb7ea61287251c43cd3c9d2fdb448fe3))\n - **BREAKING** **FEAT**: Implements ExpandedComponent ([#3662](https://github.com/flame-engine/flame/issues/3662)). ([212ed354](https://github.com/flame-engine/flame/commit/212ed354d7704915a7585424e216ca83300c9530))\n - **BREAKING** **FEAT**: Support secondary taps (right click) on new callbacks system ([#3741](https://github.com/flame-engine/flame/issues/3741)). ([46bd3856](https://github.com/flame-engine/flame/commit/46bd385675ae781c4614d997e4792f53fc43271d))\n\n#### `flame_test` - `v2.1.0`\n\n - **BREAKING** **FEAT**: Support secondary taps (right click) on new callbacks system ([#3741](https://github.com/flame-engine/flame/issues/3741)). ([46bd3856](https://github.com/flame-engine/flame/commit/46bd385675ae781c4614d997e4792f53fc43271d))\n\n#### `flame_audio` - `v2.11.11`\n\n - **DOCS**: Deprecate TapDetector in favour of TapCallbacks ([#2886](https://github.com/flame-engine/flame/issues/2886)). ([b173697b](https://github.com/flame-engine/flame/commit/b173697bfb7ea61287251c43cd3c9d2fdb448fe3))\n\n#### `flame_behaviors` - `v1.3.0`\n\n - **FEAT**: Add flame_behaviors package ([#3717](https://github.com/flame-engine/flame/issues/3717)). ([e950d79e](https://github.com/flame-engine/flame/commit/e950d79e56bf5902f2a48367a1e899e9b8903dc4))\n\n#### `flame_console` - `v0.1.2+13`\n\n - **DOCS**: Add console with backtick on flame_console example ([#3743](https://github.com/flame-engine/flame/issues/3743)). ([8534a557](https://github.com/flame-engine/flame/commit/8534a5574083ba3478979a1a619bf67820062bf1))\n\n#### `flame_fire_atlas` - `v1.8.12`\n\n - **DOCS**: Deprecate TapDetector in favour of TapCallbacks ([#2886](https://github.com/flame-engine/flame/issues/2886)). ([b173697b](https://github.com/flame-engine/flame/commit/b173697bfb7ea61287251c43cd3c9d2fdb448fe3))\n\n#### `flame_network_assets` - `v0.3.3+17`\n\n - **DOCS**: Deprecate TapDetector in favour of TapCallbacks ([#2886](https://github.com/flame-engine/flame/issues/2886)). ([b173697b](https://github.com/flame-engine/flame/commit/b173697bfb7ea61287251c43cd3c9d2fdb448fe3))\n\n#### `flame_spine` - `v0.2.2+17`\n\n - **DOCS**: Deprecate TapDetector in favour of TapCallbacks ([#2886](https://github.com/flame-engine/flame/issues/2886)). ([b173697b](https://github.com/flame-engine/flame/commit/b173697bfb7ea61287251c43cd3c9d2fdb448fe3))\n\n#### `flame_sprite_fusion` - `v0.2.1`\n\n - **FEAT**: Add `hasAttribute` and `getAttribute` methods for `SpriteFusionTileData` ([#3751](https://github.com/flame-engine/flame/issues/3751)). ([17aeb93b](https://github.com/flame-engine/flame/commit/17aeb93b6f0e1bab11f6e237446811824e512b77))\n\n#### `flame_steering_behaviors` - `v0.2.1`\n\n - **FEAT**: Add flame_steering_behaviors package ([#3748](https://github.com/flame-engine/flame/issues/3748)). ([2d4f0d43](https://github.com/flame-engine/flame/commit/2d4f0d43ef472b5a473cde3fc97579b8a1c0a9fc))\n\n#### `flame_tiled` - `v3.0.8`\n\n - **FIX**: Show assertion when tiled file is loaded in the wrong way ([#3747](https://github.com/flame-engine/flame/issues/3747)). ([3efedc46](https://github.com/flame-engine/flame/commit/3efedc4653fa33cb239a38cc6ac8c999bcd25a7f))\n\n\n## 2025-09-08\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_forge2d` - `v0.19.2`](#flame_forge2d---v0192)\n\n---\n\n#### `flame_forge2d` - `v0.19.2`\n\n - **FEAT**: Allow bodies to not be destroyed when the world is removed from the component tree ([#3716](https://github.com/flame-engine/flame/issues/3716)). ([7d18fd3d](https://github.com/flame-engine/flame/commit/7d18fd3d1cf076bce7032eb60dbfd7777643539d))\n\n\n## 2025-09-07\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_forge2d` - `v0.19.1`](#flame_forge2d---v0191)\n - [`flame_3d` - `v0.1.1+2`](#flame_3d---v0112)\n\n---\n\n#### `flame_forge2d` - `v0.19.1`\n\n - **FIX**: Use correct `Forge2DWorld` in `onRemove` ([#3713](https://github.com/flame-engine/flame/issues/3713)). ([140833d1](https://github.com/flame-engine/flame/commit/140833d1eacf6b756f81f0452ec237c6991f2ae0))\n\n#### `flame_3d` - `v0.1.1+2`\n\n - **DOCS**: Update README.md of flame_3d with newer instructions ([#3711](https://github.com/flame-engine/flame/issues/3711)). ([ad7dc059](https://github.com/flame-engine/flame/commit/ad7dc059f2f97e7bc78a74bdff149b26adb22fbc))\n\n\n## 2025-08-28\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_3d` - `v0.1.1+1`](#flame_3d---v0111)\n\n---\n\n#### `flame_3d` - `v0.1.1+1`\n\n - **FIX**: When parsing OBJs with no normals, fallback to normal generation ([#3708](https://github.com/flame-engine/flame/issues/3708)). ([a59ccaa3](https://github.com/flame-engine/flame/commit/a59ccaa341a562c25ad54f4af567b160084e06d8))\n - **FIX**: Update compiled shaderbundle for flame_3d and add color example ([#3704](https://github.com/flame-engine/flame/issues/3704)). ([a1070d97](https://github.com/flame-engine/flame/commit/a1070d975fd3af52d44f3febb13dd97afd4990a6))\n\n\n## 2025-08-26\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.32.0`](#flame---v1320)\n - [`flame_3d` - `v0.1.1`](#flame_3d---v011)\n - [`flame_tiled` - `v3.0.7`](#flame_tiled---v307)\n - [`flame_behavior_tree` - `v0.1.3+16`](#flame_behavior_tree---v01316)\n - [`flame_test` - `v2.0.3`](#flame_test---v203)\n - [`flame_oxygen` - `v0.2.3+16`](#flame_oxygen---v02316)\n - [`flame_isolate` - `v0.6.2+16`](#flame_isolate---v06216)\n - [`flame_texturepacker` - `v5.0.1`](#flame_texturepacker---v501)\n - [`flame_sprite_fusion` - `v0.2.0+3`](#flame_sprite_fusion---v0203)\n - [`flame_fire_atlas` - `v1.8.11`](#flame_fire_atlas---v1811)\n - [`flame_audio` - `v2.11.10`](#flame_audio---v21110)\n - [`flame_spine` - `v0.2.2+16`](#flame_spine---v02216)\n - [`flame_bloc` - `v1.12.17`](#flame_bloc---v11217)\n - [`flame_kenney_xml` - `v0.1.1+16`](#flame_kenney_xml---v01116)\n - [`flame_lottie` - `v0.4.2+16`](#flame_lottie---v04216)\n - [`flame_markdown` - `v0.2.4+9`](#flame_markdown---v0249)\n - [`flame_console` - `v0.1.2+12`](#flame_console---v01212)\n - [`flame_rive` - `v1.10.19`](#flame_rive---v11019)\n - [`flame_forge2d` - `v0.19.0+6`](#flame_forge2d---v01906)\n - [`flame_noise` - `v0.3.2+16`](#flame_noise---v03216)\n - [`flame_riverpod` - `v5.4.19`](#flame_riverpod---v5419)\n - [`flame_svg` - `v1.11.16`](#flame_svg---v11116)\n - [`flame_network_assets` - `v0.3.3+16`](#flame_network_assets---v03316)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_behavior_tree` - `v0.1.3+16`\n - `flame_test` - `v2.0.3`\n - `flame_oxygen` - `v0.2.3+16`\n - `flame_isolate` - `v0.6.2+16`\n - `flame_texturepacker` - `v5.0.1`\n - `flame_sprite_fusion` - `v0.2.0+3`\n - `flame_fire_atlas` - `v1.8.11`\n - `flame_audio` - `v2.11.10`\n - `flame_spine` - `v0.2.2+16`\n - `flame_bloc` - `v1.12.17`\n - `flame_kenney_xml` - `v0.1.1+16`\n - `flame_lottie` - `v0.4.2+16`\n - `flame_markdown` - `v0.2.4+9`\n - `flame_console` - `v0.1.2+12`\n - `flame_rive` - `v1.10.19`\n - `flame_forge2d` - `v0.19.0+6`\n - `flame_noise` - `v0.3.2+16`\n - `flame_riverpod` - `v5.4.19`\n - `flame_svg` - `v1.11.16`\n - `flame_network_assets` - `v0.3.3+16`\n\n---\n\n#### `flame` - `v1.32.0`\n\n - **REFACTOR**: Move MutableRSTransform out of flame_tiled package and into flame package ([#3695](https://github.com/flame-engine/flame/issues/3695)). ([7d644dd8](https://github.com/flame-engine/flame/commit/7d644dd84ce27e292b53f7310967393cf4c60618))\n - **FEAT**: Add renderLine helper to canvas extensions ([#3697](https://github.com/flame-engine/flame/issues/3697)). ([7ede916f](https://github.com/flame-engine/flame/commit/7ede916f77f20d8c1b0c89627800214dba9facec))\n\n#### `flame_3d` - `v0.1.1`\n\n - **FIX**: Cleanup and make OBJ parser more resilient ([#3702](https://github.com/flame-engine/flame/issues/3702)). ([b96421d5](https://github.com/flame-engine/flame/commit/b96421d5035393d764a9ec34aeba8db380222f45))\n - **FIX**: Fix color to byte conversion for shaders ([#3700](https://github.com/flame-engine/flame/issues/3700)). ([2426c06b](https://github.com/flame-engine/flame/commit/2426c06b799797720c9df8fbd73e337422654d00))\n - **FIX**: Add fallback default material to avoid crashes ([#3701](https://github.com/flame-engine/flame/issues/3701)). ([8e6b04e3](https://github.com/flame-engine/flame/commit/8e6b04e38855b6fae6761a9f8d20c82c6bff6d76))\n - **FIX**: Expose Line3D on components.dart ([#3698](https://github.com/flame-engine/flame/issues/3698)). ([d58d7b54](https://github.com/flame-engine/flame/commit/d58d7b5482f646e4536160449f4a17317b8aff2b))\n - **FEAT**: Introduce cone mesh ([#3699](https://github.com/flame-engine/flame/issues/3699)). ([874a97fe](https://github.com/flame-engine/flame/commit/874a97fe421dadbed9f3825c8562f8bd7e6c95c9))\n - **DOCS**: Update instructions on how to use flame_3d ([#3696](https://github.com/flame-engine/flame/issues/3696)). ([11cc2a8f](https://github.com/flame-engine/flame/commit/11cc2a8fae0f1c17106f91ef1ac319e6f8e39036))\n\n#### `flame_tiled` - `v3.0.7`\n\n - **REFACTOR**: Move MutableRSTransform out of flame_tiled package and into flame package ([#3695](https://github.com/flame-engine/flame/issues/3695)). ([7d644dd8](https://github.com/flame-engine/flame/commit/7d644dd84ce27e292b53f7310967393cf4c60618))\n\n\n## 2025-08-23\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.31.0`](#flame---v1310)\n - [`flame_texturepacker` - `v5.0.0`](#flame_texturepacker---v500)\n\nPackages with other changes:\n\n - [`flame_3d` - `v0.1.0`](#flame_3d---v010)\n - [`flame_lint` - `v1.4.2`](#flame_lint---v142)\n - [`flame_behavior_tree` - `v0.1.3+15`](#flame_behavior_tree---v01315)\n - [`flame_test` - `v2.0.2`](#flame_test---v202)\n - [`flame_tiled` - `v3.0.6`](#flame_tiled---v306)\n - [`flame_oxygen` - `v0.2.3+15`](#flame_oxygen---v02315)\n - [`flame_isolate` - `v0.6.2+15`](#flame_isolate---v06215)\n - [`flame_sprite_fusion` - `v0.2.0+2`](#flame_sprite_fusion---v0202)\n - [`flame_fire_atlas` - `v1.8.10`](#flame_fire_atlas---v1810)\n - [`flame_audio` - `v2.11.9`](#flame_audio---v2119)\n - [`flame_spine` - `v0.2.2+15`](#flame_spine---v02215)\n - [`flame_bloc` - `v1.12.16`](#flame_bloc---v11216)\n - [`flame_kenney_xml` - `v0.1.1+15`](#flame_kenney_xml---v01115)\n - [`flame_lottie` - `v0.4.2+15`](#flame_lottie---v04215)\n - [`flame_markdown` - `v0.2.4+8`](#flame_markdown---v0248)\n - [`flame_console` - `v0.1.2+11`](#flame_console---v01211)\n - [`flame_rive` - `v1.10.18`](#flame_rive---v11018)\n - [`flame_forge2d` - `v0.19.0+5`](#flame_forge2d---v01905)\n - [`flame_noise` - `v0.3.2+15`](#flame_noise---v03215)\n - [`flame_riverpod` - `v5.4.18`](#flame_riverpod---v5418)\n - [`flame_svg` - `v1.11.15`](#flame_svg---v11115)\n - [`flame_network_assets` - `v0.3.3+15`](#flame_network_assets---v03315)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_behavior_tree` - `v0.1.3+15`\n - `flame_test` - `v2.0.2`\n - `flame_tiled` - `v3.0.6`\n - `flame_oxygen` - `v0.2.3+15`\n - `flame_isolate` - `v0.6.2+15`\n - `flame_sprite_fusion` - `v0.2.0+2`\n - `flame_fire_atlas` - `v1.8.10`\n - `flame_audio` - `v2.11.9`\n - `flame_spine` - `v0.2.2+15`\n - `flame_bloc` - `v1.12.16`\n - `flame_kenney_xml` - `v0.1.1+15`\n - `flame_lottie` - `v0.4.2+15`\n - `flame_markdown` - `v0.2.4+8`\n - `flame_console` - `v0.1.2+11`\n - `flame_rive` - `v1.10.18`\n - `flame_forge2d` - `v0.19.0+5`\n - `flame_noise` - `v0.3.2+15`\n - `flame_riverpod` - `v5.4.18`\n - `flame_svg` - `v1.11.15`\n - `flame_network_assets` - `v0.3.3+15`\n\n---\n\n#### `flame` - `v1.31.0`\n\n - **FIX**: Resume engine on mount if paused by backgrounding ([#3631](https://github.com/flame-engine/flame/issues/3631)) ([#3637](https://github.com/flame-engine/flame/issues/3637)). ([b556dc35](https://github.com/flame-engine/flame/commit/b556dc3557d4b655d605c8e2b3744cafd0635841))\n - **FIX**: Export `ComponentRenderContext` ([#3669](https://github.com/flame-engine/flame/issues/3669)). ([086096ca](https://github.com/flame-engine/flame/commit/086096ca73236aaea79a2651cb9e3fa8b6211d50))\n - **FIX**: The `ParallaxComponent` should respect the `virtualSize` ([#3666](https://github.com/flame-engine/flame/issues/3666)). ([9f29c785](https://github.com/flame-engine/flame/commit/9f29c785a1e17428d3a59965b2bf484267c4b2a8))\n - **FIX**: Attach layout listeners to new children ([#3648](https://github.com/flame-engine/flame/issues/3648)). ([4821ec2c](https://github.com/flame-engine/flame/commit/4821ec2ca9cccbf8017d0b539373f599d168c45c))\n - **FEAT**: Add support for model parsing and rendering in flame_3d, including skeletal animations ([#3675](https://github.com/flame-engine/flame/issues/3675)). ([cc58aef5](https://github.com/flame-engine/flame/commit/cc58aef5b53f208fb1cbb116bfb9f9af9a351e8e))\n - **FEAT**: Add Random extensions ([#3672](https://github.com/flame-engine/flame/issues/3672)). ([50e5f296](https://github.com/flame-engine/flame/commit/50e5f29610e9bcc8d939d1e86b5c8bc398516eb1))\n - **FEAT**: Padding component ([#3661](https://github.com/flame-engine/flame/issues/3661)). ([6c953a28](https://github.com/flame-engine/flame/commit/6c953a2862b46c66b91785c5d481385567596adb))\n - **FEAT**: Add canPop to RouterComponent ([#3659](https://github.com/flame-engine/flame/issues/3659)). ([6bd3b48f](https://github.com/flame-engine/flame/commit/6bd3b48ff34c92b221b8a66ac951238d7e6176e0))\n - **FEAT**: Add children and priority to SpriteBatchComponent ([#3649](https://github.com/flame-engine/flame/issues/3649)). ([97b9ba83](https://github.com/flame-engine/flame/commit/97b9ba837e094d79f9e8d8c1ed413717b9d11663))\n - **BREAKING** **REFACTOR**: Remove shrinkwrap ([#3660](https://github.com/flame-engine/flame/issues/3660)). ([e8860f62](https://github.com/flame-engine/flame/commit/e8860f622acaf7df97ca8fcfbdf94fdae26d5921))\n\n#### `flame_texturepacker` - `v5.0.0`\n\n - **BREAKING** **PERF**: TexturePacker optimizations ([#3647](https://github.com/flame-engine/flame/issues/3647)). ([5cc2eedb](https://github.com/flame-engine/flame/commit/5cc2eedb1cb17f249c97889ba924e763f83d774e))\n\n#### `flame_3d` - `v0.1.0`\n\n - **REFACTOR**: Add collections library to flame_3d ([#3680](https://github.com/flame-engine/flame/issues/3680)). ([89e5e58e](https://github.com/flame-engine/flame/commit/89e5e58efb580ec267a0dca78a3a0f320203d4ee))\n - **FIX**: Update flame_3d to support both old and newer Flutter APIs ([#3663](https://github.com/flame-engine/flame/issues/3663)). ([d9f1fe7f](https://github.com/flame-engine/flame/commit/d9f1fe7f9abd8f0307ecc22ff24d3b492e9ca332))\n - **FEAT**: Add ability to run on flame_3d example ([#3679](https://github.com/flame-engine/flame/issues/3679)). ([801692bf](https://github.com/flame-engine/flame/commit/801692bfa1226e01f1540166a8140ab42e36ed87))\n - **FEAT**: Add support for model parsing and rendering in flame_3d, including skeletal animations ([#3675](https://github.com/flame-engine/flame/issues/3675)). ([cc58aef5](https://github.com/flame-engine/flame/commit/cc58aef5b53f208fb1cbb116bfb9f9af9a351e8e))\n - **FEAT**: Add setup command to flame_3d example ([#3671](https://github.com/flame-engine/flame/issues/3671)). ([2f5ba87b](https://github.com/flame-engine/flame/commit/2f5ba87be8068b12c2604b79f7db9c1f3307a4b6))\n - **FEAT**: Organize components and add destroy command to flame_3d example ([#3665](https://github.com/flame-engine/flame/issues/3665)). ([d5915752](https://github.com/flame-engine/flame/commit/d591575263d8b13aee862efe05842001aa60f89d))\n - **FEAT**: Add keybind to jump on flame_3d example ([#3664](https://github.com/flame-engine/flame/issues/3664)). ([7be0bccd](https://github.com/flame-engine/flame/commit/7be0bccda0040bfc3734f90b6bca7d5e99455bee))\n\n#### `flame_lint` - `v1.4.2`\n\n - **FIX**: Update flame_lint to use lints 6.0.0 ([#3612](https://github.com/flame-engine/flame/issues/3612)). ([ba5f6789](https://github.com/flame-engine/flame/commit/ba5f6789bed68e4cc7ca95584e35ed62d0111da2))\n\n\n## 2025-07-13\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.30.1`](#flame---v1301)\n - [`flame_lint` - `v1.4.1`](#flame_lint---v141)\n - [`jenny` - `v1.5.0`](#jenny---v150)\n - [`flame_behavior_tree` - `v0.1.3+14`](#flame_behavior_tree---v01314)\n - [`flame_test` - `v2.0.1`](#flame_test---v201)\n - [`flame_tiled` - `v3.0.5`](#flame_tiled---v305)\n - [`flame_oxygen` - `v0.2.3+14`](#flame_oxygen---v02314)\n - [`flame_isolate` - `v0.6.2+14`](#flame_isolate---v06214)\n - [`flame_texturepacker` - `v4.4.1`](#flame_texturepacker---v441)\n - [`flame_sprite_fusion` - `v0.2.0+1`](#flame_sprite_fusion---v0201)\n - [`flame_fire_atlas` - `v1.8.9`](#flame_fire_atlas---v189)\n - [`flame_audio` - `v2.11.8`](#flame_audio---v2118)\n - [`flame_spine` - `v0.2.2+14`](#flame_spine---v02214)\n - [`flame_bloc` - `v1.12.15`](#flame_bloc---v11215)\n - [`flame_kenney_xml` - `v0.1.1+14`](#flame_kenney_xml---v01114)\n - [`flame_lottie` - `v0.4.2+14`](#flame_lottie---v04214)\n - [`flame_markdown` - `v0.2.4+7`](#flame_markdown---v0247)\n - [`flame_console` - `v0.1.2+10`](#flame_console---v01210)\n - [`flame_rive` - `v1.10.17`](#flame_rive---v11017)\n - [`flame_forge2d` - `v0.19.0+4`](#flame_forge2d---v01904)\n - [`flame_noise` - `v0.3.2+14`](#flame_noise---v03214)\n - [`flame_riverpod` - `v5.4.17`](#flame_riverpod---v5417)\n - [`flame_svg` - `v1.11.14`](#flame_svg---v11114)\n - [`flame_network_assets` - `v0.3.3+14`](#flame_network_assets---v03314)\n - [`flame_3d` - `v0.1.0-dev.14`](#flame_3d---v010-dev14)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_behavior_tree` - `v0.1.3+14`\n - `flame_test` - `v2.0.1`\n - `flame_tiled` - `v3.0.5`\n - `flame_oxygen` - `v0.2.3+14`\n - `flame_isolate` - `v0.6.2+14`\n - `flame_texturepacker` - `v4.4.1`\n - `flame_sprite_fusion` - `v0.2.0+1`\n - `flame_fire_atlas` - `v1.8.9`\n - `flame_audio` - `v2.11.8`\n - `flame_spine` - `v0.2.2+14`\n - `flame_bloc` - `v1.12.15`\n - `flame_kenney_xml` - `v0.1.1+14`\n - `flame_lottie` - `v0.4.2+14`\n - `flame_markdown` - `v0.2.4+7`\n - `flame_console` - `v0.1.2+10`\n - `flame_rive` - `v1.10.17`\n - `flame_forge2d` - `v0.19.0+4`\n - `flame_noise` - `v0.3.2+14`\n - `flame_riverpod` - `v5.4.17`\n - `flame_svg` - `v1.11.14`\n - `flame_network_assets` - `v0.3.3+14`\n - `flame_3d` - `v0.1.0-dev.14`\n\n---\n\n#### `flame` - `v1.30.1`\n\n - **FIX**: Hitboxes with vertically flipped ancestor should not reflect angle for vertices ([#3642](https://github.com/flame-engine/flame/issues/3642)). ([7e8d3a98](https://github.com/flame-engine/flame/commit/7e8d3a9885f77da12456b148cd1f425395a00f71))\n - **FIX**: Remove unnecessary breaks ([#3638](https://github.com/flame-engine/flame/issues/3638)). ([ea29929c](https://github.com/flame-engine/flame/commit/ea29929cd86ed00407f2d2aa69dcf6f34ffc5bbd))\n - **FEAT**: Adding priority to layout components ([#3639](https://github.com/flame-engine/flame/issues/3639)). ([2eff267d](https://github.com/flame-engine/flame/commit/2eff267d795fbfbf9f5b3215b6dca4a2da9864e1))\n\n#### `flame_lint` - `v1.4.1`\n\n - **FIX**: Remove unnecessary breaks ([#3638](https://github.com/flame-engine/flame/issues/3638)). ([ea29929c](https://github.com/flame-engine/flame/commit/ea29929cd86ed00407f2d2aa69dcf6f34ffc5bbd))\n\n#### `jenny` - `v1.5.0`\n\n - **FEAT**: Expose additional flame_jenny Command classes ([#3641](https://github.com/flame-engine/flame/issues/3641)). ([8ef2587d](https://github.com/flame-engine/flame/commit/8ef2587de4a1bef1d745cc1f5a626a7e84c6230c))\n\n\n## 2025-06-30\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.30.0`](#flame---v1300)\n - [`flame_sprite_fusion` - `v0.2.0`](#flame_sprite_fusion---v020)\n - [`flame_test` - `v2.0.0`](#flame_test---v200)\n\nPackages with other changes:\n\n - [`flame_svg` - `v1.11.13`](#flame_svg---v11113)\n - [`flame_texturepacker` - `v4.4.0`](#flame_texturepacker---v440)\n - [`flame_tiled` - `v3.0.4`](#flame_tiled---v304)\n - [`flame_behavior_tree` - `v0.1.3+13`](#flame_behavior_tree---v01313)\n - [`flame_oxygen` - `v0.2.3+13`](#flame_oxygen---v02313)\n - [`flame_isolate` - `v0.6.2+13`](#flame_isolate---v06213)\n - [`flame_fire_atlas` - `v1.8.8`](#flame_fire_atlas---v188)\n - [`flame_audio` - `v2.11.7`](#flame_audio---v2117)\n - [`flame_spine` - `v0.2.2+13`](#flame_spine---v02213)\n - [`flame_bloc` - `v1.12.14`](#flame_bloc---v11214)\n - [`flame_kenney_xml` - `v0.1.1+13`](#flame_kenney_xml---v01113)\n - [`flame_lottie` - `v0.4.2+13`](#flame_lottie---v04213)\n - [`flame_markdown` - `v0.2.4+6`](#flame_markdown---v0246)\n - [`flame_console` - `v0.1.2+9`](#flame_console---v0129)\n - [`flame_rive` - `v1.10.16`](#flame_rive---v11016)\n - [`flame_forge2d` - `v0.19.0+3`](#flame_forge2d---v01903)\n - [`flame_noise` - `v0.3.2+13`](#flame_noise---v03213)\n - [`flame_riverpod` - `v5.4.16`](#flame_riverpod---v5416)\n - [`flame_network_assets` - `v0.3.3+13`](#flame_network_assets---v03313)\n - [`flame_3d` - `v0.1.0-dev.13`](#flame_3d---v010-dev13)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_behavior_tree` - `v0.1.3+13`\n - `flame_oxygen` - `v0.2.3+13`\n - `flame_isolate` - `v0.6.2+13`\n - `flame_fire_atlas` - `v1.8.8`\n - `flame_audio` - `v2.11.7`\n - `flame_spine` - `v0.2.2+13`\n - `flame_bloc` - `v1.12.14`\n - `flame_kenney_xml` - `v0.1.1+13`\n - `flame_lottie` - `v0.4.2+13`\n - `flame_markdown` - `v0.2.4+6`\n - `flame_console` - `v0.1.2+9`\n - `flame_rive` - `v1.10.16`\n - `flame_forge2d` - `v0.19.0+3`\n - `flame_noise` - `v0.3.2+13`\n - `flame_riverpod` - `v5.4.16`\n - `flame_network_assets` - `v0.3.3+13`\n - `flame_3d` - `v0.1.0-dev.13`\n\n---\n\n#### `flame` - `v1.30.0`\n\n - **FIX**: `angleTo` and `lookAt` should consider parental transformations ([#3629](https://github.com/flame-engine/flame/issues/3629)). ([e6f3d105](https://github.com/flame-engine/flame/commit/e6f3d105577cd346d377aaaed42d4ceb93aec077))\n - **FIX**: `angleTo`, `absoluteAngle` and the `angle` setter now returns normalized angles between `[-pi, pi]` ([#3629](https://github.com/flame-engine/flame/issues/3629)). ([e6f3d105](https://github.com/flame-engine/flame/commit/e6f3d105577cd346d377aaaed42d4ceb93aec077))\n - **FIX**: Delay should work with SpeedEffectControllers ([#3618](https://github.com/flame-engine/flame/issues/3618)). ([bfbb49f5](https://github.com/flame-engine/flame/commit/bfbb49f5b6aac4f69c8602cd20a457e95fe02973))\n - **FIX**: Pass in intended parent to remove ([#3626](https://github.com/flame-engine/flame/issues/3626)). ([7a05f74d](https://github.com/flame-engine/flame/commit/7a05f74dff7c3dbac96d8c8eb52ad7f0625266a1))\n - **FIX**: Call `super.onDispose` last and check `mounted` before `setState` ([#3623](https://github.com/flame-engine/flame/issues/3623)). ([3d2716c1](https://github.com/flame-engine/flame/commit/3d2716c1fb2b64d363dbc8e9aea852723e909710))\n - **FIX**: Angled line intersections should work with 32-bit vectors ([#3617](https://github.com/flame-engine/flame/issues/3617)). ([e32bff45](https://github.com/flame-engine/flame/commit/e32bff455c0f5715c1a7018f865b44b2410ed7db))\n - **FIX**: PostProcessComponent should size dynamically ([#3611](https://github.com/flame-engine/flame/issues/3611)). ([baecb861](https://github.com/flame-engine/flame/commit/baecb86186a1bff7f21d804e7867f894d2f9d23c))\n - **FEAT**: Add `target` argument to `SpawnComponent` ([#3635](https://github.com/flame-engine/flame/issues/3635)). ([3747e1e8](https://github.com/flame-engine/flame/commit/3747e1e8bd1f4bde3c6b64fff0f336690f9da6c8))\n - **FEAT**: Add `spawnCount` to `SpawnComponent` ([#3634](https://github.com/flame-engine/flame/issues/3634)). ([f377d7e7](https://github.com/flame-engine/flame/commit/f377d7e702892836a5fded1c8d4f648682e69e50))\n - **FEAT**: Adding RasterSpriteComponent.fromImage constructor ([#3627](https://github.com/flame-engine/flame/issues/3627)). ([74a84ba7](https://github.com/flame-engine/flame/commit/74a84ba7c159631296961eec994179e227ccd1d3))\n - **FEAT**: Implement measure to fix ghost lines and graphical artifacts in Sprites ([#3590](https://github.com/flame-engine/flame/issues/3590)). ([6fd36bc1](https://github.com/flame-engine/flame/commit/6fd36bc1d883d61621806fba54a792dc6924c4e8))\n - **BREAKING** **FEAT**: Pass `WidgetTester` for `testGolden` prepare function ([#3624](https://github.com/flame-engine/flame/issues/3624)). ([10509326](https://github.com/flame-engine/flame/commit/105093266431408db0f9e74042e03e2234d9b22e))\n\n#### `flame_sprite_fusion` - `v0.2.0`\n\n - **BREAKING** **FEAT**: Pass `WidgetTester` for `testGolden` prepare function ([#3624](https://github.com/flame-engine/flame/issues/3624)). ([10509326](https://github.com/flame-engine/flame/commit/105093266431408db0f9e74042e03e2234d9b22e))\n\n#### `flame_test` - `v2.0.0`\n\n - **BREAKING** **FEAT**: Pass `WidgetTester` for `testGolden` prepare function ([#3624](https://github.com/flame-engine/flame/issues/3624)). ([10509326](https://github.com/flame-engine/flame/commit/105093266431408db0f9e74042e03e2234d9b22e))\n\n#### `flame_svg` - `v1.11.13`\n\n - **FIX**: Calculate zoom ratio before rendering svg ([#3616](https://github.com/flame-engine/flame/issues/3616)). ([f8b7ef82](https://github.com/flame-engine/flame/commit/f8b7ef82b7fc54af7171c94ae2112b18cebb236a))\n\n#### `flame_texturepacker` - `v4.4.0`\n\n - **FEAT**: Implement measure to fix ghost lines and graphical artifacts in Sprites ([#3590](https://github.com/flame-engine/flame/issues/3590)). ([6fd36bc1](https://github.com/flame-engine/flame/commit/6fd36bc1d883d61621806fba54a792dc6924c4e8))\n\n#### `flame_tiled` - `v3.0.4`\n\n - **FIX**: Add optional key parameter to TiledComponent.load method ([#3630](https://github.com/flame-engine/flame/issues/3630)). ([5a27746e](https://github.com/flame-engine/flame/commit/5a27746ee4dbab17565472133274dd1308525978))\n - **FIX**: PostProcessComponent should size dynamically ([#3611](https://github.com/flame-engine/flame/issues/3611)). ([baecb861](https://github.com/flame-engine/flame/commit/baecb86186a1bff7f21d804e7867f894d2f9d23c))\n\n\n## 2025-05-23\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.29.0`](#flame---v1290)\n\nPackages with other changes:\n\n - [`flame_3d` - `v0.1.0-dev.12`](#flame_3d---v010-dev12)\n - [`flame_lint` - `v1.4.0`](#flame_lint---v140)\n - [`jenny` - `v1.4.0`](#jenny---v140)\n - [`flame_behavior_tree` - `v0.1.3+12`](#flame_behavior_tree---v01312)\n - [`flame_test` - `v1.19.2`](#flame_test---v1192)\n - [`flame_tiled` - `v3.0.3`](#flame_tiled---v303)\n - [`flame_oxygen` - `v0.2.3+12`](#flame_oxygen---v02312)\n - [`flame_isolate` - `v0.6.2+12`](#flame_isolate---v06212)\n - [`flame_texturepacker` - `v4.3.1`](#flame_texturepacker---v431)\n - [`flame_sprite_fusion` - `v0.1.3+12`](#flame_sprite_fusion---v01312)\n - [`flame_fire_atlas` - `v1.8.7`](#flame_fire_atlas---v187)\n - [`flame_audio` - `v2.11.6`](#flame_audio---v2116)\n - [`flame_spine` - `v0.2.2+12`](#flame_spine---v02212)\n - [`flame_bloc` - `v1.12.13`](#flame_bloc---v11213)\n - [`flame_kenney_xml` - `v0.1.1+12`](#flame_kenney_xml---v01112)\n - [`flame_lottie` - `v0.4.2+12`](#flame_lottie---v04212)\n - [`flame_markdown` - `v0.2.4+5`](#flame_markdown---v0245)\n - [`flame_console` - `v0.1.2+8`](#flame_console---v0128)\n - [`flame_rive` - `v1.10.15`](#flame_rive---v11015)\n - [`flame_forge2d` - `v0.19.0+2`](#flame_forge2d---v01902)\n - [`flame_noise` - `v0.3.2+12`](#flame_noise---v03212)\n - [`flame_riverpod` - `v5.4.15`](#flame_riverpod---v5415)\n - [`flame_svg` - `v1.11.12`](#flame_svg---v11112)\n - [`flame_network_assets` - `v0.3.3+12`](#flame_network_assets---v03312)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_behavior_tree` - `v0.1.3+12`\n - `flame_test` - `v1.19.2`\n - `flame_tiled` - `v3.0.3`\n - `flame_oxygen` - `v0.2.3+12`\n - `flame_isolate` - `v0.6.2+12`\n - `flame_texturepacker` - `v4.3.1`\n - `flame_sprite_fusion` - `v0.1.3+12`\n - `flame_fire_atlas` - `v1.8.7`\n - `flame_audio` - `v2.11.6`\n - `flame_spine` - `v0.2.2+12`\n - `flame_bloc` - `v1.12.13`\n - `flame_kenney_xml` - `v0.1.1+12`\n - `flame_lottie` - `v0.4.2+12`\n - `flame_markdown` - `v0.2.4+5`\n - `flame_console` - `v0.1.2+8`\n - `flame_rive` - `v1.10.15`\n - `flame_forge2d` - `v0.19.0+2`\n - `flame_noise` - `v0.3.2+12`\n - `flame_riverpod` - `v5.4.15`\n - `flame_svg` - `v1.11.12`\n - `flame_network_assets` - `v0.3.3+12`\n\n---\n\n#### `flame` - `v1.29.0`\n\n - **FIX**: Only expose `ReadOnlyOrderedset` from `component.children` ([#3606](https://github.com/flame-engine/flame/issues/3606)). ([79351d19](https://github.com/flame-engine/flame/commit/79351d195ea968b8016129e79a489ef113a0fdf3))\n - **FIX**: Dispose picture is postprocess  ([#3604](https://github.com/flame-engine/flame/issues/3604)). ([3b24cdac](https://github.com/flame-engine/flame/commit/3b24cdac18ec6d846dbc4d08905fbcb897f90be8))\n - **FIX**: Materialize post process list before removing items ([#3591](https://github.com/flame-engine/flame/issues/3591)). ([e858cc1f](https://github.com/flame-engine/flame/commit/e858cc1fc74814769fc11f49014190d37bda5cbe))\n - **DOCS**: Update structure and add RowComponent + ColumnComponent docs ([#3599](https://github.com/flame-engine/flame/issues/3599)). ([d04843a4](https://github.com/flame-engine/flame/commit/d04843a44c9987825cc927a2ec8952395b423ba4))\n - **BREAKING** **FEAT**: Children should retain `parent` after parent is remove from tree ([#3602](https://github.com/flame-engine/flame/issues/3602)). ([008829af](https://github.com/flame-engine/flame/commit/008829af67e3556a92b926db6b6368acf10e249b))\n\n#### `flame_3d` - `v0.1.0-dev.12`\n\n - **FIX**: Only expose `ReadOnlyOrderedset` from `component.children` ([#3606](https://github.com/flame-engine/flame/issues/3606)). ([79351d19](https://github.com/flame-engine/flame/commit/79351d195ea968b8016129e79a489ef113a0fdf3))\n\n#### `flame_lint` - `v1.4.0`\n\n - **FEAT**: Preserve trailing commas in Dart ^3.8.0 ([#3607](https://github.com/flame-engine/flame/issues/3607)). ([433829cb](https://github.com/flame-engine/flame/commit/433829cbdaafa9b1e9f0250b68f5143ec1a4d562))\n\n#### `jenny` - `v1.4.0`\n\n - **FEAT**(Jenny): Add onCommandFinish lifecycle method to DialogueView ([#3600](https://github.com/flame-engine/flame/issues/3600)). ([bd5a4ca6](https://github.com/flame-engine/flame/commit/bd5a4ca68a46feb6734a70d5320bb7bf23b782d5))\n\n\n## 2025-04-23\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.28.1`](#flame---v1281)\n - [`flame_3d` - `v0.1.0-dev.11`](#flame_3d---v010-dev11)\n - [`flame_console` - `v0.1.2+7`](#flame_console---v0127)\n - [`flame_texturepacker` - `v4.3.0`](#flame_texturepacker---v430)\n - [`flame_behavior_tree` - `v0.1.3+11`](#flame_behavior_tree---v01311)\n - [`flame_test` - `v1.19.1`](#flame_test---v1191)\n - [`flame_tiled` - `v3.0.2`](#flame_tiled---v302)\n - [`flame_oxygen` - `v0.2.3+11`](#flame_oxygen---v02311)\n - [`flame_isolate` - `v0.6.2+11`](#flame_isolate---v06211)\n - [`flame_sprite_fusion` - `v0.1.3+11`](#flame_sprite_fusion---v01311)\n - [`flame_fire_atlas` - `v1.8.6`](#flame_fire_atlas---v186)\n - [`flame_audio` - `v2.11.5`](#flame_audio---v2115)\n - [`flame_spine` - `v0.2.2+11`](#flame_spine---v02211)\n - [`flame_bloc` - `v1.12.12`](#flame_bloc---v11212)\n - [`flame_kenney_xml` - `v0.1.1+11`](#flame_kenney_xml---v01111)\n - [`flame_lottie` - `v0.4.2+11`](#flame_lottie---v04211)\n - [`flame_markdown` - `v0.2.4+4`](#flame_markdown---v0244)\n - [`flame_rive` - `v1.10.14`](#flame_rive---v11014)\n - [`flame_forge2d` - `v0.19.0+1`](#flame_forge2d---v01901)\n - [`flame_noise` - `v0.3.2+11`](#flame_noise---v03211)\n - [`flame_riverpod` - `v5.4.14`](#flame_riverpod---v5414)\n - [`flame_svg` - `v1.11.11`](#flame_svg---v11111)\n - [`flame_network_assets` - `v0.3.3+11`](#flame_network_assets---v03311)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_behavior_tree` - `v0.1.3+11`\n - `flame_test` - `v1.19.1`\n - `flame_tiled` - `v3.0.2`\n - `flame_oxygen` - `v0.2.3+11`\n - `flame_isolate` - `v0.6.2+11`\n - `flame_sprite_fusion` - `v0.1.3+11`\n - `flame_fire_atlas` - `v1.8.6`\n - `flame_audio` - `v2.11.5`\n - `flame_spine` - `v0.2.2+11`\n - `flame_bloc` - `v1.12.12`\n - `flame_kenney_xml` - `v0.1.1+11`\n - `flame_lottie` - `v0.4.2+11`\n - `flame_markdown` - `v0.2.4+4`\n - `flame_rive` - `v1.10.14`\n - `flame_forge2d` - `v0.19.0+1`\n - `flame_noise` - `v0.3.2+11`\n - `flame_riverpod` - `v5.4.14`\n - `flame_svg` - `v1.11.11`\n - `flame_network_assets` - `v0.3.3+11`\n\n---\n\n#### `flame` - `v1.28.1`\n\n - **REFACTOR**: Replace dart:io usage with defaultTargetPlatform ([#3567](https://github.com/flame-engine/flame/issues/3567)). ([77925eb8](https://github.com/flame-engine/flame/commit/77925eb84e3ab23c301d504ccc85cc84a91cb3e4))\n - **FIX**: Add fragment shader extension from flutter_shaders ([#3578](https://github.com/flame-engine/flame/issues/3578)). ([27115729](https://github.com/flame-engine/flame/commit/271157295209cc3f147d38582c7c9447e2e84844))\n - **FIX**: Use `virtualSize` when calling `onParentResize` on children of `Viewport` ([#3577](https://github.com/flame-engine/flame/issues/3577)). ([245fb3f5](https://github.com/flame-engine/flame/commit/245fb3f5cf286b19076e758b8fea75a410680ffe))\n - **FEAT**: Add method to toggle overlays ([#3581](https://github.com/flame-engine/flame/issues/3581)). ([ad7c37e1](https://github.com/flame-engine/flame/commit/ad7c37e16b20b71c8049d68fd57414b174fd9492))\n\n#### `flame_3d` - `v0.1.0-dev.11`\n\n - **FEAT**: Add flame_console to flame_3d example to enable more complex setups ([#3580](https://github.com/flame-engine/flame/issues/3580)). ([15a6f8b0](https://github.com/flame-engine/flame/commit/15a6f8b001deb714134976d7cbb5ef0a6ec31c86))\n - **DOCS**: Update flame_3d example to fix movement and camera, organize files ([#3573](https://github.com/flame-engine/flame/issues/3573)). ([54ca8433](https://github.com/flame-engine/flame/commit/54ca8433f20b451eb1a2c3c5f5a47a3430e71a6e))\n\n#### `flame_console` - `v0.1.2+7`\n\n - **FIX**: Export necessary classes to build custom commands, update docs [flame_console] ([#3579](https://github.com/flame-engine/flame/issues/3579)). ([b05d55bc](https://github.com/flame-engine/flame/commit/b05d55bcc5f331ac8a8d82619b6df3a546848e10))\n\n#### `flame_texturepacker` - `v4.3.0`\n\n - **FEAT**: Use `XFile` to support web platform ([#3569](https://github.com/flame-engine/flame/issues/3569)). ([20731167](https://github.com/flame-engine/flame/commit/20731167e8574e98d784b9734dbcee9ba6e6aa88))\n\n\n## 2025-04-18\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.28.0`](#flame---v1280)\n - [`flame_test` - `v1.19.0`](#flame_test---v1190)\n - [`flame_forge2d` - `v0.19.0`](#flame_forge2d---v0190)\n\nPackages with other changes:\n\n - [`flame_3d` - `v0.1.0-dev.10`](#flame_3d---v010-dev10)\n - [`flame_svg` - `v1.11.10`](#flame_svg---v11110)\n - [`flame_texturepacker` - `v4.2.0`](#flame_texturepacker---v420)\n - [`flame_behavior_tree` - `v0.1.3+10`](#flame_behavior_tree---v01310)\n - [`flame_tiled` - `v3.0.1`](#flame_tiled---v301)\n - [`flame_oxygen` - `v0.2.3+10`](#flame_oxygen---v02310)\n - [`flame_isolate` - `v0.6.2+10`](#flame_isolate---v06210)\n - [`flame_sprite_fusion` - `v0.1.3+10`](#flame_sprite_fusion---v01310)\n - [`flame_fire_atlas` - `v1.8.5`](#flame_fire_atlas---v185)\n - [`flame_audio` - `v2.11.4`](#flame_audio---v2114)\n - [`flame_spine` - `v0.2.2+10`](#flame_spine---v02210)\n - [`flame_bloc` - `v1.12.11`](#flame_bloc---v11211)\n - [`flame_kenney_xml` - `v0.1.1+10`](#flame_kenney_xml---v01110)\n - [`flame_lottie` - `v0.4.2+10`](#flame_lottie---v04210)\n - [`flame_markdown` - `v0.2.4+3`](#flame_markdown---v0243)\n - [`flame_console` - `v0.1.2+6`](#flame_console---v0126)\n - [`flame_rive` - `v1.10.13`](#flame_rive---v11013)\n - [`flame_noise` - `v0.3.2+10`](#flame_noise---v03210)\n - [`flame_riverpod` - `v5.4.13`](#flame_riverpod---v5413)\n - [`flame_network_assets` - `v0.3.3+10`](#flame_network_assets---v03310)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_behavior_tree` - `v0.1.3+10`\n - `flame_tiled` - `v3.0.1`\n - `flame_oxygen` - `v0.2.3+10`\n - `flame_isolate` - `v0.6.2+10`\n - `flame_sprite_fusion` - `v0.1.3+10`\n - `flame_fire_atlas` - `v1.8.5`\n - `flame_audio` - `v2.11.4`\n - `flame_spine` - `v0.2.2+10`\n - `flame_bloc` - `v1.12.11`\n - `flame_kenney_xml` - `v0.1.1+10`\n - `flame_lottie` - `v0.4.2+10`\n - `flame_markdown` - `v0.2.4+3`\n - `flame_console` - `v0.1.2+6`\n - `flame_rive` - `v1.10.13`\n - `flame_noise` - `v0.3.2+10`\n - `flame_riverpod` - `v5.4.13`\n - `flame_network_assets` - `v0.3.3+10`\n\n---\n\n#### `flame` - `v1.28.0`\n\n - **FIX**: Priority change should be reflected directly ([#3564](https://github.com/flame-engine/flame/issues/3564)). ([ab2fd639](https://github.com/flame-engine/flame/commit/ab2fd639f73896c0859dd133337ec2adc7adf832))\n - **FIX**: Deprecate `HasGameRef` in favor of `HasGameReference` ([#3559](https://github.com/flame-engine/flame/issues/3559)). ([a882261b](https://github.com/flame-engine/flame/commit/a882261b63ef21e29dde041d99b2eaf94264d7ad))\n - **FIX**: The SpriteButton label should be nullable ([#3557](https://github.com/flame-engine/flame/issues/3557)). ([80a598cd](https://github.com/flame-engine/flame/commit/80a598cd006f2cf90b1b799bbb51c0c073a94743))\n - **FIX**: Update ordered_set, remove ComponentSet ([#3554](https://github.com/flame-engine/flame/issues/3554)). ([728e13f8](https://github.com/flame-engine/flame/commit/728e13f855d988e8f8e24976557b213b98221603))\n - **FEAT**: Post Process API ([#3404](https://github.com/flame-engine/flame/issues/3404)). ([c3316ae4](https://github.com/flame-engine/flame/commit/c3316ae4a50230e6d9720cb4653a8e3e309f3234))\n - **FEAT**: Add helper methods on LineSegment (translate, inflate, deflate, spread) ([#3561](https://github.com/flame-engine/flame/issues/3561)). ([6d388870](https://github.com/flame-engine/flame/commit/6d388870138b9e02e120647d241d7cf9093795f6))\n - **FEAT**: Support for disabled state for `SpriteButton` ([#3560](https://github.com/flame-engine/flame/issues/3560)). ([eaa2b442](https://github.com/flame-engine/flame/commit/eaa2b442b717ae086cac2d715a322ffa7c7a1116))\n - **FEAT**: Add a Render Context API ([#3409](https://github.com/flame-engine/flame/issues/3409)). ([532f0191](https://github.com/flame-engine/flame/commit/532f0191f658e767fde4c200cf1902cbe36d6e7d))\n - **FEAT**: Adding tickCount to TimerComponent ([#3552](https://github.com/flame-engine/flame/issues/3552)). ([dcd694e8](https://github.com/flame-engine/flame/commit/dcd694e8554c59b4b92f6d05928320c175d433f0))\n - **FEAT**: Ability to reset SpriteAnimation on removal ([#3553](https://github.com/flame-engine/flame/issues/3553)). ([59ae5831](https://github.com/flame-engine/flame/commit/59ae58318eba93e3909bdb2deaa13f6aa7b7d35e))\n - **BREAKING** **FIX**: Use 32bit Vector2 in Flame to be compatible with shaders and forge2d ([#3515](https://github.com/flame-engine/flame/issues/3515)). ([19dcecf5](https://github.com/flame-engine/flame/commit/19dcecf5df85345fd4fac168e1f360cee4665658))\n\n#### `flame_test` - `v1.19.0`\n\n - **BREAKING** **FIX**: Use 32bit Vector2 in Flame to be compatible with shaders and forge2d ([#3515](https://github.com/flame-engine/flame/issues/3515)). ([19dcecf5](https://github.com/flame-engine/flame/commit/19dcecf5df85345fd4fac168e1f360cee4665658))\n\n#### `flame_forge2d` - `v0.19.0`\n\n - **BREAKING** **FIX**: Use 32bit Vector2 in Flame to be compatible with shaders and forge2d ([#3515](https://github.com/flame-engine/flame/issues/3515)). ([19dcecf5](https://github.com/flame-engine/flame/commit/19dcecf5df85345fd4fac168e1f360cee4665658))\n\n#### `flame_3d` - `v0.1.0-dev.10`\n\n - **FIX**: Update ordered_set, remove ComponentSet ([#3554](https://github.com/flame-engine/flame/issues/3554)). ([728e13f8](https://github.com/flame-engine/flame/commit/728e13f855d988e8f8e24976557b213b98221603))\n\n#### `flame_svg` - `v1.11.10`\n\n - **FIX**: SvgComponent should use `drawImage` on first render too ([#3549](https://github.com/flame-engine/flame/issues/3549)). ([91721a6b](https://github.com/flame-engine/flame/commit/91721a6b7b2ecda338d64d3c982e448e9cd71122))\n\n#### `flame_texturepacker` - `v4.2.0`\n\n - **FEAT**: Whitelist textures ([#3505](https://github.com/flame-engine/flame/issues/3505)). ([9ca9e858](https://github.com/flame-engine/flame/commit/9ca9e858442031cf91798e0fe09cbadc232b3900))\n\n\n## 2025-04-02\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame_tiled` - `v3.0.0`](#flame_tiled---v300)\n\nPackages with other changes:\n\n - [`jenny` - `v1.3.3`](#jenny---v133)\n - [`flame` - `v1.27.0`](#flame---v1270)\n - [`flame_3d` - `v0.1.0-dev.9`](#flame_3d---v010-dev9)\n - [`flame_forge2d` - `v0.18.3+1`](#flame_forge2d---v01831)\n - [`flame_isolate` - `v0.6.2+9`](#flame_isolate---v0629)\n - [`flame_lint` - `v1.3.0`](#flame_lint---v130)\n - [`flame_rive` - `v1.10.12`](#flame_rive---v11012)\n - [`flame_texturepacker` - `v4.1.9`](#flame_texturepacker---v419)\n - [`flame_behavior_tree` - `v0.1.3+9`](#flame_behavior_tree---v0139)\n - [`flame_test` - `v1.18.3`](#flame_test---v1183)\n - [`flame_oxygen` - `v0.2.3+9`](#flame_oxygen---v0239)\n - [`flame_sprite_fusion` - `v0.1.3+9`](#flame_sprite_fusion---v0139)\n - [`flame_fire_atlas` - `v1.8.4`](#flame_fire_atlas---v184)\n - [`flame_audio` - `v2.11.3`](#flame_audio---v2113)\n - [`flame_spine` - `v0.2.2+9`](#flame_spine---v0229)\n - [`flame_bloc` - `v1.12.10`](#flame_bloc---v11210)\n - [`flame_kenney_xml` - `v0.1.1+9`](#flame_kenney_xml---v0119)\n - [`flame_lottie` - `v0.4.2+9`](#flame_lottie---v0429)\n - [`flame_markdown` - `v0.2.4+2`](#flame_markdown---v0242)\n - [`flame_console` - `v0.1.2+5`](#flame_console---v0125)\n - [`flame_noise` - `v0.3.2+9`](#flame_noise---v0329)\n - [`flame_riverpod` - `v5.4.12`](#flame_riverpod---v5412)\n - [`flame_svg` - `v1.11.9`](#flame_svg---v1119)\n - [`flame_network_assets` - `v0.3.3+9`](#flame_network_assets---v0339)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_behavior_tree` - `v0.1.3+9`\n - `flame_test` - `v1.18.3`\n - `flame_oxygen` - `v0.2.3+9`\n - `flame_sprite_fusion` - `v0.1.3+9`\n - `flame_fire_atlas` - `v1.8.4`\n - `flame_audio` - `v2.11.3`\n - `flame_spine` - `v0.2.2+9`\n - `flame_bloc` - `v1.12.10`\n - `flame_kenney_xml` - `v0.1.1+9`\n - `flame_lottie` - `v0.4.2+9`\n - `flame_markdown` - `v0.2.4+2`\n - `flame_console` - `v0.1.2+5`\n - `flame_noise` - `v0.3.2+9`\n - `flame_riverpod` - `v5.4.12`\n - `flame_svg` - `v1.11.9`\n - `flame_network_assets` - `v0.3.3+9`\n\n---\n\n#### `flame_tiled` - `v3.0.0`\n\n - **BREAKING** **FIX**: Add APIs to get TileData by layer index ([#3539](https://github.com/flame-engine/flame/issues/3539)). ([4676b1b7](https://github.com/flame-engine/flame/commit/4676b1b7a5aefe7a958de55b1d209e554c9b02a6))\n\n#### `jenny` - `v1.3.3`\n\n - **FEAT**: Bump to new lint package ([#3545](https://github.com/flame-engine/flame/issues/3545)). ([bf6ee518](https://github.com/flame-engine/flame/commit/bf6ee51897591b7ad6e5f9da2193b1eeeaf026f4))\n\n#### `flame` - `v1.27.0`\n\n - **FIX**: Remove outdated deprecations ([#3541](https://github.com/flame-engine/flame/issues/3541)). ([b918e40d](https://github.com/flame-engine/flame/commit/b918e40d0ce14b89ba9b5c82aed8ff51d6f549c3))\n - **FEAT**: Bump to new lint package ([#3545](https://github.com/flame-engine/flame/issues/3545)). ([bf6ee518](https://github.com/flame-engine/flame/commit/bf6ee51897591b7ad6e5f9da2193b1eeeaf026f4))\n - **FEAT**: The `FunctionEffect`, run any function as an `Effect` ([#3537](https://github.com/flame-engine/flame/issues/3537)). ([f4ac1ec6](https://github.com/flame-engine/flame/commit/f4ac1ec63a22b7a7d0c17d7119787f3ce2acadc1))\n\n#### `flame_3d` - `v0.1.0-dev.9`\n\n - **FEAT**: Bump to new lint package ([#3545](https://github.com/flame-engine/flame/issues/3545)). ([bf6ee518](https://github.com/flame-engine/flame/commit/bf6ee51897591b7ad6e5f9da2193b1eeeaf026f4))\n\n#### `flame_forge2d` - `v0.18.3+1`\n\n - **FIX**: Remove outdated deprecations ([#3541](https://github.com/flame-engine/flame/issues/3541)). ([b918e40d](https://github.com/flame-engine/flame/commit/b918e40d0ce14b89ba9b5c82aed8ff51d6f549c3))\n\n#### `flame_isolate` - `v0.6.2+9`\n\n - **FIX**: Remove outdated deprecations ([#3541](https://github.com/flame-engine/flame/issues/3541)). ([b918e40d](https://github.com/flame-engine/flame/commit/b918e40d0ce14b89ba9b5c82aed8ff51d6f549c3))\n\n#### `flame_lint` - `v1.3.0`\n\n - **FEAT**: Bump to new lint package ([#3545](https://github.com/flame-engine/flame/issues/3545)). ([bf6ee518](https://github.com/flame-engine/flame/commit/bf6ee51897591b7ad6e5f9da2193b1eeeaf026f4))\n\n#### `flame_rive` - `v1.10.12`\n\n - **FIX**: Bump Rive version and skip tests ([#3544](https://github.com/flame-engine/flame/issues/3544)). ([a3a7dd51](https://github.com/flame-engine/flame/commit/a3a7dd51faee57d74e89fbc29e7581ed44459832))\n\n#### `flame_texturepacker` - `v4.1.9`\n\n - **FIX**: Remove outdated deprecations ([#3541](https://github.com/flame-engine/flame/issues/3541)). ([b918e40d](https://github.com/flame-engine/flame/commit/b918e40d0ce14b89ba9b5c82aed8ff51d6f549c3))\n\n\n## 2025-03-21\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_forge2d` - `v0.18.3`](#flame_forge2d---v0183)\n - [`flame` - `v1.26.1`](#flame---v1261)\n - [`flame_spine` - `v0.2.2+8`](#flame_spine---v0228)\n - [`flame_behavior_tree` - `v0.1.3+8`](#flame_behavior_tree---v0138)\n - [`flame_test` - `v1.18.2`](#flame_test---v1182)\n - [`flame_tiled` - `v2.0.3`](#flame_tiled---v203)\n - [`flame_oxygen` - `v0.2.3+8`](#flame_oxygen---v0238)\n - [`flame_isolate` - `v0.6.2+8`](#flame_isolate---v0628)\n - [`flame_texturepacker` - `v4.1.8`](#flame_texturepacker---v418)\n - [`flame_sprite_fusion` - `v0.1.3+8`](#flame_sprite_fusion---v0138)\n - [`flame_fire_atlas` - `v1.8.3`](#flame_fire_atlas---v183)\n - [`flame_audio` - `v2.11.2`](#flame_audio---v2112)\n - [`flame_bloc` - `v1.12.9`](#flame_bloc---v1129)\n - [`flame_kenney_xml` - `v0.1.1+8`](#flame_kenney_xml---v0118)\n - [`flame_lottie` - `v0.4.2+8`](#flame_lottie---v0428)\n - [`flame_markdown` - `v0.2.4+1`](#flame_markdown---v0241)\n - [`flame_console` - `v0.1.2+4`](#flame_console---v0124)\n - [`flame_rive` - `v1.10.11`](#flame_rive---v11011)\n - [`flame_noise` - `v0.3.2+8`](#flame_noise---v0328)\n - [`flame_riverpod` - `v5.4.11`](#flame_riverpod---v5411)\n - [`flame_svg` - `v1.11.8`](#flame_svg---v1118)\n - [`flame_network_assets` - `v0.3.3+8`](#flame_network_assets---v0338)\n - [`flame_3d` - `v0.1.0-dev.8`](#flame_3d---v010-dev8)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_behavior_tree` - `v0.1.3+8`\n - `flame_test` - `v1.18.2`\n - `flame_tiled` - `v2.0.3`\n - `flame_oxygen` - `v0.2.3+8`\n - `flame_isolate` - `v0.6.2+8`\n - `flame_texturepacker` - `v4.1.8`\n - `flame_sprite_fusion` - `v0.1.3+8`\n - `flame_fire_atlas` - `v1.8.3`\n - `flame_audio` - `v2.11.2`\n - `flame_bloc` - `v1.12.9`\n - `flame_kenney_xml` - `v0.1.1+8`\n - `flame_lottie` - `v0.4.2+8`\n - `flame_markdown` - `v0.2.4+1`\n - `flame_console` - `v0.1.2+4`\n - `flame_rive` - `v1.10.11`\n - `flame_noise` - `v0.3.2+8`\n - `flame_riverpod` - `v5.4.11`\n - `flame_svg` - `v1.11.8`\n - `flame_network_assets` - `v0.3.3+8`\n - `flame_3d` - `v0.1.0-dev.8`\n\n---\n\n#### `flame_forge2d` - `v0.18.3`\n\n - **FIX**: Expose event dispatcher classes ([#3532](https://github.com/flame-engine/flame/issues/3532)). ([db8e0b20](https://github.com/flame-engine/flame/commit/db8e0b20746dc96a221dc4e85b09f5a35ecc7339))\n\n#### `flame` - `v1.26.1`\n\n - **FIX**: Fix priority rebalancing causing concurrent mutation of component ordered_set ([#3530](https://github.com/flame-engine/flame/issues/3530)). ([c2afe11f](https://github.com/flame-engine/flame/commit/c2afe11f2ce736791a35e77afa5e1ddef0ae7cbb))\n - **FIX**: Expose event dispatcher classes ([#3532](https://github.com/flame-engine/flame/issues/3532)). ([db8e0b20](https://github.com/flame-engine/flame/commit/db8e0b20746dc96a221dc4e85b09f5a35ecc7339))\n\n#### `flame_spine` - `v0.2.2+8`\n\n - **FIX**: Bump spine version and update example files ([#3534](https://github.com/flame-engine/flame/issues/3534)). ([f346e3f6](https://github.com/flame-engine/flame/commit/f346e3f67793f2ebece3f11c1f440c5d485bf959))\n\n\n## 2025-03-13\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`behavior_tree` - `v0.1.3+2`](#behavior_tree---v0132)\n - [`flame` - `v1.26.0`](#flame---v1260)\n - [`flame_3d` - `v0.1.0-dev.7`](#flame_3d---v010-dev7)\n - [`flame_audio` - `v2.11.1`](#flame_audio---v2111)\n - [`flame_behavior_tree` - `v0.1.3+7`](#flame_behavior_tree---v0137)\n - [`flame_bloc` - `v1.12.8`](#flame_bloc---v1128)\n - [`flame_fire_atlas` - `v1.8.2`](#flame_fire_atlas---v182)\n - [`flame_forge2d` - `v0.18.2+7`](#flame_forge2d---v01827)\n - [`flame_isolate` - `v0.6.2+7`](#flame_isolate---v0627)\n - [`flame_kenney_xml` - `v0.1.1+7`](#flame_kenney_xml---v0117)\n - [`flame_lint` - `v1.2.3`](#flame_lint---v123)\n - [`flame_lottie` - `v0.4.2+7`](#flame_lottie---v0427)\n - [`flame_markdown` - `v0.2.4`](#flame_markdown---v024)\n - [`flame_noise` - `v0.3.2+7`](#flame_noise---v0327)\n - [`flame_oxygen` - `v0.2.3+7`](#flame_oxygen---v0237)\n - [`flame_rive` - `v1.10.10`](#flame_rive---v11010)\n - [`flame_splash_screen` - `v0.3.1+2`](#flame_splash_screen---v0312)\n - [`flame_sprite_fusion` - `v0.1.3+7`](#flame_sprite_fusion---v0137)\n - [`flame_svg` - `v1.11.7`](#flame_svg---v1117)\n - [`flame_test` - `v1.18.1`](#flame_test---v1181)\n - [`flame_texturepacker` - `v4.1.7`](#flame_texturepacker---v417)\n - [`flame_tiled` - `v2.0.2`](#flame_tiled---v202)\n - [`flame_spine` - `v0.2.2+7`](#flame_spine---v0227)\n - [`flame_console` - `v0.1.2+3`](#flame_console---v0123)\n - [`flame_riverpod` - `v5.4.10`](#flame_riverpod---v5410)\n - [`flame_network_assets` - `v0.3.3+7`](#flame_network_assets---v0337)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_spine` - `v0.2.2+7`\n - `flame_console` - `v0.1.2+3`\n - `flame_riverpod` - `v5.4.10`\n - `flame_network_assets` - `v0.3.3+7`\n\n---\n\n#### `behavior_tree` - `v0.1.3+2`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame` - `v1.26.0`\n\n - **FIX**: RouterComponent should be on top ([#3524](https://github.com/flame-engine/flame/issues/3524)). ([aa52a2a5](https://github.com/flame-engine/flame/commit/aa52a2a58de9661557113c3d6ae5cc760842b1e7))\n - **FEAT**: Support custom attributes syntax to allow for multiple styles in the text rendering pipeline ([#3519](https://github.com/flame-engine/flame/issues/3519)). ([fbc58053](https://github.com/flame-engine/flame/commit/fbc58053dd12e6dc62b09cb14e4b438ef7b7f1b2))\n - **FEAT**: Layout shrinkwrap ([#3513](https://github.com/flame-engine/flame/issues/3513)). ([b3fbdd9d](https://github.com/flame-engine/flame/commit/b3fbdd9d3fd031083ecf7c53a28e2381579e846c))\n - **FEAT**: Layout Components ([#3507](https://github.com/flame-engine/flame/issues/3507)). ([678cf057](https://github.com/flame-engine/flame/commit/678cf05780580dd2cb61dde5e40c0efb1f3bc928))\n - **FEAT**: Add `RotateAroundEffect` ([#3499](https://github.com/flame-engine/flame/issues/3499)). ([0688f410](https://github.com/flame-engine/flame/commit/0688f41093cd451269366a2c2001a0d88bc6e532))\n - **DOCS**: Fix missing reference on documentation for InlineTextNode ([#3520](https://github.com/flame-engine/flame/issues/3520)). ([e3aa78b2](https://github.com/flame-engine/flame/commit/e3aa78b28206150eb85621e2a788fc31f218ff1d))\n - **DOCS**: Make onRemove() behavior more clear in API doc ([#3502](https://github.com/flame-engine/flame/issues/3502)). ([f387ad76](https://github.com/flame-engine/flame/commit/f387ad7604fca4b652d3c7521004a5d420137634))\n\n#### `flame_3d` - `v0.1.0-dev.7`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_audio` - `v2.11.1`\n\n - **REFACTOR**(flame_audio): Set AudioContext for AudioPool only ([#3511](https://github.com/flame-engine/flame/issues/3511)). ([d5ae35f2](https://github.com/flame-engine/flame/commit/d5ae35f2bbd214159fcb81e2e94e45085bdc2e66))\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_behavior_tree` - `v0.1.3+7`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_bloc` - `v1.12.8`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_fire_atlas` - `v1.8.2`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_forge2d` - `v0.18.2+7`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_isolate` - `v0.6.2+7`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_kenney_xml` - `v0.1.1+7`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_lint` - `v1.2.3`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_lottie` - `v0.4.2+7`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_markdown` - `v0.2.4`\n\n - **FEAT**: Support custom attributes syntax to allow for multiple styles in the text rendering pipeline ([#3519](https://github.com/flame-engine/flame/issues/3519)). ([fbc58053](https://github.com/flame-engine/flame/commit/fbc58053dd12e6dc62b09cb14e4b438ef7b7f1b2))\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_noise` - `v0.3.2+7`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_oxygen` - `v0.2.3+7`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_rive` - `v1.10.10`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_splash_screen` - `v0.3.1+2`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_sprite_fusion` - `v0.1.3+7`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_svg` - `v1.11.7`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_test` - `v1.18.1`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_texturepacker` - `v4.1.7`\n\n - **FIX**: Use game's asset cache for texture packer ([#3523](https://github.com/flame-engine/flame/issues/3523)). ([835c40fc](https://github.com/flame-engine/flame/commit/835c40fc6bbc81218fe5c7d321a4a81e1853cf85))\n - **FIX**: Unhandled Exception: Unable to load asset. Introduced on Texturepacker 4.16 ([#3506](https://github.com/flame-engine/flame/issues/3506)). ([3af91b0e](https://github.com/flame-engine/flame/commit/3af91b0e6cbb28c0862317d572dde5f659592c2b))\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n#### `flame_tiled` - `v2.0.2`\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n\n## 2025-02-13\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`behavior_tree` - `v0.1.3+1`](#behavior_tree---v0131)\n - [`flame` - `v1.25.0`](#flame---v1250)\n - [`flame_3d` - `v0.1.0-dev.6`](#flame_3d---v010-dev6)\n - [`flame_audio` - `v2.11.0`](#flame_audio---v2110)\n - [`flame_behavior_tree` - `v0.1.3+6`](#flame_behavior_tree---v0136)\n - [`flame_bloc` - `v1.12.7`](#flame_bloc---v1127)\n - [`flame_fire_atlas` - `v1.8.1`](#flame_fire_atlas---v181)\n - [`flame_forge2d` - `v0.18.2+6`](#flame_forge2d---v01826)\n - [`flame_isolate` - `v0.6.2+6`](#flame_isolate---v0626)\n - [`flame_kenney_xml` - `v0.1.1+6`](#flame_kenney_xml---v0116)\n - [`flame_lint` - `v1.2.2`](#flame_lint---v122)\n - [`flame_lottie` - `v0.4.2+6`](#flame_lottie---v0426)\n - [`flame_markdown` - `v0.2.3+2`](#flame_markdown---v0232)\n - [`flame_noise` - `v0.3.2+6`](#flame_noise---v0326)\n - [`flame_oxygen` - `v0.2.3+6`](#flame_oxygen---v0236)\n - [`flame_rive` - `v1.10.9`](#flame_rive---v1109)\n - [`flame_splash_screen` - `v0.3.1+1`](#flame_splash_screen---v0311)\n - [`flame_sprite_fusion` - `v0.1.3+6`](#flame_sprite_fusion---v0136)\n - [`flame_svg` - `v1.11.6`](#flame_svg---v1116)\n - [`flame_test` - `v1.18.0`](#flame_test---v1180)\n - [`flame_texturepacker` - `v4.1.6`](#flame_texturepacker---v416)\n - [`flame_tiled` - `v2.0.1`](#flame_tiled---v201)\n - [`flame_spine` - `v0.2.2+6`](#flame_spine---v0226)\n - [`flame_console` - `v0.1.2+2`](#flame_console---v0122)\n - [`flame_riverpod` - `v5.4.9`](#flame_riverpod---v549)\n - [`flame_network_assets` - `v0.3.3+6`](#flame_network_assets---v0336)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_spine` - `v0.2.2+6`\n - `flame_console` - `v0.1.2+2`\n - `flame_riverpod` - `v5.4.9`\n - `flame_network_assets` - `v0.3.3+6`\n\n---\n\n#### `behavior_tree` - `v0.1.3+1`\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame` - `v1.25.0`\n\n - **FEAT**: Add a setter for TextBoxComponent.boxConfig and add a convenience method to skip per-char buildup ([#3490](https://github.com/flame-engine/flame/issues/3490)). ([d1777b7a](https://github.com/flame-engine/flame/commit/d1777b7a9efcf065c4474b7c6702c45d37bf710c))\n\n#### `flame_3d` - `v0.1.0-dev.6`\n\n - **FEAT**: Add helper methods to create matrix4 with sensible defaults ([#3486](https://github.com/flame-engine/flame/issues/3486)). ([9345c870](https://github.com/flame-engine/flame/commit/9345c870d4de57d8d3a4d07a014d18cb71c01618))\n - **FEAT**: Add setFromMatrix4 to Transform3D ([#3484](https://github.com/flame-engine/flame/issues/3484)). ([1dbb4433](https://github.com/flame-engine/flame/commit/1dbb4433ca9e06b93a49b430fe9c084885551ff2))\n - **FEAT**: Add Line3D mesh component [flame_3d] ([#3412](https://github.com/flame-engine/flame/issues/3412)). ([c332c965](https://github.com/flame-engine/flame/commit/c332c9651ad8b930281c1d0bc13b89754fd0b2c1))\n\n#### `flame_audio` - `v2.11.0`\n\n - **FEAT**(audio): Set audio context AudioContextConfigFocus.mixWithOthers by default ([#3483](https://github.com/flame-engine/flame/issues/3483)). ([762e0ad8](https://github.com/flame-engine/flame/commit/762e0ad8423e7bf3920139ca71a03b186d09c063))\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_behavior_tree` - `v0.1.3+6`\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_bloc` - `v1.12.7`\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_fire_atlas` - `v1.8.1`\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_forge2d` - `v0.18.2+6`\n\n - **FIX**: Use correct matrix indices in BodyComponent ([#3491](https://github.com/flame-engine/flame/issues/3491)). ([d6c83fab](https://github.com/flame-engine/flame/commit/d6c83fab6c5cf56b047dcd22b9f1f0a075c26201))\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_isolate` - `v0.6.2+6`\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_kenney_xml` - `v0.1.1+6`\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_lint` - `v1.2.2`\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_lottie` - `v0.4.2+6`\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_markdown` - `v0.2.3+2`\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_noise` - `v0.3.2+6`\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_oxygen` - `v0.2.3+6`\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_rive` - `v1.10.9`\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_splash_screen` - `v0.3.1+1`\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_sprite_fusion` - `v0.1.3+6`\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_svg` - `v1.11.6`\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_test` - `v1.18.0`\n\n - **FEAT**: Implement closeToVector4 and closeToQuaternion by extracing a generic CloseToVector base ([#3480](https://github.com/flame-engine/flame/issues/3480)). ([57e58514](https://github.com/flame-engine/flame/commit/57e58514091248884505d3936e3e0aa076efb30a))\n - **FEAT**: Add closeToMatrix4 test matcher ([#3478](https://github.com/flame-engine/flame/issues/3478)). ([8db2476e](https://github.com/flame-engine/flame/commit/8db2476e8c39723670641f1c2646ecf0d7bb09fb))\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_texturepacker` - `v4.1.6`\n\n - **FIX**: Remove forced location of the atlas file. ([#3481](https://github.com/flame-engine/flame/issues/3481)). ([bac68dcb](https://github.com/flame-engine/flame/commit/bac68dcbb95ec420c1401e32e60adf42dc338695))\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n#### `flame_tiled` - `v2.0.1`\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n\n## 2025-02-05\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame_fire_atlas` - `v1.8.0`](#flame_fire_atlas---v180)\n - [`flame_3d` - `v0.1.0-dev.5`](#flame_3d---v010-dev5)\n - [`flame_tiled` - `v2.0.0`](#flame_tiled---v200)\n\nPackages with other changes:\n\n - [`flame` - `v1.24.0`](#flame---v1240)\n - [`flame_test` - `v1.17.5`](#flame_test---v1175)\n - [`flame_behavior_tree` - `v0.1.3+5`](#flame_behavior_tree---v0135)\n - [`flame_oxygen` - `v0.2.3+5`](#flame_oxygen---v0235)\n - [`flame_isolate` - `v0.6.2+5`](#flame_isolate---v0625)\n - [`flame_texturepacker` - `v4.1.5`](#flame_texturepacker---v415)\n - [`flame_sprite_fusion` - `v0.1.3+5`](#flame_sprite_fusion---v0135)\n - [`flame_audio` - `v2.10.8`](#flame_audio---v2108)\n - [`flame_spine` - `v0.2.2+5`](#flame_spine---v0225)\n - [`flame_bloc` - `v1.12.6`](#flame_bloc---v1126)\n - [`flame_kenney_xml` - `v0.1.1+5`](#flame_kenney_xml---v0115)\n - [`flame_lottie` - `v0.4.2+5`](#flame_lottie---v0425)\n - [`flame_markdown` - `v0.2.3+1`](#flame_markdown---v0231)\n - [`flame_console` - `v0.1.2+1`](#flame_console---v0121)\n - [`flame_rive` - `v1.10.8`](#flame_rive---v1108)\n - [`flame_forge2d` - `v0.18.2+5`](#flame_forge2d---v01825)\n - [`flame_noise` - `v0.3.2+5`](#flame_noise---v0325)\n - [`flame_riverpod` - `v5.4.8`](#flame_riverpod---v548)\n - [`flame_svg` - `v1.11.5`](#flame_svg---v1115)\n - [`flame_network_assets` - `v0.3.3+5`](#flame_network_assets---v0335)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_behavior_tree` - `v0.1.3+5`\n - `flame_oxygen` - `v0.2.3+5`\n - `flame_isolate` - `v0.6.2+5`\n - `flame_texturepacker` - `v4.1.5`\n - `flame_sprite_fusion` - `v0.1.3+5`\n - `flame_audio` - `v2.10.8`\n - `flame_spine` - `v0.2.2+5`\n - `flame_bloc` - `v1.12.6`\n - `flame_kenney_xml` - `v0.1.1+5`\n - `flame_lottie` - `v0.4.2+5`\n - `flame_markdown` - `v0.2.3+1`\n - `flame_console` - `v0.1.2+1`\n - `flame_rive` - `v1.10.8`\n - `flame_forge2d` - `v0.18.2+5`\n - `flame_noise` - `v0.3.2+5`\n - `flame_riverpod` - `v5.4.8`\n - `flame_svg` - `v1.11.5`\n - `flame_network_assets` - `v0.3.3+5`\n\n---\n\n#### `flame_fire_atlas` - `v1.8.0`\n\n - **BREAKING** **FIX**: Bump tiled to 0.11.0 and add ColorData extension (#3473).\n\n#### `flame_3d` - `v0.1.0-dev.5`\n\n - **BREAKING** **FEAT**: Make resource creation be on demand to enable testing (#3411).\n\n#### `flame_tiled` - `v2.0.0`\n\n - **BREAKING** **FIX**: Bump tiled to 0.11.0 and add ColorData extension (#3473).\n\n#### `flame` - `v1.24.0`\n\n - **PERF**: Switch from forEach to regular for-loops for about 30% improvement in raw update performance (#3472).\n - **FIX**: SpawnComponent.periodRange should change range each iteration (#3464).\n - **FIX**: Don't use a future when assets for SpriteButton is already loaded (#3456).\n - **FIX**: Darkness increases with higher values (#3448).\n - **FEAT**: NineTileBoxComponent with HasPaint to enable effects (#3459).\n - **FEAT**: Devtools overlay navigation (#3449).\n - **FEAT**: Add direction and length getters and constructor to LineSegment (#3446).\n - **FEAT**: Add multiFactory to SpawnComponent (#3440).\n\n#### `flame_test` - `v1.17.5`\n\n - **FIX**: Don't use a future when assets for SpriteButton is already loaded (#3456).\n\n\n## 2025-01-02\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame_3d` - `v0.1.0-dev.4`](#flame_3d---v010-dev4)\n\nPackages with other changes:\n\n - [`flame` - `v1.23.0`](#flame---v1230)\n - [`flame_console` - `v0.1.2`](#flame_console---v012)\n - [`flame_forge2d` - `v0.18.2+4`](#flame_forge2d---v01824)\n - [`flame_isolate` - `v0.6.2+4`](#flame_isolate---v0624)\n - [`flame_markdown` - `v0.2.3`](#flame_markdown---v023)\n - [`flame_oxygen` - `v0.2.3+4`](#flame_oxygen---v0234)\n - [`flame_sprite_fusion` - `v0.1.3+4`](#flame_sprite_fusion---v0134)\n - [`flame_svg` - `v1.11.4`](#flame_svg---v1114)\n - [`flame_test` - `v1.17.4`](#flame_test---v1174)\n - [`flame_tiled` - `v1.21.2`](#flame_tiled---v1212)\n - [`flame_behavior_tree` - `v0.1.3+4`](#flame_behavior_tree---v0134)\n - [`flame_texturepacker` - `v4.1.4`](#flame_texturepacker---v414)\n - [`flame_fire_atlas` - `v1.7.1`](#flame_fire_atlas---v171)\n - [`flame_audio` - `v2.10.7`](#flame_audio---v2107)\n - [`flame_spine` - `v0.2.2+4`](#flame_spine---v0224)\n - [`flame_bloc` - `v1.12.5`](#flame_bloc---v1125)\n - [`flame_kenney_xml` - `v0.1.1+4`](#flame_kenney_xml---v0114)\n - [`flame_lottie` - `v0.4.2+4`](#flame_lottie---v0424)\n - [`flame_rive` - `v1.10.7`](#flame_rive---v1107)\n - [`flame_noise` - `v0.3.2+4`](#flame_noise---v0324)\n - [`flame_riverpod` - `v5.4.7`](#flame_riverpod---v547)\n - [`flame_network_assets` - `v0.3.3+4`](#flame_network_assets---v0334)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_behavior_tree` - `v0.1.3+4`\n - `flame_texturepacker` - `v4.1.4`\n - `flame_fire_atlas` - `v1.7.1`\n - `flame_audio` - `v2.10.7`\n - `flame_spine` - `v0.2.2+4`\n - `flame_bloc` - `v1.12.5`\n - `flame_kenney_xml` - `v0.1.1+4`\n - `flame_lottie` - `v0.4.2+4`\n - `flame_rive` - `v1.10.7`\n - `flame_noise` - `v0.3.2+4`\n - `flame_riverpod` - `v5.4.7`\n - `flame_network_assets` - `v0.3.3+4`\n\n---\n\n#### `flame_3d` - `v0.1.0-dev.4`\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n - **FIX**: Use saner default value for camera's target ([#3238](https://github.com/flame-engine/flame/issues/3238)). ([78522c62](https://github.com/flame-engine/flame/commit/78522c624d846c827a1c0d7377837e04a30ba4e7))\n - **FIX**: Use saner default value for camera's target ([#3238](https://github.com/flame-engine/flame/issues/3238)). ([99ca355b](https://github.com/flame-engine/flame/commit/99ca355b3d4a3e9c7677454dc9bc2c874ee2e4a9))\n - **FIX**: Fix albedo texture binding on shader ([#3400](https://github.com/flame-engine/flame/issues/3400)). ([c380c91f](https://github.com/flame-engine/flame/commit/c380c91f7a7e22ab71c3617c0386159861a64abc))\n - **FIX**: Make build_shaders script compatible with other platforms [flame_3d] ([#3395](https://github.com/flame-engine/flame/issues/3395)). ([38331309](https://github.com/flame-engine/flame/commit/38331309ca5ee609e85422ee9d740569a35e5e9e))\n - **FIX**: Make `flame_3d` work with latest stable ([#3387](https://github.com/flame-engine/flame/issues/3387)). ([f940d3f9](https://github.com/flame-engine/flame/commit/f940d3f9420d3ce001f47a9155c582b8b4cd1dcb))\n - **FIX**: MeshComponent.bind should bind to the provided device ([#3278](https://github.com/flame-engine/flame/issues/3278)). ([d06b4ce6](https://github.com/flame-engine/flame/commit/d06b4ce6935987705447772f912719c405d39c9e))\n - **FIX**: Improve behavior of Quaternion.slerp function [flame_3d] ([#3306](https://github.com/flame-engine/flame/issues/3306)). ([8872a45a](https://github.com/flame-engine/flame/commit/8872a45a1b49d7ba7688d62ca25b2a8d31c495cb))\n - **FIX**: Improve quaternion slerp logic to avoid NaN edge cases ([#3303](https://github.com/flame-engine/flame/issues/3303)). ([712b03fb](https://github.com/flame-engine/flame/commit/712b03fbe176cf238a3780aee641254436520366))\n - **FIX**: Make the `build_shader` script work for windows ([#3402](https://github.com/flame-engine/flame/issues/3402)). ([ce7822a0](https://github.com/flame-engine/flame/commit/ce7822a0d0c5ac1c766635a5cf2c242d5e5f98ec))\n - **FIX**: MeshComponent.bind should bind to the provided device ([#3278](https://github.com/flame-engine/flame/issues/3278)). ([3ae3ef54](https://github.com/flame-engine/flame/commit/3ae3ef5476fa5f9ead7069efeee35cc31c0e9dd2))\n - **FIX**: Fix typo on loadTexture ([#3253](https://github.com/flame-engine/flame/issues/3253)). ([3a20a8cd](https://github.com/flame-engine/flame/commit/3a20a8cd61543aad21c1015de5c31ec1cbe71aed))\n - **FIX**: Improve behavior of Quaternion.slerp function [flame_3d] ([#3306](https://github.com/flame-engine/flame/issues/3306)). ([b9d6a0f1](https://github.com/flame-engine/flame/commit/b9d6a0f1d34e009cd91ae9d2ab0eed09b546d110))\n - **FIX**: Improve quaternion slerp logic to avoid NaN edge cases ([#3303](https://github.com/flame-engine/flame/issues/3303)). ([565b68b9](https://github.com/flame-engine/flame/commit/565b68b9da52d44281e93f9ae8617f9dbe9551f3))\n - **FIX**: Fix typo on loadTexture ([#3253](https://github.com/flame-engine/flame/issues/3253)). ([3d1f1437](https://github.com/flame-engine/flame/commit/3d1f143700506467798e030450227d8029b74ef2))\n - **FIX**: Add missing export for CylinderMesh [flame_3d] ([#3256](https://github.com/flame-engine/flame/issues/3256)). ([d8fb65c1](https://github.com/flame-engine/flame/commit/d8fb65c158a3cce442972fb24f55fb522729085f))\n - **FIX**: Add missing export for CylinderMesh [flame_3d] ([#3256](https://github.com/flame-engine/flame/issues/3256)). ([d517c169](https://github.com/flame-engine/flame/commit/d517c169ed9b4d4457df6ac1ae363277577597fa))\n - **FEAT**: More Lights! [flame_3d] ([#3250](https://github.com/flame-engine/flame/issues/3250)). ([5c508e81](https://github.com/flame-engine/flame/commit/5c508e81bddfb17857355da80e57f1ac77ab368d))\n - **FEAT**(flame_3d): Add helpful extension functions to Vector ([#3141](https://github.com/flame-engine/flame/issues/3141)). ([92195989](https://github.com/flame-engine/flame/commit/9219598904131d8fceba8d1ad980bea2805e3515))\n - **FEAT**: Add CylinderMesh [flame_3d] ([#3239](https://github.com/flame-engine/flame/issues/3239)). ([d5de1733](https://github.com/flame-engine/flame/commit/d5de1733c64c4fbaeee83089a3b0f9a3fbb3355d))\n - **FEAT**: Refactor shader uniform binding to support shader arrays [flame_3d] ([#3282](https://github.com/flame-engine/flame/issues/3282)). ([edae1662](https://github.com/flame-engine/flame/commit/edae166252a519d38496bb6bf7c84fe9f401b8d4))\n - **FEAT**(flame_3d): Make shader api more useful ([#3085](https://github.com/flame-engine/flame/issues/3085)). ([fe2e4f20](https://github.com/flame-engine/flame/commit/fe2e4f20195b453268b34e589616343fdce6201a))\n - **FEAT**(flame_3d): Make shader api more useful ([#3085](https://github.com/flame-engine/flame/issues/3085)). ([4042d300](https://github.com/flame-engine/flame/commit/4042d3002ce619bf6b8597aa7dc17a803bc57c69))\n - **FEAT**: Add more useful extensions to VectorN and Quaternion [flame_3d] ([#3296](https://github.com/flame-engine/flame/issues/3296)). ([f5f03e5e](https://github.com/flame-engine/flame/commit/f5f03e5ed7095ede713727d6bbab39db0505e7bf))\n - **FEAT**: Expose vector classes on core file [flame_3d] ([#3211](https://github.com/flame-engine/flame/issues/3211)). ([8f403ac2](https://github.com/flame-engine/flame/commit/8f403ac23ae7cdf5343652c30f9c0ee71d627b0a))\n - **FEAT**(flame_3d): Add helpful extension functions to Vector ([#3141](https://github.com/flame-engine/flame/issues/3141)). ([39e15fb3](https://github.com/flame-engine/flame/commit/39e15fb30256cbfaa86f4d7b8e3453c52942d1a5))\n - **FEAT**: Add CylinderMesh [flame_3d] ([#3239](https://github.com/flame-engine/flame/issues/3239)). ([01872fb6](https://github.com/flame-engine/flame/commit/01872fb6e45e10dc380fee7a176a8b37eeaef880))\n - **FEAT**(flame_3d): initial implementation of 3D support ([#3012](https://github.com/flame-engine/flame/issues/3012)). ([acb8e6fc](https://github.com/flame-engine/flame/commit/acb8e6fc07592a7df041512ed9e033b33eda8799))\n - **FEAT**: Support skeletal animation basics [flame_3d]  ([#3291](https://github.com/flame-engine/flame/issues/3291)). ([9c0d1500](https://github.com/flame-engine/flame/commit/9c0d15006047597097dc0e054c1e03a04491cff9))\n - **FEAT**: Add normals to surfaces when not specified ([#3257](https://github.com/flame-engine/flame/issues/3257)). ([1dd21d7d](https://github.com/flame-engine/flame/commit/1dd21d7d6d4f58c9ac54a38363ee2fd0978a9d0c))\n - **FEAT**(flame_3d): initial implementation of 3D support ([#3012](https://github.com/flame-engine/flame/issues/3012)). ([0242e1dd](https://github.com/flame-engine/flame/commit/0242e1dd12a9b50a411d895b662f9df33536f6d9))\n - **FEAT**: Support skeletal animation basics [flame_3d]  ([#3291](https://github.com/flame-engine/flame/issues/3291)). ([12927e41](https://github.com/flame-engine/flame/commit/12927e4100a7b4b46e4218db6792d25be1623f88))\n - **FEAT**: Add more useful extensions to VectorN and Quaternion [flame_3d] ([#3296](https://github.com/flame-engine/flame/issues/3296)). ([9cb95279](https://github.com/flame-engine/flame/commit/9cb9527909a4faa38609d25ebd7463f1e2e1a1ab))\n - **FEAT**: Make it easier work with the Mesh class [flame_3d] ([#3212](https://github.com/flame-engine/flame/issues/3212)). ([ebf2ee62](https://github.com/flame-engine/flame/commit/ebf2ee62e535fd1d0f499112b314e1d88e59bbc1))\n - **FEAT**: More Lights! [flame_3d] ([#3250](https://github.com/flame-engine/flame/issues/3250)). ([1780630e](https://github.com/flame-engine/flame/commit/1780630e7fcb386a331ba1219c15cb1ae8b139e6))\n - **FEAT**: Add normals to surfaces when not specified ([#3257](https://github.com/flame-engine/flame/issues/3257)). ([844c1d72](https://github.com/flame-engine/flame/commit/844c1d726e04e9c3c5739214720cf26fc62d3f9f))\n - **FEAT**(flame_3d): Fix minor nits on flame_3d ([#3140](https://github.com/flame-engine/flame/issues/3140)). ([11cdfb5e](https://github.com/flame-engine/flame/commit/11cdfb5ebeb62dd1aec2d51fd7fadfbfb17c6da5))\n - **FEAT**: Expose vector classes on core file [flame_3d] ([#3211](https://github.com/flame-engine/flame/issues/3211)). ([c3e68dff](https://github.com/flame-engine/flame/commit/c3e68dffd2e53a8dc8d4d3804c47e956dfc0ebb4))\n - **FEAT**(flame_3d): Fix minor nits on flame_3d ([#3140](https://github.com/flame-engine/flame/issues/3140)). ([b537d20a](https://github.com/flame-engine/flame/commit/b537d20ab65ce0312e9c05ba9156d794234d93e0))\n - **FEAT**: Make it easier work with the Mesh class [flame_3d] ([#3212](https://github.com/flame-engine/flame/issues/3212)). ([7f80b530](https://github.com/flame-engine/flame/commit/7f80b53078c037f81d386a44fa9b749cf7835ffa))\n - **DOCS**: Update docs and comments (flame_3d) ([#3057](https://github.com/flame-engine/flame/issues/3057)). ([14047879](https://github.com/flame-engine/flame/commit/14047879a13e1f13e51ce3411feb7c7962d6d7ee))\n - **DOCS**: Update docs and comments (flame_3d) ([#3057](https://github.com/flame-engine/flame/issues/3057)). ([b5fd457a](https://github.com/flame-engine/flame/commit/b5fd457a6b6bcad73006fc77a538c7e8521178a5))\n - **DOCS**: Update README.md docs to reflect current state of affairs ([#3305](https://github.com/flame-engine/flame/issues/3305)). ([ac3f48ff](https://github.com/flame-engine/flame/commit/ac3f48ffa6527c595035e8a9cc24307343dacb31))\n - **DOCS**: Update wording on README to match newer instructions [flame_3d] ([#3399](https://github.com/flame-engine/flame/issues/3399)). ([9c416cbf](https://github.com/flame-engine/flame/commit/9c416cbfcb4106ab7b1ad14324ca9cb89593b80d))\n - **DOCS**: Update README.md docs to reflect current state of affairs ([#3305](https://github.com/flame-engine/flame/issues/3305)). ([be72daee](https://github.com/flame-engine/flame/commit/be72daee6b92bcef2af3af78c1f64abe94c49d18))\n - **BREAKING** **FEAT**: Allow for custom shaders and materials ([#3384](https://github.com/flame-engine/flame/issues/3384)). ([ae731814](https://github.com/flame-engine/flame/commit/ae73181466c7e32b0e5e9e814f5170310c20f263))\n - **BREAKING** **FEAT**: Refactor the `CameraComponent3D` ([#3394](https://github.com/flame-engine/flame/issues/3394)). ([4a61718d](https://github.com/flame-engine/flame/commit/4a61718d3b8d45d18a74f662f1bf1eb8e6069983))\n\n#### `flame` - `v1.23.0`\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n - **FIX**: Take into consideration when child is added to parent that is removed in the same tick ([#3428](https://github.com/flame-engine/flame/issues/3428)). ([9a5c54be](https://github.com/flame-engine/flame/commit/9a5c54bea858fc8e9e84878f3ac0a0f7bc190b46))\n - **FIX**: Add missing export of GroupTextElement to text.dart ([#3424](https://github.com/flame-engine/flame/issues/3424)). ([c9c0f691](https://github.com/flame-engine/flame/commit/c9c0f691412bb026c1d766ec7b424a468f8929f7))\n - **FIX**: Add missing export of GroupElement to text.dart ([#3423](https://github.com/flame-engine/flame/issues/3423)). ([c0c4bb02](https://github.com/flame-engine/flame/commit/c0c4bb02a32306120a8770122116631f55c1c700))\n - **FIX**: Fix brighten and darken alpha issue ([#3414](https://github.com/flame-engine/flame/issues/3414)). ([de8e3bce](https://github.com/flame-engine/flame/commit/de8e3bcea2c2c2fa5e01dd288176c8f5623d21fb))\n - **FIX**: Set button size in onMount if not set ([#3413](https://github.com/flame-engine/flame/issues/3413)). ([916aa5ce](https://github.com/flame-engine/flame/commit/916aa5ce2ad3851b3044e043d2be7cbe923f2c40))\n - **FIX**: Fix bug preventing removeAll(children) from be called before mount ([#3408](https://github.com/flame-engine/flame/issues/3408)). ([726cb8b6](https://github.com/flame-engine/flame/commit/726cb8b6390c839f9cbab959b2268a7b45fa691c))\n - **FEAT**: Add support for strike-through text for flame_markdown ([#3426](https://github.com/flame-engine/flame/issues/3426)). ([1f9b0ea9](https://github.com/flame-engine/flame/commit/1f9b0ea9f35a7180725ec7f8f79a561c5f544bb7))\n - **FEAT**: Warning and docs about fullscreen methods outside the mobile platforms ([#3419](https://github.com/flame-engine/flame/issues/3419)). ([994e098b](https://github.com/flame-engine/flame/commit/994e098bd699a30aa13aed65f2bd0ab7254ad779))\n - **FEAT**: Add baseColor to Shadow3DDecorator ([#3375](https://github.com/flame-engine/flame/issues/3375)). ([b5d7ee07](https://github.com/flame-engine/flame/commit/b5d7ee0752ee1f2dddf1da4ac817f138296e1c96))\n\n#### `flame_console` - `v0.1.2`\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n - **FEAT**: Refactoring flame_console to use terminui ([#3388](https://github.com/flame-engine/flame/issues/3388)). ([de74a93b](https://github.com/flame-engine/flame/commit/de74a93b44f442341f816a2988c854f40902ff7e))\n\n#### `flame_forge2d` - `v0.18.2+4`\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n\n#### `flame_isolate` - `v0.6.2+4`\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n\n#### `flame_markdown` - `v0.2.3`\n\n - **FIX**: Do not encode HTML by default when parsing markdown [flame_markdown] ([#3425](https://github.com/flame-engine/flame/issues/3425)). ([3067da94](https://github.com/flame-engine/flame/commit/3067da94fbc6df2da5197771cb9617588006a9b9))\n - **FEAT**: Add support for strike-through text for flame_markdown ([#3426](https://github.com/flame-engine/flame/issues/3426)). ([1f9b0ea9](https://github.com/flame-engine/flame/commit/1f9b0ea9f35a7180725ec7f8f79a561c5f544bb7))\n\n#### `flame_oxygen` - `v0.2.3+4`\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n\n#### `flame_sprite_fusion` - `v0.1.3+4`\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n\n#### `flame_svg` - `v1.11.4`\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n\n#### `flame_test` - `v1.17.4`\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n\n#### `flame_tiled` - `v1.21.2`\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n\n\n## 2024-12-10\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_3d` - `v0.1.0-dev.3`](#flame_3d---v010-dev3)\n\n---\n\n#### `flame_3d` - `v0.1.0-dev.3`\n\n - **FIX**: Improve behavior of Quaternion.slerp function [flame_3d] ([#3306](https://github.com/flame-engine/flame/issues/3306)). ([b9d6a0f1](https://github.com/flame-engine/flame/commit/b9d6a0f1d34e009cd91ae9d2ab0eed09b546d110))\n - **DOCS**: Update README.md docs to reflect current state of affairs ([#3305](https://github.com/flame-engine/flame/issues/3305)). ([be72daee](https://github.com/flame-engine/flame/commit/be72daee6b92bcef2af3af78c1f64abe94c49d18))\n\n\n## 2024-12-10\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_3d` - `v0.1.0-dev.2`](#flame_3d---v010-dev2)\n\n---\n\n#### `flame_3d` - `v0.1.0-dev.2`\n\n - **FIX**: Improve quaternion slerp logic to avoid NaN edge cases ([#3303](https://github.com/flame-engine/flame/issues/3303)). ([565b68b9](https://github.com/flame-engine/flame/commit/565b68b9da52d44281e93f9ae8617f9dbe9551f3))\n - **FIX**: MeshComponent.bind should bind to the provided device ([#3278](https://github.com/flame-engine/flame/issues/3278)). ([3ae3ef54](https://github.com/flame-engine/flame/commit/3ae3ef5476fa5f9ead7069efeee35cc31c0e9dd2))\n - **FIX**: Add missing export for CylinderMesh [flame_3d] ([#3256](https://github.com/flame-engine/flame/issues/3256)). ([d517c169](https://github.com/flame-engine/flame/commit/d517c169ed9b4d4457df6ac1ae363277577597fa))\n - **FIX**: Fix typo on loadTexture ([#3253](https://github.com/flame-engine/flame/issues/3253)). ([3a20a8cd](https://github.com/flame-engine/flame/commit/3a20a8cd61543aad21c1015de5c31ec1cbe71aed))\n - **FIX**: Use saner default value for camera's target ([#3238](https://github.com/flame-engine/flame/issues/3238)). ([78522c62](https://github.com/flame-engine/flame/commit/78522c624d846c827a1c0d7377837e04a30ba4e7))\n - **FIX**: Revert \"feat(flame_3d): initial implementation of 3D support\" ([#3060](https://github.com/flame-engine/flame/issues/3060)). ([741d9384](https://github.com/flame-engine/flame/commit/741d9384dbfea7bb692f181a7689a7b10a947ef0))\n - **FEAT**: Support skeletal animation basics [flame_3d]  ([#3291](https://github.com/flame-engine/flame/issues/3291)). ([12927e41](https://github.com/flame-engine/flame/commit/12927e4100a7b4b46e4218db6792d25be1623f88))\n - **FEAT**: Add more useful extensions to VectorN and Quaternion [flame_3d] ([#3296](https://github.com/flame-engine/flame/issues/3296)). ([9cb95279](https://github.com/flame-engine/flame/commit/9cb9527909a4faa38609d25ebd7463f1e2e1a1ab))\n - **FEAT**: More Lights! [flame_3d] ([#3250](https://github.com/flame-engine/flame/issues/3250)). ([1780630e](https://github.com/flame-engine/flame/commit/1780630e7fcb386a331ba1219c15cb1ae8b139e6))\n - **FEAT**: Add normals to surfaces when not specified ([#3257](https://github.com/flame-engine/flame/issues/3257)). ([844c1d72](https://github.com/flame-engine/flame/commit/844c1d726e04e9c3c5739214720cf26fc62d3f9f))\n - **FEAT**: Add CylinderMesh [flame_3d] ([#3239](https://github.com/flame-engine/flame/issues/3239)). ([01872fb6](https://github.com/flame-engine/flame/commit/01872fb6e45e10dc380fee7a176a8b37eeaef880))\n - **FEAT**(flame_3d): Make shader api more useful ([#3085](https://github.com/flame-engine/flame/issues/3085)). ([fe2e4f20](https://github.com/flame-engine/flame/commit/fe2e4f20195b453268b34e589616343fdce6201a))\n - **FEAT**: Make it easier work with the Mesh class [flame_3d] ([#3212](https://github.com/flame-engine/flame/issues/3212)). ([ebf2ee62](https://github.com/flame-engine/flame/commit/ebf2ee62e535fd1d0f499112b314e1d88e59bbc1))\n - **FEAT**: Expose vector classes on core file [flame_3d] ([#3211](https://github.com/flame-engine/flame/issues/3211)). ([c3e68dff](https://github.com/flame-engine/flame/commit/c3e68dffd2e53a8dc8d4d3804c47e956dfc0ebb4))\n - **FEAT**(flame_3d): Add helpful extension functions to Vector ([#3141](https://github.com/flame-engine/flame/issues/3141)). ([92195989](https://github.com/flame-engine/flame/commit/9219598904131d8fceba8d1ad980bea2805e3515))\n - **FEAT**(flame_3d): Fix minor nits on flame_3d ([#3140](https://github.com/flame-engine/flame/issues/3140)). ([11cdfb5e](https://github.com/flame-engine/flame/commit/11cdfb5ebeb62dd1aec2d51fd7fadfbfb17c6da5))\n - **FEAT**(flame_3d): initial implementation of 3D support ([#3012](https://github.com/flame-engine/flame/issues/3012)). ([0242e1dd](https://github.com/flame-engine/flame/commit/0242e1dd12a9b50a411d895b662f9df33536f6d9))\n - **FEAT**(flame_3d): initial implementation of 3D support ([#3012](https://github.com/flame-engine/flame/issues/3012)). ([e434bafb](https://github.com/flame-engine/flame/commit/e434bafb15fc486c51b43aaa9d9190b8b7e783cb))\n - **DOCS**: Update docs and comments (flame_3d) ([#3057](https://github.com/flame-engine/flame/issues/3057)). ([14047879](https://github.com/flame-engine/flame/commit/14047879a13e1f13e51ce3411feb7c7962d6d7ee))\n\n## 2024-11-24\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.22.0`](#flame---v1220)\n - [`flame_console` - `v0.1.1`](#flame_console---v011)\n - [`flame_fire_atlas` - `v1.7.0`](#flame_fire_atlas---v170)\n - [`flame_behavior_tree` - `v0.1.3+3`](#flame_behavior_tree---v0133)\n - [`flame_test` - `v1.17.3`](#flame_test---v1173)\n - [`flame_tiled` - `v1.21.1`](#flame_tiled---v1211)\n - [`flame_oxygen` - `v0.2.3+3`](#flame_oxygen---v0233)\n - [`flame_isolate` - `v0.6.2+3`](#flame_isolate---v0623)\n - [`flame_texturepacker` - `v4.1.3`](#flame_texturepacker---v413)\n - [`flame_sprite_fusion` - `v0.1.3+3`](#flame_sprite_fusion---v0133)\n - [`flame_audio` - `v2.10.6`](#flame_audio---v2106)\n - [`flame_spine` - `v0.2.2+3`](#flame_spine---v0223)\n - [`flame_bloc` - `v1.12.4`](#flame_bloc---v1124)\n - [`flame_kenney_xml` - `v0.1.1+3`](#flame_kenney_xml---v0113)\n - [`flame_lottie` - `v0.4.2+3`](#flame_lottie---v0423)\n - [`flame_rive` - `v1.10.6`](#flame_rive---v1106)\n - [`flame_markdown` - `v0.2.2+3`](#flame_markdown---v0223)\n - [`flame_svg` - `v1.11.3`](#flame_svg---v1113)\n - [`flame_forge2d` - `v0.18.2+3`](#flame_forge2d---v01823)\n - [`flame_noise` - `v0.3.2+3`](#flame_noise---v0323)\n - [`flame_riverpod` - `v5.4.6`](#flame_riverpod---v546)\n - [`flame_network_assets` - `v0.3.3+3`](#flame_network_assets---v0333)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_behavior_tree` - `v0.1.3+3`\n - `flame_test` - `v1.17.3`\n - `flame_tiled` - `v1.21.1`\n - `flame_oxygen` - `v0.2.3+3`\n - `flame_isolate` - `v0.6.2+3`\n - `flame_texturepacker` - `v4.1.3`\n - `flame_sprite_fusion` - `v0.1.3+3`\n - `flame_audio` - `v2.10.6`\n - `flame_spine` - `v0.2.2+3`\n - `flame_bloc` - `v1.12.4`\n - `flame_kenney_xml` - `v0.1.1+3`\n - `flame_lottie` - `v0.4.2+3`\n - `flame_rive` - `v1.10.6`\n - `flame_markdown` - `v0.2.2+3`\n - `flame_svg` - `v1.11.3`\n - `flame_forge2d` - `v0.18.2+3`\n - `flame_noise` - `v0.3.2+3`\n - `flame_riverpod` - `v5.4.6`\n - `flame_network_assets` - `v0.3.3+3`\n\n---\n\n#### `flame` - `v1.22.0`\n\n - **FIX**: Remove extra `implements SizeProvider`s ([#3358](https://github.com/flame-engine/flame/issues/3358)). ([47ba0d87](https://github.com/flame-engine/flame/commit/47ba0d8738b101ed59781f8ba384cf05a16d65f1))\n - **FEAT**: Add WorldRoute to enable swapping worlds from the RouterComponent ([#3372](https://github.com/flame-engine/flame/issues/3372)). ([497f128f](https://github.com/flame-engine/flame/commit/497f128f8c32758f94d8d4752e9166fd3b625608))\n - **FEAT**(overlays): Added the 'priority' parameter for overlays ([#3349](https://github.com/flame-engine/flame/issues/3349)). ([e591ebf8](https://github.com/flame-engine/flame/commit/e591ebf8a320ff3d55b9ae9e50390bf2ab5a8919))\n\n#### `flame_console` - `v0.1.1`\n\n - **FIX**(flame_console): MemoryRepository can't be const ([#3362](https://github.com/flame-engine/flame/issues/3362)). ([e977bd49](https://github.com/flame-engine/flame/commit/e977bd495b196368582eda4e7d8019adc6c268f4))\n - **FEAT**: Adding FlameConsole ([#3329](https://github.com/flame-engine/flame/issues/3329)). ([cf5358cd](https://github.com/flame-engine/flame/commit/cf5358cd9069dab9e327e766553bd65e151f1540))\n\n#### `flame_fire_atlas` - `v1.7.0`\n\n - **FEAT**: Adding getters and methods for easier manipulation of selections ([#3350](https://github.com/flame-engine/flame/issues/3350)). ([291af57d](https://github.com/flame-engine/flame/commit/291af57deb7d742a73438b026ca2f4fd1c6a3454))\n\n\n## 2024-10-16\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.21.0`](#flame---v1210)\n - [`flame_fire_atlas` - `v1.6.0`](#flame_fire_atlas---v160)\n - [`flame_test` - `v1.17.2`](#flame_test---v1172)\n - [`flame_tiled` - `v1.21.0`](#flame_tiled---v1210)\n - [`flame_audio` - `v2.10.5`](#flame_audio---v2105)\n - [`flame_forge2d` - `v0.18.2+2`](#flame_forge2d---v01822)\n - [`flame_oxygen` - `v0.2.3+2`](#flame_oxygen---v0232)\n - [`flame_rive` - `v1.10.5`](#flame_rive---v1105)\n - [`flame_texturepacker` - `v4.1.2`](#flame_texturepacker---v412)\n - [`flame_behavior_tree` - `v0.1.3+2`](#flame_behavior_tree---v0132)\n - [`flame_spine` - `v0.2.2+2`](#flame_spine---v0222)\n - [`flame_riverpod` - `v5.4.5`](#flame_riverpod---v545)\n - [`flame_kenney_xml` - `v0.1.1+2`](#flame_kenney_xml---v0112)\n - [`flame_bloc` - `v1.12.3`](#flame_bloc---v1123)\n - [`flame_noise` - `v0.3.2+2`](#flame_noise---v0322)\n - [`flame_lottie` - `v0.4.2+2`](#flame_lottie---v0422)\n - [`flame_network_assets` - `v0.3.3+2`](#flame_network_assets---v0332)\n - [`flame_svg` - `v1.11.2`](#flame_svg---v1112)\n - [`flame_sprite_fusion` - `v0.1.3+2`](#flame_sprite_fusion---v0132)\n - [`flame_markdown` - `v0.2.2+2`](#flame_markdown---v0222)\n - [`flame_isolate` - `v0.6.2+2`](#flame_isolate---v0622)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_audio` - `v2.10.5`\n - `flame_forge2d` - `v0.18.2+2`\n - `flame_oxygen` - `v0.2.3+2`\n - `flame_rive` - `v1.10.5`\n - `flame_texturepacker` - `v4.1.2`\n - `flame_behavior_tree` - `v0.1.3+2`\n - `flame_spine` - `v0.2.2+2`\n - `flame_riverpod` - `v5.4.5`\n - `flame_kenney_xml` - `v0.1.1+2`\n - `flame_bloc` - `v1.12.3`\n - `flame_noise` - `v0.3.2+2`\n - `flame_lottie` - `v0.4.2+2`\n - `flame_network_assets` - `v0.3.3+2`\n - `flame_svg` - `v1.11.2`\n - `flame_sprite_fusion` - `v0.1.3+2`\n - `flame_markdown` - `v0.2.2+2`\n - `flame_isolate` - `v0.6.2+2`\n\n---\n\n#### `flame` - `v1.21.0`\n\n - **FIX**: Widgets flickering ([#3343](https://github.com/flame-engine/flame/issues/3343)). ([ff170dc5](https://github.com/flame-engine/flame/commit/ff170dc5c2acc41190249b48e61767ea459fabb4))\n - **FIX**: Ray should not be able to escape `CircleHitbox` ([#3341](https://github.com/flame-engine/flame/issues/3341)). ([7311d034](https://github.com/flame-engine/flame/commit/7311d034d4c3b43592b49472384fe8576809e6a5))\n - **FIX**: Fix SpriteBatch to comply with new drawAtlas requirement ([#3338](https://github.com/flame-engine/flame/issues/3338)). ([a17fe4cd](https://github.com/flame-engine/flame/commit/a17fe4cdfaafa071cfd2ab8ef8279b26b79f00a7))\n - **FIX**: Set SpriteButtonComponent sprites in `onMount` ([#3327](https://github.com/flame-engine/flame/issues/3327)). ([f36533e7](https://github.com/flame-engine/flame/commit/f36533e78c7634866680ab5fb202a3e230529943))\n - **FIX**: Export TapConfig to make visible ([#3323](https://github.com/flame-engine/flame/issues/3323)). ([8e00115c](https://github.com/flame-engine/flame/commit/8e00115cd299423564dfce4b9d1674c9257a3c42))\n - **FIX**: Clarify `SpriteGroupComponent.updateSprite` assertion ([#3317](https://github.com/flame-engine/flame/issues/3317)). ([d976ee8c](https://github.com/flame-engine/flame/commit/d976ee8c7e4fbbca08e549412ca8b5af6928d4f4))\n - **FEAT**: Adding spawnWhenLoaded flag on SpawnComponent ([#3334](https://github.com/flame-engine/flame/issues/3334)). ([51a7e26b](https://github.com/flame-engine/flame/commit/51a7e26b1ab0ef2a2d040548c74aef84b164272d))\n - **FEAT**: Add a getter for images cache keys ([#3324](https://github.com/flame-engine/flame/issues/3324)). ([7746f2f8](https://github.com/flame-engine/flame/commit/7746f2f867092c19222a40aec2b66dc80558dccb))\n\n#### `flame_fire_atlas` - `v1.6.0`\n\n - **FEAT**: Adding getter for the atlas image on flame fire atlas ([#3326](https://github.com/flame-engine/flame/issues/3326)). ([ae230ffa](https://github.com/flame-engine/flame/commit/ae230ffaaa588df7a99a3e2e8fa8980dc32104c0))\n\n#### `flame_test` - `v1.17.2`\n\n - **FIX**: Widgets flickering ([#3343](https://github.com/flame-engine/flame/issues/3343)). ([ff170dc5](https://github.com/flame-engine/flame/commit/ff170dc5c2acc41190249b48e61767ea459fabb4))\n - **FIX**: Fix SpriteBatch to comply with new drawAtlas requirement ([#3338](https://github.com/flame-engine/flame/issues/3338)). ([a17fe4cd](https://github.com/flame-engine/flame/commit/a17fe4cdfaafa071cfd2ab8ef8279b26b79f00a7))\n\n#### `flame_tiled` - `v1.21.0`\n\n - **FEAT**: Add a getter for images cache keys ([#3324](https://github.com/flame-engine/flame/issues/3324)). ([7746f2f8](https://github.com/flame-engine/flame/commit/7746f2f867092c19222a40aec2b66dc80558dccb))\n\n\n## 2024-09-20\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.20.0`](#flame---v1200)\n - [`flame_oxygen` - `v0.2.3+1`](#flame_oxygen---v0231)\n - [`flame_behavior_tree` - `v0.1.3+1`](#flame_behavior_tree---v0131)\n - [`flame_isolate` - `v0.6.2+1`](#flame_isolate---v0621)\n - [`flame_noise` - `v0.3.2+1`](#flame_noise---v0321)\n - [`flame_svg` - `v1.11.1`](#flame_svg---v1111)\n - [`flame_rive` - `v1.10.4`](#flame_rive---v1104)\n - [`flame_audio` - `v2.10.4`](#flame_audio---v2104)\n - [`flame_texturepacker` - `v4.1.1`](#flame_texturepacker---v411)\n - [`flame_spine` - `v0.2.2+1`](#flame_spine---v0221)\n - [`flame_sprite_fusion` - `v0.1.3+1`](#flame_sprite_fusion---v0131)\n - [`flame_markdown` - `v0.2.2+1`](#flame_markdown---v0221)\n - [`flame_forge2d` - `v0.18.2+1`](#flame_forge2d---v01821)\n - [`flame_test` - `v1.17.1`](#flame_test---v1171)\n - [`flame_fire_atlas` - `v1.5.5`](#flame_fire_atlas---v155)\n - [`flame_bloc` - `v1.12.2`](#flame_bloc---v1122)\n - [`flame_kenney_xml` - `v0.1.1+1`](#flame_kenney_xml---v0111)\n - [`flame_riverpod` - `v5.4.4`](#flame_riverpod---v544)\n - [`flame_network_assets` - `v0.3.3+1`](#flame_network_assets---v0331)\n - [`flame_tiled` - `v1.20.4`](#flame_tiled---v1204)\n - [`flame_lottie` - `v0.4.2+1`](#flame_lottie---v0421)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_oxygen` - `v0.2.3+1`\n - `flame_behavior_tree` - `v0.1.3+1`\n - `flame_isolate` - `v0.6.2+1`\n - `flame_noise` - `v0.3.2+1`\n - `flame_svg` - `v1.11.1`\n - `flame_rive` - `v1.10.4`\n - `flame_audio` - `v2.10.4`\n - `flame_texturepacker` - `v4.1.1`\n - `flame_spine` - `v0.2.2+1`\n - `flame_sprite_fusion` - `v0.1.3+1`\n - `flame_markdown` - `v0.2.2+1`\n - `flame_forge2d` - `v0.18.2+1`\n - `flame_test` - `v1.17.1`\n - `flame_fire_atlas` - `v1.5.5`\n - `flame_bloc` - `v1.12.2`\n - `flame_kenney_xml` - `v0.1.1+1`\n - `flame_riverpod` - `v5.4.4`\n - `flame_network_assets` - `v0.3.3+1`\n - `flame_tiled` - `v1.20.4`\n - `flame_lottie` - `v0.4.2+1`\n\n---\n\n#### `flame` - `v1.20.0`\n\n - **FIX**: SpriteButtonComponent to initialize sprites in `onLoad` ([#3302](https://github.com/flame-engine/flame/issues/3302)). ([1204216c](https://github.com/flame-engine/flame/commit/1204216cb227d3831b546a54818075065fa7beec))\n - **FIX**: ViewportAwareBounds component and lifecycle issues ([#3276](https://github.com/flame-engine/flame/issues/3276)). ([026bf41f](https://github.com/flame-engine/flame/commit/026bf41f020de66ae9adfcdda9209bfbb75cf60c))\n - **FEAT**: Add ComponentTreeRoot.lifecycleEventsProcessed future ([#3308](https://github.com/flame-engine/flame/issues/3308)). ([ebc47418](https://github.com/flame-engine/flame/commit/ebc474189ceb587bcdebef7d3645ed2f3b3dba6f))\n - **FEAT**: Adding paint attribute to SpriteWidget and SpriteAnimationWidget ([#3298](https://github.com/flame-engine/flame/issues/3298)). ([a5338d0c](https://github.com/flame-engine/flame/commit/a5338d0c20d01bbe461c6d7fed5951d11e1c76f0))\n - **FEAT**: Adding tickOnLoad to TimerComponent ([#3285](https://github.com/flame-engine/flame/issues/3285)). ([0113aa37](https://github.com/flame-engine/flame/commit/0113aa376145109079a89bd310b9e528631ce9d4))\n - **DOCS**: Include information about the Flame DevTools extension in example readme ([#3288](https://github.com/flame-engine/flame/issues/3288)). ([76a9abaf](https://github.com/flame-engine/flame/commit/76a9abaf3c70659323e02bf7b6531b4fbba1f7a2))\n\n\n## 2024-08-27\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.19.0`](#flame---v1190)\n\nPackages with other changes:\n\n - [`behavior_tree` - `v0.1.3`](#behavior_tree---v013)\n - [`flame_behavior_tree` - `v0.1.3`](#flame_behavior_tree---v013)\n - [`flame_bloc` - `v1.12.1`](#flame_bloc---v1121)\n - [`flame_forge2d` - `v0.18.2`](#flame_forge2d---v0182)\n - [`flame_isolate` - `v0.6.2`](#flame_isolate---v062)\n - [`flame_kenney_xml` - `v0.1.1`](#flame_kenney_xml---v011)\n - [`flame_lint` - `v1.2.1`](#flame_lint---v121)\n - [`flame_lottie` - `v0.4.2`](#flame_lottie---v042)\n - [`flame_markdown` - `v0.2.2`](#flame_markdown---v022)\n - [`flame_noise` - `v0.3.2`](#flame_noise---v032)\n - [`flame_oxygen` - `v0.2.3`](#flame_oxygen---v023)\n - [`flame_rive` - `v1.10.3`](#flame_rive---v1103)\n - [`flame_spine` - `v0.2.2`](#flame_spine---v022)\n - [`flame_splash_screen` - `v0.3.1`](#flame_splash_screen---v031)\n - [`flame_sprite_fusion` - `v0.1.3`](#flame_sprite_fusion---v013)\n - [`flame_svg` - `v1.11.0`](#flame_svg---v1110)\n - [`flame_test` - `v1.17.0`](#flame_test---v1170)\n - [`flame_texturepacker` - `v4.1.0`](#flame_texturepacker---v410)\n - [`flame_tiled` - `v1.20.3`](#flame_tiled---v1203)\n - [`jenny` - `v1.3.2`](#jenny---v132)\n - [`flame_fire_atlas` - `v1.5.4`](#flame_fire_atlas---v154)\n - [`flame_riverpod` - `v5.4.3`](#flame_riverpod---v543)\n - [`flame_network_assets` - `v0.3.3`](#flame_network_assets---v033)\n - [`flame_audio` - `v2.10.3`](#flame_audio---v2103)\n\n---\n\n#### `flame` - `v1.19.0`\n\n - **REFACTOR**: Use a temp vector for delta calculations of `FollowBehavior` ([#3230](https://github.com/flame-engine/flame/issues/3230)). ([524793d4](https://github.com/flame-engine/flame/commit/524793d4a0dbe384d42fb9f844685b85abb05574))\n - **FIX**: Add assertion when trying to set \"current\" that doesn't exist ([#3258](https://github.com/flame-engine/flame/issues/3258)). ([267d6801](https://github.com/flame-engine/flame/commit/267d6801cb7e6cbbaa450e24e38aaa7d8fcfc03f))\n - **FIX**: Update version of lints to comply with new pub requirements ([#3223](https://github.com/flame-engine/flame/issues/3223)). ([1b0bee72](https://github.com/flame-engine/flame/commit/1b0bee726b5937f73d4be5e304bc8780aa3ca6f0))\n - **FIX**: Replace CurvedParticle inheritance with Particle in ScaledParticle ([#3221](https://github.com/flame-engine/flame/issues/3221)). ([8cd054d0](https://github.com/flame-engine/flame/commit/8cd054d02b614d1ee35a71f32dcbacf0952c9780))\n - **FIX**: Fix text rendering issue where spaces are missing ([#3192](https://github.com/flame-engine/flame/issues/3192)). ([28fd2a0f](https://github.com/flame-engine/flame/commit/28fd2a0f0f1ea04872d0c4e8b674c8ce7bca69ee))\n - **FIX**: Add nativeAngle to constructors where it makes sense ([#3197](https://github.com/flame-engine/flame/issues/3197)). ([e8704934](https://github.com/flame-engine/flame/commit/e8704934b19d9ed1982d35ce62819f01ac3de189))\n - **FIX**: Wire in background and foreground colors in TextPaint ([#3191](https://github.com/flame-engine/flame/issues/3191)). ([983cfab6](https://github.com/flame-engine/flame/commit/983cfab6def86dbf68455fb021281caaf0135793))\n - **FIX**: Disallow mutatation of `SpriteGroupComponent.sprites` ([#3185](https://github.com/flame-engine/flame/issues/3185)). ([7c40034d](https://github.com/flame-engine/flame/commit/7c40034d20ed26114b14fd262130d11cf226fb6a))\n - **FIX**: Disallow mutatation of `SpriteAnimationGroupComponent.animations` ([#3183](https://github.com/flame-engine/flame/issues/3183)). ([52773407](https://github.com/flame-engine/flame/commit/527734071b030ec7dbe0f3c017108db0dfda3ced))\n - **FEAT**: Adding scale and angle to devtools attributes ([#3267](https://github.com/flame-engine/flame/issues/3267)). ([b2a5e658](https://github.com/flame-engine/flame/commit/b2a5e6581ebaebc8044d65504efc58309f8a2b9b))\n - **FEAT**: Adding x,y,width and height inputs to position components on Dev Tools ([#3263](https://github.com/flame-engine/flame/issues/3263)). ([003ec3a1](https://github.com/flame-engine/flame/commit/003ec3a17beed2bad5540b968a0f5602c19ada79))\n - **FEAT**: Adding component snapshot to Dev tools ([#3261](https://github.com/flame-engine/flame/issues/3261)). ([1a574917](https://github.com/flame-engine/flame/commit/1a574917cd5311aea2576942d5cf4ea579218aaf))\n - **FEAT**: Fixing tests on flutter 3.24.0 ([#3259](https://github.com/flame-engine/flame/issues/3259)). ([bf9a2481](https://github.com/flame-engine/flame/commit/bf9a2481fbeb77413a26ae96b57843ca51411f9f))\n - **FEAT**: Loading builder for Route ([#3113](https://github.com/flame-engine/flame/issues/3113)). ([1e62b342](https://github.com/flame-engine/flame/commit/1e62b3424578150718514aa762f184485dba024a))\n - **FEAT**: Take in super.curve in ScalingParticle ([#3220](https://github.com/flame-engine/flame/issues/3220)). ([0fbc73cc](https://github.com/flame-engine/flame/commit/0fbc73ccdf36938a20f2eb8ae544881a8dbeae1e))\n - **FEAT**: Add `pause` and `resume` to `HasTimeScale` mixin ([#3216](https://github.com/flame-engine/flame/issues/3216)). ([9a86e7b5](https://github.com/flame-engine/flame/commit/9a86e7b54b55047ec9c63997015f71b7308dec27))\n - **FEAT**: Add missing background and foreground properties to InlineTextStyle ([#3187](https://github.com/flame-engine/flame/issues/3187)). ([34dde50f](https://github.com/flame-engine/flame/commit/34dde50f978f810df89fb1c051d13aee9214b307))\n - **FEAT**: Support inline code blocks on markdown rich text ([#3186](https://github.com/flame-engine/flame/issues/3186)). ([67e069c0](https://github.com/flame-engine/flame/commit/67e069c00dcb32c258231a326b0918739c6f80e6))\n - **DOCS**: Remove `PositionType` from the docs ([#3198](https://github.com/flame-engine/flame/issues/3198)). ([b0ff5c41](https://github.com/flame-engine/flame/commit/b0ff5c41c572da4dfa4221bef89b93b6f6be74c6))\n - **DOCS**: Add dartdocs to inline text node classes ([#3189](https://github.com/flame-engine/flame/issues/3189)). ([84c1ee87](https://github.com/flame-engine/flame/commit/84c1ee87f827a85c7accd92e061077ef291cb433))\n - **BREAKING** **REFACTOR**: Make query() result an Iterable ([#3209](https://github.com/flame-engine/flame/issues/3209)). ([c094caa7](https://github.com/flame-engine/flame/commit/c094caa77b17b1d69856396e27c88db8515bb44a))\n\n#### `behavior_tree` - `v0.1.3`\n\n - **FIX**: Update version of lints to comply with new pub requirements ([#3223](https://github.com/flame-engine/flame/issues/3223)). ([1b0bee72](https://github.com/flame-engine/flame/commit/1b0bee726b5937f73d4be5e304bc8780aa3ca6f0))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n - **DOCS**: Fix capitalization of the Dart programming language on pubspec description field ([#3222](https://github.com/flame-engine/flame/issues/3222)). ([9404241e](https://github.com/flame-engine/flame/commit/9404241e8a14d8d510f693c8557ca62ed76bd390))\n\n#### `flame_behavior_tree` - `v0.1.3`\n\n - **FIX**: Update version of lints to comply with new pub requirements ([#3223](https://github.com/flame-engine/flame/issues/3223)). ([1b0bee72](https://github.com/flame-engine/flame/commit/1b0bee726b5937f73d4be5e304bc8780aa3ca6f0))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n - **DOCS**: Fix capitalization of the Dart programming language on pubspec description field ([#3222](https://github.com/flame-engine/flame/issues/3222)). ([9404241e](https://github.com/flame-engine/flame/commit/9404241e8a14d8d510f693c8557ca62ed76bd390))\n\n#### `flame_bloc` - `v1.12.1`\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n#### `flame_forge2d` - `v0.18.2`\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n#### `flame_isolate` - `v0.6.2`\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n#### `flame_kenney_xml` - `v0.1.1`\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n#### `flame_lint` - `v1.2.1`\n\n - **FIX**: Update version of lints to comply with new pub requirements ([#3223](https://github.com/flame-engine/flame/issues/3223)). ([1b0bee72](https://github.com/flame-engine/flame/commit/1b0bee726b5937f73d4be5e304bc8780aa3ca6f0))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n#### `flame_lottie` - `v0.4.2`\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n#### `flame_markdown` - `v0.2.2`\n\n - **FEAT**: Support inline code blocks on markdown rich text ([#3186](https://github.com/flame-engine/flame/issues/3186)). ([67e069c0](https://github.com/flame-engine/flame/commit/67e069c00dcb32c258231a326b0918739c6f80e6))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n#### `flame_noise` - `v0.3.2`\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n#### `flame_oxygen` - `v0.2.3`\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n#### `flame_rive` - `v1.10.3`\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n#### `flame_spine` - `v0.2.2`\n\n - **DOCS**: Homepage link typo for flame_spine ([#3277](https://github.com/flame-engine/flame/issues/3277)). ([f76355f1](https://github.com/flame-engine/flame/commit/f76355f151a61fa0eddb5356b7e2a7c27b96c221))\n\n#### `flame_splash_screen` - `v0.3.1`\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n#### `flame_sprite_fusion` - `v0.1.3`\n\n - **FIX**: Add nativeAngle to constructors where it makes sense ([#3197](https://github.com/flame-engine/flame/issues/3197)). ([e8704934](https://github.com/flame-engine/flame/commit/e8704934b19d9ed1982d35ce62819f01ac3de189))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n#### `flame_svg` - `v1.11.0`\n\n - **FEAT**: Fixing tests on flutter 3.24.0 ([#3259](https://github.com/flame-engine/flame/issues/3259)). ([bf9a2481](https://github.com/flame-engine/flame/commit/bf9a2481fbeb77413a26ae96b57843ca51411f9f))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n#### `flame_test` - `v1.17.0`\n\n - **FEAT**: Add a closeToVector3 matcher to flame_test ([#3242](https://github.com/flame-engine/flame/issues/3242)). ([965b684a](https://github.com/flame-engine/flame/commit/965b684a286ae2e2a89ba303839004d0b12cb3ef))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n#### `flame_texturepacker` - `v4.1.0`\n\n - **PERF**: Optimize `TexturePackerSprite` when sprites do not need to be rotated ([#3236](https://github.com/flame-engine/flame/issues/3236)). ([e9512e9b](https://github.com/flame-engine/flame/commit/e9512e9b28188476d5956e875430f1ef195f5882))\n - **FEAT**: Enhance TexturePackerSprite ([#3224](https://github.com/flame-engine/flame/issues/3224)). ([0b0a6c1b](https://github.com/flame-engine/flame/commit/0b0a6c1bacfca8772d1b9518e9433d994e68bae1))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n#### `flame_tiled` - `v1.20.3`\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n#### `jenny` - `v1.3.2`\n\n - **FIX**: Fix analyze issue on main ([#3265](https://github.com/flame-engine/flame/issues/3265)). ([f60b6e13](https://github.com/flame-engine/flame/commit/f60b6e134177495bcfd0f405a50f9e0e666b8b42))\n\n#### `flame_fire_atlas` - `v1.5.4`\n\n#### `flame_riverpod` - `v5.4.3`\n\n - Bump \"flame_riverpod\" to `5.4.3`.\n\n#### `flame_network_assets` - `v0.3.3`\n\n#### `flame_audio` - `v2.10.3`\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n\n## 2024-07-29\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_fire_atlas` - `v1.5.3`](#flame_fire_atlas---v153)\n\n---\n\n#### `flame_fire_atlas` - `v1.5.3`\n\n - **FEAT**: Adding group to flame fire atlas ([#3245](https://github.com/flame-engine/flame/issues/3245)). ([0fab444c](https://github.com/flame-engine/flame/commit/0fab444c3dd9ad8faa1e0e9e702150b950dbf30f))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n\n## 2024-05-27\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.18.0`](#flame---v1180)\n\nPackages with other changes:\n\n - [`behavior_tree` - `v0.1.2`](#behavior_tree---v012)\n - [`flame_behavior_tree` - `v0.1.2`](#flame_behavior_tree---v012)\n - [`flame_forge2d` - `v0.18.1`](#flame_forge2d---v0181)\n - [`flame_isolate` - `v0.6.1`](#flame_isolate---v061)\n - [`flame_markdown` - `v0.2.1`](#flame_markdown---v021)\n - [`flame_oxygen` - `v0.2.2`](#flame_oxygen---v022)\n - [`flame_sprite_fusion` - `v0.1.2`](#flame_sprite_fusion---v012)\n - [`flame_lottie` - `v0.4.1`](#flame_lottie---v041)\n - [`flame_noise` - `v0.3.1`](#flame_noise---v031)\n - [`flame_network_assets` - `v0.3.2`](#flame_network_assets---v032)\n - [`flame_spine` - `v0.2.1`](#flame_spine---v021)\n - [`flame_audio` - `v2.10.2`](#flame_audio---v2102)\n - [`flame_bloc` - `v1.12.0`](#flame_bloc---v1120)\n - [`flame_lint` - `v1.2.0`](#flame_lint---v120)\n - [`flame_rive` - `v1.10.2`](#flame_rive---v1102)\n - [`flame_texturepacker` - `v4.0.1`](#flame_texturepacker---v401)\n - [`flame_tiled` - `v1.20.2`](#flame_tiled---v1202)\n - [`jenny` - `v1.3.1`](#jenny---v131)\n - [`flame_test` - `v1.16.2`](#flame_test---v1162)\n - [`flame_fire_atlas` - `v1.5.2`](#flame_fire_atlas---v152)\n - [`flame_riverpod` - `v5.4.2`](#flame_riverpod---v542)\n - [`flame_svg` - `v1.10.2`](#flame_svg---v1102)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_test` - `v1.16.2`\n - `flame_fire_atlas` - `v1.5.2`\n - `flame_riverpod` - `v5.4.2`\n - `flame_svg` - `v1.10.2`\n\n---\n\n#### `flame` - `v1.18.0`\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n - **FIX**: Add key parameters to the rest of the components ([#3170](https://github.com/flame-engine/flame/issues/3170)). ([2477ea0f](https://github.com/flame-engine/flame/commit/2477ea0fcee99e71597983146f4af2dffd866971))\n - **FIX**: Invoke `setToStart` on child effect controller of wrapping effect controllers ([#3168](https://github.com/flame-engine/flame/issues/3168)). ([217c95f0](https://github.com/flame-engine/flame/commit/217c95f0a53fd5a7933bfa57833f951cc0037878))\n - **FIX**: Fix cascading and fallback propagation of text styles ([#3129](https://github.com/flame-engine/flame/issues/3129)). ([7b706d5f](https://github.com/flame-engine/flame/commit/7b706d5f63207aaf82d12a4b26233bc476771b1e))\n - **FEAT**: Add `onReleased` action to `AdvancedButtonComponent` which will be called within `onTapUp` ([#3152](https://github.com/flame-engine/flame/issues/3152)). ([2269732e](https://github.com/flame-engine/flame/commit/2269732e64a2acef2451d283c85b03e1101229ec))\n - **FEAT**: Support text align on new text rendering pipeline ([#3147](https://github.com/flame-engine/flame/issues/3147)). ([194d5536](https://github.com/flame-engine/flame/commit/194d5536560e464644bff8d5582a8ca8996539f5))\n - **FEAT**: Add missing parameters to InlineTextStyle ([#3146](https://github.com/flame-engine/flame/issues/3146)). ([ce9392ab](https://github.com/flame-engine/flame/commit/ce9392abd85fe5fd3ae6f766c3a2957275c6fb8c))\n - **FEAT**: Expand flame_lint to respect required pub.dev checks ([#3139](https://github.com/flame-engine/flame/issues/3139)). ([6e80bf5e](https://github.com/flame-engine/flame/commit/6e80bf5e679d1cdeeb9362d4103690b0b381161d))\n - **FEAT**: Add accessor to determine a TextElement size ([#3130](https://github.com/flame-engine/flame/issues/3130)). ([8a63a07a](https://github.com/flame-engine/flame/commit/8a63a07ae3b569c316eafa23f0378e00180e0963))\n - **FEAT**: Add ability to convert between TextPaint and InlineTextStyle ([#3128](https://github.com/flame-engine/flame/issues/3128)). ([6b63a57a](https://github.com/flame-engine/flame/commit/6b63a57a4888211b284f3a074c17519cb31341e0))\n - **FEAT**: Add completed future for effects ([#3123](https://github.com/flame-engine/flame/issues/3123)). ([5e967deb](https://github.com/flame-engine/flame/commit/5e967deb876ed39fa4ee6839471bbfbcd3b72463))\n - **FEAT**: Add custom long tap delay ([#3110](https://github.com/flame-engine/flame/issues/3110)). ([a95d7df6](https://github.com/flame-engine/flame/commit/a95d7df606bd2119423cc8a7ae51cacfb7b4dbed))\n - **DOCS**: Update the dartdocs for `FixedResolutionViewport` ([#3132](https://github.com/flame-engine/flame/issues/3132)). ([db4b6fd6](https://github.com/flame-engine/flame/commit/db4b6fd6fa5968462d3f89238a92edbb93e4898d))\n - **BREAKING** **FIX**: Update IsometricTileMapComponent to have better defined position and size ([#3142](https://github.com/flame-engine/flame/issues/3142)). ([9a7bdc74](https://github.com/flame-engine/flame/commit/9a7bdc7439322a26a388e3ac1b9c1a7c43742222))\n\n#### `behavior_tree` - `v0.1.2`\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n\n#### `flame_behavior_tree` - `v0.1.2`\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n\n#### `flame_forge2d` - `v0.18.1`\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n\n#### `flame_isolate` - `v0.6.1`\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n\n#### `flame_markdown` - `v0.2.1`\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n\n#### `flame_oxygen` - `v0.2.2`\n\n#### `flame_sprite_fusion` - `v0.1.2`\n\n#### `flame_lottie` - `v0.4.1`\n\n#### `flame_noise` - `v0.3.1`\n\n#### `flame_network_assets` - `v0.3.2`\n\n#### `flame_spine` - `v0.2.1`\n\n#### `flame_audio` - `v2.10.2`\n\n - **DOCS**: Update flame_audio readme ([#3119](https://github.com/flame-engine/flame/issues/3119)). ([843984de](https://github.com/flame-engine/flame/commit/843984dee5f5f6afd351ef29ad2adb39650f30bb))\n\n#### `flame_bloc` - `v1.12.0`\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n - **FIX**: Call `super.onLoad` from `FlameBlockReader` ([#3175](https://github.com/flame-engine/flame/issues/3175)). ([349f7bd7](https://github.com/flame-engine/flame/commit/349f7bd71437abad666d05f973b6983970ccd0c6))\n - **FEAT**: Expand flame_lint to respect required pub.dev checks ([#3139](https://github.com/flame-engine/flame/issues/3139)). ([6e80bf5e](https://github.com/flame-engine/flame/commit/6e80bf5e679d1cdeeb9362d4103690b0b381161d))\n\n#### `flame_lint` - `v1.2.0`\n\n - **FEAT**: Expand flame_lint to respect required pub.dev checks ([#3139](https://github.com/flame-engine/flame/issues/3139)). ([6e80bf5e](https://github.com/flame-engine/flame/commit/6e80bf5e679d1cdeeb9362d4103690b0b381161d))\n\n#### `flame_rive` - `v1.10.2`\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n\n#### `flame_texturepacker` - `v4.0.1`\n\n - **FIX**: TexturePacker fixes the wrong path for the atlas file. ([#3124](https://github.com/flame-engine/flame/issues/3124)). ([69f5c388](https://github.com/flame-engine/flame/commit/69f5c388ce4e0a64ba5f7331a596777a9eab1e40))\n\n#### `flame_tiled` - `v1.20.2`\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n\n#### `jenny` - `v1.3.1`\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n\n\n## 2024-04-05\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame_texturepacker` - `v4.0.0`](#flame_texturepacker---v400)\n\nPackages with other changes:\n\n - [`flame_forge2d` - `v0.18.0`](#flame_forge2d---v0180)\n - [`flame_tiled` - `v1.20.1`](#flame_tiled---v1201)\n\n---\n\n#### `flame_texturepacker` - `v4.0.0`\n\n - **BREAKING** **FEAT**: Use `Flame.images` in flame_texturepacker ([#3103](https://github.com/flame-engine/flame/issues/3103)). ([418cc578](https://github.com/flame-engine/flame/commit/418cc578053d969a4a5c3789b1713b9e1a4b3bdd))\n\n#### `flame_forge2d` - `v0.18.0`\n\n - **FIX**: Use camera argument name in Forge2DGame ([#3115](https://github.com/flame-engine/flame/issues/3115)). ([9d97b123](https://github.com/flame-engine/flame/commit/9d97b12348161b4b150ee4166ba552f28d5f9d8b))\n - **DOCS**: Upgrade dashbook version ([#3109](https://github.com/flame-engine/flame/issues/3109)). ([a717bcb4](https://github.com/flame-engine/flame/commit/a717bcb475a5604c5d8c66a3a5ac53f0dc173109))\n\n#### `flame_tiled` - `v1.20.1`\n\n - **FIX**: Respect tile offset when drawing tiles ([#3112](https://github.com/flame-engine/flame/issues/3112)). ([e3477474](https://github.com/flame-engine/flame/commit/e34774743038bc75fec14afc3c753fa997e71577))\n\n\n## 2024-03-29\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.17.0`](#flame---v1170)\n\nPackages with other changes:\n\n - [`flame_forge2d` - `v0.17.1`](#flame_forge2d---v0171)\n - [`flame_oxygen` - `v0.2.1`](#flame_oxygen---v021)\n - [`behavior_tree` - `v0.1.1`](#behavior_tree---v011)\n - [`flame_behavior_tree` - `v0.1.1`](#flame_behavior_tree---v011)\n - [`flame_network_assets` - `v0.3.1`](#flame_network_assets---v031)\n - [`flame_sprite_fusion` - `v0.1.1`](#flame_sprite_fusion---v011)\n - [`flame_texturepacker` - `v3.2.0`](#flame_texturepacker---v320)\n - [`flame_tiled` - `v1.20.0`](#flame_tiled---v1200)\n - [`flame_test` - `v1.16.1`](#flame_test---v1161)\n - [`flame_isolate` - `v0.6.0+1`](#flame_isolate---v0601)\n - [`flame_fire_atlas` - `v1.5.1`](#flame_fire_atlas---v151)\n - [`flame_audio` - `v2.10.1`](#flame_audio---v2101)\n - [`flame_spine` - `v0.2.0+1`](#flame_spine---v0201)\n - [`flame_bloc` - `v1.11.1`](#flame_bloc---v1111)\n - [`flame_lottie` - `v0.4.0+1`](#flame_lottie---v0401)\n - [`flame_markdown` - `v0.2.0+1`](#flame_markdown---v0201)\n - [`flame_rive` - `v1.10.1`](#flame_rive---v1101)\n - [`flame_noise` - `v0.3.0+1`](#flame_noise---v0301)\n - [`flame_riverpod` - `v5.4.1`](#flame_riverpod---v541)\n - [`flame_svg` - `v1.10.1`](#flame_svg---v1101)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_test` - `v1.16.1`\n - `flame_isolate` - `v0.6.0+1`\n - `flame_fire_atlas` - `v1.5.1`\n - `flame_audio` - `v2.10.1`\n - `flame_spine` - `v0.2.0+1`\n - `flame_bloc` - `v1.11.1`\n - `flame_lottie` - `v0.4.0+1`\n - `flame_markdown` - `v0.2.0+1`\n - `flame_rive` - `v1.10.1`\n - `flame_noise` - `v0.3.0+1`\n - `flame_riverpod` - `v5.4.1`\n - `flame_svg` - `v1.10.1`\n\n---\n\n#### `flame` - `v1.17.0`\n\n - **REFACTOR**: Change the ClipComponent factory Constructor to redirect Constructor ([#3089](https://github.com/flame-engine/flame/issues/3089)). ([cc035fb4](https://github.com/flame-engine/flame/commit/cc035fb4a3e123473d4e5e0db1fa0253e533bc61))\n - **FIX**: Call `render` properly from nested `FlameGame`s ([#3106](https://github.com/flame-engine/flame/issues/3106)). ([cb1e3701](https://github.com/flame-engine/flame/commit/cb1e37014bac7bb68b647234b718a37e26ad7559))\n - **FIX**: CircleHitbox should properly detect when ray is outside ([#3100](https://github.com/flame-engine/flame/issues/3100)). ([8cd9e123](https://github.com/flame-engine/flame/commit/8cd9e12319c0715def655fcf42f3976fa5f45e11))\n - **FIX**: Clamp opacity set by the `ColorEffect` to 1.0 ([#3069](https://github.com/flame-engine/flame/issues/3069)). ([9282cc38](https://github.com/flame-engine/flame/commit/9282cc38f06cd6c87ed3a1880d28d5c9f290cc04))\n - **FIX**: FutureOr return type of ComponentViewportMargin.onLoad ([#3059](https://github.com/flame-engine/flame/issues/3059)). ([72678c67](https://github.com/flame-engine/flame/commit/72678c676020480beae1d944ee687fd73eac9cf7))\n - **FIX**: Size for `SpriteComponent.fromImage` should be nullable ([#3054](https://github.com/flame-engine/flame/issues/3054)). ([2ed71a3c](https://github.com/flame-engine/flame/commit/2ed71a3c89b3c2182828f2812d8515811483f4d5))\n - **FIX**: Check for removing state while adding a child ([#3050](https://github.com/flame-engine/flame/issues/3050)). ([3a24a51d](https://github.com/flame-engine/flame/commit/3a24a51d108b1138ac3dd735956f4276f16b2974))\n - **FEAT**: Add onFinished callback to ScrollTextBoxComponent ([#3105](https://github.com/flame-engine/flame/issues/3105)). ([233cc94c](https://github.com/flame-engine/flame/commit/233cc94c557e0af2fcf7599943ddf75180abf801))\n - **FEAT**: Add `copyWith` method on the `TextBoxConfig` ([#3099](https://github.com/flame-engine/flame/issues/3099)). ([b946ba70](https://github.com/flame-engine/flame/commit/b946ba70cbfb5793a8d4d7c61d6ba029fbc303ab))\n - **FEAT**: Component tree for the devtools extension tab ([#3094](https://github.com/flame-engine/flame/issues/3094)). ([bf5d68e9](https://github.com/flame-engine/flame/commit/bf5d68e9b5147dd5e7c10d72bf9c2f705733d688))\n - **FEAT**: Add PositionComponent.toString ([#3095](https://github.com/flame-engine/flame/issues/3095)). ([b1f01986](https://github.com/flame-engine/flame/commit/b1f01986b440ac18bb35b0d76963b2c66f49ea33))\n - **FEAT**: Add SpriteBatch.replace to allow the replacement of the batch information ([#3079](https://github.com/flame-engine/flame/issues/3079)). ([bf3c282d](https://github.com/flame-engine/flame/commit/bf3c282dd669e9a32a550b86770dba7fb8472afa))\n - **FEAT**: Initial functionality of flame_devtools ([#3061](https://github.com/flame-engine/flame/issues/3061)). ([c92910c6](https://github.com/flame-engine/flame/commit/c92910c688f5dc4463e129132759102e7ebf2e36))\n - **FEAT**: Add `HasPerformanceTracker` mixin on `Game` ([#3043](https://github.com/flame-engine/flame/issues/3043)). ([6270353a](https://github.com/flame-engine/flame/commit/6270353af9a6ec58ee9275ddfa6a8b26276a2c20))\n - **BREAKING** **REFACTOR**: Use HasTimeScale for Route ([#3064](https://github.com/flame-engine/flame/issues/3064)). ([30fde805](https://github.com/flame-engine/flame/commit/30fde805b4650cc802f9908f9a1149dae19669d4))\n - **BREAKING** **FIX**: Removed unused parameters from SpriteWidget ([#3074](https://github.com/flame-engine/flame/issues/3074)). ([f49d24c0](https://github.com/flame-engine/flame/commit/f49d24c02dd0d9b781926908bad1fb6dfcbda5f2))\n\n#### `flame_forge2d` - `v0.17.1`\n\n - **FIX**: Null gravity override by Forge2dGame ([#3092](https://github.com/flame-engine/flame/issues/3092)). ([3c35d59b](https://github.com/flame-engine/flame/commit/3c35d59b4a4ec064106d24a17e748005a20d9fde))\n\n#### `flame_oxygen` - `v0.2.1`\n\n - **FIX**: Updated oxygen dep to v0.3.1 and added removing components ([#3087](https://github.com/flame-engine/flame/issues/3087)). ([8f50c927](https://github.com/flame-engine/flame/commit/8f50c9279581999b4ff7f506682148425b248e28))\n\n#### `behavior_tree` - `v0.1.1`\n\n - **FEAT**: Add initial version of `behavior_tree` and `flame_behavior_tree` package ([#3045](https://github.com/flame-engine/flame/issues/3045)). ([faf2df4b](https://github.com/flame-engine/flame/commit/faf2df4b8c68015a1bfbdd96f93c950cb14963ef))\n\n#### `flame_behavior_tree` - `v0.1.1`\n\n - **FEAT**: Add initial version of `behavior_tree` and `flame_behavior_tree` package ([#3045](https://github.com/flame-engine/flame/issues/3045)). ([faf2df4b](https://github.com/flame-engine/flame/commit/faf2df4b8c68015a1bfbdd96f93c950cb14963ef))\n\n#### `flame_network_assets` - `v0.3.1`\n\n - **FEAT**: Update http dependency on flame_network_assets ([#3084](https://github.com/flame-engine/flame/issues/3084)). ([e3e755c6](https://github.com/flame-engine/flame/commit/e3e755c6dec35f36b4a42893afeea5f64ff025b7))\n\n#### `flame_sprite_fusion` - `v0.1.1`\n\n - **FEAT**: Add initial version of `flame_sprite_fusion` package ([#3062](https://github.com/flame-engine/flame/issues/3062)). ([1c51334e](https://github.com/flame-engine/flame/commit/1c51334e865ae7000f93832574e24707e8c9dfa0))\n\n#### `flame_texturepacker` - `v3.2.0`\n\n - **REFACTOR**: Deprecate `fromAtlas` in favour of `atlasFromAssets` and `atlasFromStorage` ([#3098](https://github.com/flame-engine/flame/issues/3098)). ([6c8190b7](https://github.com/flame-engine/flame/commit/6c8190b7215671e7d6e1e271b6aac2a9723ec20d))\n - **FEAT**: Support for new atlas format and rotated sprites ([#3097](https://github.com/flame-engine/flame/issues/3097)). ([ed690b30](https://github.com/flame-engine/flame/commit/ed690b3048924749f829c7c44156e258bf4ab3e7))\n - **FEAT**(flame_texturepacker): Expose TexturePackerAtlas ([#3047](https://github.com/flame-engine/flame/issues/3047)). ([892052b9](https://github.com/flame-engine/flame/commit/892052b99a21a8e371c4163e1e1918fd187c6e11))\n\n#### `flame_tiled` - `v1.20.0`\n\n - **FEAT**: Export `TileAtlas` from `flame_tiled` package ([#3049](https://github.com/flame-engine/flame/issues/3049)). ([41e9e4e3](https://github.com/flame-engine/flame/commit/41e9e4e38c643b07a3a7269b1cd8d3fa60cbeebb))\n\n\n## 2024-03-15\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_riverpod` - `v5.4.0`](#flame_riverpod---v540)\n\n---\n\n#### `flame_riverpod` - `v5.4.0`\n\n - **FIX**: Resolve logic error with assignment of ComponentRef's game property in flame_riverpod ([#3082](https://github.com/flame-engine/flame/issues/3082)). ([b44011fd](https://github.com/flame-engine/flame/commit/b44011fd714ec5919de5407f53d0772f31ed1a13))\n - **FIX**: Resolve breaking changes from Riverpod affecting flame_riverpod ([#3080](https://github.com/flame-engine/flame/issues/3080)). ([e3aaa7c2](https://github.com/flame-engine/flame/commit/e3aaa7c21d89a6679c3ae70de6e676d1f11501fa))\n - **FIX**: Implement necessary `ProviderSubscription` getters ([#3075](https://github.com/flame-engine/flame/issues/3075)). ([17da92b2](https://github.com/flame-engine/flame/commit/17da92b2d1c527162106778f459d72f19a5c5607))\n - **FEAT**: Allow ComponentRef access in RiverpodGameMixin ([#3010](https://github.com/flame-engine/flame/issues/3010)). ([44b10fd6](https://github.com/flame-engine/flame/commit/44b10fd60c61392d449a8d12020c45724ad19625))\n\n\n## 2024-02-17\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.16.0`](#flame---v1160)\n - [`flame_audio` - `v2.10.0`](#flame_audio---v2100)\n - [`flame_bloc` - `v1.11.0`](#flame_bloc---v1110)\n - [`flame_fire_atlas` - `v1.5.0`](#flame_fire_atlas---v150)\n - [`flame_rive` - `v1.10.0`](#flame_rive---v1100)\n - [`flame_riverpod` - `v5.3.0`](#flame_riverpod---v530)\n - [`flame_svg` - `v1.10.0`](#flame_svg---v1100)\n - [`flame_test` - `v1.16.0`](#flame_test---v1160)\n - [`flame_texturepacker` - `v3.1.0`](#flame_texturepacker---v310)\n - [`flame_tiled` - `v1.19.0`](#flame_tiled---v1190)\n - [`flame_forge2d` - `v0.17.0`](#flame_forge2d---v0170)\n - [`flame_isolate` - `v0.6.0`](#flame_isolate---v060)\n - [`flame_lottie` - `v0.4.0`](#flame_lottie---v040)\n - [`flame_markdown` - `v0.2.0`](#flame_markdown---v020)\n - [`flame_network_assets` - `v0.3.0`](#flame_network_assets---v030)\n - [`flame_noise` - `v0.3.0`](#flame_noise---v030)\n - [`flame_oxygen` - `v0.2.0`](#flame_oxygen---v020)\n - [`flame_spine` - `v0.2.0`](#flame_spine---v020)\n - [`flame_splash_screen` - `v0.3.0`](#flame_splash_screen---v030)\n\nPackages with other changes:\n\n - [`jenny` - `v1.3.0`](#jenny---v130)\n\n---\n\n#### `flame` - `v1.16.0`\n\n - **REFACTOR**: Fix unrelated types reported by DCM ([#3023](https://github.com/flame-engine/flame/issues/3023)). ([1d020a52](https://github.com/flame-engine/flame/commit/1d020a525b81df1cb45345d3e36a9c4e9caf701e))\n - **FIX**: Vertices in `PolygonComponent` should subtract vertices positioning ([#3040](https://github.com/flame-engine/flame/issues/3040)). ([4f053ed7](https://github.com/flame-engine/flame/commit/4f053ed74c09d4e19a53694130b5d5c0d3e23aa6))\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_audio` - `v2.10.0`\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_bloc` - `v1.11.0`\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_fire_atlas` - `v1.5.0`\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_rive` - `v1.10.0`\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_riverpod` - `v5.3.0`\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_svg` - `v1.10.0`\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_test` - `v1.16.0`\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_texturepacker` - `v3.1.0`\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_tiled` - `v1.19.0`\n\n - **FEAT**: Add TiledObjectHealpers extension on TiledObject ([#3032](https://github.com/flame-engine/flame/issues/3032)). ([78380b9d](https://github.com/flame-engine/flame/commit/78380b9d3bb895e20f382c4a1227bcc11e5038b9))\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_forge2d` - `v0.17.0`\n\n - **FIX**: BodyComponent fixtures should test with global point ([#3042](https://github.com/flame-engine/flame/issues/3042)). ([7c3038be](https://github.com/flame-engine/flame/commit/7c3038becba91550eb47a033cbed7208d570e012))\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_isolate` - `v0.6.0`\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_lottie` - `v0.4.0`\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_markdown` - `v0.2.0`\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_network_assets` - `v0.3.0`\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_noise` - `v0.3.0`\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_oxygen` - `v0.2.0`\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_spine` - `v0.2.0`\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `flame_splash_screen` - `v0.3.0`\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n#### `jenny` - `v1.3.0`\n\n - **FEAT**: Add new methods to CommandStorage to support more arguments ([#3035](https://github.com/flame-engine/flame/issues/3035)). ([21922620](https://github.com/flame-engine/flame/commit/219226201a8d0c6e301c388299277be95b585c0e))\n\n\n## 2024-02-11\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_svg` - `v1.9.0`](#flame_svg---v190)\n\n---\n\n#### `flame_svg` - `v1.9.0`\n\n - **FEAT**: Add `loadFromString` to Svg class ([#3030](https://github.com/flame-engine/flame/issues/3030)). ([b0cafb2a](https://github.com/flame-engine/flame/commit/b0cafb2a5561de136af93eb7a09df37b93d38ce0))\n\n\n## 2024-02-07\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame_noise` - `v0.2.0`](#flame_noise---v020)\n - [`flame_texturepacker` - `v3.0.0`](#flame_texturepacker---v300)\n\nPackages with other changes:\n\n - [`flame` - `v1.15.0`](#flame---v1150)\n - [`flame_isolate` - `v0.5.1`](#flame_isolate---v051)\n - [`flame_riverpod` - `v5.2.0`](#flame_riverpod---v520)\n - [`flame_test` - `v1.15.4`](#flame_test---v1154)\n - [`flame_oxygen` - `v0.1.9+8`](#flame_oxygen---v0198)\n - [`flame_tiled` - `v1.18.4`](#flame_tiled---v1184)\n - [`flame_fire_atlas` - `v1.4.8`](#flame_fire_atlas---v148)\n - [`flame_audio` - `v2.1.8`](#flame_audio---v218)\n - [`flame_spine` - `v0.1.1+10`](#flame_spine---v01110)\n - [`flame_bloc` - `v1.10.10`](#flame_bloc---v11010)\n - [`flame_rive` - `v1.9.11`](#flame_rive---v1911)\n - [`flame_lottie` - `v0.3.0+8`](#flame_lottie---v0308)\n - [`flame_markdown` - `v0.1.1+8`](#flame_markdown---v0118)\n - [`flame_forge2d` - `v0.16.0+5`](#flame_forge2d---v01605)\n - [`flame_svg` - `v1.8.10`](#flame_svg---v1810)\n - [`flame_network_assets` - `v0.2.0+13`](#flame_network_assets---v02013)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_oxygen` - `v0.1.9+8`\n - `flame_tiled` - `v1.18.4`\n - `flame_fire_atlas` - `v1.4.8`\n - `flame_audio` - `v2.1.8`\n - `flame_spine` - `v0.1.1+10`\n - `flame_bloc` - `v1.10.10`\n - `flame_rive` - `v1.9.11`\n - `flame_lottie` - `v0.3.0+8`\n - `flame_markdown` - `v0.1.1+8`\n - `flame_forge2d` - `v0.16.0+5`\n - `flame_svg` - `v1.8.10`\n - `flame_network_assets` - `v0.2.0+13`\n\n---\n\n#### `flame_noise` - `v0.2.0`\n\n - **BREAKING** **FEAT**: Update flame_noise to use latest version of fast_noise ([#3015](https://github.com/flame-engine/flame/issues/3015)). ([2fd84c84](https://github.com/flame-engine/flame/commit/2fd84c846f808bf593ef568150ffb49eecaebf30))\n\n#### `flame_texturepacker` - `v3.0.0`\n\n - **REFACTOR**: Update `flame_texturepacker`'s file structure ([#3014](https://github.com/flame-engine/flame/issues/3014)). ([982f2263](https://github.com/flame-engine/flame/commit/982f2263daae882fb456e750298c874b77c5471b))\n - **FEAT**: TexturePacker atlas can be generated from device's file ([#3006](https://github.com/flame-engine/flame/issues/3006)). ([4e6968a0](https://github.com/flame-engine/flame/commit/4e6968a05c659aae09e9f613870c6e5b326f4b44))\n - **BREAKING** **FEAT**: Transfer flame_texturepacker to monorepo ([#2987](https://github.com/flame-engine/flame/issues/2987)). ([45c87ddf](https://github.com/flame-engine/flame/commit/45c87ddfb668b035f095cdc17f1a4b7662a3ae11))\n\n#### `flame` - `v1.15.0`\n\n - **REFACTOR**: Minimize `Vector2` creation in `IsometricTileMapComponent` ([#3018](https://github.com/flame-engine/flame/issues/3018)). ([5d3be313](https://github.com/flame-engine/flame/commit/5d3be3137a177c8900158fce10cffc01f729ed7a))\n - **FIX**: Set margins of `JoystickComponent` properly ([#3019](https://github.com/flame-engine/flame/issues/3019)). ([e27818d8](https://github.com/flame-engine/flame/commit/e27818d8721c577507411fca085859335206391f))\n - **FIX**: Properly update sprites in SpriteButtonComponent ([#3013](https://github.com/flame-engine/flame/issues/3013)). ([23cf8b9d](https://github.com/flame-engine/flame/commit/23cf8b9de81cade9ce90b8401c39432bc70f9d0d))\n - **FIX**: Lifecycle completers to be called for FlameGame ([#3007](https://github.com/flame-engine/flame/issues/3007)). ([3804f524](https://github.com/flame-engine/flame/commit/3804f52434cf1bcaf28b501bf96858ecd3636164))\n - **FIX**: CameraComponent no longer throws Concurrent modification on stop ([#2997](https://github.com/flame-engine/flame/issues/2997)). ([6a1059b0](https://github.com/flame-engine/flame/commit/6a1059b0a6e381020cdaa7a96ceecbcaa45b9a42))\n - **FIX**: Updated PolygonComponent.containsPoint to account for concave polygons ([#2979](https://github.com/flame-engine/flame/issues/2979)). ([a6fe62a2](https://github.com/flame-engine/flame/commit/a6fe62a2c3b74d9b4781531e0c53470b6d3242ea))\n - **FIX**: Add missing generic to `ComponentViewportMargin` ([#2983](https://github.com/flame-engine/flame/issues/2983)). ([1d9fe613](https://github.com/flame-engine/flame/commit/1d9fe6139287b984a05e0056c279b9c5d277e026))\n - **FEAT**: Add support for base64 encoded images to be manually added to Images cache. ([#3008](https://github.com/flame-engine/flame/issues/3008)). ([1e56293c](https://github.com/flame-engine/flame/commit/1e56293c1f89e7636c97b0ed518642bd493d7a40))\n - **FEAT**: Make `Component.key` public ([#2988](https://github.com/flame-engine/flame/issues/2988)). ([7fbd5af9](https://github.com/flame-engine/flame/commit/7fbd5af935211264822f89bc1beb4062d3efdf7a))\n - **FEAT**: Add a hitboxFilter argument to raycast() ([#2968](https://github.com/flame-engine/flame/issues/2968)). ([d7c53e23](https://github.com/flame-engine/flame/commit/d7c53e230f32b4b224e23483d99b0b276d14686f))\n\n#### `flame_isolate` - `v0.5.1`\n\n - **FEAT**: Bumped integral_isolates package for flame_isolate ([#2994](https://github.com/flame-engine/flame/issues/2994)). ([3c38ee60](https://github.com/flame-engine/flame/commit/3c38ee6058e7c8b7546c3fcdb1b08e3e40ba138b))\n\n#### `flame_riverpod` - `v5.2.0`\n\n - **FIX**: Add Template param to RiverpodGameMixin ([#2972](https://github.com/flame-engine/flame/issues/2972)). ([622c8553](https://github.com/flame-engine/flame/commit/622c855318b6c1731891b023ddc6429ba1f32329))\n - **FEAT**: Make `Component.key` public ([#2988](https://github.com/flame-engine/flame/issues/2988)). ([7fbd5af9](https://github.com/flame-engine/flame/commit/7fbd5af935211264822f89bc1beb4062d3efdf7a))\n\n#### `flame_test` - `v1.15.4`\n\n - **FIX**: Lifecycle completers to be called for FlameGame ([#3007](https://github.com/flame-engine/flame/issues/3007)). ([3804f524](https://github.com/flame-engine/flame/commit/3804f52434cf1bcaf28b501bf96858ecd3636164))\n\n\n## 2024-01-07\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_riverpod` - `v5.1.5`](#flame_riverpod---v515)\n\n---\n\n#### `flame_riverpod` - `v5.1.5`\n\n - **FIX**: Change return type of RiverpodComponentMixin.onLoad to FutureOr<void> ([#2964](https://github.com/flame-engine/flame/issues/2964)). ([7ac80a78](https://github.com/flame-engine/flame/commit/7ac80a78e95b06bb1287fb74773634483d80b1c9))\n\n\n## 2024-01-04\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.14.0`](#flame---v1140)\n\nPackages with other changes:\n\n - [`flame_forge2d` - `v0.16.0+4`](#flame_forge2d---v01604)\n - [`flame_test` - `v1.15.3`](#flame_test---v1153)\n - [`flame_tiled` - `v1.18.3`](#flame_tiled---v1183)\n - [`flame_oxygen` - `v0.1.9+7`](#flame_oxygen---v0197)\n - [`flame_isolate` - `v0.5.0+7`](#flame_isolate---v0507)\n - [`flame_fire_atlas` - `v1.4.7`](#flame_fire_atlas---v147)\n - [`flame_audio` - `v2.1.7`](#flame_audio---v217)\n - [`flame_spine` - `v0.1.1+9`](#flame_spine---v0119)\n - [`flame_bloc` - `v1.10.9`](#flame_bloc---v1109)\n - [`flame_lottie` - `v0.3.0+7`](#flame_lottie---v0307)\n - [`flame_markdown` - `v0.1.1+7`](#flame_markdown---v0117)\n - [`flame_rive` - `v1.9.10`](#flame_rive---v1910)\n - [`flame_noise` - `v0.1.1+12`](#flame_noise---v01112)\n - [`flame_riverpod` - `v5.1.4`](#flame_riverpod---v514)\n - [`flame_svg` - `v1.8.9`](#flame_svg---v189)\n - [`flame_network_assets` - `v0.2.0+12`](#flame_network_assets---v02012)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_test` - `v1.15.3`\n - `flame_tiled` - `v1.18.3`\n - `flame_oxygen` - `v0.1.9+7`\n - `flame_isolate` - `v0.5.0+7`\n - `flame_fire_atlas` - `v1.4.7`\n - `flame_audio` - `v2.1.7`\n - `flame_spine` - `v0.1.1+9`\n - `flame_bloc` - `v1.10.9`\n - `flame_lottie` - `v0.3.0+7`\n - `flame_markdown` - `v0.1.1+7`\n - `flame_rive` - `v1.9.10`\n - `flame_noise` - `v0.1.1+12`\n - `flame_riverpod` - `v5.1.4`\n - `flame_svg` - `v1.8.9`\n - `flame_network_assets` - `v0.2.0+12`\n\n---\n\n#### `flame` - `v1.14.0`\n\n - **FIX**: Set hitbox `debugColor` to yellow ([#2958](https://github.com/flame-engine/flame/issues/2958)). ([6858eae0](https://github.com/flame-engine/flame/commit/6858eae0766225bb7c940c2aa453459063f7d514))\n - **FIX**: Consider displaced hitboxes in GestureHitboxes mixin ([#2957](https://github.com/flame-engine/flame/issues/2957)). ([1085518f](https://github.com/flame-engine/flame/commit/1085518fe279e674bef9a7b938d59926472511f3))\n - **FIX**: PolygonComponent.containsLocalPoint to use anchor ([#2953](https://github.com/flame-engine/flame/issues/2953)). ([7969321e](https://github.com/flame-engine/flame/commit/7969321e8662515aa9efe305831ff36d51dd43cb))\n - **FEAT**: Notifier for changing current sprite/animation in group components ([#2956](https://github.com/flame-engine/flame/issues/2956)). ([75cf2390](https://github.com/flame-engine/flame/commit/75cf23908e5d509a25cd794d6810162f22b978cb))\n - **BREAKING** **REFACTOR**: Remove the Projector interface that is no longer used for coordinate transformations ([#2955](https://github.com/flame-engine/flame/issues/2955)). ([0979dc97](https://github.com/flame-engine/flame/commit/0979dc97f54af1b71b200ced609d874d390c1ca6))\n\n#### `flame_forge2d` - `v0.16.0+4`\n\n - **FIX**: Wake up bodies on gravity change ([#2954](https://github.com/flame-engine/flame/issues/2954)). ([4f58329c](https://github.com/flame-engine/flame/commit/4f58329ceacef84d91cd41019d72bc2351bc50cd))\n\n\n## 2023-12-22\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_riverpod` - `v5.1.3`](#flame_riverpod---v513)\n\n---\n\n#### `flame_riverpod` - `v5.1.3`\n\n - **FIX**: Fix logic inside flame_riverpod persistent frame callback. ([#2950](https://github.com/flame-engine/flame/issues/2950)). ([230fb88f](https://github.com/flame-engine/flame/commit/230fb88fa9f9d82711461d10fe4aff9f8520cd29))\n\n\n## 2023-12-21\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.13.1`](#flame---v1131)\n - [`flame_riverpod` - `v5.1.2`](#flame_riverpod---v512)\n - [`flame_test` - `v1.15.3`](#flame_test---v1153)\n - [`flame_tiled` - `v1.18.3`](#flame_tiled---v1183)\n - [`flame_oxygen` - `v0.1.9+7`](#flame_oxygen---v0197)\n - [`flame_isolate` - `v0.5.0+7`](#flame_isolate---v0507)\n - [`flame_fire_atlas` - `v1.4.7`](#flame_fire_atlas---v147)\n - [`flame_audio` - `v2.1.7`](#flame_audio---v217)\n - [`flame_spine` - `v0.1.1+9`](#flame_spine---v0119)\n - [`flame_bloc` - `v1.10.9`](#flame_bloc---v1109)\n - [`flame_lottie` - `v0.3.0+7`](#flame_lottie---v0307)\n - [`flame_markdown` - `v0.1.1+7`](#flame_markdown---v0117)\n - [`flame_rive` - `v1.9.10`](#flame_rive---v1910)\n - [`flame_forge2d` - `v0.16.0+4`](#flame_forge2d---v01604)\n - [`flame_noise` - `v0.1.1+12`](#flame_noise---v01112)\n - [`flame_svg` - `v1.8.9`](#flame_svg---v189)\n - [`flame_network_assets` - `v0.2.0+12`](#flame_network_assets---v02012)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_test` - `v1.15.3`\n - `flame_tiled` - `v1.18.3`\n - `flame_oxygen` - `v0.1.9+7`\n - `flame_isolate` - `v0.5.0+7`\n - `flame_fire_atlas` - `v1.4.7`\n - `flame_audio` - `v2.1.7`\n - `flame_spine` - `v0.1.1+9`\n - `flame_bloc` - `v1.10.9`\n - `flame_lottie` - `v0.3.0+7`\n - `flame_markdown` - `v0.1.1+7`\n - `flame_rive` - `v1.9.10`\n - `flame_forge2d` - `v0.16.0+4`\n - `flame_noise` - `v0.1.1+12`\n - `flame_svg` - `v1.8.9`\n - `flame_network_assets` - `v0.2.0+12`\n\n---\n\n#### `flame` - `v1.13.1`\n\n - **FIX**: The `visibleGameSize` should be based on `viewport.virtualSize` ([#2945](https://github.com/flame-engine/flame/issues/2945)). ([bd130b71](https://github.com/flame-engine/flame/commit/bd130b711b5cb486b8f05225711c6e6c3fe574e6))\n - **FEAT**: Adding ability for a SpawnComponent to not auto start ([#2947](https://github.com/flame-engine/flame/issues/2947)). ([37c7a075](https://github.com/flame-engine/flame/commit/37c7a075a37cfc7c298f02542715b18e87f4cf99))\n\n#### `flame_riverpod` - `v5.1.2`\n\n - **FIX**: Package flame_riverpod, setState() or markNeedsBuild() called during build. ([#2943](https://github.com/flame-engine/flame/issues/2943)). ([54d0e95d](https://github.com/flame-engine/flame/commit/54d0e95d863cc40e95f0310b4964343085f422e9))\n\n\n## 2023-12-19\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.13.0`](#flame---v1130)\n - [`flame_rive` - `v1.9.9`](#flame_rive---v199)\n - [`flame_riverpod` - `v5.1.1`](#flame_riverpod---v511)\n - [`flame_tiled` - `v1.18.2`](#flame_tiled---v1182)\n - [`flame_test` - `v1.15.2`](#flame_test---v1152)\n - [`flame_oxygen` - `v0.1.9+6`](#flame_oxygen---v0196)\n - [`flame_isolate` - `v0.5.0+6`](#flame_isolate---v0506)\n - [`flame_fire_atlas` - `v1.4.6`](#flame_fire_atlas---v146)\n - [`flame_audio` - `v2.1.6`](#flame_audio---v216)\n - [`flame_spine` - `v0.1.1+8`](#flame_spine---v0118)\n - [`flame_bloc` - `v1.10.8`](#flame_bloc---v1108)\n - [`flame_lottie` - `v0.3.0+6`](#flame_lottie---v0306)\n - [`flame_markdown` - `v0.1.1+6`](#flame_markdown---v0116)\n - [`flame_svg` - `v1.8.8`](#flame_svg---v188)\n - [`flame_forge2d` - `v0.16.0+3`](#flame_forge2d---v01603)\n - [`flame_noise` - `v0.1.1+11`](#flame_noise---v01111)\n - [`flame_network_assets` - `v0.2.0+11`](#flame_network_assets---v02011)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_test` - `v1.15.2`\n - `flame_oxygen` - `v0.1.9+6`\n - `flame_isolate` - `v0.5.0+6`\n - `flame_fire_atlas` - `v1.4.6`\n - `flame_audio` - `v2.1.6`\n - `flame_spine` - `v0.1.1+8`\n - `flame_bloc` - `v1.10.8`\n - `flame_lottie` - `v0.3.0+6`\n - `flame_markdown` - `v0.1.1+6`\n - `flame_svg` - `v1.8.8`\n - `flame_forge2d` - `v0.16.0+3`\n - `flame_noise` - `v0.1.1+11`\n - `flame_network_assets` - `v0.2.0+11`\n\n---\n\n#### `flame` - `v1.13.0`\n\n - **FIX**: Logic error in MemoryCache.setValue() ([#2931](https://github.com/flame-engine/flame/issues/2931)). ([8cee80c3](https://github.com/flame-engine/flame/commit/8cee80c35ca676ad25a25c771f0aade88b58150b))\n - **FIX**: Export `ScalingParticle` ([#2928](https://github.com/flame-engine/flame/issues/2928)). ([3730cb1d](https://github.com/flame-engine/flame/commit/3730cb1d834c73c87dc3597554039fd0f0a32bae))\n - **FIX**: Misalignment of the hittest area of PolygonHitbox ([#2930](https://github.com/flame-engine/flame/issues/2930)). ([dbdb1379](https://github.com/flame-engine/flame/commit/dbdb1379d0bc1b6ac02b3ee27f62263bd1be3fc3))\n - **FIX**: Allow setting `bounds` while `BoundedPositionBehavior`'s target is null ([#2926](https://github.com/flame-engine/flame/issues/2926)). ([bab9be6e](https://github.com/flame-engine/flame/commit/bab9be6e7051b7be6c84fc9760c7347692dbf140))\n - **FEAT**: Ability to use `selfPositioning` in `SpawnComponent` ([#2927](https://github.com/flame-engine/flame/issues/2927)). ([b526aa14](https://github.com/flame-engine/flame/commit/b526aa1488c0f891edb356007ebc2c5c2de596b5))\n - **FEAT**: Add `margin` and `spacing` properties to `SpriteSheet` ([#2925](https://github.com/flame-engine/flame/issues/2925)). ([67f7c126](https://github.com/flame-engine/flame/commit/67f7c126b4c8052df99ffa8c657a90cc7fb6f867))\n - **FEAT**: Add `children` to `SpriteAnimationComponent.fromFrameData` ([#2914](https://github.com/flame-engine/flame/issues/2914)). ([caf2b909](https://github.com/flame-engine/flame/commit/caf2b90930ca500c85b9f9f63e7d3d7a5d82c18e))\n - **DOCS**: Remove references to Tappable and Draggable ([#2912](https://github.com/flame-engine/flame/issues/2912)). ([d12e4544](https://github.com/flame-engine/flame/commit/d12e45444e49bbe0b24a7acbd24f0cda20a13755))\n\n#### `flame_rive` - `v1.9.9`\n\n - **DOCS**: Remove references to Tappable and Draggable ([#2912](https://github.com/flame-engine/flame/issues/2912)). ([d12e4544](https://github.com/flame-engine/flame/commit/d12e45444e49bbe0b24a7acbd24f0cda20a13755))\n\n#### `flame_riverpod` - `v5.1.1`\n\n - **FIX**: Add super constructor fields to RiverpodAwareGameWidget ([#2932](https://github.com/flame-engine/flame/issues/2932)). ([c2e6ea71](https://github.com/flame-engine/flame/commit/c2e6ea71e5c3c5f0d7ae6bc01a6c2f1f4d4d563b))\n\n#### `flame_tiled` - `v1.18.2`\n\n - **FIX**: Image layers repeat indefinitely if repeated in Tiled ([#2921](https://github.com/flame-engine/flame/issues/2921)). ([6f79bc5e](https://github.com/flame-engine/flame/commit/6f79bc5ef920ace17d09c88156a73043357d514f))\n\n\n## 2023-12-08\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.12.0`](#flame---v1120)\n - [`flame_riverpod` - `v5.1.0`](#flame_riverpod---v510)\n - [`flame_test` - `v1.15.1`](#flame_test---v1151)\n - [`flame_tiled` - `v1.18.1`](#flame_tiled---v1181)\n - [`flame_oxygen` - `v0.1.9+5`](#flame_oxygen---v0195)\n - [`flame_isolate` - `v0.5.0+5`](#flame_isolate---v0505)\n - [`flame_fire_atlas` - `v1.4.5`](#flame_fire_atlas---v145)\n - [`flame_audio` - `v2.1.5`](#flame_audio---v215)\n - [`flame_spine` - `v0.1.1+7`](#flame_spine---v0117)\n - [`flame_bloc` - `v1.10.7`](#flame_bloc---v1107)\n - [`flame_lottie` - `v0.3.0+5`](#flame_lottie---v0305)\n - [`flame_markdown` - `v0.1.1+5`](#flame_markdown---v0115)\n - [`flame_rive` - `v1.9.8`](#flame_rive---v198)\n - [`flame_forge2d` - `v0.16.0+2`](#flame_forge2d---v01602)\n - [`flame_noise` - `v0.1.1+10`](#flame_noise---v01110)\n - [`flame_svg` - `v1.8.7`](#flame_svg---v187)\n - [`flame_network_assets` - `v0.2.0+10`](#flame_network_assets---v02010)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_test` - `v1.15.1`\n - `flame_tiled` - `v1.18.1`\n - `flame_oxygen` - `v0.1.9+5`\n - `flame_isolate` - `v0.5.0+5`\n - `flame_fire_atlas` - `v1.4.5`\n - `flame_audio` - `v2.1.5`\n - `flame_spine` - `v0.1.1+7`\n - `flame_bloc` - `v1.10.7`\n - `flame_lottie` - `v0.3.0+5`\n - `flame_markdown` - `v0.1.1+5`\n - `flame_rive` - `v1.9.8`\n - `flame_forge2d` - `v0.16.0+2`\n - `flame_noise` - `v0.1.1+10`\n - `flame_svg` - `v1.8.7`\n - `flame_network_assets` - `v0.2.0+10`\n\n---\n\n#### `flame` - `v1.12.0`\n\n - **FIX**: SpriteAnimationWidget was resetting the ticker even when the playing didn't changed ([#2891](https://github.com/flame-engine/flame/issues/2891)). ([9aed8b4d](https://github.com/flame-engine/flame/commit/9aed8b4dea3074c9ca708ad991cdc90b12707fbe))\n - **FEAT**: Scrollable TextBoxComponent ([#2901](https://github.com/flame-engine/flame/issues/2901)). ([8c3cb725](https://github.com/flame-engine/flame/commit/8c3cb725413c46089854713f6ecc4617e1f15600))\n - **FEAT**: Add collision completed listener ([#2896](https://github.com/flame-engine/flame/issues/2896)). ([957db3c1](https://github.com/flame-engine/flame/commit/957db3c1ed476b22f7cc62d4df22595449f8756c))\n - **FEAT**: Adding autoResetTicker option to SpriteAnimationGroupComponent ([#2899](https://github.com/flame-engine/flame/issues/2899)). ([001c870d](https://github.com/flame-engine/flame/commit/001c870d61be6e7e7aae798df0dc33e5321ed882))\n - **FEAT**: Add clearSnapshot function ([#2897](https://github.com/flame-engine/flame/issues/2897)). ([d4decd21](https://github.com/flame-engine/flame/commit/d4decd21eb7506ffd6d84ab5a3ebf1f2067083b6))\n\n#### `flame_riverpod` - `v5.1.0`\n\n - **FIX**: SpriteAnimationWidget was resetting the ticker even when the playing didn't changed ([#2891](https://github.com/flame-engine/flame/issues/2891)). ([9aed8b4d](https://github.com/flame-engine/flame/commit/9aed8b4dea3074c9ca708ad991cdc90b12707fbe))\n - **FEAT**: Integration of flame_riverpod ([#2367](https://github.com/flame-engine/flame/issues/2367)). ([0c74560b](https://github.com/flame-engine/flame/commit/0c74560b2e25e86163c6c678ef6515bc11f9c3e7))\n\n\n## 2023-11-30\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.11.0`](#flame---v1110)\n\nPackages with other changes:\n\n - [`flame_audio` - `v2.1.4`](#flame_audio---v214)\n - [`flame_bloc` - `v1.10.6`](#flame_bloc---v1106)\n - [`flame_fire_atlas` - `v1.4.4`](#flame_fire_atlas---v144)\n - [`flame_isolate` - `v0.5.0+4`](#flame_isolate---v0504)\n - [`flame_lint` - `v1.1.2`](#flame_lint---v112)\n - [`flame_markdown` - `v0.1.1+4`](#flame_markdown---v0114)\n - [`flame_network_assets` - `v0.2.0+9`](#flame_network_assets---v0209)\n - [`flame_noise` - `v0.1.1+9`](#flame_noise---v0119)\n - [`flame_oxygen` - `v0.1.9+4`](#flame_oxygen---v0194)\n - [`flame_rive` - `v1.9.7`](#flame_rive---v197)\n - [`flame_spine` - `v0.1.1+6`](#flame_spine---v0116)\n - [`flame_svg` - `v1.8.6`](#flame_svg---v186)\n - [`flame_test` - `v1.15.0`](#flame_test---v1150)\n - [`flame_tiled` - `v1.18.0`](#flame_tiled---v1180)\n - [`jenny` - `v1.2.1`](#jenny---v121)\n - [`flame_lottie` - `v0.3.0+4`](#flame_lottie---v0304)\n - [`flame_forge2d` - `v0.16.0+1`](#flame_forge2d---v01601)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_lottie` - `v0.3.0+4`\n - `flame_forge2d` - `v0.16.0+1`\n\n---\n\n#### `flame` - `v1.11.0`\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n - **FIX**: Properly resize ScreenHitbox when needed ([#2826](https://github.com/flame-engine/flame/issues/2826)). ([24fed757](https://github.com/flame-engine/flame/commit/24fed757ac313453639ddf122ba84b1012a4b606))\n - **FIX**: Setting world on FlameGame camera setter ([#2831](https://github.com/flame-engine/flame/issues/2831)). ([3a8e2464](https://github.com/flame-engine/flame/commit/3a8e2464420f2b513f4f0d99cd7d64ab0eda9826))\n - **FIX**: Allow null passthrough parent ([#2821](https://github.com/flame-engine/flame/issues/2821)). ([c4d2f86e](https://github.com/flame-engine/flame/commit/c4d2f86e1214e9895ff858c511fa3c686313f204))\n - **FIX**: Do not scale debug texts with zoom ([#2818](https://github.com/flame-engine/flame/issues/2818)). ([c2f3f040](https://github.com/flame-engine/flame/commit/c2f3f040c6128d8fd3340d8f7622a2d4c2f22819))\n - **FIX**(flame): Export `FixedResolutionViewport` and make `withFixedResolution` a redirect constructor ([#2817](https://github.com/flame-engine/flame/issues/2817)). ([3420d0e6](https://github.com/flame-engine/flame/commit/3420d0e6f8af6f2dd8695ea61231aa93944c602b))\n - **FEAT**: Using viewport scale on debug mode text paint ([#2883](https://github.com/flame-engine/flame/issues/2883)). ([07ef46ca](https://github.com/flame-engine/flame/commit/07ef46cab01ae08749e678211245896572bb1081))\n - **FEAT**: Make Viewfinder and Viewport comply with CoordinateTransform interface ([#2872](https://github.com/flame-engine/flame/issues/2872)). ([685e1d95](https://github.com/flame-engine/flame/commit/685e1d9529df90f203e7827950ed5d9261b2ce42))\n - **FEAT**: Allow sequence effect to be extended ([#2864](https://github.com/flame-engine/flame/issues/2864)). ([ee11aae9](https://github.com/flame-engine/flame/commit/ee11aae9f519fdb967eb384aaffdb5a6f87a808f))\n - **FEAT**: Adding children argument to all constructors in the shape components ([#2862](https://github.com/flame-engine/flame/issues/2862)). ([082743d3](https://github.com/flame-engine/flame/commit/082743d3ba0860a87a58377a7b5a9cd6b5ae7c70))\n - **FEAT**: Optimization in sprite batch ([#2861](https://github.com/flame-engine/flame/issues/2861)). ([208d7897](https://github.com/flame-engine/flame/commit/208d7897f1e9e512f0bc235233e41e1953a8d546))\n - **FEAT**: Add TimeTrackComponent and ChildCounterComponent ([#2846](https://github.com/flame-engine/flame/issues/2846)). ([6269551a](https://github.com/flame-engine/flame/commit/6269551a77cfbc27094e262c131dec09e489e583))\n - **FEAT**: MoveAlongPathEffect should maintain initial angle of the component ([#2835](https://github.com/flame-engine/flame/issues/2835)). ([e6e78c0d](https://github.com/flame-engine/flame/commit/e6e78c0d66bc958dbe1c2295a7cc946dc5852455))\n - **FEAT**: Add a method to adapt the camera bounds to the world ([#2769](https://github.com/flame-engine/flame/issues/2769)). ([87b69df6](https://github.com/flame-engine/flame/commit/87b69df6a1d29261a514a7ee7d28d2d1f730920e))\n - **FEAT**: Scaling particle feature ([#2830](https://github.com/flame-engine/flame/issues/2830)). ([9faae8a2](https://github.com/flame-engine/flame/commit/9faae8a2371efdcbdf03cad70bded05470d4719a))\n - **BREAKING** **REFACTOR**: Replace `Offset` with `opacityFrom` and `opacityTo` in ColorEffect ([#2876](https://github.com/flame-engine/flame/issues/2876)). ([0fd2662d](https://github.com/flame-engine/flame/commit/0fd2662d4b1187285ee168271a38e1576b6e444a))\n - **BREAKING** **FIX**: Add DisplacementEvent to fix delta coordinate transformations for drag events ([#2871](https://github.com/flame-engine/flame/issues/2871)). ([63994ebc](https://github.com/flame-engine/flame/commit/63994ebcd8e850f68622f4a89ea17224574a8214))\n\n#### `flame_audio` - `v2.1.4`\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n#### `flame_bloc` - `v1.10.6`\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n#### `flame_fire_atlas` - `v1.4.4`\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n#### `flame_isolate` - `v0.5.0+4`\n\n - **DOCS**: Update flame_isolate to point at repository ([#2880](https://github.com/flame-engine/flame/issues/2880)). ([06fdeac6](https://github.com/flame-engine/flame/commit/06fdeac684b2be26206d50282e6a7f2cbac4264c))\n\n#### `flame_lint` - `v1.1.2`\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n#### `flame_markdown` - `v0.1.1+4`\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n#### `flame_network_assets` - `v0.2.0+9`\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n#### `flame_noise` - `v0.1.1+9`\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n#### `flame_oxygen` - `v0.1.9+4`\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n#### `flame_rive` - `v1.9.7`\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n#### `flame_spine` - `v0.1.1+6`\n\n - **FIX**: Removing spine flutter overriding ([#2877](https://github.com/flame-engine/flame/issues/2877)). ([f4ff3117](https://github.com/flame-engine/flame/commit/f4ff31174a0498dd8af90f815ad9c098df3b30b7))\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n#### `flame_svg` - `v1.8.6`\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n#### `flame_test` - `v1.15.0`\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n - **FEAT**: Expose addition test arguments on flame test ([#2832](https://github.com/flame-engine/flame/issues/2832)). ([08b4bd6d](https://github.com/flame-engine/flame/commit/08b4bd6d3f308013d18fec4eb126927239cd6481))\n\n#### `flame_tiled` - `v1.18.0`\n\n - **FIX**: TiledComponent.atlases had duplicated values ([#2867](https://github.com/flame-engine/flame/issues/2867)). ([e56ad187](https://github.com/flame-engine/flame/commit/e56ad1878333ba19e0c8af3fb9c9758603662330))\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n - **FEAT**: Adding configurable padding to Tiled atlas packing ([#2868](https://github.com/flame-engine/flame/issues/2868)). ([d0c10cbb](https://github.com/flame-engine/flame/commit/d0c10cbbea20415de471ad0269a22c168082b02d))\n - **FEAT**: Exposing atlases for reading in a TiledComponent ([#2865](https://github.com/flame-engine/flame/issues/2865)). ([e1b4d93a](https://github.com/flame-engine/flame/commit/e1b4d93ad43a4e1b1b55a3843e26612b73d45ed7))\n\n#### `jenny` - `v1.2.1`\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n\n## 2023-11-15\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_tiled` - `v1.17.0`](#flame_tiled---v1170)\n\n---\n\n#### `flame_tiled` - `v1.17.0`\n\n - **FIX**: Configuration useAtlas was not been propagated correctly everywhere ([#2853](https://github.com/flame-engine/flame/issues/2853)). ([2f0dab9e](https://github.com/flame-engine/flame/commit/2f0dab9e59958176e6c46f6e417188e6c4fa3831))\n - **FEAT**: Adding way to configure a layer paint in flame tiled ([#2851](https://github.com/flame-engine/flame/issues/2851)). ([e893d115](https://github.com/flame-engine/flame/commit/e893d1152c2aeb1c976668c875a1c267bbf819c0))\n - **FEAT**: Expose useAtlas on Flame Tiled ([#2852](https://github.com/flame-engine/flame/issues/2852)). ([c4efb4f8](https://github.com/flame-engine/flame/commit/c4efb4f859fe08cc7fbd3e0ddb35c806d0060c78))\n\n\n## 2023-11-10\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_rive` - `v1.9.6`](#flame_rive---v196)\n\n---\n\n#### `flame_rive` - `v1.9.6`\n\n - Bump to Rive 0.12.3\n\n\n## 2023-11-10\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_tiled` - `v1.16.0`](#flame_tiled---v1160)\n\n---\n\n#### `flame_tiled` - `v1.16.0`\n\n - **FEAT**: Allow flame tiled to skip tilesets when packing into a tile atlas ([#2847](https://github.com/flame-engine/flame/issues/2847)). ([b93bdd38](https://github.com/flame-engine/flame/commit/b93bdd38313fd273e3e4cf55f1b142969effbde4))\n\n\n## 2023-11-03\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_forge2d` - `v0.16.0`](#flame_forge2d---v0160)\n - [`flame` - `v1.10.1`](#flame---v1101)\n - [`flame_test` - `v1.14.0`](#flame_test---v1140)\n - [`flame_spine` - `v0.1.1+5`](#flame_spine---v0115)\n - [`flame_markdown` - `v0.1.1+3`](#flame_markdown---v0113)\n - [`flame_network_assets` - `v0.2.0+8`](#flame_network_assets---v0208)\n - [`flame_oxygen` - `v0.1.9+3`](#flame_oxygen---v0193)\n - [`flame_audio` - `v2.1.3`](#flame_audio---v213)\n - [`flame_fire_atlas` - `v1.4.3`](#flame_fire_atlas---v143)\n - [`flame_svg` - `v1.8.5`](#flame_svg---v185)\n - [`flame_lottie` - `v0.3.0+3`](#flame_lottie---v0303)\n - [`flame_isolate` - `v0.5.0+3`](#flame_isolate---v0503)\n - [`flame_noise` - `v0.1.1+8`](#flame_noise---v0118)\n - [`flame_bloc` - `v1.10.5`](#flame_bloc---v1105)\n - [`flame_rive` - `v1.9.5`](#flame_rive---v195)\n - [`flame_tiled` - `v1.15.1`](#flame_tiled---v1151)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_spine` - `v0.1.1+5`\n - `flame_markdown` - `v0.1.1+3`\n - `flame_network_assets` - `v0.2.0+8`\n - `flame_oxygen` - `v0.1.9+3`\n - `flame_audio` - `v2.1.3`\n - `flame_fire_atlas` - `v1.4.3`\n - `flame_svg` - `v1.8.5`\n - `flame_lottie` - `v0.3.0+3`\n - `flame_isolate` - `v0.5.0+3`\n - `flame_noise` - `v0.1.1+8`\n - `flame_bloc` - `v1.10.5`\n - `flame_rive` - `v1.9.5`\n - `flame_tiled` - `v1.15.1`\n\n---\n\n#### `flame_forge2d` - `v0.16.0`\n\n#### `flame` - `v1.10.1`\n\n - **FIX**: Properly resize ScreenHitbox when needed ([#2826](https://github.com/flame-engine/flame/issues/2826)). ([24fed757](https://github.com/flame-engine/flame/commit/24fed757ac313453639ddf122ba84b1012a4b606))\n - **FIX**: Setting world on FlameGame camera setter ([#2831](https://github.com/flame-engine/flame/issues/2831)). ([3a8e2464](https://github.com/flame-engine/flame/commit/3a8e2464420f2b513f4f0d99cd7d64ab0eda9826))\n - **FIX**: Allow null passthrough parent ([#2821](https://github.com/flame-engine/flame/issues/2821)). ([c4d2f86e](https://github.com/flame-engine/flame/commit/c4d2f86e1214e9895ff858c511fa3c686313f204))\n - **FIX**: Do not scale debug texts with zoom ([#2818](https://github.com/flame-engine/flame/issues/2818)). ([c2f3f040](https://github.com/flame-engine/flame/commit/c2f3f040c6128d8fd3340d8f7622a2d4c2f22819))\n - **FIX**(flame): Export `FixedResolutionViewport` and make `withFixedResolution` a redirect constructor ([#2817](https://github.com/flame-engine/flame/issues/2817)). ([3420d0e6](https://github.com/flame-engine/flame/commit/3420d0e6f8af6f2dd8695ea61231aa93944c602b))\n - **FEAT**: Scaling particle feature ([#2830](https://github.com/flame-engine/flame/issues/2830)). ([9faae8a2](https://github.com/flame-engine/flame/commit/9faae8a2371efdcbdf03cad70bded05470d4719a))\n\n#### `flame_test` - `v1.14.0`\n\n - **FEAT**: Expose addition test arguments on flame test ([#2832](https://github.com/flame-engine/flame/issues/2832)). ([08b4bd6d](https://github.com/flame-engine/flame/commit/08b4bd6d3f308013d18fec4eb126927239cd6481))\n\n\n## 2023-10-17\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`jenny` - `v1.2.0`](#jenny---v120)\n\n---\n\n#### `jenny` - `v1.2.0`\n\n - **FEAT**: Added lastline before choice ([#2822](https://github.com/flame-engine/flame/issues/2822)). ([3ef52524](https://github.com/flame-engine/flame/commit/3ef525246a0d3b1d02c470b5696164e677cdb6c4))\n\n\n## 2023-10-12\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.10.0`](#flame---v1100)\n - [`flame_audio` - `v2.1.2`](#flame_audio---v212)\n - [`flame_bloc` - `v1.10.4`](#flame_bloc---v1104)\n - [`flame_fire_atlas` - `v1.4.2`](#flame_fire_atlas---v142)\n - [`flame_forge2d` - `v0.15.1`](#flame_forge2d---v0151)\n - [`flame_isolate` - `v0.5.0+2`](#flame_isolate---v0502)\n - [`flame_lottie` - `v0.3.0+2`](#flame_lottie---v0302)\n - [`flame_network_assets` - `v0.2.0+7`](#flame_network_assets---v0207)\n - [`flame_rive` - `v1.9.4`](#flame_rive---v194)\n - [`flame_svg` - `v1.8.4`](#flame_svg---v184)\n - [`flame_test` - `v1.13.2`](#flame_test---v1132)\n - [`flame_tiled` - `v1.15.0`](#flame_tiled---v1150)\n - [`jenny` - `v1.1.1`](#jenny---v111)\n - [`flame_spine` - `v0.1.1+4`](#flame_spine---v0114)\n - [`flame_markdown` - `v0.1.1+2`](#flame_markdown---v0112)\n - [`flame_oxygen` - `v0.1.9+2`](#flame_oxygen---v0192)\n - [`flame_noise` - `v0.1.1+7`](#flame_noise---v0117)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_spine` - `v0.1.1+4`\n - `flame_markdown` - `v0.1.1+2`\n - `flame_oxygen` - `v0.1.9+2`\n - `flame_noise` - `v0.1.1+7`\n\n---\n\n#### `flame` - `v1.10.0`\n\n - **REFACTOR**: Remove unnecessary 'async' keyword across the codebase [DCM] ([#2803](https://github.com/flame-engine/flame/issues/2803)). ([2dfe0e5a](https://github.com/flame-engine/flame/commit/2dfe0e5a431213c7148ab6389e3e8c8dc49fbf3d))\n - **REFACTOR**: Avoid nested conditional expressions whenever possible [DCM] ([#2784](https://github.com/flame-engine/flame/issues/2784)). ([7b6a5712](https://github.com/flame-engine/flame/commit/7b6a571263ece3aa1a8267644d9118230a850830))\n - **REFACTOR**: Mark semantically final variables as final (or const) proper [DCM] ([#2783](https://github.com/flame-engine/flame/issues/2783)). ([71f7b475](https://github.com/flame-engine/flame/commit/71f7b475e33dd6fa7224c4a3ab29ffdb89702c34))\n - **FIX**: Remove deprecations for 1.10.0 ([#2809](https://github.com/flame-engine/flame/issues/2809)). ([5b67b8f1](https://github.com/flame-engine/flame/commit/5b67b8f14ad4fdb38a249d0a41ecba49ba2fcc44))\n - **FIX**: Un-register component keys down the component tree ([#2792](https://github.com/flame-engine/flame/issues/2792)). ([0f679b3f](https://github.com/flame-engine/flame/commit/0f679b3f3d4a643ff4c29569388941c459e35021))\n - **FIX**: AlignComponent set child (remove compare) ([#2774](https://github.com/flame-engine/flame/issues/2774)). ([20aaf656](https://github.com/flame-engine/flame/commit/20aaf656617cef6538b49c067a562f9daf0a5972))\n - **FIX**: Hardcode initCurrentGame lifecycle state as resumed ([#2775](https://github.com/flame-engine/flame/issues/2775)). ([0cd5037c](https://github.com/flame-engine/flame/commit/0cd5037c6a837706891d5f1b85a91715cf85ebb1))\n - **FIX**: Fix TextBoxComponent alignment bug ([#2781](https://github.com/flame-engine/flame/issues/2781)). ([0fb53efb](https://github.com/flame-engine/flame/commit/0fb53efb661ae2a3b4a39407655efb69e92dced0))\n - **FIX**(flame): The `component.removeFromParent` method should use `parent.remove` internally ([#2779](https://github.com/flame-engine/flame/issues/2779)). ([bdb1c79a](https://github.com/flame-engine/flame/commit/bdb1c79a0524801ab425982dae206915c691e4b2))\n - **FIX**: Take unmounted adds into consideration ([#2770](https://github.com/flame-engine/flame/issues/2770)). ([be28a440](https://github.com/flame-engine/flame/commit/be28a4405f4024a3066b764d6dbad0713665665d))\n - **FEAT**: Add `IgnoreEvents` mixin to ignore events for the whole subtree ([#2811](https://github.com/flame-engine/flame/issues/2811)). ([313411c3](https://github.com/flame-engine/flame/commit/313411c311a6a3c2d36e12abf16bdd27ae801f29))\n - **FEAT**: Add advanced button component ([#2742](https://github.com/flame-engine/flame/issues/2742)). ([97fff0ed](https://github.com/flame-engine/flame/commit/97fff0ed2baab53f2780eca29a9d08ea5d90426a))\n - **FEAT**: Introduce the `FixedResolutionViewport` ([#2796](https://github.com/flame-engine/flame/issues/2796)). ([4c762f94](https://github.com/flame-engine/flame/commit/4c762f94d40d200bb2b8a102867b0859a345dbf0))\n - **FEAT**: AssetsBundle can be customized in Images and AssetsCache. ([#2807](https://github.com/flame-engine/flame/issues/2807)). ([a23f80e9](https://github.com/flame-engine/flame/commit/a23f80e94a5d935fc8ba232956fe02e001d5a8f9))\n - **FEAT**: Backdrop (static backgrounds) component for CameraComponent ([#2787](https://github.com/flame-engine/flame/issues/2787)). ([ab329f71](https://github.com/flame-engine/flame/commit/ab329f718a581b8331749fed6f942b6ade0da5ac))\n - **FEAT**: Align component refactoring ([#2767](https://github.com/flame-engine/flame/issues/2767)). ([bde34efe](https://github.com/flame-engine/flame/commit/bde34efef7264c91f49b237b589c74ba80a1554e))\n - **DOCS**: Remove last broad cSpell bypass regex and fix all violations ([#2802](https://github.com/flame-engine/flame/issues/2802)). ([9b16b178](https://github.com/flame-engine/flame/commit/9b16b178048eb19b6596273fcf4daec83277c3b5))\n\n#### `flame_audio` - `v2.1.2`\n\n - **REFACTOR**: Remove unnecessary 'async' keyword across the codebase [DCM] ([#2803](https://github.com/flame-engine/flame/issues/2803)). ([2dfe0e5a](https://github.com/flame-engine/flame/commit/2dfe0e5a431213c7148ab6389e3e8c8dc49fbf3d))\n - **REFACTOR**: Mark semantically final variables as final (or const) proper [DCM] ([#2783](https://github.com/flame-engine/flame/issues/2783)). ([71f7b475](https://github.com/flame-engine/flame/commit/71f7b475e33dd6fa7224c4a3ab29ffdb89702c34))\n - **FIX**: Remove deprecations for 1.10.0 ([#2809](https://github.com/flame-engine/flame/issues/2809)). ([5b67b8f1](https://github.com/flame-engine/flame/commit/5b67b8f14ad4fdb38a249d0a41ecba49ba2fcc44))\n\n#### `flame_bloc` - `v1.10.4`\n\n - **FIX**: Remove deprecations for 1.10.0 ([#2809](https://github.com/flame-engine/flame/issues/2809)). ([5b67b8f1](https://github.com/flame-engine/flame/commit/5b67b8f14ad4fdb38a249d0a41ecba49ba2fcc44))\n\n#### `flame_fire_atlas` - `v1.4.2`\n\n - **REFACTOR**: Remove unnecessary 'async' keyword across the codebase [DCM] ([#2803](https://github.com/flame-engine/flame/issues/2803)). ([2dfe0e5a](https://github.com/flame-engine/flame/commit/2dfe0e5a431213c7148ab6389e3e8c8dc49fbf3d))\n - **FIX**: Remove deprecations for 1.10.0 ([#2809](https://github.com/flame-engine/flame/issues/2809)). ([5b67b8f1](https://github.com/flame-engine/flame/commit/5b67b8f14ad4fdb38a249d0a41ecba49ba2fcc44))\n\n#### `flame_forge2d` - `v0.15.1`\n\n - **FEAT**: Allow for bodyDef and fixtureDefs to be prepared earlier ([#2768](https://github.com/flame-engine/flame/issues/2768)). ([21357bca](https://github.com/flame-engine/flame/commit/21357bcac1e7e1cebfa6f2a496ec4b627d62d0e7))\n\n#### `flame_isolate` - `v0.5.0+2`\n\n - **REFACTOR**: Mark semantically final variables as final (or const) proper [DCM] ([#2783](https://github.com/flame-engine/flame/issues/2783)). ([71f7b475](https://github.com/flame-engine/flame/commit/71f7b475e33dd6fa7224c4a3ab29ffdb89702c34))\n\n#### `flame_lottie` - `v0.3.0+2`\n\n - **REFACTOR**: Remove unnecessary 'async' keyword across the codebase [DCM] ([#2803](https://github.com/flame-engine/flame/issues/2803)). ([2dfe0e5a](https://github.com/flame-engine/flame/commit/2dfe0e5a431213c7148ab6389e3e8c8dc49fbf3d))\n - **FIX**: Duration in `LottieRenderer` rounds down milliseconds ([#2808](https://github.com/flame-engine/flame/issues/2808)). ([cccae2e1](https://github.com/flame-engine/flame/commit/cccae2e1476de456c15ee3779b746f5fe6dadee2))\n\n#### `flame_network_assets` - `v0.2.0+7`\n\n - **FIX**: Remove deprecations for 1.10.0 ([#2809](https://github.com/flame-engine/flame/issues/2809)). ([5b67b8f1](https://github.com/flame-engine/flame/commit/5b67b8f14ad4fdb38a249d0a41ecba49ba2fcc44))\n\n#### `flame_rive` - `v1.9.4`\n\n - **REFACTOR**: Remove unnecessary 'async' keyword across the codebase [DCM] ([#2803](https://github.com/flame-engine/flame/issues/2803)). ([2dfe0e5a](https://github.com/flame-engine/flame/commit/2dfe0e5a431213c7148ab6389e3e8c8dc49fbf3d))\n\n#### `flame_svg` - `v1.8.4`\n\n - **FIX**: Do not render SVGs bigger than requested when pixelRatio > 1 and no match in _imageCache ([#2795](https://github.com/flame-engine/flame/issues/2795)). ([5fa4e09f](https://github.com/flame-engine/flame/commit/5fa4e09f7c464ce2f676df81049a95dad46bf2bd))\n\n#### `flame_test` - `v1.13.2`\n\n - **REFACTOR**: Remove unnecessary 'async' keyword across the codebase [DCM] ([#2803](https://github.com/flame-engine/flame/issues/2803)). ([2dfe0e5a](https://github.com/flame-engine/flame/commit/2dfe0e5a431213c7148ab6389e3e8c8dc49fbf3d))\n\n#### `flame_tiled` - `v1.15.0`\n\n - **REFACTOR**: Remove unnecessary 'async' keyword across the codebase [DCM] ([#2803](https://github.com/flame-engine/flame/issues/2803)). ([2dfe0e5a](https://github.com/flame-engine/flame/commit/2dfe0e5a431213c7148ab6389e3e8c8dc49fbf3d))\n - **FIX**: Remove deprecations for 1.10.0 ([#2809](https://github.com/flame-engine/flame/issues/2809)). ([5b67b8f1](https://github.com/flame-engine/flame/commit/5b67b8f14ad4fdb38a249d0a41ecba49ba2fcc44))\n - **FIX**: Parallax offset calculations in flame_tiled don't scale properly ([#2766](https://github.com/flame-engine/flame/issues/2766)). ([89e8427a](https://github.com/flame-engine/flame/commit/89e8427a7a34258ec20276e4ec64d4a484277cdd))\n - **FEAT**(flame_tiled): Allowing tilesets with images in the same folder to load ([#2814](https://github.com/flame-engine/flame/issues/2814)). ([3b0d7e65](https://github.com/flame-engine/flame/commit/3b0d7e65c2bf158db378d66c4f7e687dd05b46e1))\n - **FEAT**: AssetsBundle can be customized in Images and AssetsCache. ([#2807](https://github.com/flame-engine/flame/issues/2807)). ([a23f80e9](https://github.com/flame-engine/flame/commit/a23f80e94a5d935fc8ba232956fe02e001d5a8f9))\n - **FEAT**: Add overriding of Images and Bundle in all classes ([#2806](https://github.com/flame-engine/flame/issues/2806)). ([2df90c9b](https://github.com/flame-engine/flame/commit/2df90c9ba8f2b1cc088c5270df571eee7e18bb57))\n\n#### `jenny` - `v1.1.1`\n\n - **REFACTOR**: Remove unnecessary 'async' keyword across the codebase [DCM] ([#2803](https://github.com/flame-engine/flame/issues/2803)). ([2dfe0e5a](https://github.com/flame-engine/flame/commit/2dfe0e5a431213c7148ab6389e3e8c8dc49fbf3d))\n - **REFACTOR**: Avoid nested conditional expressions whenever possible [DCM] ([#2784](https://github.com/flame-engine/flame/issues/2784)). ([7b6a5712](https://github.com/flame-engine/flame/commit/7b6a571263ece3aa1a8267644d9118230a850830))\n - **DOCS**: Remove last broad cSpell bypass regex and fix all violations ([#2802](https://github.com/flame-engine/flame/issues/2802)). ([9b16b178](https://github.com/flame-engine/flame/commit/9b16b178048eb19b6596273fcf4daec83277c3b5))\n\n\n## 2023-09-22\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.9.1`](#flame---v191)\n - [`flame_isolate` - `v0.5.0+1`](#flame_isolate---v0501)\n - [`flame_tiled` - `v1.14.1`](#flame_tiled---v1141)\n - [`flame_audio` - `v2.1.1`](#flame_audio---v211)\n - [`flame_spine` - `v0.1.1+3`](#flame_spine---v0113)\n - [`flame_svg` - `v1.8.3`](#flame_svg---v183)\n - [`flame_test` - `v1.13.1`](#flame_test---v1131)\n - [`flame_oxygen` - `v0.1.9+1`](#flame_oxygen---v0191)\n - [`flame_bloc` - `v1.10.3`](#flame_bloc---v1103)\n - [`flame_fire_atlas` - `v1.4.1`](#flame_fire_atlas---v141)\n - [`flame_markdown` - `v0.1.1+1`](#flame_markdown---v0111)\n - [`flame_forge2d` - `v0.15.0+1`](#flame_forge2d---v01501)\n - [`flame_rive` - `v1.9.3`](#flame_rive---v193)\n - [`flame_noise` - `v0.1.1+6`](#flame_noise---v0116)\n - [`flame_network_assets` - `v0.2.0+6`](#flame_network_assets---v0206)\n - [`flame_lottie` - `v0.3.0+1`](#flame_lottie---v0301)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_isolate` - `v0.5.0+1`\n - `flame_tiled` - `v1.14.1`\n - `flame_audio` - `v2.1.1`\n - `flame_spine` - `v0.1.1+3`\n - `flame_svg` - `v1.8.3`\n - `flame_test` - `v1.13.1`\n - `flame_oxygen` - `v0.1.9+1`\n - `flame_bloc` - `v1.10.3`\n - `flame_fire_atlas` - `v1.4.1`\n - `flame_markdown` - `v0.1.1+1`\n - `flame_forge2d` - `v0.15.0+1`\n - `flame_rive` - `v1.9.3`\n - `flame_noise` - `v0.1.1+6`\n - `flame_network_assets` - `v0.2.0+6`\n - `flame_lottie` - `v0.3.0+1`\n\n---\n\n#### `flame` - `v1.9.1`\n\n - **FIX**: Add necessary generics on mixins on FlameGame ([#2763](https://github.com/flame-engine/flame/issues/2763)). ([b1f5ff26](https://github.com/flame-engine/flame/commit/b1f5ff269441d55b09ce12d5ce99656f2d88a978))\n - **FIX**: Correctly refreshes the widget after new mouse detector ([#2765](https://github.com/flame-engine/flame/issues/2765)). ([64330022](https://github.com/flame-engine/flame/commit/643300222f8bf0545abdd1d8608202f388f8693f))\n - **FIX**: Allow moving to a new parent in the same tick ([#2762](https://github.com/flame-engine/flame/issues/2762)). ([313650ea](https://github.com/flame-engine/flame/commit/313650eafadca4427421ddd355fa5b373966b8d1))\n\n\n## 2023-09-21\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.9.0`](#flame---v190)\n - [`flame_oxygen` - `v0.1.9`](#flame_oxygen---v019)\n - [`flame_test` - `v1.13.0`](#flame_test---v1130)\n - [`flame_tiled` - `v1.14.0`](#flame_tiled---v1140)\n - [`flame_forge2d` - `v0.15.0`](#flame_forge2d---v0150)\n - [`flame_isolate` - `v0.5.0`](#flame_isolate---v050)\n - [`flame_lottie` - `v0.3.0`](#flame_lottie---v030)\n\nPackages with other changes:\n\n - [`flame_audio` - `v2.1.0`](#flame_audio---v210)\n - [`flame_bloc` - `v1.10.2`](#flame_bloc---v1102)\n - [`flame_fire_atlas` - `v1.4.0`](#flame_fire_atlas---v140)\n - [`flame_lint` - `v1.1.1`](#flame_lint---v111)\n - [`flame_markdown` - `v0.1.1`](#flame_markdown---v011)\n - [`flame_network_assets` - `v0.2.0+5`](#flame_network_assets---v0205)\n - [`flame_noise` - `v0.1.1+5`](#flame_noise---v0115)\n - [`flame_rive` - `v1.9.2`](#flame_rive---v192)\n - [`flame_spine` - `v0.1.1+2`](#flame_spine---v0112)\n - [`flame_svg` - `v1.8.2`](#flame_svg---v182)\n - [`jenny` - `v1.1.0`](#jenny---v110)\n\n---\n\n#### `flame` - `v1.9.0`\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **REFACTOR**: Fix lint issues across the codebase - Part 2 ([#2677](https://github.com/flame-engine/flame/issues/2677)). ([10e4109c](https://github.com/flame-engine/flame/commit/10e4109c81b266147ec35744e484c2ec7ea15acd))\n - **FIX**: Prevent `onRemove`/`onDetach` being called for initial Gesture Detector addition ([#2653](https://github.com/flame-engine/flame/issues/2653)). ([d1721464](https://github.com/flame-engine/flame/commit/d17214640548eba26f10ba0d55e70545d58cb1b9))\n - **FIX**: Use root game for gestures ([#2756](https://github.com/flame-engine/flame/issues/2756)). ([f5d0cb38](https://github.com/flame-engine/flame/commit/f5d0cb3856e5b397b11a1c4bb2dc9c49afa51de0))\n - **FIX**: Add possibility to remove a child and add it to the same parent ([#2755](https://github.com/flame-engine/flame/issues/2755)). ([285d31ab](https://github.com/flame-engine/flame/commit/285d31ab9894a8c24b995a68fc29329f142d0d09))\n - **FIX**: Adding scale parameter to RectangleComponent constructors ([#2730](https://github.com/flame-engine/flame/issues/2730)). ([173908d9](https://github.com/flame-engine/flame/commit/173908d9f26c5555ffa69d1557bf346c0ab5fbee))\n - **FIX**: Set `CameraComponent.priority` to max ([#2732](https://github.com/flame-engine/flame/issues/2732)). ([820ece1c](https://github.com/flame-engine/flame/commit/820ece1c9aba9d770326adcd2224c951ef54f6f7))\n - **FIX**: Change to `FilterQuality.medium` instead of `high` ([#2733](https://github.com/flame-engine/flame/issues/2733)). ([fc19890c](https://github.com/flame-engine/flame/commit/fc19890c87a78599ea49ee0dfb52a04ea6b09a99))\n - **FIX**: Avoid creating new `Vector2` in `globalToLocal` and `localToGlobal` ([#2727](https://github.com/flame-engine/flame/issues/2727)). ([9fb3bf8d](https://github.com/flame-engine/flame/commit/9fb3bf8dbd71bc981b00d3b4dabbe997d50030bb))\n - **FIX**: Ambiguation is not needed in render box anymore ([#2711](https://github.com/flame-engine/flame/issues/2711)). ([b3d78f58](https://github.com/flame-engine/flame/commit/b3d78f58831ec72f40f721f96f8d659111f25a88))\n - **FIX**: HasGameReference should default to FlameGame ([#2710](https://github.com/flame-engine/flame/issues/2710)). ([93dcb3a1](https://github.com/flame-engine/flame/commit/93dcb3a117c365767e3f20569b2d82abc8a7b152))\n - **FIX**: Make `debugCoordinatesPrecision` into a variable instead of a getter ([#2713](https://github.com/flame-engine/flame/issues/2713)). ([9918c051](https://github.com/flame-engine/flame/commit/9918c0515ae88c2f1bfb7423a2993c983dec16c2))\n - **FIX**: Absolute angle takes into account BodyComponent ancestors too ([#2678](https://github.com/flame-engine/flame/issues/2678)). ([75aee767](https://github.com/flame-engine/flame/commit/75aee767811ef440841956d9e467be157c4ab880))\n - **FEAT**: SpawnComponent ([#2709](https://github.com/flame-engine/flame/issues/2709)). ([83f5ea45](https://github.com/flame-engine/flame/commit/83f5ea45dcc024c3bfd3fe9002533daaf1a2be4e))\n - **FEAT**: Add globalToLocal and localToGlobal methods to viewport, viewfinder and camera ([#2720](https://github.com/flame-engine/flame/issues/2720)). ([00185a3b](https://github.com/flame-engine/flame/commit/00185a3b6b1e0e6b06e67dc724a26d4e9651e1a2))\n - **FEAT**: Add HoverCallbacks ([#2706](https://github.com/flame-engine/flame/issues/2706)). ([d460b846](https://github.com/flame-engine/flame/commit/d460b846c23fb1f67041469c99c81e4c78b89c2e))\n - **FEAT**: Add `onDispose` to `game.dart` called from `game_widget.dart` ([#2659](https://github.com/flame-engine/flame/issues/2659)). ([2f44e483](https://github.com/flame-engine/flame/commit/2f44e4832f0a9a8edf9c002783501610aa051370))\n - **FEAT**(flame): Add helper methods to create frame data on `SpriteSheet` ([#2754](https://github.com/flame-engine/flame/issues/2754)). ([47722199](https://github.com/flame-engine/flame/commit/477221998a272bf659cd86d2bf145adf0f277e65))\n - **FEAT**: Implement Snapshot mixin on PositionComponent ([#2695](https://github.com/flame-engine/flame/issues/2695)). ([c1ee24a2](https://github.com/flame-engine/flame/commit/c1ee24a2894eaffa1f6e206313cda4087a02f0a4))\n - **FEAT**: Add TextElementComponent ([#2694](https://github.com/flame-engine/flame/issues/2694)). ([10fb65f6](https://github.com/flame-engine/flame/commit/10fb65f66ca1f1dbac04a138ef4a28b1ed5e5a23))\n - **FEAT**: Component visibility (HasVisibility mixin) ([#2681](https://github.com/flame-engine/flame/issues/2681)). ([76405daf](https://github.com/flame-engine/flame/commit/76405daf48b2efd59241329d4d1fb4b451d254c0))\n - **FEAT**: Add `HasWorldReference` mixin ([#2746](https://github.com/flame-engine/flame/issues/2746)). ([9105411d](https://github.com/flame-engine/flame/commit/9105411d46e097d4b5bf84ee8921c146dcf5a6cd))\n - **FEAT**: Add `pause` and `isPaused` to SpriteAnimationTicker ([#2660](https://github.com/flame-engine/flame/issues/2660)). ([37271f5c](https://github.com/flame-engine/flame/commit/37271f5c52e75e6b086520a35361a03e0d784586))\n - **DOCS**: Improve documentation around SpriteFontTextFormatter ([#2661](https://github.com/flame-engine/flame/issues/2661)). ([8401c569](https://github.com/flame-engine/flame/commit/8401c569bfbc92a13ce5cee18cd817da06bd0bd8))\n - **DOCS**: Improved spellchecking ([#2722](https://github.com/flame-engine/flame/issues/2722)). ([2f973abe](https://github.com/flame-engine/flame/commit/2f973abe8b298a4f6f1164065783de560953d789))\n - **DOCS**: Enable CSpell on tests ([#2723](https://github.com/flame-engine/flame/issues/2723)). ([e051298c](https://github.com/flame-engine/flame/commit/e051298cba76550229780438b1a589557c7b488d))\n - **DOCS**: Improve comments and documentation for text-rendering Nodes ([#2662](https://github.com/flame-engine/flame/issues/2662)). ([96978e24](https://github.com/flame-engine/flame/commit/96978e2496ffe29dbbf19b8e7e70c2d63309b115))\n - **DOCS**: Fix examples for v1.9.0 ([#2757](https://github.com/flame-engine/flame/issues/2757)). ([152fbb61](https://github.com/flame-engine/flame/commit/152fbb61db1986632f60f3bf98c93aa2e4fbfc86))\n - **BREAKING** **REFACTOR**: Rename (Text) Elements, Nodes and Styles for clarity, add docs ([#2700](https://github.com/flame-engine/flame/issues/2700)). ([4b420b79](https://github.com/flame-engine/flame/commit/4b420b7952ab8d675140b9d8d132015ff2780f92))\n - **BREAKING** **REFACTOR**: Extract TextRendererFactory ([#2680](https://github.com/flame-engine/flame/issues/2680)). ([eeb6749f](https://github.com/flame-engine/flame/commit/eeb6749fd2baa825c7e8267a546ec8bf405a63ae))\n - **BREAKING** **REFACTOR**: Make TextElement more usable on its own ([#2679](https://github.com/flame-engine/flame/issues/2679)). ([1a64443c](https://github.com/flame-engine/flame/commit/1a64443ccaae32e71fe7d016ad1e8f18a75c93da))\n - **BREAKING** **REFACTOR**: Simplify text rendering pipeline ([#2663](https://github.com/flame-engine/flame/issues/2663)). ([34f69b95](https://github.com/flame-engine/flame/commit/34f69b953c137fbf0168aebec3860c6abc888594))\n - **BREAKING** **REFACTOR**: Kill TextRenderer, Long Live TextRenderer ([#2683](https://github.com/flame-engine/flame/issues/2683)). ([a1cb9a06](https://github.com/flame-engine/flame/commit/a1cb9a06ada6f87bf22bc20e3c190ccd53517389))\n - **BREAKING** **FIX**: Update should be called before render in first tick ([#2714](https://github.com/flame-engine/flame/issues/2714)). ([51932c09](https://github.com/flame-engine/flame/commit/51932c09c1e934ec30ffa04eda6c050440f85548))\n - **BREAKING** **FEAT**: Move `Forge2DGame` to use `CameraComponent` ([#2728](https://github.com/flame-engine/flame/issues/2728)). ([7a3d5126](https://github.com/flame-engine/flame/commit/7a3d5126a54d23cdebde20953772a53ba1a53204))\n - **BREAKING** **FEAT**: Pause game when backgrounded ([#2642](https://github.com/flame-engine/flame/issues/2642)). ([521e56b6](https://github.com/flame-engine/flame/commit/521e56b6d20c1c5b24a2818d73be58a6e6523f6b))\n - **BREAKING** **FEAT**: Add CameraComponent to FlameGame ([#2740](https://github.com/flame-engine/flame/issues/2740)). ([7c2f4000](https://github.com/flame-engine/flame/commit/7c2f4000761580dbabb5d73b27f64d5819b34e8d))\n\n#### `flame_oxygen` - `v0.1.9`\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **BREAKING** **REFACTOR**: Simplify text rendering pipeline ([#2663](https://github.com/flame-engine/flame/issues/2663)). ([34f69b95](https://github.com/flame-engine/flame/commit/34f69b953c137fbf0168aebec3860c6abc888594))\n\n#### `flame_test` - `v1.13.0`\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **FIX**: HasGameReference should default to FlameGame ([#2710](https://github.com/flame-engine/flame/issues/2710)). ([93dcb3a1](https://github.com/flame-engine/flame/commit/93dcb3a117c365767e3f20569b2d82abc8a7b152))\n - **FEAT**: Add HoverCallbacks ([#2706](https://github.com/flame-engine/flame/issues/2706)). ([d460b846](https://github.com/flame-engine/flame/commit/d460b846c23fb1f67041469c99c81e4c78b89c2e))\n - **BREAKING** **REFACTOR**: Rename (Text) Elements, Nodes and Styles for clarity, add docs ([#2700](https://github.com/flame-engine/flame/issues/2700)). ([4b420b79](https://github.com/flame-engine/flame/commit/4b420b7952ab8d675140b9d8d132015ff2780f92))\n - **BREAKING** **REFACTOR**: Kill TextRenderer, Long Live TextRenderer ([#2683](https://github.com/flame-engine/flame/issues/2683)). ([a1cb9a06](https://github.com/flame-engine/flame/commit/a1cb9a06ada6f87bf22bc20e3c190ccd53517389))\n - **BREAKING** **REFACTOR**: Make TextElement more usable on its own ([#2679](https://github.com/flame-engine/flame/issues/2679)). ([1a64443c](https://github.com/flame-engine/flame/commit/1a64443ccaae32e71fe7d016ad1e8f18a75c93da))\n - **BREAKING** **FEAT**: Add CameraComponent to FlameGame ([#2740](https://github.com/flame-engine/flame/issues/2740)). ([7c2f4000](https://github.com/flame-engine/flame/commit/7c2f4000761580dbabb5d73b27f64d5819b34e8d))\n\n#### `flame_tiled` - `v1.14.0`\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **FEAT**: Expose atlas limits for `TiledComponent` ([#2701](https://github.com/flame-engine/flame/issues/2701)). ([99a1016f](https://github.com/flame-engine/flame/commit/99a1016f72d02f4a989986f224e0e77cddd0dfa8))\n - **FEAT**: Added prefix parameter to TiledComponent.load to specify assets folder for tiled maps ([#2651](https://github.com/flame-engine/flame/issues/2651)). ([d08284dd](https://github.com/flame-engine/flame/commit/d08284ddcaf5d2ad6e5312336a71a113702dc241))\n - **DOCS**: Enable CSpell on tests ([#2723](https://github.com/flame-engine/flame/issues/2723)). ([e051298c](https://github.com/flame-engine/flame/commit/e051298cba76550229780438b1a589557c7b488d))\n - **BREAKING** **FEAT**: Add CameraComponent to FlameGame ([#2740](https://github.com/flame-engine/flame/issues/2740)). ([7c2f4000](https://github.com/flame-engine/flame/commit/7c2f4000761580dbabb5d73b27f64d5819b34e8d))\n\n#### `flame_forge2d` - `v0.15.0`\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **FIX**: Absolute angle takes into account BodyComponent ancestors too ([#2678](https://github.com/flame-engine/flame/issues/2678)). ([75aee767](https://github.com/flame-engine/flame/commit/75aee767811ef440841956d9e467be157c4ab880))\n - **FIX**: Proper Flame dependency in flame_forge2d ([#2644](https://github.com/flame-engine/flame/issues/2644)). ([9bbecb88](https://github.com/flame-engine/flame/commit/9bbecb88d86aa051626267fd69e5bf71fdca66d6))\n - **DOCS**: Enable CSpell on tests ([#2723](https://github.com/flame-engine/flame/issues/2723)). ([e051298c](https://github.com/flame-engine/flame/commit/e051298cba76550229780438b1a589557c7b488d))\n - **BREAKING** **FEAT**: Add CameraComponent to FlameGame ([#2740](https://github.com/flame-engine/flame/issues/2740)). ([7c2f4000](https://github.com/flame-engine/flame/commit/7c2f4000761580dbabb5d73b27f64d5819b34e8d))\n - **BREAKING** **FEAT**: Move `Forge2DGame` to use `CameraComponent` ([#2728](https://github.com/flame-engine/flame/issues/2728)). ([7a3d5126](https://github.com/flame-engine/flame/commit/7a3d5126a54d23cdebde20953772a53ba1a53204))\n\n#### `flame_isolate` - `v0.5.0`\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **DOCS**: Enable CSpell on tests ([#2723](https://github.com/flame-engine/flame/issues/2723)). ([e051298c](https://github.com/flame-engine/flame/commit/e051298cba76550229780438b1a589557c7b488d))\n - **BREAKING** **FEAT**: Add CameraComponent to FlameGame ([#2740](https://github.com/flame-engine/flame/issues/2740)). ([7c2f4000](https://github.com/flame-engine/flame/commit/7c2f4000761580dbabb5d73b27f64d5819b34e8d))\n\n#### `flame_lottie` - `v0.3.0`\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **REFACTOR**: Fix lint issues across the codebase ([#2672](https://github.com/flame-engine/flame/issues/2672)). ([6fe9a247](https://github.com/flame-engine/flame/commit/6fe9a24778fbe1e9cb74ec0d50d71eae7b1a048e))\n - **BREAKING** **FEAT**: Add CameraComponent to FlameGame ([#2740](https://github.com/flame-engine/flame/issues/2740)). ([7c2f4000](https://github.com/flame-engine/flame/commit/7c2f4000761580dbabb5d73b27f64d5819b34e8d))\n\n#### `flame_audio` - `v2.1.0`\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **FEAT**(flame_audio): Adding an easy way of updating the prefix from FlameAudio ([#2751](https://github.com/flame-engine/flame/issues/2751)). ([d2c9dcec](https://github.com/flame-engine/flame/commit/d2c9dcecbe661896ba8c84d81b9500cdfa8c78c8))\n\n#### `flame_bloc` - `v1.10.2`\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **DOCS**: Enable CSpell on tests ([#2723](https://github.com/flame-engine/flame/issues/2723)). ([e051298c](https://github.com/flame-engine/flame/commit/e051298cba76550229780438b1a589557c7b488d))\n\n#### `flame_fire_atlas` - `v1.4.0`\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **FEAT**(fire_atlas): Encoded option to load json instead of .fa ([#2649](https://github.com/flame-engine/flame/issues/2649)). ([5be6fc8c](https://github.com/flame-engine/flame/commit/5be6fc8caea138b577bf91244165c0a61659b4c5))\n\n#### `flame_lint` - `v1.1.1`\n\n - **REFACTOR**: Enable new DCM rule: avoid-cascade-after-if-null ([#2676](https://github.com/flame-engine/flame/issues/2676)). ([158fc34c](https://github.com/flame-engine/flame/commit/158fc34cae858cf8d0b5d3b5155763e02454779a))\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n\n#### `flame_markdown` - `v0.1.1`\n\n - **FEAT**: Create flame_markdown ([#2703](https://github.com/flame-engine/flame/issues/2703)). ([b77c2373](https://github.com/flame-engine/flame/commit/b77c23737104260aea2483c38ec3bef999975e7d))\n\n#### `flame_network_assets` - `v0.2.0+5`\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n\n#### `flame_noise` - `v0.1.1+5`\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n\n#### `flame_rive` - `v1.9.2`\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **DOCS**: Enable CSpell on tests ([#2723](https://github.com/flame-engine/flame/issues/2723)). ([e051298c](https://github.com/flame-engine/flame/commit/e051298cba76550229780438b1a589557c7b488d))\n\n#### `flame_spine` - `v0.1.1+2`\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n\n#### `flame_svg` - `v1.8.2`\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **FIX**: Change to `FilterQuality.medium` instead of `high` ([#2733](https://github.com/flame-engine/flame/issues/2733)). ([fc19890c](https://github.com/flame-engine/flame/commit/fc19890c87a78599ea49ee0dfb52a04ea6b09a99))\n\n#### `jenny` - `v1.1.0`\n\n - **REFACTOR**: Enable new DCM rule: avoid-cascade-after-if-null ([#2676](https://github.com/flame-engine/flame/issues/2676)). ([158fc34c](https://github.com/flame-engine/flame/commit/158fc34cae858cf8d0b5d3b5155763e02454779a))\n - **REFACTOR**: Remove unused variable on dialogue_view_test.dart ([#2673](https://github.com/flame-engine/flame/issues/2673)). ([b77802a5](https://github.com/flame-engine/flame/commit/b77802a5e3a3c2fa68650dfa5d5f2aaed0f9a147))\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **FIX**: Change DialogueView to a mixin class ([#2652](https://github.com/flame-engine/flame/issues/2652)). ([f3d4158b](https://github.com/flame-engine/flame/commit/f3d4158b83685b479b0e7373bfbc7f8a6c16d822))\n - **FEAT**(flame_jenny): Allow removal of functions and commands ([#2717](https://github.com/flame-engine/flame/issues/2717)). ([a097cc01](https://github.com/flame-engine/flame/commit/a097cc01bc2bf789684c23320b16018dfc9dc664))\n - **FEAT**(flame_jenny): Allow removal of variables ([#2716](https://github.com/flame-engine/flame/issues/2716)). ([eaa8c091](https://github.com/flame-engine/flame/commit/eaa8c091627e4dd743c88dda42d4da70dca40e8b))\n - **FEAT**(flame_jenny): Allow removal of characters ([#2715](https://github.com/flame-engine/flame/issues/2715)). ([3421f4f9](https://github.com/flame-engine/flame/commit/3421f4f944516d998460a5347125d8f100366c42))\n - **FEAT**(flame_jenny): Public access to variables to allow load/save ([#2689](https://github.com/flame-engine/flame/issues/2689)). ([1485f842](https://github.com/flame-engine/flame/commit/1485f8426ebb6dd1390a0062c5e83ed1c0461f21))\n - **DOCS**: Enable CSpell on tests ([#2723](https://github.com/flame-engine/flame/issues/2723)). ([e051298c](https://github.com/flame-engine/flame/commit/e051298cba76550229780438b1a589557c7b488d))\n - **DOCS**: Improved spellchecking ([#2722](https://github.com/flame-engine/flame/issues/2722)). ([2f973abe](https://github.com/flame-engine/flame/commit/2f973abe8b298a4f6f1164065783de560953d789))\n\n\n## 2023-08-08\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.8.2`](#flame---v182)\n - [`flame_lint` - `v1.1.0`](#flame_lint---v110)\n\nPackages with other changes:\n\n - [`flame_rive` - `v1.9.1`](#flame_rive---v191)\n - [`flame_tiled` - `v1.13.0`](#flame_tiled---v1130)\n - [`flame_isolate` - `v0.4.0+2`](#flame_isolate---v0402)\n - [`flame_audio` - `v2.0.5`](#flame_audio---v205)\n - [`flame_spine` - `v0.1.1+1`](#flame_spine---v0111)\n - [`flame_svg` - `v1.8.1`](#flame_svg---v181)\n - [`flame_test` - `v1.12.1`](#flame_test---v1121)\n - [`flame_oxygen` - `v0.1.8+5`](#flame_oxygen---v0185)\n - [`flame_bloc` - `v1.10.1`](#flame_bloc---v1101)\n - [`flame_fire_atlas` - `v1.3.8`](#flame_fire_atlas---v138)\n - [`flame_forge2d` - `v0.14.1+1`](#flame_forge2d---v01411)\n - [`flame_noise` - `v0.1.1+4`](#flame_noise---v0114)\n - [`flame_network_assets` - `v0.2.0+4`](#flame_network_assets---v0204)\n - [`flame_lottie` - `v0.2.1+1`](#flame_lottie---v0211)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_isolate` - `v0.4.0+2`\n - `flame_audio` - `v2.0.5`\n - `flame_spine` - `v0.1.1+1`\n - `flame_svg` - `v1.8.1`\n - `flame_test` - `v1.12.1`\n - `flame_oxygen` - `v0.1.8+5`\n - `flame_bloc` - `v1.10.1`\n - `flame_fire_atlas` - `v1.3.8`\n - `flame_forge2d` - `v0.14.1+1`\n - `flame_noise` - `v0.1.1+4`\n - `flame_network_assets` - `v0.2.0+4`\n - `flame_lottie` - `v0.2.1+1`\n\n---\n\n#### `flame` - `v1.8.2`\n\n - **PERF**: Improve performance of raycasts ([#2617](https://github.com/flame-engine/flame/issues/2617)). ([8e0a7879](https://github.com/flame-engine/flame/commit/8e0a7879d7669e09efcbcee28d9f2038fe9014c0))\n - **FIX**: Reset _completeCompleter in ticker ([#2636](https://github.com/flame-engine/flame/issues/2636)). ([a35d3a10](https://github.com/flame-engine/flame/commit/a35d3a10abfe9e5caab1a646e0980d03fbf585d1))\n - **FIX**: Viewport should recieve events before the world  ([#2630](https://github.com/flame-engine/flame/issues/2630)). ([e852064e](https://github.com/flame-engine/flame/commit/e852064e494e58ea2be19a5b035e09ed2e465608))\n - **FIX**: Use `ComponentKey`s to keep track of dispatchers ([#2629](https://github.com/flame-engine/flame/issues/2629)). ([ff59aa15](https://github.com/flame-engine/flame/commit/ff59aa152c5a2e0b360f980c78a8b3cc4fad7507))\n - **FIX**: FlameGame onRemove fix to prevent memory leak ([#2602](https://github.com/flame-engine/flame/issues/2602)). ([dac2ebbf](https://github.com/flame-engine/flame/commit/dac2ebbf506ff48ca8f34d872bbc47cba3ad6c7b))\n - **FIX**: Only use pre-set ReadonlySizeProvider for sizing in HudMarginComponent ([#2611](https://github.com/flame-engine/flame/issues/2611)). ([832c0510](https://github.com/flame-engine/flame/commit/832c051085e0fade8a7e4b262bf9941d279baef4))\n - **FIX**: TextBoxConfig dismissDelay to not be ignored ([#2607](https://github.com/flame-engine/flame/issues/2607)). ([1567b389](https://github.com/flame-engine/flame/commit/1567b3891057e4ce168d76c920bd40403febd82a))\n - **FEAT**: Adding key argument to shape components ([#2632](https://github.com/flame-engine/flame/issues/2632)). ([c542d3c3](https://github.com/flame-engine/flame/commit/c542d3c34bf911cec8332dcdeb65d0017e6cb576))\n - **FEAT**: Add optional world input to `CameraComponent.canSee` ([#2616](https://github.com/flame-engine/flame/issues/2616)). ([1cad0b23](https://github.com/flame-engine/flame/commit/1cad0b23e18db8f352da5790c8ea5ec6053936da))\n - **FEAT**: Add a Circle.fromPoints utility method ([#2603](https://github.com/flame-engine/flame/issues/2603)). ([a83f2815](https://github.com/flame-engine/flame/commit/a83f2815bbdaf9c176a34a325485a96b5a323575))\n - **FEAT**: Add a midpoint getter to LineSegment ([#2605](https://github.com/flame-engine/flame/issues/2605)). ([1f9f3509](https://github.com/flame-engine/flame/commit/1f9f35093b3b90113e32a36e1103b87246212fa4))\n - **FEAT**: Add Rectangle.fromLTWH and Rect.toFlameRectangle utility methods ([#2604](https://github.com/flame-engine/flame/issues/2604)). ([76271cee](https://github.com/flame-engine/flame/commit/76271ceef04264ec8fa5c39a23f43d638d731694))\n - **DOCS**: Add more guidance to collision detection algorithm choices ([#2624](https://github.com/flame-engine/flame/issues/2624)). ([781e8983](https://github.com/flame-engine/flame/commit/781e898315a0162117a83bf62e2650ce7244503d))\n - **BREAKING** **PERF**: Pool `CollisionProspect`s and remove some list creations from the collision detection ([#2625](https://github.com/flame-engine/flame/issues/2625)). ([e430b6cd](https://github.com/flame-engine/flame/commit/e430b6cdf2e6be52bf384efb3428bcb41ae13d30))\n - **BREAKING** **FEAT**: Make world nullable in `CameraComponent` ([#2615](https://github.com/flame-engine/flame/issues/2615)). ([14f51635](https://github.com/flame-engine/flame/commit/14f51635421b8b30049ea287b7c472e54a269250))\n\n#### `flame_lint` - `v1.1.0`\n\n - **BREAKING** **PERF**: Pool `CollisionProspect`s and remove some list creations from the collision detection ([#2625](https://github.com/flame-engine/flame/issues/2625)). ([e430b6cd](https://github.com/flame-engine/flame/commit/e430b6cdf2e6be52bf384efb3428bcb41ae13d30))\n\n#### `flame_rive` - `v1.9.1`\n\n - **FIX**: Respect artboard clip value ([#2639](https://github.com/flame-engine/flame/issues/2639)). ([4e664245](https://github.com/flame-engine/flame/commit/4e6642458494b4d4544bcc03b568476faeb0a71f))\n\n#### `flame_tiled` - `v1.13.0`\n\n - **FIX**: Compute scale in TileLayers based on native map tile size rather than image sizes to support oversized/undersized tiles. ([#2634](https://github.com/flame-engine/flame/issues/2634)). ([1c4d6cd0](https://github.com/flame-engine/flame/commit/1c4d6cd0654f133771a7af5795cc1de2343268c1))\n - **FEAT**: Possiblity to pass in FilterQuality to tiled layers ([#2627](https://github.com/flame-engine/flame/issues/2627)). ([f3de6650](https://github.com/flame-engine/flame/commit/f3de66507e623e2fe0100cfd4d002dea14f72470))\n\n\n## 2023-07-02\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.8.1`](#flame---v181)\n - [`flame_test` - `v1.12.0`](#flame_test---v1120)\n\nPackages with other changes:\n\n - [`flame_audio` - `v2.0.4`](#flame_audio---v204)\n - [`flame_bloc` - `v1.10.0`](#flame_bloc---v1100)\n - [`flame_fire_atlas` - `v1.3.7`](#flame_fire_atlas---v137)\n - [`flame_forge2d` - `v0.14.1`](#flame_forge2d---v0141)\n - [`flame_isolate` - `v0.4.0+1`](#flame_isolate---v0401)\n - [`flame_lottie` - `v0.2.1`](#flame_lottie---v021)\n - [`flame_noise` - `v0.1.1+3`](#flame_noise---v0113)\n - [`flame_oxygen` - `v0.1.8+4`](#flame_oxygen---v0184)\n - [`flame_rive` - `v1.9.0`](#flame_rive---v190)\n - [`flame_spine` - `v0.1.1`](#flame_spine---v011)\n - [`flame_svg` - `v1.8.0`](#flame_svg---v180)\n - [`flame_tiled` - `v1.12.0`](#flame_tiled---v1120)\n - [`jenny` - `v1.0.4`](#jenny---v104)\n - [`flame_network_assets` - `v0.2.0+3`](#flame_network_assets---v0203)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_network_assets` - `v0.2.0+3`\n\n---\n\n#### `flame` - `v1.8.1`\n\n - **FIX**: Adds a check to confirm the component is not loaded ([#2579](https://github.com/flame-engine/flame/issues/2579)). ([985400f2](https://github.com/flame-engine/flame/commit/985400f2955f6bed14066660711d53c5b302ab09))\n - **FIX**: Animation ticker readability improvements ([#2578](https://github.com/flame-engine/flame/issues/2578)). ([667a1698](https://github.com/flame-engine/flame/commit/667a1698115ed69cc11b2e5a598371e136c7e7f0))\n - **FIX**: Remove `mustCallSuper` from `onComponentTypeCheck` ([#2561](https://github.com/flame-engine/flame/issues/2561)). ([bcae760c](https://github.com/flame-engine/flame/commit/bcae760c7138839fee203a1693e02fade753292c))\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Reduce the Vector2 creations in Anchor ([#2550](https://github.com/flame-engine/flame/issues/2550)). ([5a9434b0](https://github.com/flame-engine/flame/commit/5a9434b09a6fbe2c86db2d8192cd2d7ae0d5868c))\n - **FIX**: Fix disappearing text on TextBoxComponent for larger pixelRatios ([#2540](https://github.com/flame-engine/flame/issues/2540)). ([6e1d5466](https://github.com/flame-engine/flame/commit/6e1d5466aadc59f90475b1a9e7658bb78ed60340))\n - **FEAT**: Option to prevent propagating collision events from ShapeHitbox to _hitboxParent ([#2594](https://github.com/flame-engine/flame/issues/2594)). ([a58d7436](https://github.com/flame-engine/flame/commit/a58d7436c9b71a2358edc6c3732aeda56d980f64))\n - **FEAT**: Adding filterQuality arguments to Parallax load methods ([#2596](https://github.com/flame-engine/flame/issues/2596)). ([ff3d9107](https://github.com/flame-engine/flame/commit/ff3d91075c49df8efb6130f8e8ac9b711a1a8a14))\n - **FEAT**: Option to use toImageSync in ImageComposition class ([#2593](https://github.com/flame-engine/flame/issues/2593)). ([66d5f97d](https://github.com/flame-engine/flame/commit/66d5f97d303aa1712673b8ca7e1a889cf5e7270e))\n - **FEAT**: ComponentKey API ([#2566](https://github.com/flame-engine/flame/issues/2566)). ([b3efb612](https://github.com/flame-engine/flame/commit/b3efb612cb3cb77f69bc030e9ba71516348035d2))\n - **FEAT**(flame): Set a default negative priority on the world for general use ([#2572](https://github.com/flame-engine/flame/issues/2572)). ([390e9700](https://github.com/flame-engine/flame/commit/390e9700b4293e12b7d4212ce04f6b3d967a24e1))\n - **FEAT**: Add useful methods to Rectangle class ([#2562](https://github.com/flame-engine/flame/issues/2562)). ([4710530b](https://github.com/flame-engine/flame/commit/4710530b420469794602bf4d8cfea98078e0d973))\n - **BREAKING** **FIX**: Convert PositionEvent.canvasPosition to local coordinates ([#2598](https://github.com/flame-engine/flame/issues/2598)). ([87139c85](https://github.com/flame-engine/flame/commit/87139c854534782638fe1b0c24d2dc92f98a3e59))\n\n#### `flame_test` - `v1.12.0`\n\n - **FIX**: Set constraint for test dependency in flame_test ([#2558](https://github.com/flame-engine/flame/issues/2558)). ([aeef9464](https://github.com/flame-engine/flame/commit/aeef9464f6ca448e3aa2b578af8b3443cbbf6f71))\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **BREAKING** **FIX**: Convert PositionEvent.canvasPosition to local coordinates ([#2598](https://github.com/flame-engine/flame/issues/2598)). ([87139c85](https://github.com/flame-engine/flame/commit/87139c854534782638fe1b0c24d2dc92f98a3e59))\n\n#### `flame_audio` - `v2.0.4`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n\n#### `flame_bloc` - `v1.10.0`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FEAT**: ComponentKey API ([#2566](https://github.com/flame-engine/flame/issues/2566)). ([b3efb612](https://github.com/flame-engine/flame/commit/b3efb612cb3cb77f69bc030e9ba71516348035d2))\n - **FEAT**: Add onInitialState to FlameBlocListener ([#2565](https://github.com/flame-engine/flame/issues/2565)). ([f440bbf5](https://github.com/flame-engine/flame/commit/f440bbf5db207d454b4abba75a62e0ff2ff5b408))\n\n#### `flame_fire_atlas` - `v1.3.7`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n\n#### `flame_forge2d` - `v0.14.1`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FEAT**: ComponentKey API ([#2566](https://github.com/flame-engine/flame/issues/2566)). ([b3efb612](https://github.com/flame-engine/flame/commit/b3efb612cb3cb77f69bc030e9ba71516348035d2))\n - **DOCS**: Fix broken link on flame_forge2d readme ([#2588](https://github.com/flame-engine/flame/issues/2588)). ([45115bbf](https://github.com/flame-engine/flame/commit/45115bbff8539010f5d7bb7cf9479183b1a27cc8))\n\n#### `flame_isolate` - `v0.4.0+1`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n\n#### `flame_lottie` - `v0.2.1`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FEAT**: ComponentKey API ([#2566](https://github.com/flame-engine/flame/issues/2566)). ([b3efb612](https://github.com/flame-engine/flame/commit/b3efb612cb3cb77f69bc030e9ba71516348035d2))\n\n#### `flame_noise` - `v0.1.1+3`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n\n#### `flame_oxygen` - `v0.1.8+4`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n\n#### `flame_rive` - `v1.9.0`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Avoid creation of unnecessary objects for RiveComponent ([#2553](https://github.com/flame-engine/flame/issues/2553)). ([52b35fbf](https://github.com/flame-engine/flame/commit/52b35fbf56a551a7585c493e2de51473266bf759))\n - **FEAT**: ComponentKey API ([#2566](https://github.com/flame-engine/flame/issues/2566)). ([b3efb612](https://github.com/flame-engine/flame/commit/b3efb612cb3cb77f69bc030e9ba71516348035d2))\n\n#### `flame_spine` - `v0.1.1`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FEAT**: ComponentKey API ([#2566](https://github.com/flame-engine/flame/issues/2566)). ([b3efb612](https://github.com/flame-engine/flame/commit/b3efb612cb3cb77f69bc030e9ba71516348035d2))\n\n#### `flame_svg` - `v1.8.0`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FEAT**: ComponentKey API ([#2566](https://github.com/flame-engine/flame/issues/2566)). ([b3efb612](https://github.com/flame-engine/flame/commit/b3efb612cb3cb77f69bc030e9ba71516348035d2))\n\n#### `flame_tiled` - `v1.12.0`\n\n - **FIX**: Tiled component orthogonal test ([#2549](https://github.com/flame-engine/flame/issues/2549)). ([34e5f0e4](https://github.com/flame-engine/flame/commit/34e5f0e443e21923c311120ce8634a14339bc71d))\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FEAT**: TiledAtlas.clearCache function ([#2592](https://github.com/flame-engine/flame/issues/2592)). ([d40fefcf](https://github.com/flame-engine/flame/commit/d40fefcf08850a986304472d5369dcd74f2b9d4b))\n - **FEAT**: ComponentKey API ([#2566](https://github.com/flame-engine/flame/issues/2566)). ([b3efb612](https://github.com/flame-engine/flame/commit/b3efb612cb3cb77f69bc030e9ba71516348035d2))\n - **FEAT**: Add option for a custom image and asset loader ([#2569](https://github.com/flame-engine/flame/issues/2569)). ([dfe18251](https://github.com/flame-engine/flame/commit/dfe18251c1bac8aaca9bf146e03320efbbc3ce9c))\n\n#### `jenny` - `v1.0.4`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n\n\n## 2023-06-09\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_tiled` - `v1.11.0`](#flame_tiled---v1110)\n\n---\n\n#### `flame_tiled` - `v1.11.0`\n\n - **FIX**: Tiled component orthogonal test ([#2549](https://github.com/flame-engine/flame/issues/2549)). ([34e5f0e4](https://github.com/flame-engine/flame/commit/34e5f0e443e21923c311120ce8634a14339bc71d))\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FEAT**: Add option for a custom image and asset loader ([#2569](https://github.com/flame-engine/flame/issues/2569)). ([dfe18251](https://github.com/flame-engine/flame/commit/dfe18251c1bac8aaca9bf146e03320efbbc3ce9c))\n\n\n## 2023-05-28\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.8.0`](#flame---v180)\n - [`flame_rive` - `v1.8.0`](#flame_rive---v180)\n - [`flame_test` - `v1.11.0`](#flame_test---v1110)\n - [`flame_forge2d` - `v0.14.0`](#flame_forge2d---v0140)\n - [`flame_isolate` - `v0.4.0`](#flame_isolate---v040)\n\nPackages with other changes:\n\n - [`flame_lint` - `v1.0.0`](#flame_lint---v100)\n - [`flame_audio` - `v2.0.3`](#flame_audio---v203)\n - [`flame_bloc` - `v1.9.0`](#flame_bloc---v190)\n - [`flame_fire_atlas` - `v1.3.6`](#flame_fire_atlas---v136)\n - [`flame_lottie` - `v0.2.0+3`](#flame_lottie---v0203)\n - [`flame_network_assets` - `v0.2.0+2`](#flame_network_assets---v0202)\n - [`flame_noise` - `v0.1.1+2`](#flame_noise---v0112)\n - [`flame_oxygen` - `v0.1.8+3`](#flame_oxygen---v0183)\n - [`flame_spine` - `v0.1.0+1`](#flame_spine---v0101)\n - [`flame_svg` - `v1.7.4`](#flame_svg---v174)\n - [`flame_tiled` - `v1.10.2`](#flame_tiled---v1102)\n - [`jenny` - `v1.0.3`](#jenny---v103)\n\n---\n\n#### `flame` - `v1.8.0`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Reduce the Vector2 creations in Anchor ([#2550](https://github.com/flame-engine/flame/issues/2550)). ([5a9434b0](https://github.com/flame-engine/flame/commit/5a9434b09a6fbe2c86db2d8192cd2d7ae0d5868c))\n - **FIX**: Fix disappearing text on TextBoxComponent for larger pixelRatios ([#2540](https://github.com/flame-engine/flame/issues/2540)). ([6e1d5466](https://github.com/flame-engine/flame/commit/6e1d5466aadc59f90475b1a9e7658bb78ed60340))\n - **FIX**: Avoid the creation of Vector2 objects in Parallax update ([#2536](https://github.com/flame-engine/flame/issues/2536)). ([3849f07d](https://github.com/flame-engine/flame/commit/3849f07d50870e4364caf9e115e869d8fed6aaed))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n - **FIX**: Move `errorBuilder` and `loadingBuilder` to constructors ([#2526](https://github.com/flame-engine/flame/issues/2526)). ([55ec0bc3](https://github.com/flame-engine/flame/commit/55ec0bc3cbebc0106dba2e0d4f3fd7693b9bc6d6))\n - **FEAT**: Add onComplete callback to `AnimationWidget` ([#2515](https://github.com/flame-engine/flame/issues/2515)). ([0b68be8a](https://github.com/flame-engine/flame/commit/0b68be8a6f306b0102b3be980dec661909d2c1e0))\n - **FEAT**: Add `stepEngine` to `Game` ([#2516](https://github.com/flame-engine/flame/issues/2516)). ([1ed2c5a2](https://github.com/flame-engine/flame/commit/1ed2c5a2974876a32f620d9dc9cb385e4e928c50))\n - **FEAT**: Customise grid of NineTileBox ([#2495](https://github.com/flame-engine/flame/issues/2495)). ([a25b0a03](https://github.com/flame-engine/flame/commit/a25b0a03a56975e1de2e15747bc3e527ac232545))\n - **FEAT**: Accept `CollisionType` in hitbox constructor ([#2509](https://github.com/flame-engine/flame/issues/2509)). ([89926227](https://github.com/flame-engine/flame/commit/89926227c5132455b971dece6ed313634d7ac873))\n - **DOCS**: Update content types of sphinx code snippets ([#2519](https://github.com/flame-engine/flame/issues/2519)). ([306ad320](https://github.com/flame-engine/flame/commit/306ad32052cfba9c6b3ab38ebb7d0604742d2993))\n - **BREAKING** **REFACTOR**: Move `CameraComponent` and events out of experimental ([#2505](https://github.com/flame-engine/flame/issues/2505)). ([87b8a067](https://github.com/flame-engine/flame/commit/87b8a067f3e0096cebff3db4f5767e68616928fd))\n - **BREAKING** **FEAT**: Add `SpriteAnimationTicker` ([#2457](https://github.com/flame-engine/flame/issues/2457)). ([a50c80cf](https://github.com/flame-engine/flame/commit/a50c80cfa34c08463ab29efe4a1f546fb47da34e))\n\n#### `flame_rive` - `v1.8.0`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Avoid creation of unnecessary objects for RiveComponent ([#2553](https://github.com/flame-engine/flame/issues/2553)). ([52b35fbf](https://github.com/flame-engine/flame/commit/52b35fbf56a551a7585c493e2de51473266bf759))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n - **BREAKING** **REFACTOR**: Move `CameraComponent` and events out of experimental ([#2505](https://github.com/flame-engine/flame/issues/2505)). ([87b8a067](https://github.com/flame-engine/flame/commit/87b8a067f3e0096cebff3db4f5767e68616928fd))\n\n#### `flame_test` - `v1.11.0`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n - **BREAKING** **REFACTOR**: Move `CameraComponent` and events out of experimental ([#2505](https://github.com/flame-engine/flame/issues/2505)). ([87b8a067](https://github.com/flame-engine/flame/commit/87b8a067f3e0096cebff3db4f5767e68616928fd))\n\n#### `flame_forge2d` - `v0.14.0`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n - **BREAKING** **REFACTOR**: Move `CameraComponent` and events out of experimental ([#2505](https://github.com/flame-engine/flame/issues/2505)). ([87b8a067](https://github.com/flame-engine/flame/commit/87b8a067f3e0096cebff3db4f5767e68616928fd))\n\n#### `flame_isolate` - `v0.4.0`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n - **BREAKING** **REFACTOR**: Move `CameraComponent` and events out of experimental ([#2505](https://github.com/flame-engine/flame/issues/2505)). ([87b8a067](https://github.com/flame-engine/flame/commit/87b8a067f3e0096cebff3db4f5767e68616928fd))\n\n#### `flame_lint` - `v1.0.0`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n#### `flame_audio` - `v2.0.3`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n#### `flame_bloc` - `v1.9.0`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n - **FEAT**: Add listener for initial state on flame_bloc ([#2382](https://github.com/flame-engine/flame/issues/2382)). ([01121c22](https://github.com/flame-engine/flame/commit/01121c220bec391e0242dfa9afc3d4a03bb3358b))\n - **FEAT**: Accept `CollisionType` in hitbox constructor ([#2509](https://github.com/flame-engine/flame/issues/2509)). ([89926227](https://github.com/flame-engine/flame/commit/89926227c5132455b971dece6ed313634d7ac873))\n\n#### `flame_fire_atlas` - `v1.3.6`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n#### `flame_lottie` - `v0.2.0+3`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n#### `flame_network_assets` - `v0.2.0+2`\n\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n#### `flame_noise` - `v0.1.1+2`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n#### `flame_oxygen` - `v0.1.8+3`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n#### `flame_spine` - `v0.1.0+1`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n#### `flame_svg` - `v1.7.4`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n#### `flame_tiled` - `v1.10.2`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n#### `jenny` - `v1.0.3`\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n\n## 2023-05-05\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_spine` - `v0.1.0`](#flame_spine---v010)\n\n---\n\n#### `flame_spine` - `v0.1.0`\n\n - **FEAT**: Spine bridge package ([#2530](https://github.com/flame-engine/flame/issues/2530)). ([5d1a6fd1](https://github.com/flame-engine/flame/commit/5d1a6fd1679c5690685e5a5f9b695a0ab6699bca))\n\n\n## 2023-04-19\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_audio` - `v2.0.2`](#flame_audio---v202)\n\n---\n\n#### `flame_audio` - `v2.0.2`\n\n - **FIX**: Release instead of dispose audioplayer in play ([#2513](https://github.com/flame-engine/flame/issues/2513)). ([e699b259](https://github.com/flame-engine/flame/commit/e699b259e99619bb97fe370ce0679157e65eb42b))\n\n\n## 2023-04-16\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.7.3`](#flame---v173)\n - [`flame_audio` - `v2.0.1`](#flame_audio---v201)\n - [`flame_bloc` - `v1.8.4`](#flame_bloc---v184)\n - [`flame_fire_atlas` - `v1.3.5`](#flame_fire_atlas---v135)\n - [`flame_flare` - `v1.5.4`](#flame_flare---v154)\n - [`flame_forge2d` - `v0.13.0+1`](#flame_forge2d---v01301)\n - [`flame_isolate` - `v0.3.0+1`](#flame_isolate---v0301)\n - [`flame_lint` - `v0.2.0+2`](#flame_lint---v0202)\n - [`flame_oxygen` - `v0.1.8+2`](#flame_oxygen---v0182)\n - [`flame_rive` - `v1.7.1`](#flame_rive---v171)\n - [`flame_svg` - `v1.7.3`](#flame_svg---v173)\n - [`flame_test` - `v1.10.1`](#flame_test---v1101)\n - [`flame_tiled` - `v1.10.1`](#flame_tiled---v1101)\n - [`jenny` - `v1.0.2`](#jenny---v102)\n - [`flame_noise` - `v0.1.1+1`](#flame_noise---v0111)\n - [`flame_network_assets` - `v0.2.0+1`](#flame_network_assets---v0201)\n - [`flame_lottie` - `v0.2.0+2`](#flame_lottie---v0202)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_noise` - `v0.1.1+1`\n - `flame_network_assets` - `v0.2.0+1`\n - `flame_lottie` - `v0.2.0+2`\n\n---\n\n#### `flame` - `v1.7.3`\n\n - **REFACTOR**: Make atlas status to be more readable ([#2502](https://github.com/flame-engine/flame/issues/2502)). ([643793d0](https://github.com/flame-engine/flame/commit/643793d06e1c9264ce8fd557552ad8405bc65ec1))\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n - **FIX**: Reverse invalid polygon definitions ([#2503](https://github.com/flame-engine/flame/issues/2503)). ([c4c516eb](https://github.com/flame-engine/flame/commit/c4c516ebf8fe6b8eaf82a3e49454b64faf6a7cd2))\n - **FIX**: Fill in mount implementation in `HasTappables` ([#2496](https://github.com/flame-engine/flame/issues/2496)). ([d51a612f](https://github.com/flame-engine/flame/commit/d51a612f8bed2a7a294444e5f11402394dfbc3cd))\n - **FIX**: Modify size only if changed while auto-resizing ([#2498](https://github.com/flame-engine/flame/issues/2498)). ([aa8d49da](https://github.com/flame-engine/flame/commit/aa8d49da9eb77c47d252ac3cc46d268eb10a2f20))\n - **FIX**: RecycleQueue cannot extends and implements Iterable at the same time ([#2497](https://github.com/flame-engine/flame/issues/2497)). ([3e5be3d6](https://github.com/flame-engine/flame/commit/3e5be3d6c23bfc61237befa5d17311474c6d4234))\n - **FIX**: Remove memory leak when creating the image from PictureRecorder ([#2493](https://github.com/flame-engine/flame/issues/2493)). ([a66f2bc0](https://github.com/flame-engine/flame/commit/a66f2bc0a97415f4f57b6c55174a2930cdf9e61b))\n - **FEAT**: Bump ordered_set version ([#2500](https://github.com/flame-engine/flame/issues/2500)). ([81303ea9](https://github.com/flame-engine/flame/commit/81303ea9d805c04c5d85c8e7c2f40ab8e43ae811))\n - **FEAT**: Deprecate `Component.changeParent` ([#2478](https://github.com/flame-engine/flame/issues/2478)). ([bd3e7886](https://github.com/flame-engine/flame/commit/bd3e7886125e60ad1386ec864a5ef33382f7f7f5))\n\n#### `flame_audio` - `v2.0.1`\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n#### `flame_bloc` - `v1.8.4`\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n#### `flame_fire_atlas` - `v1.3.5`\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n#### `flame_flare` - `v1.5.4`\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n#### `flame_forge2d` - `v0.13.0+1`\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n#### `flame_isolate` - `v0.3.0+1`\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n#### `flame_lint` - `v0.2.0+2`\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n#### `flame_oxygen` - `v0.1.8+2`\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n#### `flame_rive` - `v1.7.1`\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n#### `flame_svg` - `v1.7.3`\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n - **FIX**: Remove memory leak when creating the image from PictureRecorder ([#2493](https://github.com/flame-engine/flame/issues/2493)). ([a66f2bc0](https://github.com/flame-engine/flame/commit/a66f2bc0a97415f4f57b6c55174a2930cdf9e61b))\n\n#### `flame_test` - `v1.10.1`\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n#### `flame_tiled` - `v1.10.1`\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n#### `jenny` - `v1.0.2`\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n\n## 2023-04-11\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame_audio` - `v2.0.0`](#flame_audio---v200)\n\nPackages with other changes:\n\n - There are no other changes in this release.\n\n---\n\n#### `flame_audio` - `v2.0.0`\n\n - **BREAKING** **FEAT**: Update AudioPlayers to ^4.0.0 ([#2482](https://github.com/flame-engine/flame/issues/2482)). ([47372087](https://github.com/flame-engine/flame/commit/47372087f218e9c00d0fec82084f6edc7cbee5af))\n\n\n## 2023-04-07\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.7.2`](#flame---v172)\n - [`flame_isolate` - `v0.3.0+1`](#flame_isolate---v0301)\n - [`flame_tiled` - `v1.10.1`](#flame_tiled---v1101)\n - [`flame_audio` - `v1.4.2`](#flame_audio---v142)\n - [`flame_svg` - `v1.7.3`](#flame_svg---v173)\n - [`flame_test` - `v1.10.1`](#flame_test---v1101)\n - [`flame_flare` - `v1.5.4`](#flame_flare---v154)\n - [`flame_oxygen` - `v0.1.8+2`](#flame_oxygen---v0182)\n - [`flame_bloc` - `v1.8.4`](#flame_bloc---v184)\n - [`flame_fire_atlas` - `v1.3.5`](#flame_fire_atlas---v135)\n - [`flame_forge2d` - `v0.13.0+1`](#flame_forge2d---v01301)\n - [`flame_rive` - `v1.7.1`](#flame_rive---v171)\n - [`flame_noise` - `v0.1.1+1`](#flame_noise---v0111)\n - [`flame_network_assets` - `v0.2.0+1`](#flame_network_assets---v0201)\n - [`flame_lottie` - `v0.2.0+2`](#flame_lottie---v0202)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_isolate` - `v0.3.0+1`\n - `flame_tiled` - `v1.10.1`\n - `flame_audio` - `v1.4.2`\n - `flame_svg` - `v1.7.3`\n - `flame_test` - `v1.10.1`\n - `flame_flare` - `v1.5.4`\n - `flame_oxygen` - `v0.1.8+2`\n - `flame_bloc` - `v1.8.4`\n - `flame_fire_atlas` - `v1.3.5`\n - `flame_forge2d` - `v0.13.0+1`\n - `flame_rive` - `v1.7.1`\n - `flame_noise` - `v0.1.1+1`\n - `flame_network_assets` - `v0.2.0+1`\n - `flame_lottie` - `v0.2.0+2`\n\n---\n\n#### `flame` - `v1.7.2`\n\n - **FIX**: A mistake in auto-resizing disabling logic ([#2471](https://github.com/flame-engine/flame/issues/2471)). ([e7ebf8e5](https://github.com/flame-engine/flame/commit/e7ebf8e55a0ad7b0f3aaae769c0b8855fb1efd96))\n - **FIX**: It should be possible to re-add `ColorEffect` ([#2469](https://github.com/flame-engine/flame/issues/2469)). ([6fa9e9d5](https://github.com/flame-engine/flame/commit/6fa9e9d5470eaf36c2db5f3b040e708615dbfcf1))\n - **FEAT**: Add `isDragged` in `DragCallbacks` mixin ([#2472](https://github.com/flame-engine/flame/issues/2472)). ([de630a1c](https://github.com/flame-engine/flame/commit/de630a1c3a779cefe49a598b46e105f19aacebfb))\n\n\n## 2023-04-05\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.7.1`](#flame---v171)\n - [`flame_isolate` - `v0.3.0+1`](#flame_isolate---v0301)\n - [`flame_tiled` - `v1.10.1`](#flame_tiled---v1101)\n - [`flame_audio` - `v1.4.2`](#flame_audio---v142)\n - [`flame_svg` - `v1.7.3`](#flame_svg---v173)\n - [`flame_test` - `v1.10.1`](#flame_test---v1101)\n - [`flame_flare` - `v1.5.4`](#flame_flare---v154)\n - [`flame_oxygen` - `v0.1.8+2`](#flame_oxygen---v0182)\n - [`flame_bloc` - `v1.8.4`](#flame_bloc---v184)\n - [`flame_fire_atlas` - `v1.3.5`](#flame_fire_atlas---v135)\n - [`flame_forge2d` - `v0.13.0+1`](#flame_forge2d---v01301)\n - [`flame_rive` - `v1.7.1`](#flame_rive---v171)\n - [`flame_noise` - `v0.1.1+1`](#flame_noise---v0111)\n - [`flame_network_assets` - `v0.2.0+1`](#flame_network_assets---v0201)\n - [`flame_lottie` - `v0.2.0+2`](#flame_lottie---v0202)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_isolate` - `v0.3.0+1`\n - `flame_tiled` - `v1.10.1`\n - `flame_audio` - `v1.4.2`\n - `flame_svg` - `v1.7.3`\n - `flame_test` - `v1.10.1`\n - `flame_flare` - `v1.5.4`\n - `flame_oxygen` - `v0.1.8+2`\n - `flame_bloc` - `v1.8.4`\n - `flame_fire_atlas` - `v1.3.5`\n - `flame_forge2d` - `v0.13.0+1`\n - `flame_rive` - `v1.7.1`\n - `flame_noise` - `v0.1.1+1`\n - `flame_network_assets` - `v0.2.0+1`\n - `flame_lottie` - `v0.2.0+2`\n\n---\n\n#### `flame` - `v1.7.1`\n\n - **FIX**: Stop auto-resizing on external size change in sprite based components ([#2467](https://github.com/flame-engine/flame/issues/2467)). ([df236af4](https://github.com/flame-engine/flame/commit/df236af4f0164cc20b664ab973d91b4554b13b62))\n\n\n## 2023-04-02\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.7.0`](#flame---v170)\n - [`flame_rive` - `v1.7.0`](#flame_rive---v170)\n - [`flame_test` - `v1.10.0`](#flame_test---v1100)\n - [`flame_forge2d` - `v0.13.0`](#flame_forge2d---v0130)\n - [`flame_isolate` - `v0.3.0`](#flame_isolate---v030)\n - [`flame_network_assets` - `v0.2.0`](#flame_network_assets---v020)\n\nPackages with other changes:\n\n - [`flame_audio` - `v1.4.1`](#flame_audio---v141)\n - [`flame_bloc` - `v1.8.3`](#flame_bloc---v183)\n - [`flame_fire_atlas` - `v1.3.4`](#flame_fire_atlas---v134)\n - [`flame_flare` - `v1.5.3`](#flame_flare---v153)\n - [`flame_lint` - `v0.2.0+1`](#flame_lint---v0201)\n - [`flame_lottie` - `v0.2.0+1`](#flame_lottie---v0201)\n - [`flame_noise` - `v0.1.1`](#flame_noise---v011)\n - [`flame_oxygen` - `v0.1.8+1`](#flame_oxygen---v0181)\n - [`flame_svg` - `v1.7.2`](#flame_svg---v172)\n - [`flame_tiled` - `v1.10.0`](#flame_tiled---v1100)\n - [`jenny` - `v1.0.1`](#jenny---v101)\n\n---\n\n#### `flame` - `v1.7.0`\n\n - **REFACTOR**: Remove \"items\" variable from core Broadphase class. ([#2284](https://github.com/flame-engine/flame/issues/2284)). ([1819c575](https://github.com/flame-engine/flame/commit/1819c5759060579b8fbbf273befe622e799fef32))\n - **REFACTOR**: Added ComponentTreeRoot ([#2300](https://github.com/flame-engine/flame/issues/2300)). ([619b9b15](https://github.com/flame-engine/flame/commit/619b9b15da5c8992547b38bc88a1378933c20026))\n - **REFACTOR**: Simplify how images.dart decodes images ([#2293](https://github.com/flame-engine/flame/issues/2293)). ([b4925423](https://github.com/flame-engine/flame/commit/b4925423d78f4b152b4808e1aceadf211cc7d2e8))\n - **REFACTOR**: Use variable name on toString in component_test.dart ([#2377](https://github.com/flame-engine/flame/issues/2377)). ([f5c0e5e9](https://github.com/flame-engine/flame/commit/f5c0e5e9d0d20e2c89a57f41f968aeafb3a5a753))\n - **REFACTOR**: Remove unused variable \"tapTimes\" from multi_touch_tap_detector_test.dart ([#2379](https://github.com/flame-engine/flame/issues/2379)). ([cd2b2a10](https://github.com/flame-engine/flame/commit/cd2b2a109707e32a82d9f96b84218d30c03554ab))\n - **REFACTOR**: Component rebalancing is now performed via a global queue ([#2352](https://github.com/flame-engine/flame/issues/2352)). ([1ef51879](https://github.com/flame-engine/flame/commit/1ef518794c8b02995afb1fd0b431a804ef122a4c))\n - **REFACTOR**: Component adoption now handled via ComponentTreeRoot ([#2332](https://github.com/flame-engine/flame/issues/2332)). ([5ceb5dda](https://github.com/flame-engine/flame/commit/5ceb5dda5c6fc27bbad96445f0e99e5e006e5ed3))\n - **FIX**: Auto-resize `SpriteComponent` on sprite change ([#2430](https://github.com/flame-engine/flame/issues/2430)). ([158460d7](https://github.com/flame-engine/flame/commit/158460d7c66c49ffc6ffc99d43a9f547d6ab4e01))\n - **FIX**: Update MoveAlongPathEffect ([#2422](https://github.com/flame-engine/flame/issues/2422)). ([295cd724](https://github.com/flame-engine/flame/commit/295cd72422ee068635057fe9e6684edd5021a9e4))\n - **FIX**: Removed component to be deleted from _broadphaseCheckCache ([#2282](https://github.com/flame-engine/flame/issues/2282)). ([236a74ce](https://github.com/flame-engine/flame/commit/236a74cef160310c1b2d894835fe34157f18178e))\n - **FIX**: TextBoxComponent rendering for new line ([#2413](https://github.com/flame-engine/flame/issues/2413)). ([9008998e](https://github.com/flame-engine/flame/commit/9008998eefe052b145b1d52ef149b99cbf4ddaaa))\n - **FIX**: Buttons in ButtonComponents should not be final ([#2410](https://github.com/flame-engine/flame/issues/2410)). ([55f66add](https://github.com/flame-engine/flame/commit/55f66add6389db212559750d26570d4eeeb54f34))\n - **FIX**: Set size of viewports in `onLoad` ([#2452](https://github.com/flame-engine/flame/issues/2452)). ([d1ac01f5](https://github.com/flame-engine/flame/commit/d1ac01f5754a7ceaf8308ef0561f0bd108e04ba2))\n - **FIX**: Incorrect JoystickComponent position in landscape mode [#2387](https://github.com/flame-engine/flame/issues/2387) ([#2389](https://github.com/flame-engine/flame/issues/2389)). ([f125593a](https://github.com/flame-engine/flame/commit/f125593aaaaef160395b772180b1514f6be3ac4f))\n - **FIX**: RouterComponent replace methods to correctly handle previous/nextRoute ([#2296](https://github.com/flame-engine/flame/issues/2296)). ([2b1f2266](https://github.com/flame-engine/flame/commit/2b1f226618740542127d53a6fafe8bdba3b80593))\n - **FIX**: Use the hitboxParent instead of the parent in the componentTypeCheck ([#2335](https://github.com/flame-engine/flame/issues/2335)). ([7920e2ba](https://github.com/flame-engine/flame/commit/7920e2ba4d52a2461ae4631ffdaf8c52fbcd9dd3))\n - **FIX**: Materialize list in `Component.removeWhere` ([#2458](https://github.com/flame-engine/flame/issues/2458)). ([13cce4ae](https://github.com/flame-engine/flame/commit/13cce4aed61dfd1edd09dee902b402c2b04718cb))\n - **FIX**: TextBoxComponent's boxConfig timePerChar generates \"Optimized Out\" error [#2143](https://github.com/flame-engine/flame/issues/2143) ([#2328](https://github.com/flame-engine/flame/issues/2328)). ([5874f600](https://github.com/flame-engine/flame/commit/5874f6007ae0c71269bc72da0e420eb7bf8e2173))\n - **FIX**: Camera no longer \"sticks\" to boundary with BoundedPositionBehavior ([#2307](https://github.com/flame-engine/flame/issues/2307)). ([914dc6a7](https://github.com/flame-engine/flame/commit/914dc6a7cdd3131023f4b2f52cc18450664bd0f3))\n - **FEAT**: Add reusable vector to the Vector2 extension ([#2429](https://github.com/flame-engine/flame/issues/2429)). ([03d45df5](https://github.com/flame-engine/flame/commit/03d45df5d665c3cff353bdde66ac6fc7bed4e1fe))\n - **FEAT**: Change `HasCollisionDetection` to be on `Component` ([#2404](https://github.com/flame-engine/flame/issues/2404)). ([637c258b](https://github.com/flame-engine/flame/commit/637c258b252892fe5bd1dcc3692d49d1072b0f1d))\n - **FEAT**: Added AlignComponent layout component ([#2350](https://github.com/flame-engine/flame/issues/2350)). ([4f5e56f0](https://github.com/flame-engine/flame/commit/4f5e56f05fdcd6b9ad04077093a9eeadf503b9b3))\n - **FEAT**: Add `autoResize` for `SpriteAnimationComponent` and `SpriteAnimationGroupComponent` ([#2453](https://github.com/flame-engine/flame/issues/2453)). ([dbeba238](https://github.com/flame-engine/flame/commit/dbeba23846b229af95057fe0e260fd9e2394c261))\n - **FEAT**: Adding ImageExtension.resize ([#2418](https://github.com/flame-engine/flame/issues/2418)). ([a3f1601d](https://github.com/flame-engine/flame/commit/a3f1601db863b5b1a0eebd08311467836a7b789c))\n - **FEAT**: Add position and anchor params for Sprite and SpriteAnimation Particles ([#2370](https://github.com/flame-engine/flame/issues/2370)). ([181e0b59](https://github.com/flame-engine/flame/commit/181e0b59fd83a765392a1f1170bfa1e840629029))\n - **FEAT**: Add `autoResize` for `SpriteGroupComponent` ([#2442](https://github.com/flame-engine/flame/issues/2442)). ([1576bd83](https://github.com/flame-engine/flame/commit/1576bd83a5abfebe206d4e4f93381f216f895208))\n - **FEAT**: Introduce flame_noise, deprecate NoiseEffectController ([#2393](https://github.com/flame-engine/flame/issues/2393)). ([b2fdf06a](https://github.com/flame-engine/flame/commit/b2fdf06a79520c2b556c1c83de0b0f24df80cfd2))\n - **FEAT**: Added HardwareKeyboardDetector ([#2257](https://github.com/flame-engine/flame/issues/2257)). ([95b1fc0f](https://github.com/flame-engine/flame/commit/95b1fc0fbc1c40962350bc27a15849c32bba5326))\n - **FEAT**: Allow people to opt-out on repaint boundary ([#2341](https://github.com/flame-engine/flame/issues/2341)). ([b6aeec24](https://github.com/flame-engine/flame/commit/b6aeec24d7745626359e05ad2f0ac9acc8d09fbf))\n - **FEAT**: Add `HasTimeScale` mixin ([#2431](https://github.com/flame-engine/flame/issues/2431)). ([d2a8fe01](https://github.com/flame-engine/flame/commit/d2a8fe01fae54ffd1c2e4584dfa7fdcfbcf4068d))\n - **FEAT**: Add DoubleTapCallbacks that receives double-tap events. ([#2327](https://github.com/flame-engine/flame/issues/2327)). ([b5f79d1c](https://github.com/flame-engine/flame/commit/b5f79d1ce45276d957d0512353ca9cc890b6fef1))\n - **FEAT**: Add ability to opt-out flip ([#2316](https://github.com/flame-engine/flame/issues/2316)). ([34c3b6bd](https://github.com/flame-engine/flame/commit/34c3b6bdc4c570f4e8641b11b94efe19bdd1ef32))\n - **FEAT**: Make `limit` field mutable in the `Timer` class ([#2358](https://github.com/flame-engine/flame/issues/2358)). ([4e0a8c46](https://github.com/flame-engine/flame/commit/4e0a8c468886d57b718f853e78a25a03f3b335ae))\n - **DOCS**: Rename caveace asset to cave_ace in our examples ([#2304](https://github.com/flame-engine/flame/issues/2304)). ([e2399f91](https://github.com/flame-engine/flame/commit/e2399f91e3ce39da8db9ae2b9622c8a6050b94b9))\n - **DOCS**: Update cspell github action and configuration ([#2325](https://github.com/flame-engine/flame/issues/2325)). ([e0a4c07f](https://github.com/flame-engine/flame/commit/e0a4c07f2ad6e19830bfdd3af4eb9b148771698a))\n - **DOCS**: Fix actual typos that made into our dictionary ([#2305](https://github.com/flame-engine/flame/issues/2305)). ([343b8452](https://github.com/flame-engine/flame/commit/343b84529d8f06c0d020b97a40c082b71f0de770))\n - **DOCS**: Add Flame logo for pub.dev ([#2338](https://github.com/flame-engine/flame/issues/2338)). ([65091f34](https://github.com/flame-engine/flame/commit/65091f34bf1fbaaf5a30eab6c59486bc0bf55812))\n - **DOCS**: Refactor documentation for GameWidget ([#2344](https://github.com/flame-engine/flame/issues/2344)). ([655824fc](https://github.com/flame-engine/flame/commit/655824fc00460ec16efc861046c7290ffc14c5c4))\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix old doc code ([#2322](https://github.com/flame-engine/flame/issues/2322)). ([90321658](https://github.com/flame-engine/flame/commit/90321658c48a9279d4c82d48c6433a818270d03e))\n - **BREAKING** **REFACTOR**: Use ComponentTreeRoot for component removal ([#2317](https://github.com/flame-engine/flame/issues/2317)). ([75446185](https://github.com/flame-engine/flame/commit/754461850f5827e0cb1a4193f72492e6e78fbfa9))\n - **BREAKING** **FEAT**: HasDraggableComponents mixin is no longer needed ([#2312](https://github.com/flame-engine/flame/issues/2312)). ([3faf1149](https://github.com/flame-engine/flame/commit/3faf114994f4c6405a5d1a89559f0976b4e8c911))\n - **BREAKING** **FEAT**: The `HasTappableComponents` mixin is no longer needed ([#2450](https://github.com/flame-engine/flame/issues/2450)). ([b5bdf4ec](https://github.com/flame-engine/flame/commit/b5bdf4ec173e87907a59a9f62fcdf35cc968af2a))\n\n#### `flame_rive` - `v1.7.0`\n\n - **FIX**: Added useArtboardSize functionality ([#2294](https://github.com/flame-engine/flame/issues/2294)). ([00b0dbef](https://github.com/flame-engine/flame/commit/00b0dbef0df80433eaa78fe3cc68de867d5ca4f5))\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n - **BREAKING** **FEAT**: The `HasTappableComponents` mixin is no longer needed ([#2450](https://github.com/flame-engine/flame/issues/2450)). ([b5bdf4ec](https://github.com/flame-engine/flame/commit/b5bdf4ec173e87907a59a9f62fcdf35cc968af2a))\n\n#### `flame_test` - `v1.10.0`\n\n - **FIX**: Override `remove()` method to fix the functionality issue in the `FlameMultiBlocProvider` ([#2280](https://github.com/flame-engine/flame/issues/2280)). ([6a818464](https://github.com/flame-engine/flame/commit/6a818464f5f942ce25c3c3c59839b6bddaada386))\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n - **BREAKING** **FEAT**: The `HasTappableComponents` mixin is no longer needed ([#2450](https://github.com/flame-engine/flame/issues/2450)). ([b5bdf4ec](https://github.com/flame-engine/flame/commit/b5bdf4ec173e87907a59a9f62fcdf35cc968af2a))\n\n#### `flame_forge2d` - `v0.13.0`\n\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n - **DOCS**: Added a page for Joints documentation + ConstantVolumeJoint doc and example ([#2362](https://github.com/flame-engine/flame/issues/2362)). ([957ad240](https://github.com/flame-engine/flame/commit/957ad2402af1c44aea500d77092d387ed463b7e0))\n - **BREAKING** **FEAT**: The `HasTappableComponents` mixin is no longer needed ([#2450](https://github.com/flame-engine/flame/issues/2450)). ([b5bdf4ec](https://github.com/flame-engine/flame/commit/b5bdf4ec173e87907a59a9f62fcdf35cc968af2a))\n\n#### `flame_isolate` - `v0.3.0`\n\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n - **BREAKING** **FEAT**: The `HasTappableComponents` mixin is no longer needed ([#2450](https://github.com/flame-engine/flame/issues/2450)). ([b5bdf4ec](https://github.com/flame-engine/flame/commit/b5bdf4ec173e87907a59a9f62fcdf35cc968af2a))\n\n#### `flame_network_assets` - `v0.2.0`\n\n - **FEAT**: Add network assets package. ([#2314](https://github.com/flame-engine/flame/issues/2314)). ([61d69656](https://github.com/flame-engine/flame/commit/61d69656de2cede71cd4f1b4c469ebb4904c4ce8))\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **BREAKING** **FEAT**: The `HasTappableComponents` mixin is no longer needed ([#2450](https://github.com/flame-engine/flame/issues/2450)). ([b5bdf4ec](https://github.com/flame-engine/flame/commit/b5bdf4ec173e87907a59a9f62fcdf35cc968af2a))\n\n#### `flame_audio` - `v1.4.1`\n\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n\n#### `flame_bloc` - `v1.8.3`\n\n - **REFACTOR**: Remove unused event \"ScoreEventCleared\" from flame_block example ([#2380](https://github.com/flame-engine/flame/issues/2380)). ([a9db3f4c](https://github.com/flame-engine/flame/commit/a9db3f4ce5c7c11ddca511826bdf9ab72eb19dfe))\n - **FIX**: Override `remove()` method to fix the functionality issue in the `FlameMultiBlocProvider` ([#2280](https://github.com/flame-engine/flame/issues/2280)). ([6a818464](https://github.com/flame-engine/flame/commit/6a818464f5f942ce25c3c3c59839b6bddaada386))\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n - **DOCS**: Fix actual typos that made into our dictionary ([#2305](https://github.com/flame-engine/flame/issues/2305)). ([343b8452](https://github.com/flame-engine/flame/commit/343b84529d8f06c0d020b97a40c082b71f0de770))\n\n#### `flame_fire_atlas` - `v1.3.4`\n\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n - **DOCS**: Rename caveace asset to cave_ace in our examples ([#2304](https://github.com/flame-engine/flame/issues/2304)). ([e2399f91](https://github.com/flame-engine/flame/commit/e2399f91e3ce39da8db9ae2b9622c8a6050b94b9))\n\n#### `flame_flare` - `v1.5.3`\n\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n - **DOCS**: Update broken fireslime references ([#2324](https://github.com/flame-engine/flame/issues/2324)). ([cc1957eb](https://github.com/flame-engine/flame/commit/cc1957eb861f65540e3e25635ca046fa34b0b8b5))\n\n#### `flame_lint` - `v0.2.0+1`\n\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n\n#### `flame_lottie` - `v0.2.0+1`\n\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n\n#### `flame_noise` - `v0.1.1`\n\n - **FEAT**: Introduce flame_noise, deprecate NoiseEffectController ([#2393](https://github.com/flame-engine/flame/issues/2393)). ([b2fdf06a](https://github.com/flame-engine/flame/commit/b2fdf06a79520c2b556c1c83de0b0f24df80cfd2))\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n\n#### `flame_oxygen` - `v0.1.8+1`\n\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n\n#### `flame_svg` - `v1.7.2`\n\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n\n#### `flame_tiled` - `v1.10.0`\n\n - **REFACTOR**: Divide TileLayer by its Layer type ([#2326](https://github.com/flame-engine/flame/issues/2326)). ([0c14d4cb](https://github.com/flame-engine/flame/commit/0c14d4cb87ba81957221695547bc06111a28617a))\n - **FIX**: TiledComponent now can be safely loaded regardless of the order ([#2391](https://github.com/flame-engine/flame/issues/2391)). ([4ddc4bba](https://github.com/flame-engine/flame/commit/4ddc4bba2b67ebd8c9c0e9e761eee34d2a74f62b))\n - **FEAT**: Use cached image when creating single source TiledAtlas if available ([#2348](https://github.com/flame-engine/flame/issues/2348)). ([73467c94](https://github.com/flame-engine/flame/commit/73467c941d89f68598c6dc297937af9d9896a949))\n - **FEAT**: Add ability to opt-out flip ([#2316](https://github.com/flame-engine/flame/issues/2316)). ([34c3b6bd](https://github.com/flame-engine/flame/commit/34c3b6bdc4c570f4e8641b11b94efe19bdd1ef32))\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix broken image link on flame_tiled pub ([#2407](https://github.com/flame-engine/flame/issues/2407)). ([0d24a6c8](https://github.com/flame-engine/flame/commit/0d24a6c8ed4a5d4de2e653a6430a635ef881ee2e))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n\n#### `jenny` - `v1.0.1`\n\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Create \"dart\" domain extension ([#2278](https://github.com/flame-engine/flame/issues/2278)). ([3b87e838](https://github.com/flame-engine/flame/commit/3b87e838f6308867b52f7c0cec3fa07e5629f3dc))\n\n\n## 2023-01-28\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_lint` - `v0.2.0`](#flame_lint---v020)\n\n---\n\n#### `flame_lint` - `v0.2.0`\n\n - Removed invariant_booleans\n\n\n## 2023-01-25\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_audio` - `v1.4.0`](#flame_audio---v140)\n\n---\n\n#### `flame_audio` - `v1.4.0`\n\n\n## 2023-01-14\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.6.0`](#flame---v160)\n - [`flame_rive` - `v1.6.0`](#flame_rive---v160)\n - [`flame_isolate` - `v0.2.0`](#flame_isolate---v020)\n - [`flame_lottie` - `v0.2.0`](#flame_lottie---v020)\n\nPackages with other changes:\n\n - [`flame_forge2d` - `v0.12.5`](#flame_forge2d---v0125)\n - [`flame_jenny` - `v1.0.0`](#flame_jenny---v100)\n - [`jenny` - `v1.0.0`](#jenny---v100)\n - [`flame_oxygen` - `v0.1.8`](#flame_oxygen---v018)\n - [`flame_bloc` - `v1.8.2`](#flame_bloc---v182)\n - [`flame_test` - `v1.9.2`](#flame_test---v192)\n - [`flame_tiled` - `v1.9.1`](#flame_tiled---v191)\n - [`flame_audio` - `v1.3.5`](#flame_audio---v135)\n - [`flame_flare` - `v1.5.2`](#flame_flare---v152)\n - [`flame_svg` - `v1.7.1`](#flame_svg---v171)\n - [`flame_fire_atlas` - `v1.3.3`](#flame_fire_atlas---v133)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_tiled` - `v1.9.1`\n - `flame_audio` - `v1.3.5`\n - `flame_flare` - `v1.5.2`\n - `flame_svg` - `v1.7.1`\n - `flame_fire_atlas` - `v1.3.3`\n\n---\n\n#### `flame` - `v1.6.0`\n\n - **PERF**: Avoid Vector2 creation in `Sprite.render` ([#2261](https://github.com/flame-engine/flame/issues/2261)). ([736733d9](https://github.com/flame-engine/flame/commit/736733d91398721452edb4c2600a47277bb5abee))\n - **FIX**: Only use initialized game for tests and remove setMount from onGameResize ([#2246](https://github.com/flame-engine/flame/issues/2246)). ([2a0f1d4b](https://github.com/flame-engine/flame/commit/2a0f1d4bdc2688e596481aad39762f94bf1cc8f1))\n - **FIX**: Re-use paint object in ImageParticle ([#2210](https://github.com/flame-engine/flame/issues/2210)). ([7a945d96](https://github.com/flame-engine/flame/commit/7a945d960c9b88fde11bbc480c0429295445cf30))\n - **FIX**: Depend on test: any for flame_test ([#2207](https://github.com/flame-engine/flame/issues/2207)). ([acfd418d](https://github.com/flame-engine/flame/commit/acfd418d882ee6872f3aa9961c39680ec123c2e6))\n - **FEAT**: Add a `canSee` method to the `CameraComponent` ([#2270](https://github.com/flame-engine/flame/issues/2270)). ([2347c8f5](https://github.com/flame-engine/flame/commit/2347c8f567c88f29540ef1d8e1c7c4b65fe31b06))\n - **FEAT**: Add `moveBy` to `CameraComponent` ([#2269](https://github.com/flame-engine/flame/issues/2269)). ([51e54ebe](https://github.com/flame-engine/flame/commit/51e54ebef823258f28f3e1a60a645ba4dd12e337))\n - **FEAT**: Added computed property CameraComponent.visibleWorldRect ([#2267](https://github.com/flame-engine/flame/issues/2267)). ([f4b0e73f](https://github.com/flame-engine/flame/commit/f4b0e73fa1f068b8867177e9761b2c4b01216a31))\n - **DOCS**: Update example to not create Rect objects ([#2254](https://github.com/flame-engine/flame/issues/2254)). ([a306338b](https://github.com/flame-engine/flame/commit/a306338b112955972b56baa9ac6e419b1af43ef1))\n - **DOCS**: Teh -> the ([#2225](https://github.com/flame-engine/flame/issues/2225)). ([ff7f36d0](https://github.com/flame-engine/flame/commit/ff7f36d0f682206c6c666ea2dbdce8a2e1d19601))\n - **BREAKING** **REFACTOR**: The method `onLoad()` now returns `FutureOr<void>` ([#2228](https://github.com/flame-engine/flame/issues/2228)). ([d898b539](https://github.com/flame-engine/flame/commit/d898b539f734d3e14c47990ef0727043a0e32efb))\n - **BREAKING** **FEAT**: Adds new route methods `pushReplacement`, `pushReplacementNamed`, and `pushReplacementOverlay` ([#2249](https://github.com/flame-engine/flame/issues/2249)). ([a2772b4e](https://github.com/flame-engine/flame/commit/a2772b4e0f828ee8475603ffdaf5ff63872a1a33))\n\n#### `flame_rive` - `v1.6.0`\n\n - **FIX**: Depend on test: any for flame_test ([#2207](https://github.com/flame-engine/flame/issues/2207)). ([acfd418d](https://github.com/flame-engine/flame/commit/acfd418d882ee6872f3aa9961c39680ec123c2e6))\n - **BREAKING** **REFACTOR**: The method `onLoad()` now returns `FutureOr<void>` ([#2228](https://github.com/flame-engine/flame/issues/2228)). ([d898b539](https://github.com/flame-engine/flame/commit/d898b539f734d3e14c47990ef0727043a0e32efb))\n\n#### `flame_isolate` - `v0.2.0`\n\n - **FIX**: Depend on test: any for flame_test ([#2207](https://github.com/flame-engine/flame/issues/2207)). ([acfd418d](https://github.com/flame-engine/flame/commit/acfd418d882ee6872f3aa9961c39680ec123c2e6))\n - **BREAKING** **REFACTOR**: The method `onLoad()` now returns `FutureOr<void>` ([#2228](https://github.com/flame-engine/flame/issues/2228)). ([d898b539](https://github.com/flame-engine/flame/commit/d898b539f734d3e14c47990ef0727043a0e32efb))\n\n#### `flame_lottie` - `v0.2.0`\n\n - **FIX**: Depend on test: any for flame_test ([#2207](https://github.com/flame-engine/flame/issues/2207)). ([acfd418d](https://github.com/flame-engine/flame/commit/acfd418d882ee6872f3aa9961c39680ec123c2e6))\n - **BREAKING** **REFACTOR**: The method `onLoad()` now returns `FutureOr<void>` ([#2228](https://github.com/flame-engine/flame/issues/2228)). ([d898b539](https://github.com/flame-engine/flame/commit/d898b539f734d3e14c47990ef0727043a0e32efb))\n\n#### `flame_forge2d` - `v0.12.5`\n\n - **FIX**: Depend on test: any for flame_test ([#2207](https://github.com/flame-engine/flame/issues/2207)). ([acfd418d](https://github.com/flame-engine/flame/commit/acfd418d882ee6872f3aa9961c39680ec123c2e6))\n\n#### `flame_jenny` - `v1.0.0`\n\n - **FIX**: Jenny now always stringifies whole numbers without .0 ([#2265](https://github.com/flame-engine/flame/issues/2265)). ([f262b7ee](https://github.com/flame-engine/flame/commit/f262b7ee39a270f5bfbf3bf2be89d85549d16cd1))\n - **FIX**: Remove whitespace before a command in dialogue option ([#2187](https://github.com/flame-engine/flame/issues/2187)). ([00f0e330](https://github.com/flame-engine/flame/commit/00f0e330b429f5f7ae87742ff5814f44924cb202))\n - **FIX**: Remove flutter from jenny ([#2162](https://github.com/flame-engine/flame/issues/2162)). ([29db304d](https://github.com/flame-engine/flame/commit/29db304d36fdf791f6c9df4c69b95511190b3057))\n - **FEAT**: Added the <<character>> command to Jenny ([#2274](https://github.com/flame-engine/flame/issues/2274)). ([6548e9cb](https://github.com/flame-engine/flame/commit/6548e9cb0a91353489812e211c2aa098fbd04f55))\n - **FEAT**: Added if() built-in function in Jenny ([#2259](https://github.com/flame-engine/flame/issues/2259)). ([087229ed](https://github.com/flame-engine/flame/commit/087229ede545644026eb6c303a037a93a792eaf2))\n - **FEAT**: Added command <<visit>> ([#2233](https://github.com/flame-engine/flame/issues/2233)). ([a90f90ef](https://github.com/flame-engine/flame/commit/a90f90efc5556f9697d409fd6a1e6558ae9e8236))\n - **FEAT**: OnDialogueChoice now returns null by default ([#2234](https://github.com/flame-engine/flame/issues/2234)). ([e2ab129e](https://github.com/flame-engine/flame/commit/e2ab129e5974485241223528fc50f3049ffecf8f))\n - **FEAT**: Added DialogueView.onNodeFinish event ([#2229](https://github.com/flame-engine/flame/issues/2229)). ([19a1f09a](https://github.com/flame-engine/flame/commit/19a1f09acc45199a4411c7026b8adf61a5a5a11f))\n - **FEAT**: Arguments of a UserDefinedCommand are now accessible ([#2224](https://github.com/flame-engine/flame/issues/2224)). ([0a9eaf38](https://github.com/flame-engine/flame/commit/0a9eaf380194e93c89cb8b2f5677d476a33eb83b))\n - **FEAT**: Added escape sequence \\- in yarn language ([#2220](https://github.com/flame-engine/flame/issues/2220)). ([43eacdd1](https://github.com/flame-engine/flame/commit/43eacdd1f5e1419c310f5cd34d1476adf03eb4d6))\n - **FEAT**: Add support for user-defined functions in jenny ([#2194](https://github.com/flame-engine/flame/issues/2194)). ([9364a0dd](https://github.com/flame-engine/flame/commit/9364a0dd324a2ed57b1e9a8907108da796e59352))\n - **FEAT**: Support for builtin functions in jenny ([#2192](https://github.com/flame-engine/flame/issues/2192)). ([82d35b8a](https://github.com/flame-engine/flame/commit/82d35b8a5dc8a9378dfee348b3392d0afabf2bc8))\n - **FEAT**: Add command <<local>> ([#2185](https://github.com/flame-engine/flame/issues/2185)). ([9e677e7d](https://github.com/flame-engine/flame/commit/9e677e7dc74bbe15b8521ec945a5b92ce8a4180a))\n - **FEAT**: Added support for markup attributes ([#2183](https://github.com/flame-engine/flame/issues/2183)). ([f887545b](https://github.com/flame-engine/flame/commit/f887545b127b41412b29217c52f9ec6ea0d6c885))\n - **FEAT**: Support user-defined commands ([#2168](https://github.com/flame-engine/flame/issues/2168)). ([ffb36a89](https://github.com/flame-engine/flame/commit/ffb36a89efdcd976fe63c16f27741b77b08aa284))\n - **FEAT**: Added the <<set>> command ([#2155](https://github.com/flame-engine/flame/issues/2155)). ([2b306d9e](https://github.com/flame-engine/flame/commit/2b306d9ee9c92416fe82b42e9a4ee33b280af46f))\n - **FEAT**: Implement the <<declare>> command ([#2154](https://github.com/flame-engine/flame/issues/2154)). ([8d592f17](https://github.com/flame-engine/flame/commit/8d592f17411800a5239720687149122eaf7750f1))\n - **FEAT**: Trim whitespace at the end of dialogue lines ([#2149](https://github.com/flame-engine/flame/issues/2149)). ([9c25e631](https://github.com/flame-engine/flame/commit/9c25e631e2e5ed5c593dbca4f498105e2c8fff66))\n - **FEAT**: DialogueRunner for jenny ([#2113](https://github.com/flame-engine/flame/issues/2113)). ([5ba6ff21](https://github.com/flame-engine/flame/commit/5ba6ff21a633a9f80e15228faaa31c6f0a3df60c))\n - **FEAT**: Support commands outside of nodes ([#2145](https://github.com/flame-engine/flame/issues/2145)). ([b313d630](https://github.com/flame-engine/flame/commit/b313d6302d713bda7baee7e90ecdb2fef2a3d6fc))\n - **FEAT**: Parser for jenny ([#2103](https://github.com/flame-engine/flame/issues/2103)). ([4e4117c8](https://github.com/flame-engine/flame/commit/4e4117c8a25a24686d6f571a9a5a23e19d660282))\n - **DOCS**: Update readme for jenny ([#2266](https://github.com/flame-engine/flame/issues/2266)). ([79129e4a](https://github.com/flame-engine/flame/commit/79129e4a72cec7c5bcfd67c17f9718b7528ac08c))\n - **DOCS**: Documentation for markup in Jenny ([#2262](https://github.com/flame-engine/flame/issues/2262)). ([8b57eaa1](https://github.com/flame-engine/flame/commit/8b57eaa1abc88d154ff45fdab6932bd15fe6eef7))\n - **DOCS**: Documentation for built-in functions in Jenny ([#2258](https://github.com/flame-engine/flame/issues/2258)). ([2eac6f5a](https://github.com/flame-engine/flame/commit/2eac6f5aa9485458203df7f41dc8c3718973eb61))\n - **DOCS**: Added documentation for basic expressions in Jenny ([#2256](https://github.com/flame-engine/flame/issues/2256)). ([69c13568](https://github.com/flame-engine/flame/commit/69c13568e647225bd2a2994e24a45f8258af0d16))\n - **DOCS**: Description of jenny package ([#2102](https://github.com/flame-engine/flame/issues/2102)). ([a99c9303](https://github.com/flame-engine/flame/commit/a99c93038128f913b7df05a5ef3e041e607069b9))\n\n#### `jenny` - `v1.0.0`\n\n - **FIX**: Jenny now always stringifies whole numbers without .0 ([#2265](https://github.com/flame-engine/flame/issues/2265)). ([f262b7ee](https://github.com/flame-engine/flame/commit/f262b7ee39a270f5bfbf3bf2be89d85549d16cd1))\n - **FIX**: Remove whitespace before a command in dialogue option ([#2187](https://github.com/flame-engine/flame/issues/2187)). ([00f0e330](https://github.com/flame-engine/flame/commit/00f0e330b429f5f7ae87742ff5814f44924cb202))\n - **FIX**: Remove flutter from jenny ([#2162](https://github.com/flame-engine/flame/issues/2162)). ([29db304d](https://github.com/flame-engine/flame/commit/29db304d36fdf791f6c9df4c69b95511190b3057))\n - **FEAT**: Added the <<character>> command to Jenny ([#2274](https://github.com/flame-engine/flame/issues/2274)). ([6548e9cb](https://github.com/flame-engine/flame/commit/6548e9cb0a91353489812e211c2aa098fbd04f55))\n - **FEAT**: Added if() built-in function in Jenny ([#2259](https://github.com/flame-engine/flame/issues/2259)). ([087229ed](https://github.com/flame-engine/flame/commit/087229ede545644026eb6c303a037a93a792eaf2))\n - **FEAT**: Added command <<visit>> ([#2233](https://github.com/flame-engine/flame/issues/2233)). ([a90f90ef](https://github.com/flame-engine/flame/commit/a90f90efc5556f9697d409fd6a1e6558ae9e8236))\n - **FEAT**: OnDialogueChoice now returns null by default ([#2234](https://github.com/flame-engine/flame/issues/2234)). ([e2ab129e](https://github.com/flame-engine/flame/commit/e2ab129e5974485241223528fc50f3049ffecf8f))\n - **FEAT**: Added DialogueView.onNodeFinish event ([#2229](https://github.com/flame-engine/flame/issues/2229)). ([19a1f09a](https://github.com/flame-engine/flame/commit/19a1f09acc45199a4411c7026b8adf61a5a5a11f))\n - **FEAT**: Arguments of a UserDefinedCommand are now accessible ([#2224](https://github.com/flame-engine/flame/issues/2224)). ([0a9eaf38](https://github.com/flame-engine/flame/commit/0a9eaf380194e93c89cb8b2f5677d476a33eb83b))\n - **FEAT**: Added escape sequence \\- in yarn language ([#2220](https://github.com/flame-engine/flame/issues/2220)). ([43eacdd1](https://github.com/flame-engine/flame/commit/43eacdd1f5e1419c310f5cd34d1476adf03eb4d6))\n - **FEAT**: Add support for user-defined functions in jenny ([#2194](https://github.com/flame-engine/flame/issues/2194)). ([9364a0dd](https://github.com/flame-engine/flame/commit/9364a0dd324a2ed57b1e9a8907108da796e59352))\n - **FEAT**: Support for builtin functions in jenny ([#2192](https://github.com/flame-engine/flame/issues/2192)). ([82d35b8a](https://github.com/flame-engine/flame/commit/82d35b8a5dc8a9378dfee348b3392d0afabf2bc8))\n - **FEAT**: Add command <<local>> ([#2185](https://github.com/flame-engine/flame/issues/2185)). ([9e677e7d](https://github.com/flame-engine/flame/commit/9e677e7dc74bbe15b8521ec945a5b92ce8a4180a))\n - **FEAT**: Added support for markup attributes ([#2183](https://github.com/flame-engine/flame/issues/2183)). ([f887545b](https://github.com/flame-engine/flame/commit/f887545b127b41412b29217c52f9ec6ea0d6c885))\n - **FEAT**: Support user-defined commands ([#2168](https://github.com/flame-engine/flame/issues/2168)). ([ffb36a89](https://github.com/flame-engine/flame/commit/ffb36a89efdcd976fe63c16f27741b77b08aa284))\n - **FEAT**: Added the <<set>> command ([#2155](https://github.com/flame-engine/flame/issues/2155)). ([2b306d9e](https://github.com/flame-engine/flame/commit/2b306d9ee9c92416fe82b42e9a4ee33b280af46f))\n - **FEAT**: Implement the <<declare>> command ([#2154](https://github.com/flame-engine/flame/issues/2154)). ([8d592f17](https://github.com/flame-engine/flame/commit/8d592f17411800a5239720687149122eaf7750f1))\n - **FEAT**: Trim whitespace at the end of dialogue lines ([#2149](https://github.com/flame-engine/flame/issues/2149)). ([9c25e631](https://github.com/flame-engine/flame/commit/9c25e631e2e5ed5c593dbca4f498105e2c8fff66))\n - **FEAT**: DialogueRunner for jenny ([#2113](https://github.com/flame-engine/flame/issues/2113)). ([5ba6ff21](https://github.com/flame-engine/flame/commit/5ba6ff21a633a9f80e15228faaa31c6f0a3df60c))\n - **FEAT**: Support commands outside of nodes ([#2145](https://github.com/flame-engine/flame/issues/2145)). ([b313d630](https://github.com/flame-engine/flame/commit/b313d6302d713bda7baee7e90ecdb2fef2a3d6fc))\n - **FEAT**: Parser for jenny ([#2103](https://github.com/flame-engine/flame/issues/2103)). ([4e4117c8](https://github.com/flame-engine/flame/commit/4e4117c8a25a24686d6f571a9a5a23e19d660282))\n - **DOCS**: Update readme for jenny ([#2266](https://github.com/flame-engine/flame/issues/2266)). ([79129e4a](https://github.com/flame-engine/flame/commit/79129e4a72cec7c5bcfd67c17f9718b7528ac08c))\n - **DOCS**: Documentation for markup in Jenny ([#2262](https://github.com/flame-engine/flame/issues/2262)). ([8b57eaa1](https://github.com/flame-engine/flame/commit/8b57eaa1abc88d154ff45fdab6932bd15fe6eef7))\n - **DOCS**: Documentation for built-in functions in Jenny ([#2258](https://github.com/flame-engine/flame/issues/2258)). ([2eac6f5a](https://github.com/flame-engine/flame/commit/2eac6f5aa9485458203df7f41dc8c3718973eb61))\n - **DOCS**: Added documentation for basic expressions in Jenny ([#2256](https://github.com/flame-engine/flame/issues/2256)). ([69c13568](https://github.com/flame-engine/flame/commit/69c13568e647225bd2a2994e24a45f8258af0d16))\n - **DOCS**: Description of jenny package ([#2102](https://github.com/flame-engine/flame/issues/2102)). ([a99c9303](https://github.com/flame-engine/flame/commit/a99c93038128f913b7df05a5ef3e041e607069b9))\n\n#### `flame_oxygen` - `v0.1.8`\n\n#### `flame_bloc` - `v1.8.2`\n\n - **FIX**: Depend on test: any for flame_test ([#2207](https://github.com/flame-engine/flame/issues/2207)). ([acfd418d](https://github.com/flame-engine/flame/commit/acfd418d882ee6872f3aa9961c39680ec123c2e6))\n\n#### `flame_test` - `v1.9.2`\n\n - **FIX**: Only use initialized game for tests and remove setMount from onGameResize ([#2246](https://github.com/flame-engine/flame/issues/2246)). ([2a0f1d4b](https://github.com/flame-engine/flame/commit/2a0f1d4bdc2688e596481aad39762f94bf1cc8f1))\n - **FIX**: Depend on test: any for flame_test ([#2207](https://github.com/flame-engine/flame/issues/2207)). ([acfd418d](https://github.com/flame-engine/flame/commit/acfd418d882ee6872f3aa9961c39680ec123c2e6))\n - **FIX**: Depend on test: any for flame_test. ([fcf5521c](https://github.com/flame-engine/flame/commit/fcf5521ce4e975830f728481591a1731ce5edb77))\n\n\n## 2023-01-03\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_audio` - `v1.3.4`](#flame_audio---v134)\n\n---\n\n#### `flame_audio` - `v1.3.4`\n\n\n## 2022-11-28\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_test` - `v1.9.1`](#flame_test---v191)\n\n---\n\n#### `flame_test` - `v1.9.1`\n\n - **FIX**: Depend on test: any for flame_test. ([fcf5521c](https://github.com/flame-engine/flame/commit/fcf5521ce4e975830f728481591a1731ce5edb77))\n\n\n## 2022-11-27\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.5.0`](#flame---v150)\n\nPackages with other changes:\n\n - [`flame_bloc` - `v1.8.1`](#flame_bloc---v181)\n - [`flame_forge2d` - `v0.12.4`](#flame_forge2d---v0124)\n - [`flame_lottie` - `v0.1.1`](#flame_lottie---v011)\n - [`flame_rive` - `v1.5.3`](#flame_rive---v153)\n - [`flame_svg` - `v1.7.0`](#flame_svg---v170)\n - [`flame_test` - `v1.9.0`](#flame_test---v190)\n - [`flame_tiled` - `v1.9.0`](#flame_tiled---v190)\n - [`flame_isolate` - `v0.1.1`](#flame_isolate---v011)\n - [`flame_audio` - `v1.3.3`](#flame_audio---v133)\n - [`flame_flare` - `v1.5.1`](#flame_flare---v151)\n - [`flame_oxygen` - `v0.1.7`](#flame_oxygen---v017)\n - [`flame_fire_atlas` - `v1.3.2`](#flame_fire_atlas---v132)\n\n---\n\n#### `flame` - `v1.5.0`\n\n - **REFACTOR**: OpacityEffect now uses opacity instead of alpha internally ([#2064](https://github.com/flame-engine/flame/issues/2064)). ([b3b67301](https://github.com/flame-engine/flame/commit/b3b673011cfafc4a9add55f682e7e1074b4dc64b))\n - **REFACTOR**: Game render box cleanup ([#1691](https://github.com/flame-engine/flame/issues/1691)). ([60a5830d](https://github.com/flame-engine/flame/commit/60a5830d3e596c9c6086f8253eb663b01e4f440b))\n - **FIX**: Event mixins missing `@mustCallSuper` ([#2036](https://github.com/flame-engine/flame/issues/2036)). ([c26d5da3](https://github.com/flame-engine/flame/commit/c26d5da3730d4d38002259fbc0d314ae63c3bdff))\n - **FIX**: SpeedController advance() should execute after its effect's onStart() ([#2173](https://github.com/flame-engine/flame/issues/2173)). ([7a1e2e8b](https://github.com/flame-engine/flame/commit/7a1e2e8b657b6b18dc08afd53f52ba513cecb4d9))\n - **FIX**: Refresh vertices on size change of `RectangleComponent` ([#2167](https://github.com/flame-engine/flame/issues/2167)). ([4020d68b](https://github.com/flame-engine/flame/commit/4020d68b4afcba554f8ee493840d7b74b68f6293))\n - **FIX**: Fix coordinate system calculation in FixedAspectRationViewport ([#2175](https://github.com/flame-engine/flame/issues/2175)). ([c9c9881c](https://github.com/flame-engine/flame/commit/c9c9881ccacbdaf1759c7c85b2edff94aa633427))\n - **FIX**: SpriteButtonComponent missing `@mustCallSuper` added ([#2001](https://github.com/flame-engine/flame/issues/2001)). ([45a9d79b](https://github.com/flame-engine/flame/commit/45a9d79bc477d9d9a772d0c2812d82e8a1962468))\n - **FIX**: Focus handling with a scope on the `GameWidget` ([#1725](https://github.com/flame-engine/flame/issues/1725)). ([d1cd8517](https://github.com/flame-engine/flame/commit/d1cd8517e4f9d4aadeacf7caf3ca91440e6041d7))\n - **FIX**: RemoveEffect should work within SequenceEffect ([#2110](https://github.com/flame-engine/flame/issues/2110)). ([03e1f33d](https://github.com/flame-engine/flame/commit/03e1f33d3de1e0d6a16b1f11a7fe503ece9f5d24))\n - **FIX**: [#1966](https://github.com/flame-engine/flame/issues/1966) unit test for `Particles` ([#2097](https://github.com/flame-engine/flame/issues/2097)). ([59bd7ebb](https://github.com/flame-engine/flame/commit/59bd7ebb9deaea44001edce02b306ffaacf5afc8))\n - **FIX**: OpacityEffect custom paint override ([#2056](https://github.com/flame-engine/flame/issues/2056)). ([fe9d4d9b](https://github.com/flame-engine/flame/commit/fe9d4d9bfb97557434d2844357d70db666b02e49))\n - **FIX**: [#1998](https://github.com/flame-engine/flame/issues/1998) ([#2013](https://github.com/flame-engine/flame/issues/2013)). ([f63711dc](https://github.com/flame-engine/flame/commit/f63711dc56961fc664358b4789de5d78b43ce081))\n - **FIX**: solid circles and polygons intersection ([#2067](https://github.com/flame-engine/flame/issues/2067)). ([62c5c2e1](https://github.com/flame-engine/flame/commit/62c5c2e14479c4cb1b0e5487ab6a96182c0f1338))\n - **FIX**: [#2017](https://github.com/flame-engine/flame/issues/2017) ([#2039](https://github.com/flame-engine/flame/issues/2039)). ([7f546b0f](https://github.com/flame-engine/flame/commit/7f546b0f13306edb92a68a331bf28127a42138ce))\n - **FIX**: Exception when having multiple calls to dispose() function of a Svg instance ([#2085](https://github.com/flame-engine/flame/issues/2085)). ([a287904e](https://github.com/flame-engine/flame/commit/a287904eb5dbbe70128207a6f6a56ff98dfbf579))\n - **FIX**: Add missing hitbox parameters ([#2070](https://github.com/flame-engine/flame/issues/2070)). ([8aacb555](https://github.com/flame-engine/flame/commit/8aacb5557ac299852530c5023a1ddd2bebbad564))\n - **FIX**: Change `Vector2.zero()` to `Vector2(0, -1)` in `Vector2Extensions.fromRadians()` ([#2016](https://github.com/flame-engine/flame/issues/2016)). ([801c683c](https://github.com/flame-engine/flame/commit/801c683c6cf448e6d0ae34231a656bc72bcce00a))\n - **FEAT**: Add children to `World` constructor ([#2093](https://github.com/flame-engine/flame/issues/2093)). ([3af416dc](https://github.com/flame-engine/flame/commit/3af416dc2e61c7f43334d06add651d7c21bb511b))\n - **FEAT**: Add paint layers to HasPaint and associated component renders ([#2073](https://github.com/flame-engine/flame/issues/2073)). ([9e6bf4fb](https://github.com/flame-engine/flame/commit/9e6bf4fbccd13b8e7ef848bc77d4da510680539f))\n - **FEAT**: Add SizeProvider to clip_component and custom_paint_component. ([#2100](https://github.com/flame-engine/flame/issues/2100)). ([bb710646](https://github.com/flame-engine/flame/commit/bb71064647c71ff42be40c34fd1231ad9b1c43f0))\n - **FEAT**: Added HasGameReference mixin ([#1828](https://github.com/flame-engine/flame/issues/1828)). ([12ce270b](https://github.com/flame-engine/flame/commit/12ce270b9b3102b6a9bb1f468369a4fce1e064e6))\n - **FEAT**: Added toString method to all the drags events message handlers ([#2014](https://github.com/flame-engine/flame/issues/2014)). ([a34f1df7](https://github.com/flame-engine/flame/commit/a34f1df7904f0bd54fb8465265b24e21be0f4dc2))\n - **FEAT**: Add `maintainState` property to Route ([#2161](https://github.com/flame-engine/flame/issues/2161)). ([576ceaac](https://github.com/flame-engine/flame/commit/576ceaac178de87a3c0ed54c87373cf83f7bd868))\n - **FEAT**: add onCancelled to ButtonComponent and HudButtonComponent ([#2193](https://github.com/flame-engine/flame/issues/2193)). ([e7f08906](https://github.com/flame-engine/flame/commit/e7f089066620ed5326e94ac8d4b7f5705c3ae3f7))\n - **FEAT**: onComponentTypeCheck support for ShapeHitbox ([#1981](https://github.com/flame-engine/flame/issues/1981)). ([f840210b](https://github.com/flame-engine/flame/commit/f840210bf97f9da406282212db265a976506ebf8))\n - **FEAT**: Added glow effect using maskFilter ([#2129](https://github.com/flame-engine/flame/issues/2129)). ([bcecd3c1](https://github.com/flame-engine/flame/commit/bcecd3c1bd400c155807beb77651ebd2ee6f627c))\n - **FEAT**: Add support for styles propagating through the text node tree ([#1915](https://github.com/flame-engine/flame/issues/1915)). ([b5780d42](https://github.com/flame-engine/flame/commit/b5780d421234636144794e663559cec8987656a4))\n - **FEAT**: Added SpriteFont class ([#1992](https://github.com/flame-engine/flame/issues/1992)). ([a0d7eada](https://github.com/flame-engine/flame/commit/a0d7eadae40d4653ce0f5286e7236bedc17ed8cb))\n - **FEAT**: Added CameraComponent.withFixedResolution() constructor ([#2176](https://github.com/flame-engine/flame/issues/2176)). ([e289f118](https://github.com/flame-engine/flame/commit/e289f118eedebf512899d66e01f6234e3890a0d6))\n - **FEAT**: Add optional maxDistance to raycast ([#2012](https://github.com/flame-engine/flame/issues/2012)). ([6b78b10f](https://github.com/flame-engine/flame/commit/6b78b10fb36a9fed5d9c7b06aea89e088bc4d985))\n - **FEAT**: `clampLength` for `Vector2` extension ([#2190](https://github.com/flame-engine/flame/issues/2190)). ([51a896b2](https://github.com/flame-engine/flame/commit/51a896b2c801089968b630937fd23c12a98dbc40))\n - **FEAT**: Adding onChildrenChanged ([#1976](https://github.com/flame-engine/flame/issues/1976)). ([3d043b86](https://github.com/flame-engine/flame/commit/3d043b86f7382cf54313ac59eb3818a5b2788824))\n - **FEAT**: Adding ComponentNotifier API ([#1889](https://github.com/flame-engine/flame/issues/1889)). ([bd7f51f5](https://github.com/flame-engine/flame/commit/bd7f51f5b63e303b8b7230643dccbd040d2708a5))\n - **FEAT**: `removed` future + `isRemoved` field for `Component` ([#2080](https://github.com/flame-engine/flame/issues/2080)). ([9f322785](https://github.com/flame-engine/flame/commit/9f3227857327a99730fd4d02f099acef7c57ca67))\n - **BREAKING** **FIX**: Correct coordinate system for a circular viewport ([#2174](https://github.com/flame-engine/flame/issues/2174)). ([93dc4325](https://github.com/flame-engine/flame/commit/93dc4325476d4727a4de8dd8f0caf3ee081c0ad6))\n - **BREAKING** **FIX**: PolygonComponent no longer modifies _vertices ([#2061](https://github.com/flame-engine/flame/issues/2061)). ([8cd4793a](https://github.com/flame-engine/flame/commit/8cd4793ac2ecade740e53ad628db3f2f9ca6949a))\n - **BREAKING** **FEAT**: Add OpacityProvider ([#2062](https://github.com/flame-engine/flame/issues/2062)). ([0255cc32](https://github.com/flame-engine/flame/commit/0255cc32f0c77b9507f9ad0eddcbd8c35840c885))\n\n#### `flame_bloc` - `v1.8.1`\n\n - **FIX**: flame-bloc : Remove final keyword from subscription in FlameBlocListenable ([#2098](https://github.com/flame-engine/flame/issues/2098)). ([8a136c99](https://github.com/flame-engine/flame/commit/8a136c9985d7878940f2103484b90e1ffb202a03))\n\n#### `flame_forge2d` - `v0.12.4`\n\n - **FIX**: 🐛 unit test for `Forge2dGame` ([#2068](https://github.com/flame-engine/flame/issues/2068)). ([d659b85d](https://github.com/flame-engine/flame/commit/d659b85d090614ebb3df06fb68254c087f6f9dff))\n\n#### `flame_lottie` - `v0.1.1`\n\n - **FEAT**: Lottie bridge package ([#2157](https://github.com/flame-engine/flame/issues/2157)). ([3a73d145](https://github.com/flame-engine/flame/commit/3a73d1456c01937234f0503fd077193884912fbb))\n\n#### `flame_rive` - `v1.5.3`\n\n - **FIX**: Export rive from flame_rive ([#2130](https://github.com/flame-engine/flame/issues/2130)). ([d1833329](https://github.com/flame-engine/flame/commit/d1833329028d1d8483faa049c6e1ad478ba9ca49))\n - **FIX**: antialiasing should change the artboard([#2076](https://github.com/flame-engine/flame/issues/2076)). ([47970224](https://github.com/flame-engine/flame/commit/47970224f8c9c90718c54301ee69d9cddcced87b))\n - **FIX**: Fixed null exception when no artboard with specified name is exists ([#2069](https://github.com/flame-engine/flame/issues/2069)). ([a3a65f30](https://github.com/flame-engine/flame/commit/a3a65f30ab64c029da66f9ded08eaf730d760336))\n\n#### `flame_svg` - `v1.7.0`\n\n - **FIX**: Exception when having multiple calls to dispose() function of a Svg instance ([#2085](https://github.com/flame-engine/flame/issues/2085)). ([a287904e](https://github.com/flame-engine/flame/commit/a287904eb5dbbe70128207a6f6a56ff98dfbf579))\n - **FIX**: SvgComponent getting blurred and pixelized ([#2084](https://github.com/flame-engine/flame/issues/2084)). ([0911d10b](https://github.com/flame-engine/flame/commit/0911d10b9177c0dbcc6f9ba927f99cb7e04182a5))\n - **FEAT**: Expose paint from svgComponent to set opacity and have opacity effects ([#2092](https://github.com/flame-engine/flame/issues/2092)). ([bedacd0c](https://github.com/flame-engine/flame/commit/bedacd0c8c79f4f060b002eeddaa9d2ef68d316c))\n\n#### `flame_test` - `v1.9.0`\n\n - **FEAT**: Add support for styles propagating through the text node tree ([#1915](https://github.com/flame-engine/flame/issues/1915)). ([b5780d42](https://github.com/flame-engine/flame/commit/b5780d421234636144794e663559cec8987656a4))\n\n#### `flame_tiled` - `v1.9.0`\n\n - **FEAT**: Rename internal classes clashes with Tiled ([#2139](https://github.com/flame-engine/flame/issues/2139)). ([2224eaac](https://github.com/flame-engine/flame/commit/2224eaac701414deb76bac7f7c40a56387cdf817))\n\n#### `flame_isolate` - `v0.1.1`\n\n#### `flame_audio` - `v1.3.3`\n\n#### `flame_flare` - `v1.5.1`\n\n#### `flame_oxygen` - `v0.1.7`\n\n#### `flame_fire_atlas` - `v1.3.2`\n\n\n## 2022-11-22\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_isolate` - `v0.1.0`](#flame_isolate---v010)\n\n---\n\n#### `flame_isolate` - `v0.1.0`\n\n - **FEAT**: FlameIsolate - a neat way of handling threads ([#1909](https://github.com/flame-engine/flame/issues/1909)). ([b25b9356](https://github.com/flame-engine/flame/commit/b25b935644e258c37145bd6abfe0962d8e872801))\n\n\n## 2022-10-01\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.4.0`](#flame---v140)\n - [`flame_test` - `v1.8.0`](#flame_test---v180)\n\nPackages with other changes:\n\n - [`flame_oxygen` - `v0.1.6`](#flame_oxygen---v016)\n - [`flame_bloc` - `v1.8.0`](#flame_bloc---v180)\n - [`flame_flare` - `v1.5.0`](#flame_flare---v150)\n - [`flame_forge2d` - `v0.12.3`](#flame_forge2d---v0123)\n - [`flame_lint` - `v0.1.3`](#flame_lint---v013)\n - [`flame_svg` - `v1.6.0`](#flame_svg---v160)\n - [`flame_tiled` - `v1.8.0`](#flame_tiled---v180)\n - [`flame_rive` - `v1.5.2`](#flame_rive---v152)\n - [`flame_audio` - `v1.3.2`](#flame_audio---v132)\n - [`flame_fire_atlas` - `v1.3.1`](#flame_fire_atlas---v131)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_rive` - `v1.5.2`\n - `flame_audio` - `v1.3.2`\n - `flame_fire_atlas` - `v1.3.1`\n\n---\n\n#### `flame` - `v1.4.0`\n\n - **REFACTOR**: move broadphase-related functionality into separate subdirectory ([#1943](https://github.com/flame-engine/flame/issues/1943)). ([f23acd41](https://github.com/flame-engine/flame/commit/f23acd41e0341909200437bfb6487cbe9ca58a53))\n - **REFACTOR**: used simpler and more implicit widgets in GameWidget ([#1862](https://github.com/flame-engine/flame/issues/1862)). ([44d17c64](https://github.com/flame-engine/flame/commit/44d17c64f80601159cc7f579cef8568727d411b3))\n - **PERF**: SpriteAnimationWidget will re-render only when needed ([#1876](https://github.com/flame-engine/flame/issues/1876)). ([bb678301](https://github.com/flame-engine/flame/commit/bb6783010f3c14362dcb4ed9182c4d240080a7f6))\n - **FIX**: Hitbox children of a CompositeHitbox to return correct parent ([#1922](https://github.com/flame-engine/flame/issues/1922)). ([d518705e](https://github.com/flame-engine/flame/commit/d518705e1665bfc3f54256f113f0d2227fab14dd))\n - **FIX**: OpacityEffect rounding error calculation ([#1933](https://github.com/flame-engine/flame/issues/1933)). ([4cfcfa64](https://github.com/flame-engine/flame/commit/4cfcfa644c641e85cad891b07eac956db8534590))\n - **FIX**: Expose hitboxParent from Hitbox ([#1928](https://github.com/flame-engine/flame/issues/1928)). ([3ba93351](https://github.com/flame-engine/flame/commit/3ba933513d9a4dd73c56e0a3f304069f6989c002))\n - **FIX**: Raycast from CircleHitbox's center ([#1918](https://github.com/flame-engine/flame/issues/1918)). ([57ca47c8](https://github.com/flame-engine/flame/commit/57ca47c8ccfdb0b78c541efa833d32ae746e6616))\n - **FEAT**: quad tree broadphase support  ([#1894](https://github.com/flame-engine/flame/issues/1894)). ([e33d5410](https://github.com/flame-engine/flame/commit/e33d5410a3bfdae5fdde8939b55d7ce178a0c5c8))\n - **FEAT**: Make `_ButtonState` public for SpriteButtonComponent ([#1941](https://github.com/flame-engine/flame/issues/1941)). ([e80412c5](https://github.com/flame-engine/flame/commit/e80412c56895a51ec281e90506f0104d0a9ce47e))\n - **FEAT**: Add possibility for solid hitboxes ([#1919](https://github.com/flame-engine/flame/issues/1919)). ([205ac561](https://github.com/flame-engine/flame/commit/205ac561eef4becd90a0d5dca2301b988b15959f))\n - **FEAT**: Adding callbacks for EffectController ([#1926](https://github.com/flame-engine/flame/issues/1926)) ([#1931](https://github.com/flame-engine/flame/issues/1931)). ([8dcdf155](https://github.com/flame-engine/flame/commit/8dcdf1557903a46766c46e6cf0855f0d6b524608))\n - **FEAT**: Added DebugTextFormatter ([#1921](https://github.com/flame-engine/flame/issues/1921)). ([426827d1](https://github.com/flame-engine/flame/commit/426827d19e803158dab271dce1fbf93bd09f07de))\n - **FEAT**: Add lookAt method for PositionComponent ([#1891](https://github.com/flame-engine/flame/issues/1891)). ([720c3566](https://github.com/flame-engine/flame/commit/720c3566b02815d7ca2c4b45861041f2bddca0fc))\n - **FEAT**: add applyLifespanToChildren to Particle generate ([#1911](https://github.com/flame-engine/flame/issues/1911)). ([884d5190](https://github.com/flame-engine/flame/commit/884d5190adbe6ddfc9b7d006cda310cc656d7da1))\n - **FEAT**: Add broadphase generics to CollisionDetection ([#1908](https://github.com/flame-engine/flame/issues/1908)). ([f7714122](https://github.com/flame-engine/flame/commit/f77141229345c24abdd8a09934397dc09c622352))\n - **FEAT**: Adding ClipComponent ([#1769](https://github.com/flame-engine/flame/issues/1769)). ([f34d86db](https://github.com/flame-engine/flame/commit/f34d86db1e459fb5fe36b601631d6c1999fadf8c))\n - **FEAT**: Add support for isometric staggered maps ([#1895](https://github.com/flame-engine/flame/issues/1895)). ([96be8408](https://github.com/flame-engine/flame/commit/96be840899022a024cef1eb853818d8138592000))\n - **FEAT**: Experimental integer viewport ([#1866](https://github.com/flame-engine/flame/issues/1866)). ([63822de3](https://github.com/flame-engine/flame/commit/63822de34c7938232e4048c7bb0e9bb648929ac8))\n - **FEAT**: RecycledQueue now supports modification during iteration ([#1884](https://github.com/flame-engine/flame/issues/1884)). ([01b59493](https://github.com/flame-engine/flame/commit/01b59493024a93d7f3ecbe0627ad0c6a4b2454a1))\n - **FEAT**: Allow children of `ComposedParticle` to have varied lifespan ([#1879](https://github.com/flame-engine/flame/issues/1879)). ([6db519ec](https://github.com/flame-engine/flame/commit/6db519ecd09752e15848d29a616a5152f7269686))\n - **FEAT**: Add `removeWhere` to `Component` ([#1878](https://github.com/flame-engine/flame/issues/1878)). ([abd28f28](https://github.com/flame-engine/flame/commit/abd28f28a627799ea4602026d91f52bc97feb91e))\n - **FEAT**: Added RecycledQueue class ([#1864](https://github.com/flame-engine/flame/issues/1864)). ([9457e38e](https://github.com/flame-engine/flame/commit/9457e38ebc2485e235e3bdc01c7ba43097139db7))\n - **FEAT**: Possibility to ignore hitboxes for ray casting ([#1863](https://github.com/flame-engine/flame/issues/1863)). ([b22bc643](https://github.com/flame-engine/flame/commit/b22bc6438407808e2d4137b4021a2777c3c22afe))\n - **DOCS**: Added Style Guide and Test Writing Guide ([#1897](https://github.com/flame-engine/flame/issues/1897)). ([999caca1](https://github.com/flame-engine/flame/commit/999caca10fbeb834e85461b6cc828b1bce62bbf9))\n - **BREAKING** **FIX**: Make all `ComponentSet` modifications internal ([#1877](https://github.com/flame-engine/flame/issues/1877)). ([f26a066d](https://github.com/flame-engine/flame/commit/f26a066d77f1f79915c52c93038bde7d3571e068))\n - **BREAKING** **CHORE**: Remove functions/classes that were scheduled for removal in v1.3.0 ([#1867](https://github.com/flame-engine/flame/issues/1867)). ([00ab347c](https://github.com/flame-engine/flame/commit/00ab347c57b151c9232c85150e36a8a7781511a3))\n\n#### `flame_test` - `v1.8.0`\n\n - **FEAT**: Add avoid_final_parameters, depend_on_referenced_packages, unnecessary_to_list_in_spreads ([#1927](https://github.com/flame-engine/flame/issues/1927)). ([deccb434](https://github.com/flame-engine/flame/commit/deccb4349d38b6a91ccf5bdf229980b2a3296ce5))\n - **FEAT**: Added DebugTextFormatter ([#1921](https://github.com/flame-engine/flame/issues/1921)). ([426827d1](https://github.com/flame-engine/flame/commit/426827d19e803158dab271dce1fbf93bd09f07de))\n - **BREAKING** **CHORE**: Remove functions/classes that were scheduled for removal in v1.3.0 ([#1867](https://github.com/flame-engine/flame/issues/1867)). ([00ab347c](https://github.com/flame-engine/flame/commit/00ab347c57b151c9232c85150e36a8a7781511a3))\n\n#### `flame_oxygen` - `v0.1.6`\n\n#### `flame_bloc` - `v1.8.0`\n\n - **FEAT**: Add avoid_final_parameters, depend_on_referenced_packages, unnecessary_to_list_in_spreads ([#1927](https://github.com/flame-engine/flame/issues/1927)). ([deccb434](https://github.com/flame-engine/flame/commit/deccb4349d38b6a91ccf5bdf229980b2a3296ce5))\n - **FEAT**: Add `removeWhere` to `Component` ([#1878](https://github.com/flame-engine/flame/issues/1878)). ([abd28f28](https://github.com/flame-engine/flame/commit/abd28f28a627799ea4602026d91f52bc97feb91e))\n\n#### `flame_flare` - `v1.5.0`\n\n - **FEAT**: Add avoid_final_parameters, depend_on_referenced_packages, unnecessary_to_list_in_spreads ([#1927](https://github.com/flame-engine/flame/issues/1927)). ([deccb434](https://github.com/flame-engine/flame/commit/deccb4349d38b6a91ccf5bdf229980b2a3296ce5))\n\n#### `flame_forge2d` - `v0.12.3`\n\n - **FEAT**: Allow flame_forge2d's followBodyComponent to follow centre of mass ([#1947](https://github.com/flame-engine/flame/issues/1947)) ([#1948](https://github.com/flame-engine/flame/issues/1948)). ([c4fd2ba5](https://github.com/flame-engine/flame/commit/c4fd2ba5402f42d5a333270f401bb7208e050986))\n - **DOCS**: Fix broken link in forge2d readme ([#1853](https://github.com/flame-engine/flame/issues/1853)). ([31d39f86](https://github.com/flame-engine/flame/commit/31d39f86708295ef19624554e636e1ddd4846c4d))\n\n#### `flame_lint` - `v0.1.3`\n\n - **FEAT**: Add avoid_final_parameters, depend_on_referenced_packages, unnecessary_to_list_in_spreads ([#1927](https://github.com/flame-engine/flame/issues/1927)). ([deccb434](https://github.com/flame-engine/flame/commit/deccb4349d38b6a91ccf5bdf229980b2a3296ce5))\n\n#### `flame_svg` - `v1.6.0`\n\n - **FEAT**: Add avoid_final_parameters, depend_on_referenced_packages, unnecessary_to_list_in_spreads ([#1927](https://github.com/flame-engine/flame/issues/1927)). ([deccb434](https://github.com/flame-engine/flame/commit/deccb4349d38b6a91ccf5bdf229980b2a3296ce5))\n\n#### `flame_tiled` - `v1.8.0`\n\n - **REFACTOR**: Split layers into files ([#1916](https://github.com/flame-engine/flame/issues/1916)). ([dac2ee13](https://github.com/flame-engine/flame/commit/dac2ee1375a0ed9535ccd5052e0960043ec8d3d2))\n - **FIX**: Take scale into account ([#1906](https://github.com/flame-engine/flame/issues/1906)). ([27ab12ff](https://github.com/flame-engine/flame/commit/27ab12ff6865e5d3d567c4714c4737cd7a0bc1fa))\n - **FEAT**: Animated tile support! ([#1930](https://github.com/flame-engine/flame/issues/1930)). ([6410dc75](https://github.com/flame-engine/flame/commit/6410dc753ce1d044e2d8ea8061186c88d80589e9))\n - **FEAT**: Add avoid_final_parameters, depend_on_referenced_packages, unnecessary_to_list_in_spreads ([#1927](https://github.com/flame-engine/flame/issues/1927)). ([deccb434](https://github.com/flame-engine/flame/commit/deccb4349d38b6a91ccf5bdf229980b2a3296ce5))\n - **FEAT**: Tiled component is positionable ([#1900](https://github.com/flame-engine/flame/issues/1900)). ([88cb2a05](https://github.com/flame-engine/flame/commit/88cb2a05c37535053ece3eb19311c4c78fac249c))\n - **FEAT**: Add support for isometric staggered maps ([#1895](https://github.com/flame-engine/flame/issues/1895)). ([96be8408](https://github.com/flame-engine/flame/commit/96be840899022a024cef1eb853818d8138592000))\n - **FEAT**: Adding support for Group layer nesting for RenderableTileMap ([#1886](https://github.com/flame-engine/flame/issues/1886)). ([5ed34547](https://github.com/flame-engine/flame/commit/5ed345471da0586a2a3071a523c4e5b6d7f184c0))\n - **FEAT**: Hexagonal maps ([#1892](https://github.com/flame-engine/flame/issues/1892)). ([29bda336](https://github.com/flame-engine/flame/commit/29bda336b8febe9cb08dc621da3dc0271c6d2802))\n - **FEAT**: Add isometric support for flame_tiled ([#1885](https://github.com/flame-engine/flame/issues/1885)). ([cf828823](https://github.com/flame-engine/flame/commit/cf82882390efc45a0b2323463b4b28e557f5df48))\n\n\n## 2022-08-19\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.3.0`](#flame---v130)\n - [`flame_test` - `v1.7.0`](#flame_test---v170)\n\nPackages with other changes:\n\n - [`flame_bloc` - `v1.7.0`](#flame_bloc---v170)\n - [`flame_fire_atlas` - `v1.3.0`](#flame_fire_atlas---v130)\n - [`flame_flare` - `v1.4.0`](#flame_flare---v140)\n - [`flame_forge2d` - `v0.12.2`](#flame_forge2d---v0122)\n - [`flame_lint` - `v0.1.2`](#flame_lint---v012)\n - [`flame_oxygen` - `v0.1.5`](#flame_oxygen---v015)\n - [`flame_svg` - `v1.5.0`](#flame_svg---v150)\n - [`flame_tiled` - `v1.7.2`](#flame_tiled---v172)\n - [`flame_rive` - `v1.5.1`](#flame_rive---v151)\n - [`flame_audio` - `v1.3.1`](#flame_audio---v131)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_rive` - `v1.5.1`\n - `flame_audio` - `v1.3.1`\n\n---\n\n#### `flame` - `v1.3.0`\n\n - **REFACTOR**: Use new \"super\"-constructors in ShapeComponents ([#1752](https://github.com/flame-engine/flame/issues/1752)). ([b69e8d85](https://github.com/flame-engine/flame/commit/b69e8d85c77346081d1fc5a2ee5cbf9c204a9edf))\n - **REFACTOR**: Game is now a class, not a mixin ([#1751](https://github.com/flame-engine/flame/issues/1751)). ([5225a4eb](https://github.com/flame-engine/flame/commit/5225a4ebd55a21f5709ccab9a1e24c728b2747ed))\n - **PERF**: Use TextElements within the TextComponent ([#1802](https://github.com/flame-engine/flame/issues/1802)). ([7b044430](https://github.com/flame-engine/flame/commit/7b04443046978c9bcdcf3eacab4813f3bcb545af))\n - **PERF**: Avoid unnecessary copy in AssetsCache.readBinaryFile ([#1749](https://github.com/flame-engine/flame/issues/1749)). ([7e79638d](https://github.com/flame-engine/flame/commit/7e79638dd577cb07d55402d4e862de08ce832b85))\n - **FIX**: ButtonComponent behavior when the engine is paused ([#1726](https://github.com/flame-engine/flame/issues/1726)). ([197e63d6](https://github.com/flame-engine/flame/commit/197e63d69e2a4c6779e49b918d05a60447ce9462))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FIX**: World component can now be queried with `componentsAtPoint` ([#1739](https://github.com/flame-engine/flame/issues/1739)). ([f750d705](https://github.com/flame-engine/flame/commit/f750d705d14dd0ba95d550b2b8a320201a96584b))\n - **FIX**: Merge basic and advanced gesture detectors ([#1718](https://github.com/flame-engine/flame/issues/1718)). ([f08f8e12](https://github.com/flame-engine/flame/commit/f08f8e12f5322c7bea1491908f06b350e13c14b7))\n - **FIX**: Correct key events in GameWidget.controller ([#1745](https://github.com/flame-engine/flame/issues/1745)). ([01ed2ec9](https://github.com/flame-engine/flame/commit/01ed2ec967ee29c946c967786eec6bf7cc6ec958))\n - **FIX**: Camera incorrect follow with zoom and world boundaries. ([c1756177](https://github.com/flame-engine/flame/commit/c175617714e2f15f4379ed8ea412c7cb8bfa1842))\n - **FIX**: Add missing paint arguments on shapes ([#1727](https://github.com/flame-engine/flame/issues/1727)). ([e59f3428](https://github.com/flame-engine/flame/commit/e59f3428469e4298d812bb665171679df8895daf))\n - **FIX**: Delay camera update ([#1811](https://github.com/flame-engine/flame/issues/1811)). ([a5598a8f](https://github.com/flame-engine/flame/commit/a5598a8fa43552028654a3a4b760b7b375dd81e5))\n - **FIX**: Overlays can now be properly added during onLoad ([#1759](https://github.com/flame-engine/flame/issues/1759)). ([9f35b154](https://github.com/flame-engine/flame/commit/9f35b15420bea9ac5eeeddc245484b854e8eed38))\n - **FIX**: SpriteAnimationWidget can now be update animation safely ([#1738](https://github.com/flame-engine/flame/issues/1738)). ([eb070195](https://github.com/flame-engine/flame/commit/eb0701951c165576fac1f540c8860e560a8961e6))\n - **FIX**: JoystickComponent drags using the delta Viewport ([#1831](https://github.com/flame-engine/flame/issues/1831)). ([54e40de6](https://github.com/flame-engine/flame/commit/54e40de674f628282ea19af4f5ce2173ee48fd6e))\n - **FIX**: Specify size for the SpriteWidget ([#1760](https://github.com/flame-engine/flame/issues/1760)). ([82f75fcb](https://github.com/flame-engine/flame/commit/82f75fcb57c8185a7138ee6ceb9082a418099df8))\n - **FEAT**: New colors to palette.dart ([#1783](https://github.com/flame-engine/flame/issues/1783)). ([85cd60e1](https://github.com/flame-engine/flame/commit/85cd60e16c7b4dafdf1823bf85a7ae8a50fd05f2))\n - **FEAT**: add `children` argument to `SpriteComponent.fromImage` ([#1793](https://github.com/flame-engine/flame/issues/1793)). ([80a63362](https://github.com/flame-engine/flame/commit/80a633622a5784f377ef08515115d66ff200b848))\n - **FEAT**: Added Decorator class and HasDecorator mixin ([#1781](https://github.com/flame-engine/flame/issues/1781)). ([8d00847c](https://github.com/flame-engine/flame/commit/8d00847cfcecb60a96772ccba1bcf3aec56b78ff))\n - **FEAT**: Added TextFormatter classes ([#1720](https://github.com/flame-engine/flame/issues/1720)). ([c44272be](https://github.com/flame-engine/flame/commit/c44272be45eadfabc8f03ef250eb663e59ef2aab))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **FEAT**: Added Rotate3DDecorator ([#1805](https://github.com/flame-engine/flame/issues/1805)). ([f05194c8](https://github.com/flame-engine/flame/commit/f05194c80c4d09d024be486882e5defbb10dd506))\n - **FEAT**: Added Shadow3DDecorator ([#1812](https://github.com/flame-engine/flame/issues/1812)). ([0a41b2da](https://github.com/flame-engine/flame/commit/0a41b2dabe51dbdfcd0b4c9f441bc4cc2e9e1b5e))\n - **FEAT**: Add tertiary tap detector mixin ([#1815](https://github.com/flame-engine/flame/issues/1815)). ([e9e7b0d5](https://github.com/flame-engine/flame/commit/e9e7b0d598dac588daf37010b53221da4aea24be))\n - **FEAT**: Add `Ray2` class to be used in raytracing/casting ([#1788](https://github.com/flame-engine/flame/issues/1788)). ([26196c01](https://github.com/flame-engine/flame/commit/26196c0152911c6d20b3feffe96319df4a625a7f))\n - **FEAT**: Added RouterComponent  ([#1755](https://github.com/flame-engine/flame/issues/1755)). ([24092bd7](https://github.com/flame-engine/flame/commit/24092bd72d2e615c06908d9784f19fecb4d0b8b9))\n - **FEAT**: Structured text and text styles ([#1830](https://github.com/flame-engine/flame/issues/1830)). ([bfdc3a29](https://github.com/flame-engine/flame/commit/bfdc3a291ba08ee0df07a80f0709c8470ed8a739))\n - **FEAT**: Drag events that dispatch using componentsAtPoint ([#1715](https://github.com/flame-engine/flame/issues/1715)). ([10669c12](https://github.com/flame-engine/flame/commit/10669c12702a3a82fcf5be9161107dce4349a79f))\n - **FEAT**: Added routes that can return a value ([#1848](https://github.com/flame-engine/flame/issues/1848)). ([f1b276e0](https://github.com/flame-engine/flame/commit/f1b276e020c6f80a18764e63ffbea21abb52b1f2))\n - **FEAT**: PositionComponent now has a built-in Decorator ([#1846](https://github.com/flame-engine/flame/issues/1846)). ([8dd52c33](https://github.com/flame-engine/flame/commit/8dd52c338bbd66938dd90c068f99107337bae4ea))\n - **FEAT**: add `HasAncestor` mixin ([#1711](https://github.com/flame-engine/flame/issues/1711)). ([987a44f4](https://github.com/flame-engine/flame/commit/987a44f441429534c743388b44e6d84b28e8f5ca))\n - **FEAT**: Added ability to control overlays via the RouterComponent ([#1840](https://github.com/flame-engine/flame/issues/1840)). ([e2de70c9](https://github.com/flame-engine/flame/commit/e2de70c98afabb6e570c3442213b2246a724bdd9))\n - **FEAT**: Add vector projection and inversion ([#1787](https://github.com/flame-engine/flame/issues/1787)). ([d197870f](https://github.com/flame-engine/flame/commit/d197870f529829adc51bbafc28180bde33d6f2cb))\n - **DOCS**: Klondike tutorial, part 4 ([#1740](https://github.com/flame-engine/flame/issues/1740)). ([02d0b71b](https://github.com/flame-engine/flame/commit/02d0b71b2379d12b36b53a76b6bcf5f4018ec9df))\n - **BREAKING** **REFACTOR**: Matcher closeToVector() now accepts Vector2 as an argument ([#1761](https://github.com/flame-engine/flame/issues/1761)). ([c5083501](https://github.com/flame-engine/flame/commit/c5083501d54023f04d3f09e3358bce039ade9a20))\n - **BREAKING** **PERF**: Game.images/assets are now same as Flame.images/assets by default ([#1775](https://github.com/flame-engine/flame/issues/1775)). ([0ccb0e2e](https://github.com/flame-engine/flame/commit/0ccb0e2ef525661830c7b4662662ba64fda830fe))\n - **BREAKING** **FEAT**: Raycasting and raytracing ([#1785](https://github.com/flame-engine/flame/issues/1785)). ([ed452dd1](https://github.com/flame-engine/flame/commit/ed452dd172289d49b6a9fbf02ee5b61b33f84c4c))\n\n#### `flame_test` - `v1.7.0`\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Added size parameter for testGolden() ([#1780](https://github.com/flame-engine/flame/issues/1780)). ([8e41d83e](https://github.com/flame-engine/flame/commit/8e41d83ea4e057e1a428f0456450d697351683bf))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **BREAKING** **REFACTOR**: Matcher closeToVector() now accepts Vector2 as an argument ([#1761](https://github.com/flame-engine/flame/issues/1761)). ([c5083501](https://github.com/flame-engine/flame/commit/c5083501d54023f04d3f09e3358bce039ade9a20))\n\n#### `flame_bloc` - `v1.7.0`\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Adding bloc getter to FlameBlocListenable mixin ([#1732](https://github.com/flame-engine/flame/issues/1732)). ([3d19caa3](https://github.com/flame-engine/flame/commit/3d19caa36dcb470b306b841ef9c03647a2f307d7))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **DOCS**: Fixing typo in `FlameMultiBlocProvider` dartdoc. ([67be6ab8](https://github.com/flame-engine/flame/commit/67be6ab86264f6def4b1b3b0e4ba00763c7dab4e))\n - **DOCS**: updating README to the new flame bloc version ([#1737](https://github.com/flame-engine/flame/issues/1737)). ([6a2356aa](https://github.com/flame-engine/flame/commit/6a2356aa5eba1696caa6f88ecfe8143c4ffdb507))\n\n#### `flame_fire_atlas` - `v1.3.0`\n\n - **PERF**: Avoid unnecessary copy in AssetsCache.readBinaryFile ([#1749](https://github.com/flame-engine/flame/issues/1749)). ([7e79638d](https://github.com/flame-engine/flame/commit/7e79638dd577cb07d55402d4e862de08ce832b85))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n#### `flame_flare` - `v1.4.0`\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **DOCS**: Documenting how to write documentation ([#1721](https://github.com/flame-engine/flame/issues/1721)). ([ea354e3a](https://github.com/flame-engine/flame/commit/ea354e3a81e3810a8d2b9e3783d9833ae92349e0))\n\n#### `flame_forge2d` - `v0.12.2`\n\n - **FIX**: `renderChain` should allow open-ended chain drawing ([#1804](https://github.com/flame-engine/flame/issues/1804)). ([60daa196](https://github.com/flame-engine/flame/commit/60daa196a8b2f9d3b022bf4d25b0dc8af29f40b8))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n#### `flame_lint` - `v0.1.2`\n\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **REFACTOR**: Add a few more rules to flame_lint, including use_key_in_widget_constructors ([#1248](https://github.com/flame-engine/flame/issues/1248)). ([bac6c8a4](https://github.com/flame-engine/flame/commit/bac6c8a4469f2c5c2926335f2f589eec9b1a5b5b))\n - **FIX**: Upgrade dartdoc (upgrade analyzer transitive dependency) ([#1630](https://github.com/flame-engine/flame/issues/1630)). ([6da8adb2](https://github.com/flame-engine/flame/commit/6da8adb28cffd8fcb43e6bf8a33aae22578f1b40))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Add non_constant_identifier_names rule ([#1656](https://github.com/flame-engine/flame/issues/1656)). ([1b40de09](https://github.com/flame-engine/flame/commit/1b40de094f4e66be7622d077a6e18cecf1964dde))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n - **DOCS**: Fix various dartdoc warnings ([#1353](https://github.com/flame-engine/flame/issues/1353)). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n\n#### `flame_oxygen` - `v0.1.5`\n\n - **REFACTOR**: Game is now a class, not a mixin ([#1751](https://github.com/flame-engine/flame/issues/1751)). ([5225a4eb](https://github.com/flame-engine/flame/commit/5225a4ebd55a21f5709ccab9a1e24c728b2747ed))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n#### `flame_svg` - `v1.5.0`\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n#### `flame_tiled` - `v1.7.2`\n\n - **FIX**: Remove unnecessary x offset ([#1838](https://github.com/flame-engine/flame/issues/1838)). ([4ea12b72](https://github.com/flame-engine/flame/commit/4ea12b724e04843b3b7dcd02dc2fb5060c9cf283))\n\n\n## 2022-08-10\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_tiled` - `v1.7.1`](#flame_tiled---v171)\n\n---\n\n#### `flame_tiled` - `v1.7.1`\n\n - **FIX**: Remove unnecessary x offset ([#1838](https://github.com/flame-engine/flame/issues/1838)). ([4ea12b72](https://github.com/flame-engine/flame/commit/4ea12b724e04843b3b7dcd02dc2fb5060c9cf283))\n - **FEAT**: Adding support for additional layer rendering options ([#1794](https://github.com/flame-engine/flame/issues/1794)). ([112acf2a](https://github.com/flame-engine/flame/commit/112acf2aa70ded86e6c2b661f5d6a4855d043f99))\n\n\n## 2022-08-09\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_tiled` - `v1.7.0`](#flame_tiled---v170)\n\n---\n\n#### `flame_tiled` - `v1.7.0`\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FIX**: tiled example size ([#1729](https://github.com/flame-engine/flame/issues/1729)). ([8306fc11](https://github.com/flame-engine/flame/commit/8306fc1104cb752ce71108abb3768f05ce1b1dac))\n - **FEAT**: Adding support for additional layer rendering options ([#1794](https://github.com/flame-engine/flame/issues/1794)). ([112acf2a](https://github.com/flame-engine/flame/commit/112acf2aa70ded86e6c2b661f5d6a4855d043f99))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n\n## 2022-07-08\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.2.1`](#flame---v121)\n - [`flame_audio` - `v1.3.0`](#flame_audio---v130)\n - [`flame_test` - `v1.6.0`](#flame_test---v160)\n\nPackages with other changes:\n\n - [`flame_bloc` - `v1.6.0`](#flame_bloc---v160)\n - [`flame_fire_atlas` - `v1.2.0`](#flame_fire_atlas---v120)\n - [`flame_flare` - `v1.3.0`](#flame_flare---v130)\n - [`flame_forge2d` - `v0.12.1`](#flame_forge2d---v0121)\n - [`flame_lint` - `v0.1.1`](#flame_lint---v011)\n - [`flame_oxygen` - `v0.1.4`](#flame_oxygen---v014)\n - [`flame_rive` - `v1.5.0`](#flame_rive---v150)\n - [`flame_svg` - `v1.4.0`](#flame_svg---v140)\n - [`flame_tiled` - `v1.6.0`](#flame_tiled---v160)\n\n---\n\n#### `flame` - `v1.2.1`\n\n - **REFACTOR**: Game is now a class, not a mixin ([#1751](https://github.com/flame-engine/flame/issues/1751)). ([5225a4eb](https://github.com/flame-engine/flame/commit/5225a4ebd55a21f5709ccab9a1e24c728b2747ed))\n - **REFACTOR**: Use new \"super\"-constructors in ShapeComponents ([#1752](https://github.com/flame-engine/flame/issues/1752)). ([b69e8d85](https://github.com/flame-engine/flame/commit/b69e8d85c77346081d1fc5a2ee5cbf9c204a9edf))\n - **PERF**: Avoid unnecessary copy in AssetsCache.readBinaryFile ([#1749](https://github.com/flame-engine/flame/issues/1749)). ([7e79638d](https://github.com/flame-engine/flame/commit/7e79638dd577cb07d55402d4e862de08ce832b85))\n - **FIX**: Specify size for the SpriteWidget ([#1760](https://github.com/flame-engine/flame/issues/1760)). ([82f75fcb](https://github.com/flame-engine/flame/commit/82f75fcb57c8185a7138ee6ceb9082a418099df8))\n - **FIX**: SpriteAnimationWidget can now be update animation safely ([#1738](https://github.com/flame-engine/flame/issues/1738)). ([eb070195](https://github.com/flame-engine/flame/commit/eb0701951c165576fac1f540c8860e560a8961e6))\n - **FIX**: Overlays can now be properly added during onLoad ([#1759](https://github.com/flame-engine/flame/issues/1759)). ([9f35b154](https://github.com/flame-engine/flame/commit/9f35b15420bea9ac5eeeddc245484b854e8eed38))\n - **FIX**: Camera incorrect follow with zoom and world boundaries. ([c1756177](https://github.com/flame-engine/flame/commit/c175617714e2f15f4379ed8ea412c7cb8bfa1842))\n - **FIX**: Correct key events in GameWidget.controller ([#1745](https://github.com/flame-engine/flame/issues/1745)). ([01ed2ec9](https://github.com/flame-engine/flame/commit/01ed2ec967ee29c946c967786eec6bf7cc6ec958))\n - **FIX**: World component can now be queried with `componentsAtPoint` ([#1739](https://github.com/flame-engine/flame/issues/1739)). ([f750d705](https://github.com/flame-engine/flame/commit/f750d705d14dd0ba95d550b2b8a320201a96584b))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FIX**: ButtonComponent behavior when the engine is paused ([#1726](https://github.com/flame-engine/flame/issues/1726)). ([197e63d6](https://github.com/flame-engine/flame/commit/197e63d69e2a4c6779e49b918d05a60447ce9462))\n - **FIX**: Add missing paint arguments on shapes ([#1727](https://github.com/flame-engine/flame/issues/1727)). ([e59f3428](https://github.com/flame-engine/flame/commit/e59f3428469e4298d812bb665171679df8895daf))\n - **FIX**: Merge basic and advanced gesture detectors ([#1718](https://github.com/flame-engine/flame/issues/1718)). ([f08f8e12](https://github.com/flame-engine/flame/commit/f08f8e12f5322c7bea1491908f06b350e13c14b7))\n - **FEAT**: New colors to palette.dart ([#1783](https://github.com/flame-engine/flame/issues/1783)). ([85cd60e1](https://github.com/flame-engine/flame/commit/85cd60e16c7b4dafdf1823bf85a7ae8a50fd05f2))\n - **FEAT**: Added TextFormatter classes ([#1720](https://github.com/flame-engine/flame/issues/1720)). ([c44272be](https://github.com/flame-engine/flame/commit/c44272be45eadfabc8f03ef250eb663e59ef2aab))\n - **FEAT**: Drag events that dispatch using componentsAtPoint ([#1715](https://github.com/flame-engine/flame/issues/1715)). ([10669c12](https://github.com/flame-engine/flame/commit/10669c12702a3a82fcf5be9161107dce4349a79f))\n - **FEAT**: add `HasAncestor` mixin ([#1711](https://github.com/flame-engine/flame/issues/1711)). ([987a44f4](https://github.com/flame-engine/flame/commit/987a44f441429534c743388b44e6d84b28e8f5ca))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **DOCS**: Klondike tutorial, part 4 ([#1740](https://github.com/flame-engine/flame/issues/1740)). ([02d0b71b](https://github.com/flame-engine/flame/commit/02d0b71b2379d12b36b53a76b6bcf5f4018ec9df))\n - **BREAKING** **REFACTOR**: Matcher closeToVector() now accepts Vector2 as an argument ([#1761](https://github.com/flame-engine/flame/issues/1761)). ([c5083501](https://github.com/flame-engine/flame/commit/c5083501d54023f04d3f09e3358bce039ade9a20))\n - **BREAKING** **PERF**: Game.images/assets are now same as Flame.images/assets by default ([#1775](https://github.com/flame-engine/flame/issues/1775)). ([0ccb0e2e](https://github.com/flame-engine/flame/commit/0ccb0e2ef525661830c7b4662662ba64fda830fe))\n\n#### `flame_audio` - `v1.3.0`\n\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **BREAKING** **FEAT**: Update flame_audio to AP 1.0.0 ([#1724](https://github.com/flame-engine/flame/issues/1724)). ([d6bf920d](https://github.com/flame-engine/flame/commit/d6bf920d28eea5f08adcba2601104271078e7a3d))\n\n#### `flame_test` - `v1.6.0`\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Added size parameter for testGolden() ([#1780](https://github.com/flame-engine/flame/issues/1780)). ([8e41d83e](https://github.com/flame-engine/flame/commit/8e41d83ea4e057e1a428f0456450d697351683bf))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **BREAKING** **REFACTOR**: Matcher closeToVector() now accepts Vector2 as an argument ([#1761](https://github.com/flame-engine/flame/issues/1761)). ([c5083501](https://github.com/flame-engine/flame/commit/c5083501d54023f04d3f09e3358bce039ade9a20))\n\n#### `flame_bloc` - `v1.6.0`\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Adding bloc getter to FlameBlocListenable mixin ([#1732](https://github.com/flame-engine/flame/issues/1732)). ([3d19caa3](https://github.com/flame-engine/flame/commit/3d19caa36dcb470b306b841ef9c03647a2f307d7))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **DOCS**: updating README to the new flame bloc version ([#1737](https://github.com/flame-engine/flame/issues/1737)). ([6a2356aa](https://github.com/flame-engine/flame/commit/6a2356aa5eba1696caa6f88ecfe8143c4ffdb507))\n\n#### `flame_fire_atlas` - `v1.2.0`\n\n - **PERF**: Avoid unnecessary copy in AssetsCache.readBinaryFile ([#1749](https://github.com/flame-engine/flame/issues/1749)). ([7e79638d](https://github.com/flame-engine/flame/commit/7e79638dd577cb07d55402d4e862de08ce832b85))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n#### `flame_flare` - `v1.3.0`\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **DOCS**: Documenting how to write documentation ([#1721](https://github.com/flame-engine/flame/issues/1721)). ([ea354e3a](https://github.com/flame-engine/flame/commit/ea354e3a81e3810a8d2b9e3783d9833ae92349e0))\n\n#### `flame_forge2d` - `v0.12.1`\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n#### `flame_lint` - `v0.1.1`\n\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **REFACTOR**: Add a few more rules to flame_lint, including use_key_in_widget_constructors ([#1248](https://github.com/flame-engine/flame/issues/1248)). ([bac6c8a4](https://github.com/flame-engine/flame/commit/bac6c8a4469f2c5c2926335f2f589eec9b1a5b5b))\n - **FIX**: Upgrade dartdoc (upgrade analyzer transitive dependency) ([#1630](https://github.com/flame-engine/flame/issues/1630)). ([6da8adb2](https://github.com/flame-engine/flame/commit/6da8adb28cffd8fcb43e6bf8a33aae22578f1b40))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Add non_constant_identifier_names rule ([#1656](https://github.com/flame-engine/flame/issues/1656)). ([1b40de09](https://github.com/flame-engine/flame/commit/1b40de094f4e66be7622d077a6e18cecf1964dde))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n - **DOCS**: Fix various dartdoc warnings ([#1353](https://github.com/flame-engine/flame/issues/1353)). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n\n#### `flame_oxygen` - `v0.1.4`\n\n - **REFACTOR**: Game is now a class, not a mixin ([#1751](https://github.com/flame-engine/flame/issues/1751)). ([5225a4eb](https://github.com/flame-engine/flame/commit/5225a4ebd55a21f5709ccab9a1e24c728b2747ed))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n#### `flame_rive` - `v1.5.0`\n\n - **FIX**: Flame_rive now can load Nested Artboards and update to 0.9.0 rive package  ([#1741](https://github.com/flame-engine/flame/issues/1741)). ([82e4be96](https://github.com/flame-engine/flame/commit/82e4be96f3090908e95659a96006bf50fbb5b08c))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n#### `flame_svg` - `v1.4.0`\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n#### `flame_tiled` - `v1.6.0`\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FIX**: tiled example size ([#1729](https://github.com/flame-engine/flame/issues/1729)). ([8306fc11](https://github.com/flame-engine/flame/commit/8306fc1104cb752ce71108abb3768f05ce1b1dac))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n\n## 2022-06-19\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_rive` - `v1.4.0`](#flame_rive---v140)\n\n---\n\n#### `flame_rive` - `v1.4.0`\n\n - **FIX**: Flame_rive now can load Nested Artboards and update to 0.9.0 rive package  ([#1741](https://github.com/flame-engine/flame/issues/1741)). ([82e4be96](https://github.com/flame-engine/flame/commit/82e4be96f3090908e95659a96006bf50fbb5b08c))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n\n## 2022-06-14\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame_audio` - `v1.2.0`](#flame_audio---v120)\n\nPackages with other changes:\n\n - There are no other changes in this release.\n\n---\n\n#### `flame_audio` - `v1.2.0`\n\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **BREAKING** **FEAT**: Update flame_audio to AP 1.0.0 ([#1724](https://github.com/flame-engine/flame/issues/1724)). ([d6bf920d](https://github.com/flame-engine/flame/commit/d6bf920d28eea5f08adcba2601104271078e7a3d))\n\n\n## 2022-06-07\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.2.0`](#flame---v120)\n - [`flame_forge2d` - `v0.12.0`](#flame_forge2d---v0120)\n\nPackages with other changes:\n\n - [`flame_audio` - `v1.1.0`](#flame_audio---v110)\n - [`flame_bloc` - `v1.5.0`](#flame_bloc---v150)\n - [`flame_fire_atlas` - `v1.1.0`](#flame_fire_atlas---v110)\n - [`flame_flare` - `v1.2.0`](#flame_flare---v120)\n - [`flame_oxygen` - `v0.1.3`](#flame_oxygen---v013)\n - [`flame_rive` - `v1.3.0`](#flame_rive---v130)\n - [`flame_svg` - `v1.3.0`](#flame_svg---v130)\n - [`flame_test` - `v1.5.0`](#flame_test---v150)\n - [`flame_tiled` - `v1.5.0`](#flame_tiled---v150)\n\n---\n\n#### `flame` - `v1.2.0`\n\n - **REFACTOR**: Organize Component class ([#1608](https://github.com/flame-engine/flame/issues/1608)). ([069294f4](https://github.com/flame-engine/flame/commit/069294f44082a5d4ae6e9eff1d29be9cb06ee4a7))\n - **REFACTOR**: Remove unecessary copy operation on Camera ([#1708](https://github.com/flame-engine/flame/issues/1708)). ([94cc115a](https://github.com/flame-engine/flame/commit/94cc115a9ee6660d1f3a72378e8b35523b83bfad))\n - **REFACTOR**: Update and guarantee consistency on mocktail dev dependency version across repo ([#1701](https://github.com/flame-engine/flame/issues/1701)). ([f4a98878](https://github.com/flame-engine/flame/commit/f4a98878062dbd4fe8238a8b014e6be3e528c5d8))\n - **REFACTOR**: Add onComplete as optional parameter ([#1686](https://github.com/flame-engine/flame/issues/1686)). ([4ca65f8a](https://github.com/flame-engine/flame/commit/4ca65f8a2c330d61527e071434441f2df9deefb4))\n - **REFACTOR**: Added MultiDragListener - common API between HasDraggables and MultiTouchDragDetector ([#1668](https://github.com/flame-engine/flame/issues/1668)). ([801dbba1](https://github.com/flame-engine/flame/commit/801dbba1d8b6fd721d4e2fc752c70f97d4771198))\n - **REFACTOR**: Simplify Component.firstChild, .lastChild, and .findParent ([#1673](https://github.com/flame-engine/flame/issues/1673)). ([84f2f57e](https://github.com/flame-engine/flame/commit/84f2f57e5fddb82572177b2bcd0f8309a891ea4e))\n - **REFACTOR**: Replace some usages of fold<> with .sum ([#1670](https://github.com/flame-engine/flame/issues/1670)). ([dd05ecb6](https://github.com/flame-engine/flame/commit/dd05ecb6b8b105b4d1fc894dc6ce7ca3f8cf793e))\n - **REFACTOR**: Deprecate ComponentSet.createDefault() ([#1676](https://github.com/flame-engine/flame/issues/1676)). ([f37e3a20](https://github.com/flame-engine/flame/commit/f37e3a2028e16143d8bb3218691904c38fb848a4))\n - **REFACTOR**: Simplify HudButtonComponent ([#1647](https://github.com/flame-engine/flame/issues/1647)). ([30d84b7c](https://github.com/flame-engine/flame/commit/30d84b7caea128c7dc579dce170129e462bc03bf))\n - **REFACTOR**: Component's lifecycle futures moved into LifecycleManager ([#1613](https://github.com/flame-engine/flame/issues/1613)). ([39201c40](https://github.com/flame-engine/flame/commit/39201c40fa3eea5dbdbaa823309cdf8856f912a6))\n - **REFACTOR**: TextRenderer and TextPaint moved to separate files ([#1628](https://github.com/flame-engine/flame/issues/1628)). ([5e1f5966](https://github.com/flame-engine/flame/commit/5e1f59663bf7e09a02475979d1eded54dbaaefd7))\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **REFACTOR**: Improve tests ([#1609](https://github.com/flame-engine/flame/issues/1609)). ([f33b3986](https://github.com/flame-engine/flame/commit/f33b3986cd913416ae3955c922d6cc8b0db872e3))\n - **FIX**: Fix tile flips when using canvas.drawAtlas ([#1610](https://github.com/flame-engine/flame/issues/1610)). ([b4ad498f](https://github.com/flame-engine/flame/commit/b4ad498fe5488795deb2c2e098fcde357b448bf0))\n - **FIX**: Expose `CompositeHitbox` ([#1589](https://github.com/flame-engine/flame/issues/1589)). ([78775798](https://github.com/flame-engine/flame/commit/7877579868041f4844ebae885da559097b7aa8a5))\n - **FIX**: Anchor equality operator is now more reliable ([#1560](https://github.com/flame-engine/flame/issues/1560)). ([0d6581ef](https://github.com/flame-engine/flame/commit/0d6581ef1aaff4437b2a84f9e57d7d0e1d093d1f))\n - **FIX**: Deprecate Anchor.translate() ([#1672](https://github.com/flame-engine/flame/issues/1672)). ([80c648fc](https://github.com/flame-engine/flame/commit/80c648fc94dc00e37f2c0876fec39b2628b3128a))\n - **FIX**: Avoid leaks when using PictureRecorders ([#1643](https://github.com/flame-engine/flame/issues/1643)). ([d67065e5](https://github.com/flame-engine/flame/commit/d67065e52db453b0f4f190a7aec1bec6bc389e45))\n - **FIX**: Remove nonVirtual method shouldRemove ([#1707](https://github.com/flame-engine/flame/issues/1707)). ([1efd067e](https://github.com/flame-engine/flame/commit/1efd067e31ad425941e5b83891c7289ba063ec90))\n - **FIX**: Fix flame package example app ([#1709](https://github.com/flame-engine/flame/issues/1709)). ([bd2ef967](https://github.com/flame-engine/flame/commit/bd2ef967e10eb0309e0a468652a657cae3d5e7d5))\n - **FIX**: Subscription for events when game changes in GameWidget ([#1659](https://github.com/flame-engine/flame/issues/1659)). ([04f0d5d1](https://github.com/flame-engine/flame/commit/04f0d5d172ca5065e58e8b9b5536cbce706147d4))\n - **FIX**: performance improvements on `SpriteBatch` APIs ([#1637](https://github.com/flame-engine/flame/issues/1637)). ([4b19a1b2](https://github.com/flame-engine/flame/commit/4b19a1b203c5cfca5bb412b91c795fe6a215506e))\n - **FIX**: Removed warnings using flutter v3 ([#1640](https://github.com/flame-engine/flame/issues/1640)). ([69214827](https://github.com/flame-engine/flame/commit/69214827a0edb563468951256eccecab408f89df))\n - **FIX**: ParallaxComponent.update mustCallSuper ([#1635](https://github.com/flame-engine/flame/issues/1635)). ([9474ce74](https://github.com/flame-engine/flame/commit/9474ce7425ffc18f6b1a1a35c35f59b76f435166))\n - **FIX**: Isometric tile map component uses scale when getting block from position ([#1569](https://github.com/flame-engine/flame/issues/1569)). ([0c430786](https://github.com/flame-engine/flame/commit/0c430786e2774174424a21a13464e93d04c69295))\n - **FIX**: Dispose `TextBoxComponent` image cache properly ([#1579](https://github.com/flame-engine/flame/issues/1579)). ([c0e3257a](https://github.com/flame-engine/flame/commit/c0e3257a0b348885275f2659c351bacbfa5a8732))\n - **FIX**: `ParentIsA` missing `mustCallSuper` ([#1604](https://github.com/flame-engine/flame/issues/1604)). ([72129019](https://github.com/flame-engine/flame/commit/721290198cc7062f8cfb958cb8499e64be7a1e9c))\n - **FIX**: Component can now be removed in any lifecycle stage ([#1601](https://github.com/flame-engine/flame/issues/1601)). ([c0a14156](https://github.com/flame-engine/flame/commit/c0a141563b9e832b1a81bf32d860d4dfb2b359ae))\n - **FIX**: Export NotifyingVector2 ([#1633](https://github.com/flame-engine/flame/issues/1633)). ([aeaf9999](https://github.com/flame-engine/flame/commit/aeaf9999b0b4f69e394063d3af8e18f67dff5ed9))\n - **FIX**: RectangleHitbox should shrink to bounds ([#1596](https://github.com/flame-engine/flame/issues/1596)). ([60df3b9f](https://github.com/flame-engine/flame/commit/60df3b9f60f538fbad7a3d806f5d38262ab6d66c))\n - **FIX**: Components in uninitialized state can now be safely removed ([#1551](https://github.com/flame-engine/flame/issues/1551)). ([ba617790](https://github.com/flame-engine/flame/commit/ba617790e4a7ca4dc03f4a2e29de43d42efd3482))\n - **FIX**: Bug in \"tty\" TextBoxComponent ([#1619](https://github.com/flame-engine/flame/issues/1619)). ([6cc3e827](https://github.com/flame-engine/flame/commit/6cc3e82727509f8877873b095c84eef3543fe01e))\n - **FIX**: Component.loaded future sometimes failed to complete ([#1593](https://github.com/flame-engine/flame/issues/1593)). ([89ee9b98](https://github.com/flame-engine/flame/commit/89ee9b984bfc3784dedde1ada1daa992a9f0dedc))\n - **FIX**: correctly calculating frame length ([#1578](https://github.com/flame-engine/flame/issues/1578)). ([efda45e7](https://github.com/flame-engine/flame/commit/efda45e76c38a2d38a4cd0bb66ece9792f5832df))\n - **FIX**: Optimize AcceleratedParticle and MovingParticle ([#1568](https://github.com/flame-engine/flame/issues/1568)). ([5591c109](https://github.com/flame-engine/flame/commit/5591c109437309907cdac72f0bb479a6a6bfa00a))\n - **FEAT**: Method `componentsAtPoint` now reports the \"stacktrace\" of points ([#1615](https://github.com/flame-engine/flame/issues/1615)). ([e2398966](https://github.com/flame-engine/flame/commit/e239896624f1e2736de83148ff172ca1b0f97dae))\n - **FEAT**: Allow changing parent from null parent ([#1662](https://github.com/flame-engine/flame/issues/1662)). ([53268b5f](https://github.com/flame-engine/flame/commit/53268b5f5fd81f3822bfda9721b97be4e72e48e3))\n - **FEAT**: Callbacks in `HudButtonComponent` constructor and `ViewportMargin` mixin to avoid code duplication ([#1685](https://github.com/flame-engine/flame/issues/1685)). ([f55b2e0d](https://github.com/flame-engine/flame/commit/f55b2e0dc01c98718e4871430c6745472c221821))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Keep stacktrace when rethrowing an error from GameWidget ([#1675](https://github.com/flame-engine/flame/issues/1675)). ([dd28183b](https://github.com/flame-engine/flame/commit/dd28183bc4ebe2ea2f80d1dab3b5ab22d11b8382))\n - **FEAT**: Aligned text in the TextBoxComponent ([#1620](https://github.com/flame-engine/flame/issues/1620)). ([c64aedae](https://github.com/flame-engine/flame/commit/c64aedaeb3fed908722b8872b71e288ff87bc761))\n - **FEAT**: Included `completed` completer in `SpriteAnimation` ([#1564](https://github.com/flame-engine/flame/issues/1564)). ([71999b19](https://github.com/flame-engine/flame/commit/71999b191af0285e8d61583b041da58afd40d8d2))\n - **FEAT**: Optional key for Images.load ([#1624](https://github.com/flame-engine/flame/issues/1624)). ([067c34b5](https://github.com/flame-engine/flame/commit/067c34b5f29e1a9bd51861d872092ae5ee0a551f))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n - **FEAT**: Add `isFirstFrame` and `onStart` event to `SpriteAnimation` ([#1492](https://github.com/flame-engine/flame/issues/1492)). ([701d0706](https://github.com/flame-engine/flame/commit/701d0706af74e6437d71376d468b32bb2537e5b7))\n - **FEAT**: Add `FpsComponent` and `FpsTextComponent` ([#1595](https://github.com/flame-engine/flame/issues/1595)). ([4c68c2b0](https://github.com/flame-engine/flame/commit/4c68c2b0a2660e705b30099234da4ab1eb4616d0))\n - **FEAT**: Added FollowBehavior and ability for the new Camera to follow a component ([#1561](https://github.com/flame-engine/flame/issues/1561)). ([b583388c](https://github.com/flame-engine/flame/commit/b583388ca432f799ad13b92a3a7bf25ddf98ceb0))\n - **FEAT**: Added componentsAtPoint() iterable ([#1518](https://github.com/flame-engine/flame/issues/1518)). ([b99e3512](https://github.com/flame-engine/flame/commit/b99e35120dc4fe81ebfedc89a666286ec489384c))\n - **FEAT**: Added AnchorToEffect and AnchorByEffect ([#1556](https://github.com/flame-engine/flame/issues/1556)). ([eff72794](https://github.com/flame-engine/flame/commit/eff72794afed73bdb1df8e14b17d50f0f446e92b))\n - **FEAT**: Added utility function solveCubic() ([#1696](https://github.com/flame-engine/flame/issues/1696)). ([31784ca0](https://github.com/flame-engine/flame/commit/31784ca0b05082042003f847be2b4004da83edb6))\n - **FEAT**: add FutureOr support on SpriteButton ([#1645](https://github.com/flame-engine/flame/issues/1645)). ([2e82dc95](https://github.com/flame-engine/flame/commit/2e82dc95ecd6d7298239cadad5a746341c37fcd9))\n - **FEAT**: MoveAlongPathEffect can now be applied to any PositionProvider ([#1555](https://github.com/flame-engine/flame/issues/1555)). ([a0ff2d18](https://github.com/flame-engine/flame/commit/a0ff2d18a1efc54f648a277453fa9cf6414ce44c))\n - **FEAT**: adding KeyboardListenerComponent ([#1594](https://github.com/flame-engine/flame/issues/1594)). ([c887c361](https://github.com/flame-engine/flame/commit/c887c3616e9f65209b8e29cb8575a0052db3e2bb))\n - **FEAT**: new flame bloc API ([#1538](https://github.com/flame-engine/flame/issues/1538)). ([f98970a9](https://github.com/flame-engine/flame/commit/f98970a91f91fe70e4a38834d7b69bfcb438d197))\n - **FEAT**: Added the onLongTapDown event ([#1587](https://github.com/flame-engine/flame/issues/1587)). ([ed302d89](https://github.com/flame-engine/flame/commit/ed302d89160cd7391e3aaf66a0038cd8f57ceca9))\n - **FEAT**: Add ability to add/remove multiple overlays at once ([#1657](https://github.com/flame-engine/flame/issues/1657)). ([0ac84c00](https://github.com/flame-engine/flame/commit/0ac84c0024338cbe87fcff264b83e01192aa355b))\n - **FEAT**: Helpers for whether a `PositionComponent` is flipped. ([#1700](https://github.com/flame-engine/flame/issues/1700)). ([cf67147e](https://github.com/flame-engine/flame/commit/cf67147ea37aed8e5f1dd12def442dccbe4576fd))\n - **FEAT**: World bounds for a CameraComponent ([#1605](https://github.com/flame-engine/flame/issues/1605)). ([abb497ab](https://github.com/flame-engine/flame/commit/abb497abe47f6366d27f44d25535924bd7de8a28))\n - **FEAT**: Implement tap events based on `componentsAtPoint` ([#1661](https://github.com/flame-engine/flame/issues/1661)). ([2711ba60](https://github.com/flame-engine/flame/commit/2711ba60c2c700984d8a90d90519e17850038ab4))\n - **FEAT**: add `ParentIsA` to force parent child relations ([#1566](https://github.com/flame-engine/flame/issues/1566)). ([2cdf3868](https://github.com/flame-engine/flame/commit/2cdf3868460f04cee76079e3f81cdd12fb407d3a))\n - **FEAT**: Adding classes for raw geometric shapes ([#1528](https://github.com/flame-engine/flame/issues/1528)). ([666a2b19](https://github.com/flame-engine/flame/commit/666a2b199fc740d02628321bb19511ba98de1700))\n - **FEAT**: Add solveQuadratic() utility function ([#1665](https://github.com/flame-engine/flame/issues/1665)). ([d8bbfc06](https://github.com/flame-engine/flame/commit/d8bbfc067e3885cedd133de47a98134fc15c9c82))\n - **FEAT**: allow external packages to await for game to be loaded ([#1699](https://github.com/flame-engine/flame/issues/1699)). ([a15eda0b](https://github.com/flame-engine/flame/commit/a15eda0b67d6020bcb72162f0186e3c5069674bb))\n - **FEAT**: Children as argument to FlameGame ([#1680](https://github.com/flame-engine/flame/issues/1680)). ([db336c03](https://github.com/flame-engine/flame/commit/db336c03b607b878faf618cb1ab5833cd859d0e6))\n - **FEAT**: Add range constructor on SpriteAnimationData ([#1572](https://github.com/flame-engine/flame/issues/1572)). ([e42b4958](https://github.com/flame-engine/flame/commit/e42b495805efd2e969cfe412b069ffcc6e828ad6))\n - **FEAT**: Add helper function for creating golden tests ([#1623](https://github.com/flame-engine/flame/issues/1623)). ([d0faaada](https://github.com/flame-engine/flame/commit/d0faaada2bb971c2dde5a37dfa20d316c532ea28))\n - **FEAT**: Added ability to render spritesheet-based fonts ([#1634](https://github.com/flame-engine/flame/issues/1634)). ([3f287898](https://github.com/flame-engine/flame/commit/3f2878988195606b90d9e48b981444792af08ebe))\n - **BREAKING** **FIX**: `FixedResolutionViewport` noClip -> clip ([#1612](https://github.com/flame-engine/flame/issues/1612)). ([02be4acd](https://github.com/flame-engine/flame/commit/02be4acd8798254eeaf832863d4000e1c5240db1))\n - **BREAKING** **FIX**: Game.mouseCursor and Game.overlays can now be safely set during onLoad ([#1498](https://github.com/flame-engine/flame/issues/1498)). ([821d01c3](https://github.com/flame-engine/flame/commit/821d01c3fab3cdd9e80d6ead8d491ea2e8ec0643))\n - **BREAKING** **FEAT**: Added anchor for the Viewport ([#1611](https://github.com/flame-engine/flame/issues/1611)). ([c3bb14b7](https://github.com/flame-engine/flame/commit/c3bb14b7ca9513fc75f51b0a5cbc9d986db48dd6))\n - **BREAKING** **FEAT**: remove `onTimingsCallback` for Flutter 3.0 ([#1626](https://github.com/flame-engine/flame/issues/1626)). ([0761a79d](https://github.com/flame-engine/flame/commit/0761a79df6c88a5a6ba74ec78d4f600983657c06))\n - **BREAKING** **FEAT**: Add ability to render without loading on image related widgets ([#1674](https://github.com/flame-engine/flame/issues/1674)). ([40a061bc](https://github.com/flame-engine/flame/commit/40a061bcf06b5bf028911964617c1d1e2599460a))\n - **BREAKING** **FEAT**: Adding GameWidget.controlled ([#1650](https://github.com/flame-engine/flame/issues/1650)). ([7ef6a51e](https://github.com/flame-engine/flame/commit/7ef6a51ec60a70807a126b6121a1fd4379b8e19b))\n - **BREAKING** **FEAT**: Size effects will now work only on components implementing SizeProvider ([#1571](https://github.com/flame-engine/flame/issues/1571)). ([1bfed571](https://github.com/flame-engine/flame/commit/1bfed57132330fb948962261735a0545eb37e7b9))\n\n#### `flame_forge2d` - `v0.12.0`\n\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **FIX**: flips back defaultGravity on y axis ([#1585](https://github.com/flame-engine/flame/issues/1585)). ([6b217ac4](https://github.com/flame-engine/flame/commit/6b217ac466f7522772cf1f974b39af1392f5a807))\n - **FIX**: MouseJoint gets less and less reactive ([#1562](https://github.com/flame-engine/flame/issues/1562)). ([90747bf4](https://github.com/flame-engine/flame/commit/90747bf4a52bb4c82611fa1e9c50f0f11e309baa))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: allow controlling when a fixture is rendered ([#1648](https://github.com/flame-engine/flame/issues/1648)). ([1b59d801](https://github.com/flame-engine/flame/commit/1b59d801c6c1bcc325948ac4e18dfa536baa5a9c))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n - **FEAT**: allowed specifying renderBody via constructor ([#1548](https://github.com/flame-engine/flame/issues/1548)). ([ceb72666](https://github.com/flame-engine/flame/commit/ceb726666e39e20cd12786be86da60ab9cc61c9a))\n - **DOCS**: Move flame_forge2d examples to main examples ([#1588](https://github.com/flame-engine/flame/issues/1588)). ([6dd0a970](https://github.com/flame-engine/flame/commit/6dd0a970e6f106d8927b542d688f3bc9231e1b69))\n - **BREAKING** **FEAT**: enhance ContactCallback process ([#1547](https://github.com/flame-engine/flame/issues/1547)). ([a50d4a1e](https://github.com/flame-engine/flame/commit/a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f))\n\n#### `flame_audio` - `v1.1.0`\n\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **FIX**: Removed warnings using flutter v3 ([#1640](https://github.com/flame-engine/flame/issues/1640)). ([69214827](https://github.com/flame-engine/flame/commit/69214827a0edb563468951256eccecab408f89df))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n\n#### `flame_bloc` - `v1.5.0`\n\n - **REFACTOR**: Update and guarantee consistency on mocktail dev dependency version across repo ([#1701](https://github.com/flame-engine/flame/issues/1701)). ([f4a98878](https://github.com/flame-engine/flame/commit/f4a98878062dbd4fe8238a8b014e6be3e528c5d8))\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n - **FEAT**: new flame bloc API ([#1538](https://github.com/flame-engine/flame/issues/1538)). ([f98970a9](https://github.com/flame-engine/flame/commit/f98970a91f91fe70e4a38834d7b69bfcb438d197))\n\n#### `flame_fire_atlas` - `v1.1.0`\n\n - **REFACTOR**: Update and guarantee consistency on mocktail dev dependency version across repo ([#1701](https://github.com/flame-engine/flame/issues/1701)). ([f4a98878](https://github.com/flame-engine/flame/commit/f4a98878062dbd4fe8238a8b014e6be3e528c5d8))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Optional key for Images.load ([#1624](https://github.com/flame-engine/flame/issues/1624)). ([067c34b5](https://github.com/flame-engine/flame/commit/067c34b5f29e1a9bd51861d872092ae5ee0a551f))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n\n#### `flame_flare` - `v1.2.0`\n\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n\n#### `flame_oxygen` - `v0.1.3`\n\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **FIX**: Fix setter in Oxygen's SizeComponent ([#1557](https://github.com/flame-engine/flame/issues/1557)). ([b1fae297](https://github.com/flame-engine/flame/commit/b1fae2976ef5445a52c99399dd8cc284fc272684))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n - **FEAT**: Add `FpsComponent` and `FpsTextComponent` ([#1595](https://github.com/flame-engine/flame/issues/1595)). ([4c68c2b0](https://github.com/flame-engine/flame/commit/4c68c2b0a2660e705b30099234da4ab1eb4616d0))\n\n#### `flame_rive` - `v1.3.0`\n\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n - **FEAT**: update to Rive 0.8.4 ([#1542](https://github.com/flame-engine/flame/issues/1542)). ([ac3d4bf6](https://github.com/flame-engine/flame/commit/ac3d4bf61b1386df555de4673e2bb6da1f0edd50))\n\n#### `flame_svg` - `v1.3.0`\n\n - **REFACTOR**: Update and guarantee consistency on mocktail dev dependency version across repo ([#1701](https://github.com/flame-engine/flame/issues/1701)). ([f4a98878](https://github.com/flame-engine/flame/commit/f4a98878062dbd4fe8238a8b014e6be3e528c5d8))\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Optional key for Images.load ([#1624](https://github.com/flame-engine/flame/issues/1624)). ([067c34b5](https://github.com/flame-engine/flame/commit/067c34b5f29e1a9bd51861d872092ae5ee0a551f))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n\n#### `flame_test` - `v1.5.0`\n\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Callbacks in `HudButtonComponent` constructor and `ViewportMargin` mixin to avoid code duplication ([#1685](https://github.com/flame-engine/flame/issues/1685)). ([f55b2e0d](https://github.com/flame-engine/flame/commit/f55b2e0dc01c98718e4871430c6745472c221821))\n - **FEAT**: Aligned text in the TextBoxComponent ([#1620](https://github.com/flame-engine/flame/issues/1620)). ([c64aedae](https://github.com/flame-engine/flame/commit/c64aedaeb3fed908722b8872b71e288ff87bc761))\n - **FEAT**: add options to flutter test ([#1690](https://github.com/flame-engine/flame/issues/1690)). ([5dcf2664](https://github.com/flame-engine/flame/commit/5dcf26642363dd245f541c76d6190f8d523c1acb))\n - **FEAT**: Add helper function for creating golden tests ([#1623](https://github.com/flame-engine/flame/issues/1623)). ([d0faaada](https://github.com/flame-engine/flame/commit/d0faaada2bb971c2dde5a37dfa20d316c532ea28))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n\n#### `flame_tiled` - `v1.5.0`\n\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **FIX**: performance improvements on `SpriteBatch` APIs ([#1637](https://github.com/flame-engine/flame/issues/1637)). ([4b19a1b2](https://github.com/flame-engine/flame/commit/4b19a1b203c5cfca5bb412b91c795fe6a215506e))\n - **FIX**: Avoid leaks when using PictureRecorders ([#1643](https://github.com/flame-engine/flame/issues/1643)). ([d67065e5](https://github.com/flame-engine/flame/commit/d67065e52db453b0f4f190a7aec1bec6bc389e45))\n - **FIX**: Fix tile flips when using canvas.drawAtlas ([#1610](https://github.com/flame-engine/flame/issues/1610)). ([b4ad498f](https://github.com/flame-engine/flame/commit/b4ad498fe5488795deb2c2e098fcde357b448bf0))\n - **FIX**: Add centered anchor point for tile rotation ([#1570](https://github.com/flame-engine/flame/issues/1570)). ([f64d5264](https://github.com/flame-engine/flame/commit/f64d5264abb9e1548d26ae15269654e563cd0ee9))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n\n\n## 2022-05-05\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_bloc` - `v1.4.0`](#flame_bloc---v140)\n\n---\n\n#### `flame_bloc` - `v1.4.0`\n\n - **FEAT**: new flame bloc API (#1538). ([f98970a9](https://github.com/flame-engine/flame/commit/f98970a91f91fe70e4a38834d7b69bfcb438d197))\n - **FEAT**: flame tests can now generate golden tests (#1501). ([316a0b3b](https://github.com/flame-engine/flame/commit/316a0b3bb0996ed20a3b93175102524b38bfa3e2))\n\n\n## 2022-04-12\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame_forge2d` - `v0.11.0`](#flame_forge2d---v0110)\n\nPackages with other changes:\n\n - [`flame` - `v1.1.1`](#flame---v111)\n - [`flame_oxygen` - `v0.1.2`](#flame_oxygen---v012)\n - [`flame_bloc` - `v1.3.0`](#flame_bloc---v130)\n - [`flame_rive` - `v1.2.0`](#flame_rive---v120)\n - [`flame_svg` - `v1.2.0`](#flame_svg---v120)\n - [`flame_test` - `v1.4.0`](#flame_test---v140)\n - [`flame_tiled` - `v1.4.0`](#flame_tiled---v140)\n - [`flame_audio` - `v1.0.2`](#flame_audio---v102)\n - [`flame_flare` - `v1.1.1`](#flame_flare---v111)\n - [`flame_fire_atlas` - `v1.0.2`](#flame_fire_atlas---v102)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_audio` - `v1.0.2`\n - `flame_flare` - `v1.1.1`\n - `flame_fire_atlas` - `v1.0.2`\n\n---\n\n#### `flame_forge2d` - `v0.11.0`\n\n - **FEAT**: Bump forg2d version and have flame_forge2d examples use latest syntax (#1535). ([4f7a12eb](https://github.com/flame-engine/flame/commit/4f7a12eb2c00d370fd093de4af6a3f9f740aa03a))\n - **FEAT**: Added children parameter to Component constructor (#1525). ([f0b31fcf](https://github.com/flame-engine/flame/commit/f0b31fcfc0fc6b0f8f96895ef6a68fd5a30a3159))\n - **DOCS**: Fix flame_forge2d readme links (#1540). ([c51bc6db](https://github.com/flame-engine/flame/commit/c51bc6db5dbd32283a7e441b450e0dc4636891c6))\n - **BREAKING** **FEAT**: Flip gravity in flame_forge2d to be able to mix Forge2D and Flame components (#1506). ([bdb360f1](https://github.com/flame-engine/flame/commit/bdb360f18128f9305baa0e6ca77ee6fcad496bc7))\n\n#### `flame` - `v1.1.1`\n\n - **REFACTOR**: Added classes MoveByEffect and MoveToEffect (#1524). ([2171a119](https://github.com/flame-engine/flame/commit/2171a119378855872f6bece37edc95b3d68f28ae))\n - **FIX**: Invalidate polygon cache on resize (#1529). ([11bf75d0](https://github.com/flame-engine/flame/commit/11bf75d074fe9c0d3e043ce43611a1bb1824dd40))\n - **FIX**: Bug with anchor parameter in Sprite.render() (#1508). ([325df46e](https://github.com/flame-engine/flame/commit/325df46e19ebcd5ac13e3192f4360bacb3de1c37))\n - **FIX**: Make CollisionProspect's a, b have unordered equality (#1519). ([5b2471c8](https://github.com/flame-engine/flame/commit/5b2471c8ae29a1313db3b2c21dee6d4654a0132c))\n - **FEAT**: able to clear all overlays (#1536). ([7b15c9a1](https://github.com/flame-engine/flame/commit/7b15c9a1ca58c19265e65899e27c65146d42788c))\n - **FEAT**: Automatic Isometric Grid scaling (#1468). ([cae8c0ce](https://github.com/flame-engine/flame/commit/cae8c0ceb395416ed86fd644c1dd7790eae127ca))\n - **FEAT**: Added children parameter to Component constructor (#1525). ([f0b31fcf](https://github.com/flame-engine/flame/commit/f0b31fcfc0fc6b0f8f96895ef6a68fd5a30a3159))\n - **FEAT**: Camera's Viewfinder can now be affected by rotation effects (#1527). ([f46cae04](https://github.com/flame-engine/flame/commit/f46cae040e34f6037a9e0a7e259bf22b9dff7acb))\n - **FEAT**: Scale (zoom) effects can now be applied to Viewfinder in CameraComponent (#1514). ([403b6e60](https://github.com/flame-engine/flame/commit/403b6e60433f5e059b81298fd4b39a77957932fb))\n - **FEAT**: adding HasGameRef.mockGameRef (#1520). ([4f389f8b](https://github.com/flame-engine/flame/commit/4f389f8b88b181832316bae551e31a3a70907ee7))\n - **FEAT**: flame tests can now generate golden tests (#1501). ([316a0b3b](https://github.com/flame-engine/flame/commit/316a0b3bb0996ed20a3b93175102524b38bfa3e2))\n\n#### `flame_oxygen` - `v0.1.2`\n\n#### `flame_bloc` - `v1.3.0`\n\n - **FEAT**: flame tests can now generate golden tests (#1501). ([316a0b3b](https://github.com/flame-engine/flame/commit/316a0b3bb0996ed20a3b93175102524b38bfa3e2))\n\n#### `flame_rive` - `v1.2.0`\n\n - **FEAT**: Added children parameter to Component constructor (#1525). ([f0b31fcf](https://github.com/flame-engine/flame/commit/f0b31fcfc0fc6b0f8f96895ef6a68fd5a30a3159))\n\n#### `flame_svg` - `v1.2.0`\n\n - **FEAT**: Added children parameter to Component constructor (#1525). ([f0b31fcf](https://github.com/flame-engine/flame/commit/f0b31fcfc0fc6b0f8f96895ef6a68fd5a30a3159))\n\n#### `flame_test` - `v1.4.0`\n\n - **FEAT**: Added closeToAabb() (#1531). ([f7b6cc69](https://github.com/flame-engine/flame/commit/f7b6cc69abd6af89cafd892c7f2518b9b7bf3fc6))\n - **FEAT**: flame tests can now generate golden tests (#1501). ([316a0b3b](https://github.com/flame-engine/flame/commit/316a0b3bb0996ed20a3b93175102524b38bfa3e2))\n\n#### `flame_tiled` - `v1.4.0`\n\n - **FEAT**: Possibility to create RenderableTiledMap from TiledMap (#1534). ([5ed08333](https://github.com/flame-engine/flame/commit/5ed08333215658b9eaca049f6ba16b6509901bb9))\n - **FEAT**: Added children parameter to Component constructor (#1525). ([f0b31fcf](https://github.com/flame-engine/flame/commit/f0b31fcfc0fc6b0f8f96895ef6a68fd5a30a3159))\n\n\n## 2022-03-30\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_test` - `v1.3.0`](#flame_test---v130)\n\n---\n\n#### `flame_test` - `v1.3.0`\n\n - **FIX**: Fix calculation of AABB for `ShapeHitbox`es (#1481). ([a559d9a1](https://github.com/flame-engine/flame/commit/a559d9a12bfb42e161469745795fb91cdf161f8b))\n - **FEAT**: flame tests can now generate golden tests (#1501). ([316a0b3b](https://github.com/flame-engine/flame/commit/316a0b3bb0996ed20a3b93175102524b38bfa3e2))\n\n\n## 2022-03-28\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n- There are no breaking changes in this release.\n\nPackages with other changes:\n\n- There are no other changes in this release.\n\nPackages graduated to a stable release (see pre-releases prior to the stable version for changelog entries):\n\n- `flame` - `v1.1.0`\n- `flame_audio` - `v1.0.1`\n- `flame_bloc` - `v1.2.0`\n- `flame_fire_atlas` - `v1.0.1`\n- `flame_flare` - `v1.1.0`\n- `flame_forge2d` - `v0.9.0`\n- `flame_oxygen` - `v0.1.1`\n- `flame_rive` - `v1.1.0`\n- `flame_svg` - `v1.1.0`\n- `flame_test` - `v1.2.0`\n- `flame_tiled` - `v1.3.0`\n\n## 2022-03-22\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame_audio` - `v1.0.1-releasecandidate.1`](#flame_audio---v101-releasecandidate1)\n - [`flame_fire_atlas` - `v1.0.1-releasecandidate.1`](#flame_fire_atlas---v101-releasecandidate1)\n - [`flame_flare` - `v1.1.0-releasecandidate.1`](#flame_flare---v110-releasecandidate1)\n - [`flame_oxygen` - `v0.1.1-releasecandidate.1`](#flame_oxygen---v011-releasecandidate1)\n - [`flame` - `v1.1.0-releasecandidate.6`](#flame---v110-releasecandidate6)\n - [`flame_bloc` - `v1.2.0-releasecandidate.6`](#flame_bloc---v120-releasecandidate6)\n - [`flame_forge2d` - `v0.9.0-releasecandidate.6`](#flame_forge2d---v090-releasecandidate6)\n - [`flame_svg` - `v1.1.0-releasecandidate.5`](#flame_svg---v110-releasecandidate5)\n - [`flame_test` - `v1.2.0-releasecandidate.6`](#flame_test---v120-releasecandidate6)\n - [`flame_rive` - `v1.1.0-releasecandidate.6`](#flame_rive---v110-releasecandidate6)\n - [`flame_tiled` - `v1.3.0-releasecandidate.6`](#flame_tiled---v130-releasecandidate6)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_svg` - `v1.1.0-releasecandidate.5`\n - `flame_test` - `v1.2.0-releasecandidate.6`\n - `flame_rive` - `v1.1.0-releasecandidate.6`\n - `flame_tiled` - `v1.3.0-releasecandidate.6`\n\n---\n\n#### `flame_audio` - `v1.0.1-releasecandidate.1`\n\n#### `flame_fire_atlas` - `v1.0.1-releasecandidate.1`\n\n#### `flame_flare` - `v1.1.0-releasecandidate.1`\n\n#### `flame_oxygen` - `v0.1.1-releasecandidate.1`\n\n#### `flame` - `v1.1.0-releasecandidate.6`\n\n - **FIX**: Only end collisions where there was a collision (#1471). ([e1e87fc4](https://github.com/flame-engine/flame/commit/e1e87fc42226c1db2f472377901031277349beb3))\n - **FIX**: `debugMode` should be inherited from parent when mounted (#1469). ([e894d201](https://github.com/flame-engine/flame/commit/e894d20133f6e142c67286c449135e37e892f35b))\n - **FEAT**: Added method that returned descendants (#1461). ([a41f5376](https://github.com/flame-engine/flame/commit/a41f53762ab49bb3d51f1f96c37b934a7ab83844))\n - **FEAT**: Possibility to mark gesture events as handled (#1465). ([4c3960c3](https://github.com/flame-engine/flame/commit/4c3960c3418f8ff4d557c1764c6793468238a8da))\n - **FEAT**: adding loaded future to the component (#1466). ([6434829b](https://github.com/flame-engine/flame/commit/6434829b45cc131719fd950ef2d262d0bfbdff1b))\n - **FEAT**: Deprecating Rect methods (#1455). ([4ddd90aa](https://github.com/flame-engine/flame/commit/4ddd90aafc40a3f5ce3d9b181a66369436de3c9c))\n - **FEAT**: Added .anchor property to CameraComponent.Viewfinder (#1458). ([d51dc5e1](https://github.com/flame-engine/flame/commit/d51dc5e132bc3ba5763be4de36131d3739a6c906))\n - **DOCS**: `Rect` extension docs is out of date (#1451). ([7e505722](https://github.com/flame-engine/flame/commit/7e505722491dd03fea6d2329ff4df2447143d45b))\n\n#### `flame_bloc` - `v1.2.0-releasecandidate.6`\n\n - **FEAT**: Possibility to mark gesture events as handled (#1465). ([4c3960c3](https://github.com/flame-engine/flame/commit/4c3960c3418f8ff4d557c1764c6793468238a8da))\n\n#### `flame_forge2d` - `v0.9.0-releasecandidate.6`\n\n - **FEAT**: updating forge2d version (#1479). ([4678e21a](https://github.com/flame-engine/flame/commit/4678e21a0b714b8344ae2453b1ac6df68adfb4cd))\n - **FEAT**: Possibility to mark gesture events as handled (#1465). ([4c3960c3](https://github.com/flame-engine/flame/commit/4c3960c3418f8ff4d557c1764c6793468238a8da))\n\n\n## 2022-03-13\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.1.0-releasecandidate.5`](#flame---v110-releasecandidate5)\n - [`flame_forge2d` - `v0.9.0-releasecandidate.5`](#flame_forge2d---v090-releasecandidate5)\n - [`flame_svg` - `v1.1.0-releasecandidate.4`](#flame_svg---v110-releasecandidate4)\n - [`flame_test` - `v1.2.0-releasecandidate.5`](#flame_test---v120-releasecandidate5)\n - [`flame_rive` - `v1.1.0-releasecandidate.5`](#flame_rive---v110-releasecandidate5)\n - [`flame_tiled` - `v1.3.0-releasecandidate.5`](#flame_tiled---v130-releasecandidate5)\n - [`flame_bloc` - `v1.2.0-releasecandidate.5`](#flame_bloc---v120-releasecandidate5)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_svg` - `v1.1.0-releasecandidate.4`\n - `flame_test` - `v1.2.0-releasecandidate.5`\n - `flame_rive` - `v1.1.0-releasecandidate.5`\n - `flame_tiled` - `v1.3.0-releasecandidate.5`\n - `flame_bloc` - `v1.2.0-releasecandidate.5`\n\n---\n\n#### `flame` - `v1.1.0-releasecandidate.5`\n\n - **FIX**: `@mustCallSuper` missing on components (#1443). ([e01b4b1a](https://github.com/flame-engine/flame/commit/e01b4b1ac3e423037fa313672b4882e7d29210b8))\n - **FEAT**: Add setter to priority (#1444). ([34284686](https://github.com/flame-engine/flame/commit/342846860af36ed73a1fc0a9a76ed9add12cec71))\n\n#### `flame_forge2d` - `v0.9.0-releasecandidate.5`\n\n - **FEAT**: `BodyComponent` can properly have normal Flame component children (#1442). ([7fe8b6de](https://github.com/flame-engine/flame/commit/7fe8b6deb18b3579fecc99cc44e0ffea73be5f02))\n\n\n## 2022-03-11\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.1.0-releasecandidate.4`](#flame---v110-releasecandidate4)\n - [`flame_forge2d` - `v0.9.0-releasecandidate.4`](#flame_forge2d---v090-releasecandidate4)\n - [`flame_svg` - `v1.1.0-releasecandidate.3`](#flame_svg---v110-releasecandidate3)\n - [`flame_test` - `v1.2.0-releasecandidate.4`](#flame_test---v120-releasecandidate4)\n - [`flame_rive` - `v1.1.0-releasecandidate.4`](#flame_rive---v110-releasecandidate4)\n - [`flame_tiled` - `v1.3.0-releasecandidate.4`](#flame_tiled---v130-releasecandidate4)\n - [`flame_bloc` - `v1.2.0-releasecandidate.4`](#flame_bloc---v120-releasecandidate4)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_svg` - `v1.1.0-releasecandidate.3`\n - `flame_test` - `v1.2.0-releasecandidate.4`\n - `flame_rive` - `v1.1.0-releasecandidate.4`\n - `flame_tiled` - `v1.3.0-releasecandidate.4`\n - `flame_bloc` - `v1.2.0-releasecandidate.4`\n\n---\n\n#### `flame` - `v1.1.0-releasecandidate.4`\n\n - **FIX**: Setting images.prefix to empty string (#1437). ([694102bd](https://github.com/flame-engine/flame/commit/694102bd0304736ed3bdfbd596d64901d7adf57f))\n\n#### `flame_forge2d` - `v0.9.0-releasecandidate.4`\n\n - **FIX**: Don't use debug rendering by default in BodyComponent (#1439). ([33b725e8](https://github.com/flame-engine/flame/commit/33b725e8378d4060e726e99c0452b64f54ef8f67))\n\n\n## 2022-03-10\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - There are no breaking changes in this release.\n\nPackages with other changes:\n\n - [`flame` - `v1.1.0-releasecandidate.3`](#flame---v110-releasecandidate3)\n - [`flame_bloc` - `v1.2.0-releasecandidate.3`](#flame_bloc---v120-releasecandidate3)\n - [`flame_svg` - `v1.1.0-releasecandidate.2`](#flame_svg---v110-releasecandidate2)\n - [`flame_test` - `v1.2.0-releasecandidate.3`](#flame_test---v120-releasecandidate3)\n - [`flame_rive` - `v1.1.0-releasecandidate.3`](#flame_rive---v110-releasecandidate3)\n - [`flame_forge2d` - `v0.9.0-releasecandidate.3`](#flame_forge2d---v090-releasecandidate3)\n - [`flame_tiled` - `v1.3.0-releasecandidate.3`](#flame_tiled---v130-releasecandidate3)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_svg` - `v1.1.0-releasecandidate.2`\n - `flame_test` - `v1.2.0-releasecandidate.3`\n - `flame_rive` - `v1.1.0-releasecandidate.3`\n - `flame_forge2d` - `v0.9.0-releasecandidate.3`\n - `flame_tiled` - `v1.3.0-releasecandidate.3`\n\n---\n\n#### `flame` - `v1.1.0-releasecandidate.3`\n\n - **REFACTOR**: Parent change and component removal logic (#1385). ([8b9fa352](https://github.com/flame-engine/flame/commit/8b9fa3521cc44f7696c5ce0b396e3007c2ae7e8c))\n - **FIX**: viewfinders behavior under zoom (#1432). ([f3cf85b6](https://github.com/flame-engine/flame/commit/f3cf85b638cc71058e85756498e79971a1942491))\n - **FIX**: change strokeWidth in Component (#1431). ([0e174fe8](https://github.com/flame-engine/flame/commit/0e174fe8e5f1262af41c8659c0fce7ed060e69a9))\n - **FEAT**: allowing changing of the images prefix and allowing empty prefixes (#1433). ([de4d9416](https://github.com/flame-engine/flame/commit/de4d941654710add459cc1c923b92c3923556f15))\n\n#### `flame_bloc` - `v1.2.0-releasecandidate.3`\n\n - **REFACTOR**: Parent change and component removal logic (#1385). ([8b9fa352](https://github.com/flame-engine/flame/commit/8b9fa3521cc44f7696c5ce0b396e3007c2ae7e8c))\n\n\n## 2022-03-08\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame_svg` - `v1.1.0-releasecandidate.1`](#flame_svg---v110-releasecandidate1)\n - [`flame` - `v1.1.0-releasecandidate.2`](#flame---v110-releasecandidate2)\n - [`flame_bloc` - `v1.2.0-releasecandidate.2`](#flame_bloc---v120-releasecandidate2)\n - [`flame_test` - `v1.2.0-releasecandidate.2`](#flame_test---v120-releasecandidate2)\n\nPackages with other changes:\n\n - [`flame_forge2d` - `v0.9.0-releasecandidate.2`](#flame_forge2d---v090-releasecandidate2)\n - [`flame_rive` - `v1.1.0-releasecandidate.2`](#flame_rive---v110-releasecandidate2)\n - [`flame_tiled` - `v1.3.0-releasecandidate.2`](#flame_tiled---v130-releasecandidate2)\n\nPackages with dependency updates only:\n\n> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.\n\n - `flame_tiled` - `v1.3.0-releasecandidate.2`\n\n---\n\n#### `flame_svg` - `v1.1.0-releasecandidate.1`\n\n - **FIX**: flame svg perfomance (#1373). ([bce24173](https://github.com/flame-engine/flame/commit/bce2417330b5165842f15d0409a213a1c5ad1cd3))\n - **FIX**: preventing svg rendering from affecting other renderings (#1339). ([6e66baa1](https://github.com/flame-engine/flame/commit/6e66baa12fdad7b27f35ca274433acd55165f106))\n - **FEAT**: adding default constructor on SvgComponent (#1334). ([00619f80](https://github.com/flame-engine/flame/commit/00619f80475b1e66802a6d2020ea6d521c84059d))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n - **BREAKING** **FEAT**: Use a broadphase to make collision detection more efficient (#1252). ([29dd09ca](https://github.com/flame-engine/flame/commit/29dd09ca925e934f3ca4e266a8a0cdb8ad62ef3b))\n\n#### `flame` - `v1.1.0-releasecandidate.2`\n\n - **REFACTOR**: Loadable mixin no longer declares onMount and onRemove (#1243). ([b1f6a34c](https://github.com/flame-engine/flame/commit/b1f6a34c198a732d51471bf0b79a71a4f3e60973))\n - **REFACTOR**: Organize tests in the game/ folder (#1403). ([102a27cc](https://github.com/flame-engine/flame/commit/102a27cc75d15e1c0ec867d739c9ce3f7feaff56))\n - **REFACTOR**: Clean up of top-level tests (#1386). ([e50003ed](https://github.com/flame-engine/flame/commit/e50003ed609fabe4268ceaa1e728b6f29f05939e))\n - **REFACTOR**: Resize logic in GameRenderBox (#1308). ([17c45c28](https://github.com/flame-engine/flame/commit/17c45c28291862ba2c7c079fe91e994a71b1d807))\n - **REFACTOR**: Simplify GameWidgetState.loaderFuture (#1232). ([eb30c2e5](https://github.com/flame-engine/flame/commit/eb30c2e5e9b10e388a9d56283b90e3f09c5f9379))\n - **REFACTOR**: Component.ancestors() is now an iterator (#1242). ([ce48d77a](https://github.com/flame-engine/flame/commit/ce48d77ad72a3c5f865c7ec40b753678a2fbebe4))\n - **REFACTOR**: Add a few more rules to flame_lint, including use_key_in_widget_constructors (#1248). ([bac6c8a4](https://github.com/flame-engine/flame/commit/bac6c8a4469f2c5c2926335f2f589eec9b1a5b5b))\n - **REFACTOR**: Removed parameter Component.updateTree({callOwnUpdate}) (#1224). ([ed227e7c](https://github.com/flame-engine/flame/commit/ed227e7c74bae51061e9622fe6852cf020ce6fa6))\n - **REFACTOR**: Remove Loadable, optional onLoads (#1333). ([05f7a4c3](https://github.com/flame-engine/flame/commit/05f7a4c3d6b1e3b67575c4ec920cf270691bbab4))\n - **REFACTOR**: Loadable no longer declares onGameResize (#1329). ([20776e86](https://github.com/flame-engine/flame/commit/20776e8659bda57813ddc14856502d4b07b85fef))\n - **REFACTOR**: Use canvas.drawImageNine in NineTileBox (#1314). ([d77e5efe](https://github.com/flame-engine/flame/commit/d77e5efee573ddcbc84a50be7728d7a9207287f7))\n - **PERF**: Allow components to have null children (#1231). ([66ad4b08](https://github.com/flame-engine/flame/commit/66ad4b08af6153fb767667a7bed42dac6fb8f2c7))\n - **FIX**: flame svg perfomance (#1373). ([bce24173](https://github.com/flame-engine/flame/commit/bce2417330b5165842f15d0409a213a1c5ad1cd3))\n - **FIX**: Fix collision detection comments and typo (#1422). ([dfeafdd6](https://github.com/flame-engine/flame/commit/dfeafdd6f3e962d6f5148340ab461a9e805652b7))\n - **FIX**: `ParallaxComponent` should have static `positionType` (#1350). ([cfa6bd12](https://github.com/flame-engine/flame/commit/cfa6bd127be620f6016442a269a479d162241a11))\n - **FIX**: Add missing `priority` argument for `JoystickComponent` (#1227). ([23b1dd8b](https://github.com/flame-engine/flame/commit/23b1dd8bbcc98ae3c59a86c10e03b074982b6adc))\n - **FIX**: Step time in SpriteAnimation must be positive (#1387). ([08e8eac1](https://github.com/flame-engine/flame/commit/08e8eac1734a63111a5b7aba4e1bfd20d503aaf4))\n - **FIX**: HudMarginComponent positioning on zoom (#1250). ([4f0fb2de](https://github.com/flame-engine/flame/commit/4f0fb2de6f12ad950705ddb75ebd2e80114321e5))\n - **FIX**: Call onCollisionEnd on removal of Collidable (#1247). ([5ddcc6f7](https://github.com/flame-engine/flame/commit/5ddcc6f7baaae60996747d37b4d92f4f890c7fa2))\n - **FIX**: Both places should have `strictMode = false` (#1272). ([72161ad8](https://github.com/flame-engine/flame/commit/72161ad8d2f2a0916f4448d67644272fdc9ceace))\n - **FIX**: remove vector_math dependency (#1361). ([56b33da2](https://github.com/flame-engine/flame/commit/56b33da29cfe547db75c89d97a09550a324fb0f5))\n - **FIX**: Deprecate pause and resume in GameLoop (#1240). ([dc37053f](https://github.com/flame-engine/flame/commit/dc37053fb6ed46bb68cebab0ed82248051ddf86a))\n - **FIX**: Deprecate Images.decodeImageFromPixels (#1318). ([1a80130c](https://github.com/flame-engine/flame/commit/1a80130c6632cc8b1f34c19aa928ac66364ecbe5))\n - **FIX**: Properly dispose images when cache is cleared (#1312). ([825fb0cc](https://github.com/flame-engine/flame/commit/825fb0cc7e5b30911e17a2075e28f74c8d69b593))\n - **FIX**: Fix SpriteAnimationWidget lifecycle (#1212). ([86394dd3](https://github.com/flame-engine/flame/commit/86394dd3e05079494c7c3c000c3104712faf7507))\n - **FIX**: redrawing bug in TextBoxComponent (#1279). ([8bef4805](https://github.com/flame-engine/flame/commit/8bef480597024f51ac2e4f534e1977f53d768df2))\n - **FIX**: Add missing paint argument to `SpriteComponent.fromImage` (#1294). ([254a60c8](https://github.com/flame-engine/flame/commit/254a60c8475da218a61d2b179d894f469efe5486))\n - **FIX**: black frame when activating overlays (#1093). ([85caf463](https://github.com/flame-engine/flame/commit/85caf463f48ce34fdf51bfd0f511c8188dcf4481))\n - **FIX**: `prepareComponent` should never run again on a prepared component (#1237). ([7d3eeb73](https://github.com/flame-engine/flame/commit/7d3eeb73c588f2465472cd6069f28d6136b0721d))\n - **FIX**: Allow most basic and advanced gesture detectors together (#1208). ([5828b6f3](https://github.com/flame-engine/flame/commit/5828b6f369b74b8f1ab2cc42905c647bbc7dfda5))\n - **FEAT**: Added SpeedEffectController (#1260). ([20f521f5](https://github.com/flame-engine/flame/commit/20f521f5beb5ee476d345d1766a30b4ba35c079b))\n - **FEAT**: Added SineEffectController (#1262). ([c888703d](https://github.com/flame-engine/flame/commit/c888703d6e002fe5f15a82d6204a0639f92aa66a))\n - **FEAT**: Added ZigzagEffectController (#1261). ([59adc5f3](https://github.com/flame-engine/flame/commit/59adc5f34c2eebd336f7d39a703a6845227b55ed))\n - **FEAT**: Add onReleased callback for HudButtonComponent (#1296). ([87ee34ca](https://github.com/flame-engine/flame/commit/87ee34cac72b6d09b8c8f870433541361ff383c1))\n - **FEAT**: Turn off `strictMode` for children (#1271). ([6936e1d9](https://github.com/flame-engine/flame/commit/6936e1d98b63c071787d3dea09fad7659bdf0473))\n - **FEAT**: `onCollisionStart` for `Collidable` and `HitboxShape` (#1251). ([9b95686b](https://github.com/flame-engine/flame/commit/9b95686ba57c16c9f029f920150e112d180bd584))\n - **FEAT**: adding has mounted to component (#1418). ([f8f9e045](https://github.com/flame-engine/flame/commit/f8f9e0451309bfdd29ec8cefbf9d8187209a314c))\n - **FEAT**: Added NoiseEffectController (#1356). ([fad9d1d5](https://github.com/flame-engine/flame/commit/fad9d1d54f4c3500611f82a9382ffa1fed9b52b8))\n - **FEAT**: exporting cache classes (#1368). ([3e058973](https://github.com/flame-engine/flame/commit/3e0589730c49663b5c4863fc28718b3fa81b7b60))\n - **FEAT**: Update scale events to contain pan info (#1327). ([70b96b07](https://github.com/flame-engine/flame/commit/70b96b071a8e936b5c5d6014cb18277b76c646db))\n - **FEAT**: Components are now always added in the correct order (#1337). ([c753fc46](https://github.com/flame-engine/flame/commit/c753fc4636d337d850a5a5cc684be8155f08b214))\n - **FEAT**: Added `transform` to `Rect` (#1360). ([1818be41](https://github.com/flame-engine/flame/commit/1818be41761015b33aee820a0a02f50327a4df4e))\n - **FEAT**: Camera as a component (#1355). ([c61a1c18](https://github.com/flame-engine/flame/commit/c61a1c18b5bdd0b27f3ab21d73d8bbddffd48ba2))\n - **FEAT**: Effect.onComplete callback as an alternative to onFinish() (#1201). ([932a8111](https://github.com/flame-engine/flame/commit/932a81118b0faba80def677cd0db28a598e15204))\n - **FEAT**: Add RandomEffectController (#1203). ([cdb2650b](https://github.com/flame-engine/flame/commit/cdb2650b29bee6e8412a666f9f49fabb68ce0265))\n - **FEAT**: `Component.childrenFactory` can be used to set up a global `ComponentSet` factory (#1193). ([223ab758](https://github.com/flame-engine/flame/commit/223ab75886ab018053cd75af33560a03e1b9d470))\n - **DOCS**: Added documentation for GameLoop class (#1234). ([b1d4e587](https://github.com/flame-engine/flame/commit/b1d4e5872e970f8bd4020a051c35b5cac4093b5e))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n - **BREAKING** **REFACTOR**: Separate ComponentSet from the Component (#1266). ([e2655b88](https://github.com/flame-engine/flame/commit/e2655b8817411ae6b1c505719fed75a170f67aeb))\n - **BREAKING** **FIX**: Remove pointerId from Draggable callbacks (#1313). ([27adda17](https://github.com/flame-engine/flame/commit/27adda17b7b4d8c229cca53799826c7b854eae95))\n - **BREAKING** **FEAT**: Use a broadphase to make collision detection more efficient (#1252). ([29dd09ca](https://github.com/flame-engine/flame/commit/29dd09ca925e934f3ca4e266a8a0cdb8ad62ef3b))\n - **BREAKING** **FEAT**: Added SequenceEffect (#1218). ([7c6ae6de](https://github.com/flame-engine/flame/commit/7c6ae6def36ae5feb813fb2ba15d6fb3b9aaf341))\n\n#### `flame_bloc` - `v1.2.0-releasecandidate.2`\n\n - **REFACTOR**: Remove Loadable, optional onLoads (#1333). ([05f7a4c3](https://github.com/flame-engine/flame/commit/05f7a4c3d6b1e3b67575c4ec920cf270691bbab4))\n - **REFACTOR**: Loadable mixin no longer declares onMount and onRemove (#1243). ([b1f6a34c](https://github.com/flame-engine/flame/commit/b1f6a34c198a732d51471bf0b79a71a4f3e60973))\n - **FIX**: Fix collision detection comments and typo (#1422). ([dfeafdd6](https://github.com/flame-engine/flame/commit/dfeafdd6f3e962d6f5148340ab461a9e805652b7))\n - **FEAT**: adding FlameBloc mixin to allow its usage with enhanced FlameGame classes (#1399). ([78aab426](https://github.com/flame-engine/flame/commit/78aab42694c66c8b9ea749ac11187f1ed1789a4c))\n - **FEAT**: Components are now always added in the correct order (#1337). ([c753fc46](https://github.com/flame-engine/flame/commit/c753fc4636d337d850a5a5cc684be8155f08b214))\n - **FEAT**: Optional Camera argument in FlameBlocGame (#1331). ([bcb27f70](https://github.com/flame-engine/flame/commit/bcb27f706f3afcecfe417e065d6c16a6edb1463f))\n - **FEAT**: publish flame bloc (#1319). ([4d5adcb0](https://github.com/flame-engine/flame/commit/4d5adcb0d01d374ca807c71f2b8d963d0781a976))\n - **DOCS**: Upgrade documentation site (#1365). ([12cf8f70](https://github.com/flame-engine/flame/commit/12cf8f70963dc25b4e12182d0c7d80fe7d5a00e0))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n - **DOCS**: Fix typo in flame_bloc readme (#1332). ([9bff96bf](https://github.com/flame-engine/flame/commit/9bff96bf3a668fc107c0712aadc6b095ebd50788))\n - **BREAKING** **FEAT**: Use a broadphase to make collision detection more efficient (#1252). ([29dd09ca](https://github.com/flame-engine/flame/commit/29dd09ca925e934f3ca4e266a8a0cdb8ad62ef3b))\n - **BREAKING** **FEAT**: updating flame_bloc to bloc 8 (#1311). ([574e0ab5](https://github.com/flame-engine/flame/commit/574e0ab58baa14680cb0d0eded642b4729b062e7))\n\n#### `flame_test` - `v1.2.0-releasecandidate.2`\n\n - **REFACTOR**: Add a few more rules to flame_lint, including use_key_in_widget_constructors (#1248). ([bac6c8a4](https://github.com/flame-engine/flame/commit/bac6c8a4469f2c5c2926335f2f589eec9b1a5b5b))\n - **FIX**: remove vector_math dependency (#1361). ([56b33da2](https://github.com/flame-engine/flame/commit/56b33da29cfe547db75c89d97a09550a324fb0f5))\n - **FEAT**: Components are now always added in the correct order (#1337). ([c753fc46](https://github.com/flame-engine/flame/commit/c753fc4636d337d850a5a5cc684be8155f08b214))\n - **FEAT**: Added parameter repeatCount into function testRandom (#1265). ([49a2d0b9](https://github.com/flame-engine/flame/commit/49a2d0b9ec00fa9067756dd975e8b3ffd19de0bc))\n - **FEAT**: Added closeToVector in flame_test (#1245). ([af45ea6c](https://github.com/flame-engine/flame/commit/af45ea6cc4b5de80ecb27f07b827f55567cf602b))\n - **DOCS**: Upgrade documentation site (#1365). ([12cf8f70](https://github.com/flame-engine/flame/commit/12cf8f70963dc25b4e12182d0c7d80fe7d5a00e0))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n - **BREAKING** **FEAT**: Added SequenceEffect (#1218). ([7c6ae6de](https://github.com/flame-engine/flame/commit/7c6ae6def36ae5feb813fb2ba15d6fb3b9aaf341))\n\n#### `flame_forge2d` - `v0.9.0-releasecandidate.2`\n\n - **FIX**: PositionBodyComponent had an async onMount, without needing (#1424). ([7b0fd20a](https://github.com/flame-engine/flame/commit/7b0fd20a2c6d9f6cac0a88877c793608ab4d14c8))\n - **FEAT**: Make ContactCallback begin end methods optional overrides (#1415). ([29dd1891](https://github.com/flame-engine/flame/commit/29dd1891b6409ed71c54e05272100dbb180d18e7))\n\n#### `flame_rive` - `v1.1.0-releasecandidate.2`\n\n - **REFACTOR**: Remove Loadable, optional onLoads (#1333). ([05f7a4c3](https://github.com/flame-engine/flame/commit/05f7a4c3d6b1e3b67575c4ec920cf270691bbab4))\n - **FEAT**: Components are now always added in the correct order (#1337). ([c753fc46](https://github.com/flame-engine/flame/commit/c753fc4636d337d850a5a5cc684be8155f08b214))\n - **FEAT**: update rive package to 0.8.1 (now support raster graphics) (#1343). ([062962de](https://github.com/flame-engine/flame/commit/062962de087cd2a8107b1ae27472095e72bdf847))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n\n\n## 2022-02-28\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame_forge2d` - `v0.9.0-releasecandidate.1`](#flame_forge2d---v090-releasecandidate1)\n\nPackages with other changes:\n\n - There are no other changes in this release.\n\n---\n\n#### `flame_forge2d` - `v0.9.0-releasecandidate.1`\n\n - **REFACTOR**: Remove Loadable, optional onLoads (#1333). ([05f7a4c3](https://github.com/flame-engine/flame/commit/05f7a4c3d6b1e3b67575c4ec920cf270691bbab4))\n - **REFACTOR**: Add a few more rules to flame_lint, including use_key_in_widget_constructors (#1248). ([bac6c8a4](https://github.com/flame-engine/flame/commit/bac6c8a4469f2c5c2926335f2f589eec9b1a5b5b))\n - **FIX**: Clone input vector before projecting it (#1255). ([d1d6ad4d](https://github.com/flame-engine/flame/commit/d1d6ad4d8c07a5c6895e6120871e336d447ee5a8))\n - **FEAT**: improving generics on position body component (#1397). ([7edbb299](https://github.com/flame-engine/flame/commit/7edbb299855a3926693e846bc1f8e0cbc4272629))\n - **FEAT**: Add missing optional priority to SpriteBodyComponent (#1404). ([a000eb11](https://github.com/flame-engine/flame/commit/a000eb1172ae06ea397d6233c96f2b0ee1f0d93d))\n - **FEAT**: Components are now always added in the correct order (#1337). ([c753fc46](https://github.com/flame-engine/flame/commit/c753fc4636d337d850a5a5cc684be8155f08b214))\n - **FEAT**: Allow to pass a camera to Forge2D Game (#1364). ([9890e9ca](https://github.com/flame-engine/flame/commit/9890e9caada0abc9cd8942b840d72f98853e0cba))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n - **DOCS**: Add Raycast example for flame_forge2d (#1253). ([994f27d5](https://github.com/flame-engine/flame/commit/994f27d54ccfaeb1251dd5e95e566611fc967022))\n - **BREAKING** **FIX**: Remove pointerId from Draggable callbacks (#1313). ([27adda17](https://github.com/flame-engine/flame/commit/27adda17b7b4d8c229cca53799826c7b854eae95))\n\n\n## 2022-02-28\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n - [`flame` - `v1.1.0-releasecandidate.1`](#flame---v110-releasecandidate1)\n - [`flame_test` - `v1.2.0-releasecandidate.1`](#flame_test---v120-releasecandidate1)\n - [`flame_tiled` - `v1.3.0-releasecandidate.1`](#flame_tiled---v130-releasecandidate1)\n\nPackages with other changes:\n\n - [`flame_bloc` - `v1.2.0-releasecandidate.1`](#flame_bloc---v120-releasecandidate1)\n - [`flame_rive` - `v1.1.0-releasecandidate.1`](#flame_rive---v110-releasecandidate1)\n\n---\n\n#### `flame` - `v1.1.0-releasecandidate.1`\n\n - **REFACTOR**: Clean up of top-level tests (#1386). ([e50003ed](https://github.com/flame-engine/flame/commit/e50003ed609fabe4268ceaa1e728b6f29f05939e))\n - **REFACTOR**: Remove Loadable, optional onLoads (#1333). ([05f7a4c3](https://github.com/flame-engine/flame/commit/05f7a4c3d6b1e3b67575c4ec920cf270691bbab4))\n - **REFACTOR**: Loadable no longer declares onGameResize (#1329). ([20776e86](https://github.com/flame-engine/flame/commit/20776e8659bda57813ddc14856502d4b07b85fef))\n - **REFACTOR**: Organize tests in the game/ folder (#1403). ([102a27cc](https://github.com/flame-engine/flame/commit/102a27cc75d15e1c0ec867d739c9ce3f7feaff56))\n - **REFACTOR**: Use canvas.drawImageNine in NineTileBox (#1314). ([d77e5efe](https://github.com/flame-engine/flame/commit/d77e5efee573ddcbc84a50be7728d7a9207287f7))\n - **REFACTOR**: Resize logic in GameRenderBox (#1308). ([17c45c28](https://github.com/flame-engine/flame/commit/17c45c28291862ba2c7c079fe91e994a71b1d807))\n - **REFACTOR**: Loadable mixin no longer declares onMount and onRemove (#1243). ([b1f6a34c](https://github.com/flame-engine/flame/commit/b1f6a34c198a732d51471bf0b79a71a4f3e60973))\n - **REFACTOR**: Removed parameter Component.updateTree({callOwnUpdate}) (#1224). ([ed227e7c](https://github.com/flame-engine/flame/commit/ed227e7c74bae51061e9622fe6852cf020ce6fa6))\n - **REFACTOR**: Add a few more rules to flame_lint, including use_key_in_widget_constructors (#1248). ([bac6c8a4](https://github.com/flame-engine/flame/commit/bac6c8a4469f2c5c2926335f2f589eec9b1a5b5b))\n - **REFACTOR**: Component.ancestors() is now an iterator (#1242). ([ce48d77a](https://github.com/flame-engine/flame/commit/ce48d77ad72a3c5f865c7ec40b753678a2fbebe4))\n - **REFACTOR**: Simplify GameWidgetState.loaderFuture (#1232). ([eb30c2e5](https://github.com/flame-engine/flame/commit/eb30c2e5e9b10e388a9d56283b90e3f09c5f9379))\n - **PERF**: Allow components to have null children (#1231). ([66ad4b08](https://github.com/flame-engine/flame/commit/66ad4b08af6153fb767667a7bed42dac6fb8f2c7))\n - **FIX**: `prepareComponent` should never run again on a prepared component (#1237). ([7d3eeb73](https://github.com/flame-engine/flame/commit/7d3eeb73c588f2465472cd6069f28d6136b0721d))\n - **FIX**: flame svg perfomance (#1373). ([bce24173](https://github.com/flame-engine/flame/commit/bce2417330b5165842f15d0409a213a1c5ad1cd3))\n - **FIX**: Deprecate pause and resume in GameLoop (#1240). ([dc37053f](https://github.com/flame-engine/flame/commit/dc37053fb6ed46bb68cebab0ed82248051ddf86a))\n - **FIX**: Deprecate Images.decodeImageFromPixels (#1318). ([1a80130c](https://github.com/flame-engine/flame/commit/1a80130c6632cc8b1f34c19aa928ac66364ecbe5))\n - **FIX**: Properly dispose images when cache is cleared (#1312). ([825fb0cc](https://github.com/flame-engine/flame/commit/825fb0cc7e5b30911e17a2075e28f74c8d69b593))\n - **FIX**: Add missing paint argument to `SpriteComponent.fromImage` (#1294). ([254a60c8](https://github.com/flame-engine/flame/commit/254a60c8475da218a61d2b179d894f469efe5486))\n - **FIX**: Add missing `priority` argument for `JoystickComponent` (#1227). ([23b1dd8b](https://github.com/flame-engine/flame/commit/23b1dd8bbcc98ae3c59a86c10e03b074982b6adc))\n - **FIX**: remove vector_math dependency (#1361). ([56b33da2](https://github.com/flame-engine/flame/commit/56b33da29cfe547db75c89d97a09550a324fb0f5))\n - **FIX**: redrawing bug in TextBoxComponent (#1279). ([8bef4805](https://github.com/flame-engine/flame/commit/8bef480597024f51ac2e4f534e1977f53d768df2))\n - **FIX**: Fix SpriteAnimationWidget lifecycle (#1212). ([86394dd3](https://github.com/flame-engine/flame/commit/86394dd3e05079494c7c3c000c3104712faf7507))\n - **FIX**: black frame when activating overlays (#1093). ([85caf463](https://github.com/flame-engine/flame/commit/85caf463f48ce34fdf51bfd0f511c8188dcf4481))\n - **FIX**: Call onCollisionEnd on removal of Collidable (#1247). ([5ddcc6f7](https://github.com/flame-engine/flame/commit/5ddcc6f7baaae60996747d37b4d92f4f890c7fa2))\n - **FIX**: HudMarginComponent positioning on zoom (#1250). ([4f0fb2de](https://github.com/flame-engine/flame/commit/4f0fb2de6f12ad950705ddb75ebd2e80114321e5))\n - **FIX**: Both places should have `strictMode = false` (#1272). ([72161ad8](https://github.com/flame-engine/flame/commit/72161ad8d2f2a0916f4448d67644272fdc9ceace))\n - **FIX**: `ParallaxComponent` should have static `positionType` (#1350). ([cfa6bd12](https://github.com/flame-engine/flame/commit/cfa6bd127be620f6016442a269a479d162241a11))\n - **FIX**: Allow most basic and advanced gesture detectors together (#1208). ([5828b6f3](https://github.com/flame-engine/flame/commit/5828b6f369b74b8f1ab2cc42905c647bbc7dfda5))\n - **FIX**: Step time in SpriteAnimation must be positive (#1387). ([08e8eac1](https://github.com/flame-engine/flame/commit/08e8eac1734a63111a5b7aba4e1bfd20d503aaf4))\n - **FEAT**: Update scale events to contain pan info (#1327). ([70b96b07](https://github.com/flame-engine/flame/commit/70b96b071a8e936b5c5d6014cb18277b76c646db))\n - **FEAT**: Add RandomEffectController (#1203). ([cdb2650b](https://github.com/flame-engine/flame/commit/cdb2650b29bee6e8412a666f9f49fabb68ce0265))\n - **FEAT**: Components are now always added in the correct order (#1337). ([c753fc46](https://github.com/flame-engine/flame/commit/c753fc4636d337d850a5a5cc684be8155f08b214))\n - **FEAT**: Effect.onComplete callback as an alternative to onFinish() (#1201). ([932a8111](https://github.com/flame-engine/flame/commit/932a81118b0faba80def677cd0db28a598e15204))\n - **FEAT**: exporting cache classes (#1368). ([3e058973](https://github.com/flame-engine/flame/commit/3e0589730c49663b5c4863fc28718b3fa81b7b60))\n - **FEAT**: Added NoiseEffectController (#1356). ([fad9d1d5](https://github.com/flame-engine/flame/commit/fad9d1d54f4c3500611f82a9382ffa1fed9b52b8))\n - **FEAT**: Added SineEffectController (#1262). ([c888703d](https://github.com/flame-engine/flame/commit/c888703d6e002fe5f15a82d6204a0639f92aa66a))\n - **FEAT**: Added SpeedEffectController (#1260). ([20f521f5](https://github.com/flame-engine/flame/commit/20f521f5beb5ee476d345d1766a30b4ba35c079b))\n - **FEAT**: Added ZigzagEffectController (#1261). ([59adc5f3](https://github.com/flame-engine/flame/commit/59adc5f34c2eebd336f7d39a703a6845227b55ed))\n - **FEAT**: Turn off `strictMode` for children (#1271). ([6936e1d9](https://github.com/flame-engine/flame/commit/6936e1d98b63c071787d3dea09fad7659bdf0473))\n - **FEAT**: `onCollisionStart` for `Collidable` and `HitboxShape` (#1251). ([9b95686b](https://github.com/flame-engine/flame/commit/9b95686ba57c16c9f029f920150e112d180bd584))\n - **FEAT**: `Component.childrenFactory` can be used to set up a global `ComponentSet` factory (#1193). ([223ab758](https://github.com/flame-engine/flame/commit/223ab75886ab018053cd75af33560a03e1b9d470))\n - **FEAT**: Added `transform` to `Rect` (#1360). ([1818be41](https://github.com/flame-engine/flame/commit/1818be41761015b33aee820a0a02f50327a4df4e))\n - **FEAT**: Add onReleased callback for HudButtonComponent (#1296). ([87ee34ca](https://github.com/flame-engine/flame/commit/87ee34cac72b6d09b8c8f870433541361ff383c1))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n - **DOCS**: Added documentation for GameLoop class (#1234). ([b1d4e587](https://github.com/flame-engine/flame/commit/b1d4e5872e970f8bd4020a051c35b5cac4093b5e))\n - **BREAKING** **REFACTOR**: Separate ComponentSet from the Component (#1266). ([e2655b88](https://github.com/flame-engine/flame/commit/e2655b8817411ae6b1c505719fed75a170f67aeb))\n - **BREAKING** **FIX**: Remove pointerId from Draggable callbacks (#1313). ([27adda17](https://github.com/flame-engine/flame/commit/27adda17b7b4d8c229cca53799826c7b854eae95))\n - **BREAKING** **FEAT**: Added SequenceEffect (#1218). ([7c6ae6de](https://github.com/flame-engine/flame/commit/7c6ae6def36ae5feb813fb2ba15d6fb3b9aaf341))\n\n#### `flame_test` - `v1.2.0-releasecandidate.1`\n\n - **REFACTOR**: Add a few more rules to flame_lint, including use_key_in_widget_constructors (#1248). ([bac6c8a4](https://github.com/flame-engine/flame/commit/bac6c8a4469f2c5c2926335f2f589eec9b1a5b5b))\n - **FIX**: remove vector_math dependency (#1361). ([56b33da2](https://github.com/flame-engine/flame/commit/56b33da29cfe547db75c89d97a09550a324fb0f5))\n - **FEAT**: Components are now always added in the correct order (#1337). ([c753fc46](https://github.com/flame-engine/flame/commit/c753fc4636d337d850a5a5cc684be8155f08b214))\n - **FEAT**: Added parameter repeatCount into function testRandom (#1265). ([49a2d0b9](https://github.com/flame-engine/flame/commit/49a2d0b9ec00fa9067756dd975e8b3ffd19de0bc))\n - **FEAT**: Added closeToVector in flame_test (#1245). ([af45ea6c](https://github.com/flame-engine/flame/commit/af45ea6cc4b5de80ecb27f07b827f55567cf602b))\n - **DOCS**: Upgrade documentation site (#1365). ([12cf8f70](https://github.com/flame-engine/flame/commit/12cf8f70963dc25b4e12182d0c7d80fe7d5a00e0))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n - **BREAKING** **FEAT**: Added SequenceEffect (#1218). ([7c6ae6de](https://github.com/flame-engine/flame/commit/7c6ae6def36ae5feb813fb2ba15d6fb3b9aaf341))\n\n#### `flame_tiled` - `v1.3.0-releasecandidate.1`\n\n - **FEAT**: Added getImageLayer to flame_tiled (#1405). ([a037ada5](https://github.com/flame-engine/flame/commit/a037ada5ea18b0d1b0be68f9b3d3cceabc8c3b2b))\n - **FEAT**: modifiable Layer and TileData in RenderableTileMap (#1324). ([b56d5f3c](https://github.com/flame-engine/flame/commit/b56d5f3cd7d87a07ab0063defbf14a56c0cca1a5))\n - **FEAT**: Expose priority for TiledComponent (#1259). ([f6be66ab](https://github.com/flame-engine/flame/commit/f6be66abd5321a8c48f7200d62bd9e35a5aa71ff))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n - **DOCS**: Update flame_tiled readme (#1286). ([ee7298cb](https://github.com/flame-engine/flame/commit/ee7298cbe5cd02825cd7246f86ccb0c3655985a0))\n - **DOCS**: Update contributions to flame_tiled (#1197). ([93b763e1](https://github.com/flame-engine/flame/commit/93b763e1c000b1ebc538e393723aae2a85eb4838))\n - **BREAKING** **FIX**: fix multiple external tilesets (#1344). ([80a483f8](https://github.com/flame-engine/flame/commit/80a483f80df57ce6339f84d03d836efcbbf09579))\n - **BREAKING** **FIX**: Change Tiled batched rendering to batched rendering per layer (#1317). ([30fce398](https://github.com/flame-engine/flame/commit/30fce398dbe397d4368a0f721b20afcd663c489f))\n\n#### `flame_bloc` - `v1.2.0-releasecandidate.1`\n\n#### `flame_rive` - `v1.1.0-releasecandidate.1`\n\n - **REFACTOR**: Remove Loadable, optional onLoads (#1333). ([05f7a4c3](https://github.com/flame-engine/flame/commit/05f7a4c3d6b1e3b67575c4ec920cf270691bbab4))\n - **FEAT**: Components are now always added in the correct order (#1337). ([c753fc46](https://github.com/flame-engine/flame/commit/c753fc4636d337d850a5a5cc684be8155f08b214))\n - **FEAT**: update rive package to 0.8.1 (now support raster graphics) (#1343). ([062962de](https://github.com/flame-engine/flame/commit/062962de087cd2a8107b1ae27472095e72bdf847))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n\n\n## 2022-02-01\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n- [`flame_tiled` - `v1.2.1`](#flame_tiled---v121)\n\nPackages with other changes:\n\n- There are no other changes in this release.\n\n---\n\n#### `flame_tiled` - `v1.2.1`\n\n - **BREAKING** **FIX**: fix multiple external tilesets (#1344).\n\n\n## 2022-01-20\n\n### Changes\n\n---\n\nPackages with breaking changes:\n\n- There are no breaking changes in this release.\n\nPackages with other changes:\n\n- [`flame_tiled` - `v1.2.0`](#flame_tiled---v120)\n\n---\n\n#### `flame_tiled` - `v1.2.0`\n\n - **FEAT**: modifiable Layer and TileData in RenderableTileMap (#1324).\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n- Using welcoming and inclusive language\n- Being respectful of differing viewpoints and experiences\n- Gracefully accepting constructive criticism\n- Focusing on what is best for the community\n- Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n- The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n- Trolling, insulting/derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n- Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting anyone with the Flame staff role at the [Blue Fire\ndiscord](https://discord.gg/pxrBmy4). All complaints will be reviewed and\ninvestigated and will result in a response that is deemed necessary and\nappropriate to the circumstances. The project team is obligated to maintain\nconfidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html]\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\n[https://www.contributor-covenant.org/faq]\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contribution Guidelines\n\n**Note:** If these contribution guidelines are not followed your issue or PR might be closed, so\nplease read these instructions carefully.\n\n\n## Contribution types\n\n\n### Bug Reports\n\n- If you find a bug, please first report it using [Github issues].\n  - First check if there is not already an issue for it; duplicated issues will be closed.\n\n\n### Bug Fix\n\n- If you'd like to submit a fix for a bug, please read the [How To](#how-to-contribute) for how to\n   send a Pull Request.\n- Indicate on the open issue that you are working on fixing the bug and the issue will be assigned\n   to you.\n- Write `Fixes #xxxx` in your PR text, where xxxx is the issue number (if there is one).\n- Include a test that isolates the bug and verifies that it was fixed.\n\n\n### New Features\n\n- If you'd like to add a feature to the library that doesn't already exist, feel free to describe\n   the feature in a new [GitHub issue].\n  - You can also join us on [Discord] to discuss some initials thoughts.\n- If you'd like to implement the new feature, please wait for feedback from the project maintainers\n   before spending too much time writing the code. In some cases, enhancements may not align well\n   with the project's future development direction.\n- Implement the code for the new feature and please read the [How To](#how-to-contribute).\n\n\n### Documentation & Miscellaneous\n\n- If you have suggestions for improvements to the documentation, tutorial or examples (or something\n   else), we would love to hear about it.\n- As always first file a [Github issue].\n- Implement the changes to the documentation, please read the [How To](#how-to-contribute).\n\n\n## How To Contribute\n\n\n### Requirements\n\nFor a contribution to be accepted:\n\n- Follow the [Style Guide] when writing the code;\n- Format the code using `dart format .`;\n- Lint the code with `melos run analyze`;\n- Check that all tests pass: `melos run test`;\n- Documentation should always be updated or added (if applicable);\n- Examples should always be updated or added (if applicable);\n- Tests should always be updated or added (if applicable) -- check the [Test writing guide] for\n  more details;\n- The PR title should start with a [conventional commit] prefix (`feat:`, `fix:` etc).\n\nIf the contribution doesn't meet these criteria, a maintainer will discuss it with you on the issue\nor PR. You can still continue to add more commits to the branch you have sent the Pull Request from\nand it will be automatically reflected in the PR.\n\n\n## Open an issue and fork the repository\n\n- If it is a bigger change or a new feature, first of all\n   [file a bug or feature report][GitHub issue], so that we can discuss what direction to follow.\n- [Fork the project][fork guide] on GitHub.\n- Clone the forked repository to your local development machine\n   (e.g. `git clone git@github.com:<YOUR_GITHUB_USER>/flame.git`).\n\n\n### Environment Setup\n\nFlame is setup to run with the most recent `stable` version of Flutter, so make sure your version\nmatches that:\n\n```shell\nflutter channel stable\n```\n\nAlso, Flame uses [Melos] to manage the project and dependencies.\n\nTo install Melos, run the following command from your terminal:\n\n```shell\nflutter pub global activate melos\n```\n\nNext, at the root of your locally cloned repository bootstrap the projects dependencies:\n\n```shell\nmelos bootstrap\n```\n\nThe bootstrap command locally links all dependencies within the project without having to\nprovide manual [`dependency_overrides`][pubspec doc]. This allows all\nplugins, examples and tests to build from the local clone project. You should only need to run this\ncommand once.\n\n> You do not need to run `flutter pub get` once bootstrap has been completed.\n\n\n#### CSpell\n\nIf you want to run the spellchecker locally, you will have to install\n[cspell](https://github.com/streetsidesoftware/cspell/tree/main/packages/cspell);\nyou can do so using npm or yarn:\n\n```bash\nnpm install -g cspell\n```\n\nThen you can run it using the provided script:\n\n```bash\n./scripts/cspell-run.sh\n```\n\n\n#### Markdown Lint\n\nIf you want to lint the markdown files you have to install\n[markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli) and once that is installed you\ncan run `melos markdown-check` to check if the markdown follows the rules. Some markdown linting\nerrors can be automatically fixed with `melos markdown-fix`.\n\nNote that, sadly, a particularly laborious rule, MD013, [does not provide an auto-fix\noption](https://github.com/DavidAnson/markdownlint/issues/535). However, you can use other tools to\ncircumvent this. For example, the extension [Rewrap](https://stkb.github.io/Rewrap/) for VSCode, when\n[configured with](https://stkb.github.io/Rewrap/configuration/) `rewrap.wrappingColumn=100`, will do\nthe trick for you.\n\n\n### Performing changes\n\n- Create a new local branch from `main` (e.g. `git checkout -b my-new-feature`)\n- Make your changes (try to split them up with one PR per feature/fix).\n- When committing your changes, make sure that each commit message is clear\n (e.g. `git commit -m 'Take in an optional Camera as a parameter to FlameGame'`).\n- Push your new branch to your own fork into the same remote branch\n (e.g. `git push origin my-username.my-new-feature`, replace `origin` if you use another remote.)\n\n\n### Breaking changes\n\nWhen doing breaking changes a deprecation tag should be added when possible and contain a message\nthat conveys to the user what which version that the deprecated method/field will be removed in and\nwhat method they should use instead to perform the task. The version specified should be at least\ntwo versions after the current one, such that there will be at least one stable release where the\nusers get to see the deprecation warning and in the version after that (or a later version) the\ndeprecated entity should be removed.\n\nExample (if the current version is v1.3.0):\n\n```dart\n@Deprecated('Will be removed in v1.5.0, use nonDeprecatedFeature() instead')\nvoid deprecatedFeature() {}\n```\n\n\n### Open a pull request\n\nGo to the [pull request page of Flame][PRs] and in the top\nof the page it will ask you if you want to open a pull request from your newly created branch.\n\nThe title of the pull request should start with a [conventional commit] type.\n\nAllowed types are:\n\n- `fix:` -- patches a bug and is not a new feature;\n- `feat:` -- introduces a new feature;\n- `docs:` -- updates or adds documentation or examples;\n- `test:` -- updates or adds tests;\n- `refactor:` -- refactors code but doesn't introduce any changes or additions to the public API;\n- `perf:` -- code change that improves performance;\n- `build:` -- code change that affects the build system or external dependencies;\n- `ci:` -- changes to the CI configuration files and scripts;\n- `chore:` -- other changes that don't modify source or test files;\n- `revert:` -- reverts a previous commit.\n\nIf you introduce a **breaking change** the conventional commit type MUST end with an exclamation\nmark (e.g. `feat!: Remove the position argument from PositionComponent`).\n\nThe sentence of the commit (after the `:`) should start with a verb in the present tense; as a rule\nof thumb, think that the commit message will complete the sentence \"This commit will ...\".\nFor example, \"Add support for ...\" or \"Fix bug with ...\".\n\nExamples of PR titles:\n\n- feat: Component.childrenFactory can be used to set up a global ComponentSet factory\n- fix: Avoid infinite loop in `FlameGame`\n- docs: Add a `JoystickComponent` example\n- docs: Improve the Mandarin README\n- test: Add infinity test for `MoveEffect.to`\n- refactor: Optimize the structure of the game loop\n\n\n## Maintainers\n\nThese instructions are for the maintainers of Flame.\n\n\n### Merging a pull request\n\nWhen merging a pull request, make sure that the title of the merge commit has the correct\nconventional commit tag and a descriptive title. This is extra important since sometimes the title\nof the PR doesn't reflect what GitHub defaults to for the merge commit title (if the title has been\nchanged during the life time of the PR for example).\n\nAll the default text should be removed from the commit message and the PR description and the\ninstructions from the \"Migration instruction\" (if the PR is breaking) should be copied into the\ncommit message.\n\n\n### Creating a release\n\nThere are a few things to think about when doing a release:\n\n- Search through the codebase for `@Deprecated` methods/fields and remove the ones that are marked\n   for removal in the version that you are intending to release.\n- Create a PR containing the changes for removing the deprecated entities.\n- Run `melos version -V <package1>:<version> -V <package2>:<version>` for Melos to generate\n   `CHANGELOG.md` files.\n- Go through the PRs with breaking changes and add migration documentation to the changelog.\n   There should be migration docs on each PR, if they haven't been copied to the commit message.\n- Run `melos publish` to make sure that there aren't any problems with any of the packages and make\n   sure that all the versions are correct.\n- Once you are satisfied with the result of the dry run, run `melos publish --no-dry-run`\n- Create a PR containing the updated changelog and `pubspec.yaml` files.\n\n\n[GitHub issue]: https://github.com/flame-engine/flame/issues\n[GitHub issues]: https://github.com/flame-engine/flame/issues\n[PRs]: https://github.com/flame-engine/flame/pulls\n[fork guide]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects\n[Discord]: https://discord.com/invite/pxrBmy4\n[Melos]: https://github.com/invertase/melos\n[pubspec doc]: https://dart.dev/tools/pub/pubspec\n[conventional commit]: https://www.conventionalcommits.org\n[style guide]: https://docs.flame-engine.org/main/development/style_guide\n[test writing guide]: https://docs.flame-engine.org/main/development/testing_guide\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"right\">\n  <a href=\"https://docs.flutter.dev/packages-and-plugins/favorites\">\n    <img alt=\"Flutter favorite\" width=\"100px\" src=\"https://github.com/flame-engine/flame/assets/744771/aa5d5acd-e82b-48bc-ad81-2ab146d72ecb\">\n  </a>\n</p>\n\n<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nA Flutter-based game engine.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame\"><img src=\"https://img.shields.io/pub/v/flame.svg?style=popout\"/></a>\n  <a title=\"Shorebird CI\" href=\"https://console.shorebird.dev/ci\"><img src=\"https://api.shorebird.dev/api/v1/github/flame-engine/flame/badge.svg\"/></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n## Documentation\n\nThe full documentation for Flame can be found on\n[docs.flame-engine.org](https://docs.flame-engine.org/).\n\nTo change the version of the documentation, use the version selector noted with `version:` in the\ntop of the page.\n\n**Note**: The documentation that resides in the main branch is newer than the released documentation\non the docs website.\n\nOther useful links:\n\n- [The official Flame site](https://flame-engine.org/).\n- [Examples](https://examples.flame-engine.org/) of most features which can be tried out from your\n  browser.\n  - To access the code for each example, press the `< >` button in the top right corner.\n- [Tutorials](https://docs.flame-engine.org/main/tutorials/tutorials.html) - Some simple tutorials\n  to get started.\n- [API Reference](https://pub.dev/documentation/flame/latest/) - The generated dartdoc API\n  reference.\n- [awesome-flame](https://github.com/flame-engine/awesome-flame) - A curated list of Tutorials,\n  Games, Libraries and Articles.\n\n\n## Help\n\nThere is a Flame community on [Blue Fire's Discord server](https://discord.gg/5unKpdQD78) where you\ncan ask any of your Flame related questions.\n\nIf you are more comfortable with StackOverflow, you can also create a question there. Add the\n[Flame tag](https://stackoverflow.com/questions/tagged/flame), to make sure that anyone following\nthe tag can help out.\n\n\n## Features\n\nThe goal of the Flame Engine is to provide a complete set of out-of-the-way solutions for common\nproblems that games developed with Flutter might share.\n\nSome of the key features provided are:\n\n- A game loop.\n- A component/object system (FCS).\n- Effects and particles.\n- Collision detection.\n- Gesture and input handling.\n- Images, animations, sprites, and sprite sheets.\n- General utilities to make development easier.\n\nOn top of those features, you can augment Flame with bridge packages. Through these libraries,\nyou will be able to access bindings to other packages, including custom Flame components and\nhelpers, in order to make integrations seamless.\n\nFlame officially provides bridge libraries to the following packages:\n\n- [flame_audio][flame_audio] for [AudioPlayers][audioplayers]: Play multiple audio files\nsimultaneously.\n- [flame_bloc][flame_bloc] for [Bloc][bloc]: A predictable state management library.\n- [flame_fire_atlas][flame_fire_atlas] for [FireAtlas][fireatlas]: Create texture atlases for games.\n- [flame_forge2d][flame_forge2d] for [Forge2D][forge2d]: A Box2D physics engine.\n- [flame_isolate][flame_isolate] - Makes it easy to use [Flutter Isolates][flutter_isolates] in\na Flame game.\n- [flame_lint][flame_lint] - Our set of linting (`analysis_options.yaml`) rules.\n- [flame_lottie][flame_lottie] - Support for [Lottie][lottie] animation in Flame.\n- [flame_network_assets][flame_network_assets] - Helpers to load game assets from\nnetwork.\n- [flame_oxygen][flame_oxygen] for [Oxygen][oxygen]: A lightweight Entity Component System (ECS)\nframework.\n- [flame_rive][flame_rive] for [Rive][rive]: Create interactive animations.\n- [flame_svg][flame_svg] for [flutter_svg][flutter_svg]: Draw SVG files in Flutter.\n- [flame_texturepacker][flame_texturepacker]: Load and use sprite sheets generated with [TexturePacker][texturepacker]\n- [flame_tiled][flame_tiled] for [Tiled][tiled]: 2D tile map level editor.\n\n\n## Sponsors\n\nThe Flame Engine's top sponsors:\n\n[![Invertase](./media/invertase.jpeg)](https://invertase.io/)\n\nDo you or your company want to sponsor Flame?\nCheck out our [OpenCollective page](https://opencollective.com/blue-fire), which is also mentioned\nin the section below, or contact us on [Discord](https://discord.gg/pxrBmy4).\n\n\n## Support\n\nThe simplest way to show us your support is by giving the project a star! :star:\n\nYou can also support us monetarily by donating through OpenCollective:\n\n<a href=\"https://opencollective.com/blue-fire/donate\" target=\"_blank\">\n  <img src=\"https://opencollective.com/blue-fire/donate/button@2x.png?color=blue\" width=200 />\n</a>\n\nThrough GitHub Sponsors:\n\n<a href=\"https://github.com/sponsors/bluefireteam\" target=\"_blank\">\n  <img\n    src=\"https://img.shields.io/badge/Github%20Sponsor-blue?style=for-the-badge&logo=github&logoColor=white\"\n    width=200\n  />\n</a>\n\nOr by becoming a patron on Patreon:\n\n<a href=\"https://www.patreon.com/bluefireoss\" target=\"_blank\">\n  <img src=\"https://c5.patreon.com/external/logo/become_a_patron_button.png\" width=200 />\n</a>\n\nYou can also show on your repository that your game is made with Flame by using one of the following\nbadges:\n\n[![Powered by Flame](https://img.shields.io/badge/Powered%20by-%F0%9F%94%A5-272727.svg)](https://flame-engine.org)\n[![Powered by Flame](https://img.shields.io/badge/Powered%20by-%F0%9F%94%A5-272727.svg?style=flat-square)](https://flame-engine.org)\n[![Powered by Flame](https://img.shields.io/badge/Powered%20by-%F0%9F%94%A5-272727.svg?style=for-the-badge)](https://flame-engine.org)\n\n```txt\n[![Powered by Flame](https://img.shields.io/badge/Powered%20by-%F0%9F%94%A5-272727.svg)](https://flame-engine.org)\n[![Powered by Flame](https://img.shields.io/badge/Powered%20by-%F0%9F%94%A5-272727.svg?style=flat-square)](https://flame-engine.org)\n[![Powered by Flame](https://img.shields.io/badge/Powered%20by-%F0%9F%94%A5-272727.svg?style=for-the-badge)](https://flame-engine.org)\n```\n\n\n## Contributing\n\nHave you found a bug or have a suggestion of how to enhance Flame? Open an issue and we will take a\nlook at it as soon as possible.\n\nDo you want to contribute with a PR? PRs are always welcome, just make sure to create it from the\ncorrect branch (main) and follow the [checklist](.github/pull_request_template.md) which will\nappear when you open the PR.\n\nAlso, before you start, make sure to read our [Contributing Guide](CONTRIBUTING.md).\n\nFor bigger changes, or if in doubt, make sure to talk about your contribution to the team. Either\nvia an issue, GitHub discussion, or reach out to the team either using the\n[Discord server](https://discord.gg/pxrBmy4).\n\n\n## Credits\n\n- The [Blue Fire team](https://github.com/orgs/bluefireteam/people), who are continuously\n  working on maintaining and improving Flame and its ecosystem.\n- All the friendly contributors and people who are helping out in the community.\n\n[flame_audio]: https://github.com/flame-engine/flame/tree/main/packages/flame_audio\n[audioplayers]: https://github.com/bluefireteam/audioplayers\n[flame_bloc]: https://github.com/flame-engine/flame/tree/main/packages/flame_bloc\n[bloc]: https://github.com/felangel/bloc\n[flame_fire_atlas]: https://github.com/flame-engine/flame/tree/main/packages/flame_fire_atlas\n[fireatlas]: https://github.com/flame-engine/fire-atlas\n[flame_forge2d]: https://github.com/flame-engine/flame/tree/main/packages/flame_forge2d\n[forge2d]: https://github.com/flame-engine/forge2d\n[flame_isolate]: https://github.com/flame-engine/flame/tree/main/packages/flame_isolate\n[flutter_isolates]: https://api.flutter.dev/flutter/dart-isolate/Isolate-class.html\n[flame_lint]: https://github.com/flame-engine/flame/tree/main/packages/flame_lint\n[flame_lottie]: https://github.com/flame-engine/flame/tree/main/packages/flame_lottie\n[lottie]: https://airbnb.design/lottie/\n[flame_network_assets]: https://github.com/flame-engine/flame/tree/main/packages/flame_network_assets\n[flame_oxygen]: https://github.com/flame-engine/flame/tree/main/packages/flame_oxygen\n[oxygen]: https://github.com/flame-engine/oxygen\n[flame_rive]: https://github.com/flame-engine/flame/tree/main/packages/flame_rive\n[rive]: https://rive.app/\n[flame_svg]: https://github.com/flame-engine/flame/tree/main/packages/flame_svg\n[flutter_svg]: https://github.com/dnfield/flutter_svg\n[flame_texturepacker]: https://github.com/flame-engine/flame/tree/main/packages/flame_texturepacker\n[texturepacker]: https://www.codeandweb.com/texturepacker\n[flame_tiled]: https://github.com/flame-engine/flame/tree/main/packages/flame_tiled\n[tiled]: https://www.mapeditor.org/\n"
  },
  {
    "path": "doc/README.md",
    "content": "# Getting Started\n\n\n## About Flame\n\nFlame is a modular Flutter game engine that provides a complete set of out-of-the-way solutions for\ngames. It takes advantage of the powerful infrastructure provided by Flutter but simplifies the code\nyou need to build your projects.\n\nIt provides you with a simple yet effective game loop implementation, and the necessary\nfunctionalities that you might need in a game. For instance; input, images, sprites, sprite sheets,\nanimations, collision detection, and a component system that we call Flame Component System (FCS for\nshort).\n\nWe also provide stand-alone packages that extend the Flame functionality which can be found in the\n[Bridge Packages](bridge_packages/bridge_packages.md) section.\n\nYou can pick and choose whichever parts you want, as they are all independent and modular.\n\nThe engine and its ecosystem are constantly being improved by the community, so please feel free to\nreach out, open issues and PRs as well as make suggestions.\n\nGive us a star if you want to help give the engine exposure and grow the community. :)\n\n\n## Installation\n\nAdd the `flame` package as a dependency in your `pubspec.yaml` by running the following command:\n\n```console\nflutter pub add flame\n```\n\nThe latest version can be found on [pub.dev](https://pub.dev/packages/flame/install).\n\nthen run `flutter pub get` and you are ready to start using it!\n\n\n## Getting started\n\nThere is a set of tutorials that you can follow to get started in the\n[tutorials folder](https://github.com/flame-engine/flame/tree/main/doc/tutorials).\n\nSimple examples for all features can be found in the\n[examples folder](https://github.com/flame-engine/flame/tree/main/examples).\n\nTo run Flame you need use the `GameWidget`, which is just another widget that can live anywhere in\nyour widget tree. You can use it as the root widget of your app, or as a child of another widget.\n\nHere is a simple example of how to use the `GameWidget`:\n\n```dart\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(\n    GameWidget(\n      game: FlameGame(),\n    ),\n  );\n}\n```\n\nIn Flame we provide a concept called the Flame Component System (FCS), which is a way to organize\nyour game objects in a way that makes it easy to manage them. You can read more about it in the\n[Components](flame/components/components.md) section.\n\nWhen you want to start a new game you either have to extend the `FlameGame` class or the `World`\nclass. The `FlameGame` is the root of your game and is responsible for managing the game loop and\nthe components. The `World` class is a component that can be used to create a world in your game.\n\nSo to create a simple game you can do something like this:\n\n```dart\nimport 'package:flame/game.dart';\nimport 'package:flame/components.dart';\nimport 'package:flutter/widgets.dart';\n\nvoid main() {\n  runApp(\n    GameWidget(\n      game: FlameGame(world: MyWorld()),\n    ),\n  );\n}\n\nclass MyWorld extends World {\n  @override\n  Future<void> onLoad() async {\n    add(Player(position: Vector2(0, 0)));\n  }\n}\n```\n\nAs you can see, we have created a `MyWorld` class that extends the `World` class. We have overridden\nthe `onLoad` method to add a `Player` component (which doesn't exist yet) to the world. In the\n`FlameGame` class we by default have a `camera` that is watching the world, and by default it is\nlooking at the (0, 0) position of the world in the center of the screen, to learn more about the\ncamera and the world you can read the [Camera Component](flame/camera.md) section.\n\nThe `Player` component can be whatever type of component that you want, to get started we recommend\nto use the `SpriteComponent` class, which is a component that can render a sprite (image) on the\nscreen.\n\nFor example something like this:\n\n```dart\nimport 'package:flame/components.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/extensions.dart';\n\nclass Player extends SpriteComponent {\n  Player({super.position}) :\n    super(size: Vector2.all(200), anchor: Anchor.center);\n\n  @override\n  Future<void> onLoad() async {\n    sprite = await Sprite.load('player.png');\n  }\n}\n```\n\nIn this example, we have created a `Player` class that extends the `SpriteComponent` class. We have\noverridden the `onLoad` method to set the sprite of the component to a sprite that we load from an\nimage file called `player.png`. The image has to be in the `assets/images` directory in your project\n(see the [Assets Directory Structure](flame/structure.md)) and you have to add it to the\n[assets section](https://docs.flutter.dev/ui/assets/assets-and-images) of your `pubspec.yaml` file.\nIn this class we also set the size of the component to 200x200 and the [anchor](flame/components/position_component.md#anchor)\nto the center of the component by sending them to the `super` constructor. We also let the user of\nthe `Player` class set the position of the component when creating it\n(`Player(position: Vector2(0, 0))`).\n\nTo handle input on a component you can add any of our [input mixins](flame/inputs/inputs.md) to the\ncomponent. For example, if you want to handle tap input you can add the `TapCallbacks` mixin to the\nplayer component, and receive tap events within the bounds of the player component. Or if you want\nto handle tap input on the whole world you can add the `TapCallbacks` mixin to the extended `World`\nclass.\n\nThe following example handles taps on the player component, and when the player component is\ntapped the size of the player will increase by 50 pixels in both width and height.\n\n```dart\nimport 'package:flame/components.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/extensions.dart';\n\nclass Player extends SpriteComponent with TapCallbacks {\n  Player({super.position}) :\n    super(size: Vector2.all(200), anchor: Anchor.center);\n\n  @override\n  Future<void> onLoad() async {\n    sprite = await Sprite.load('player.png');\n  }\n  \n  @override\n  void onTapUp(TapUpEvent info) {\n    size += Vector2.all(50);\n  }\n}\n```\n\nThis is just a simple example of how to get started with Flame, there are many more features that you\ncan use (and probably need) to create your game, but this should give you a good starting point.\n\nYou can also check out the [awesome flame repository](https://github.com/flame-engine/awesome-flame#user-content-articles--tutorials),\nit contains quite a lot of good tutorials and articles written by the community to get you started\nwith Flame.\n\n\n## Outside of the scope of the engine\n\nGames sometimes require complex feature sets depending on what the game is all about. Some of these\nfeature sets are outside of the scope of the Flame Engine ecosystem, in this section you can find\nthem, and also some recommendations of packages/services that can be used:\n\n\n### Multiplayer (netcode)\n\nFlame doesn't bundle any network feature, which may be needed to write online multiplayer games.\n\nIf you are building a multiplayer game, here are some recommendations of packages/services:\n\n- [Nakama](https://github.com/obrunsmann/flutter_nakama/): An open-source server designed\n to power modern games and apps.\n- [Firebase](https://firebase.google.com/): Provides dozens of services that can be used to write\nsimpler multiplayer experiences.\n- [Supabase](https://supabase.com/): A cheaper alternative to Firebase, based on Postgres.\n"
  },
  {
    "path": "doc/_sphinx/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n\n#\n# To build the documentation you need to have Python3 installed\n# and working with pip, as well as Flutter of course. Then run\n# the following:\n#\n#     dart pub global activate melos\n#     dart pub global activate dartdoc_json\n#     pip install sphinx sphinxcontrib sphinxcontrib-mermaid sphinxcontrib-jquery myst_parser sphinx-autobuild sphinx-copybutton\n#\n\n# You can set these variables from the command line.\n# SPHINXOPTS and SPHINXBUILD can also be set from the environment.\nSOURCEDIR     = $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))/..\nSPHINXOPTS    ?= -c \"${SOURCEDIR}/_sphinx\"\nSPHINXBUILD   ?= sphinx-build\nBUILDDIR      = \"${SOURCEDIR}/_build\"\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\nlivehtml:\n\tsphinx-autobuild $(SOURCEDIR) $(BUILDDIR)/html $(SPHINXOPTS) $(O) --ignore \"**/.*\" --ignore \"*build*\" --open-browser\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "doc/_sphinx/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\n#\n# This file only contains a selection of the most common options. For a full\n# list see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\nimport docutils\nimport docutils.nodes\nimport html\nimport os\nimport sys\nsys.path.insert(0, os.path.abspath('.'))\n\nroot_dir = os.path.abspath(\n    os.path.join(os.path.dirname(__file__), '..', '..')\n)\n\n# -- Project information -----------------------------------------------------\n\nproject = 'Flame'\ncopyright = '2021, Blue Fire Team'\nauthor = 'Blue Fire Team'\nroot_doc = \"index\"\n\n\n# -- General configuration ---------------------------------------------------\n\nprimary_domain = 'dart'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    'myst_parser',  # Markdown support\n    'sphinxcontrib.mermaid',\n    'extensions.dart_domain',\n    'extensions.flutter_app',\n    'extensions.package',\n    'extensions.yarn_lexer',\n    'sphinxcontrib.jquery',\n    'sphinx_copybutton'\n]\n\n# Configuration options for MyST:\n# https://myst-parser.readthedocs.io/en/latest/syntax/optional.html\nmyst_enable_extensions = [\n    'attrs_inline',\n    'colon_fence',\n    'deflist',\n    'dollarmath',\n    'html_admonition',\n    'html_image',\n    'replacements',\n    'smartquotes',\n    'strikethrough',\n    'substitution',\n    'tasklist',\n]\n\n# Auto-generate link anchors for headers at levels H1 and H2\nmyst_heading_anchors = 4\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This pattern also affects html_static_path and html_extra_path.\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-the-linkcheck-builder\nlinkcheck_ignore = [\n    r'https://examples.flame-engine.org/#/.*',\n    r'https://pub.dev/documentation/flame/--VERSION--/',\n]\n\n# -- Options for dartdoc extension -------------------------------------------\ndartdoc_roots = {\n    'flame': os.path.join(root_dir, 'packages/flame/lib'),\n    'jenny': os.path.join(root_dir, 'packages/flame_jenny/jenny/lib'),\n}\n\n# -- Options for HTML output -------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.\nhtml_theme = \"flames\"\nhtml_theme_options = {}\nhtml_title = \"Flame\"\nhtml_logo = \"images/logo_flame.png\"\nhtml_favicon = \"images/favicon.ico\"\n\n# Style for syntax highlighting\npygments_style = 'monokai'\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['images', 'scripts', 'theme']\nhtml_js_files = ['versions.js', 'menu-expand.js']\nhtml_css_files = [\"copy-button.css\"]\n\n\n# -- Custom setup ------------------------------------------------------------\nclass TitleCollector(docutils.nodes.SparseNodeVisitor):\n    def __init__(self, document):\n        self.level = 0\n        self.titles = []\n        super().__init__(document)\n\n    def visit_section(self, node):\n        title_class = docutils.nodes.title\n        self.level += 1\n        if node.children and isinstance(node.children[0], title_class):\n            title = node.children[0].astext()\n            node_id = node.get(\"ids\")[0]\n            self.titles.append([title, node_id, self.level])\n\n    def depart_section(self, node):\n        self.level -= 1\n\n\ndef get_local_toc(document):\n    if not document:\n        return \"\"\n    visitor = TitleCollector(document)\n    document.walkabout(visitor)\n    titles = visitor.titles\n    if not titles:\n        return \"\"\n\n    levels = sorted(set(item[2] for item in titles))\n    if levels.index(titles[0][2]) != 0:\n        return document.reporter.error(\n            \"First title on the page is not <h1/>\")\n    del titles[0]  # remove the <h1> title\n\n    html_text = \"<div id='toc-local' class='list-group'>\\n\"\n    html_text += \" <div class='header'><i class='fa fa-list'></i> Contents</div>\\n\"\n    for title, node_id, level in titles:\n        if level <= 1:\n            return document.reporter.error(\"More than one <h1> title on the page\")\n        html_text += f\"  <a href='#{node_id}' class='list-group-item level-{level-1}'>\" \\\n                     f\"{html.escape(title)}</a>\\n\"\n    html_text += \"</div>\\n\"\n    return html_text\n\n\n# Emitted when the HTML builder has created a context dictionary to render\n# a template with – this can be used to add custom elements to the context.\ndef on_html_page_context(app, pagename, templatename, context, doctree):\n    context[\"get_local_toc\"] = lambda: get_local_toc(doctree)\n\n\ndef setup(app):\n    this_dir = os.path.dirname(__file__)\n    theme_dir = os.path.abspath(os.path.join(this_dir, 'theme'))\n    app.add_css_file('flames.css')\n    app.add_html_theme('flames', theme_dir)\n    app.connect(\"html-page-context\", on_html_page_context)\n"
  },
  {
    "path": "doc/_sphinx/extensions/dart_domain.css",
    "content": "\n.dartdoc .signature.sig1 {\n  background: black;\n  background: linear-gradient(0deg, #000 0%, #000000bb 65%, #00000088 100%);\n  font-family: var(--font-mono);\n  margin: 8pt -30pt;\n  padding: 12pt 30pt;\n}\n\n.dartdoc .signature.sig2 {\n  border-radius: 6pt;\n  box-shadow: 1px 1px 3px rgb(0 0 0 / 50%);\n  font-family: var(--font-mono);\n  margin-bottom: 6pt;\n  padding: 6pt 6pt 6pt 30pt;\n  text-indent: -21pt;\n}\n\n.dartdoc .signature .keyword { opacity: 0.5; }\n.dartdoc .signature .name { color: white; }\n.dartdoc .sig1 .name { font-weight: bold; }\n.dartdoc .signature .argument { white-space: pre; }\n.dartdoc .signature .argument .name { color: #999; }\n.dartdoc .signature .punct { color: #ffd68d; }\n.dartdoc .signature .types { margin-left: 2pt; }\n.dartdoc .signature .modifier { margin-left: 40pt; }\n.dartdoc .doc2 { margin-left: 30pt; }\n\n.dartdoc .constructor { margin-bottom: 12pt; }\n.dartdoc .constructor .signature { background: rgb(59 116 30 / 25%); }\n.dartdoc .method .signature { background: rgb(69 102 167 / 25%); }\n.dartdoc .field .signature,\n.dartdoc .getter .signature,\n.dartdoc .setter .signature { background: rgb(63 92 86 / 25%); }\n\n.dartdoc span.param {\n  background: #111;\n  border: 1px solid rgb(170 204 221 / 50%);\n  border-radius: 3pt;\n  color: #a6c3de;\n  font-family: var(--font-mono);\n  font-size: 85%;\n  padding: 1pt 3pt;\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "doc/_sphinx/extensions/dart_domain.py",
    "content": "import json\nimport os\nimport re\nimport subprocess\nimport tempfile\nfrom typing import List, Tuple, Dict, Optional, Set, Any\n\nfrom docutils import nodes\nfrom docutils.nodes import Element\nfrom docutils.parsers.rst import directives\nfrom docutils.parsers.rst.states import Inliner\nfrom sphinx.addnodes import pending_xref\nfrom sphinx.application import Sphinx\nfrom sphinx.builders import Builder\nfrom sphinx.domains import Domain\nfrom sphinx.environment import BuildEnvironment\nfrom sphinx.roles import XRefRole\nfrom sphinx.util.docutils import SphinxDirective\nfrom sphinx.util.fileutil import copy_asset_file\nfrom sphinx.util.nodes import make_refnode\n\n\nclass DartdocDirective(SphinxDirective):\n    \"\"\"\n    Directive to add auto-generated documentation for a particular symbol in\n    a Dart file. The symbol could be a class, mixin, function, etc.\n\n    Example of usage (in a markdown file):\n\n        # Component\n        ```{dartdoc}\n        :file: src/components/core/component.dart\n        :symbol: Component\n        :package: flame\n\n        [Link1]: url1\n        ...\n        [LinkN]: urlN\n        ```\n\n    We recommend documenting only one such symbol per page; however, it is\n    possible to add extra content on the page after the {dartdoc} directive.\n    Such content may include additional examples, see-also section, etc.\n    \"\"\"\n    has_content = True\n    required_arguments = 0\n    optional_arguments = 0\n    option_spec = {\n        \"file\": directives.unchanged_required,\n        \"symbol\": directives.unchanged_required,\n        \"package\": directives.unchanged,\n    }\n\n    def __init__(self, name, arguments, options, content, lineno, content_offset, block_text, state,\n                 state_machine):\n        super().__init__(name, arguments, options, content, lineno, content_offset, block_text,\n                         state, state_machine)\n        self.package: str = ''\n        self.root = None\n        self.source_file = None\n        self.symbol = None\n        self.record = None\n        # Explicit reference targets provided to the directive within its content. These are\n        # references in double-square brackets.\n        self.links: Dict[str, str] = {}\n        # Names of members (fields/methods) of the current class/mixin/enum. Within the body of\n        # this directive, references to these members may be given as plain text in square\n        # brackets.\n        self.member_set: Set[str] = set()\n        # Names of parameters to the current method. This set is populated only when processing\n        # some method/constructor within the class. Within the description of the method, the names\n        # of these parameters can be mentioned in square brackets, which will be converted into a\n        # special :param: role.\n        self.param_set: Set[str] = set()\n\n    def run(self):\n        self.package = self._parse_option_package()\n        self.root = self._get_root_from_config()\n        self.source_file = self._parse_option_file()\n        self.symbol = self._parse_option_symbol()\n        self.record = self._get_data_record()\n        self.links = self._parse_links()\n        self._scan_source_file_if_needed()\n        for member in self.record['json'].get('members', {}):\n            self.member_set.add(member['name'])\n        result = nodes.container(\n            '',\n            self._generate_node_for_declaration(self.record['json'], 1),\n            classes=['dartdoc']\n        )\n        return [result]\n\n    def _parse_option_package(self) -> str:\n        package = self.options['package']\n        if not package:\n            data = self.env.domaindata['dart']\n            package = data['default_package']\n        return package\n\n    def _get_root_from_config(self):\n        if self.package:\n            roots = self.env.config.dartdoc_roots\n            if self.package not in roots:\n                raise self.error(\n                    f'Unknown package name `{self.package}`: please include it in the '\n                    f'`dartdoc_roots` configuration setting (file conf.py).'\n                )\n            return roots[self.package]\n        else:\n            return self.env.config.dartdoc_root\n\n    def _parse_option_file(self):\n        path = os.path.join(self.root, self.options['file'])\n        path = os.path.expanduser(path)\n        path = os.path.abspath(path)\n        if not os.path.exists(path):\n            raise ValueError(f'File `{path}` does not exist')\n        if not os.path.isfile(path):\n            raise ValueError(f'Path `{path}` is not a file')\n        return path\n\n    def _parse_option_symbol(self):\n        symbol = self.options['symbol']\n        if not re.fullmatch(r'[a-zA-Z_][a-zA-Z0-9_]*', symbol):\n            raise ValueError(f'Invalid symbol name `{symbol}`')\n        return symbol\n\n    def _parse_links(self) -> Dict[str, str]:\n        rx_reference_line = re.compile(r'^\\[(.*?)]:\\s+(.*)$')\n        links = {}\n        for line in self.content:\n            line = line.strip()\n            if not line:\n                continue\n            match = re.fullmatch(rx_reference_line, line)\n            if match:\n                links[match.group(1)] = match.group(2)\n            else:\n                raise self.error(\n                    f'Invalid reference definition: `{line}`; expected the '\n                    f'following format: `[[NAME]]: TARGET`'\n                )\n        return links\n\n    def _get_data_record(self):\n        objects = self.env.domaindata['dart']['objects']\n        if self.package not in objects:\n            objects[self.package] = {}\n        if self.symbol not in objects[self.package]:\n            objects[self.package][self.symbol] = {\n                'json': None,\n                'filename': self.source_file,\n                'timestamp': 0,\n                'docname': self.env.docname,\n            }\n        return objects[self.package][self.symbol]\n\n    def _scan_source_file_if_needed(self):\n        last_scan_time = self.record['timestamp']\n        source_last_modified_time = os.path.getmtime(self.source_file)\n        if last_scan_time >= source_last_modified_time:\n            assert self.record['json']\n            return\n        json_result = self._scan_source_file()\n        self.record['json'] = json_result\n        self.record['timestamp'] = source_last_modified_time\n\n    def _scan_source_file(self):\n        with tempfile.NamedTemporaryFile(mode='rt', suffix='.json', delete=False) as temp_file:\n            # Note: on Windows, a temporary file cannot be opened in another\n            # process if it is already open in this process. Thus, we need to\n            # close the file handle first before handing the file name to\n            # `subprocess.run()`.\n            temp_file.close()\n            try:\n                executable = 'dartdoc_json'\n                if os.name == 'nt':  # Windows\n                    executable = 'dartdoc_json.bat'\n                subprocess.run(\n                    [executable, self.source_file, '--output', temp_file.name],\n                    stdout=subprocess.PIPE,\n                    stderr=subprocess.STDOUT,\n                    check=True,\n                )\n                with open(temp_file.name, 'r') as t:\n                    json_string = t.read()\n                return self._extract_symbol(json_string)\n            except subprocess.CalledProcessError as e:\n                cmd = ' '.join(e.cmd)\n                raise RuntimeError(\n                    f'Command `{cmd}` returned with exit status'\n                    f' {e.returncode}\\n{e.output.decode(\"utf-8\")}'\n                )\n            finally:\n                os.remove(temp_file.name)\n\n    def _extract_symbol(self, json_string: str) -> Dict:\n        \"\"\"\n        Locates the definition of `self.symbol` within the `json_string` output.\n        \"\"\"\n        json_obj = json.loads(json_string)\n        if type(json_obj) != list or len(json_obj) != 1:\n            string = str(json_obj)\n            if len(string) > 100:\n                string = string[:97] + '...'\n            raise TypeError(\n                f'Invalid JSON output: a list of length 1 was expected, '\n                f'instead received `{string}`'\n            )\n        declared_symbols = []\n        for declaration in json_obj[0]['declarations']:\n            if declaration['name'] == self.symbol:\n                return declaration\n            declared_symbols.append(declaration['name'])\n        file = self.options['file']\n        raise ValueError(\n            f'Symbol {self.symbol} was not found in file {file}; available '\n            f'symbols were: {declared_symbols}'\n        )\n\n    # ----------------------------------------------------------------------------------------------\n    # Generate documentation nodes\n    # ----------------------------------------------------------------------------------------------\n\n    def _generate_node_for_declaration(self, data: Dict, level: int) -> Element:\n        \"\"\"\n        Generic method, this will handle the `data` field of arbitrary type,\n        and route it to the appropriate generator.\n        \"\"\"\n        kind = data['kind']\n        result = nodes.container(classes=[kind])\n        if kind in {'class', 'mixin', 'extension'}:\n            result += self._generate_class_signature_node(data, level)\n            result += self._generate_description(data, level)\n            result += self._generate_constructors_section(data, level)\n            result += self._generate_properties_section(data, level)\n            result += self._generate_methods_section(data, level)\n        elif kind in {'constructor', 'method', 'function'}:\n            for param in data.get('parameters', {}).get('all', []):\n                self.param_set.add(param['name'])\n            result += self._generate_function_signature_node(data, level)\n            result += self._generate_description(data, level)\n            self.param_set.clear()\n        elif kind in {'field', 'getter', 'setter'}:\n            result += self._generate_field_signature_node(data, level)\n            result += self._generate_description(data, level)\n        else:\n            raise self.error(f'Unknown symbol kind: {kind}')\n        self.state.document.note_explicit_target(result[0])\n        return result\n\n    def _generate_class_signature_node(self, data: Dict, level: int) -> Element:\n        result = nodes.container(classes=['signature', f'sig{level}'], ids=[data['name']])\n        first_line = nodes.container(\n            '',  # rawsource\n            nodes.inline(text=data['kind'] + ' ', classes=['keyword']),\n            nodes.inline(text=data['name'], classes=['name']),\n        )\n        first_line += self._generate_type_parameters_node(data)\n        result += first_line\n        for keyword in ['extends', 'on', 'implements', 'with']:\n            if keyword not in data:\n                continue\n            line = nodes.container(\n                '',  # rawsource\n                nodes.inline(text=keyword, classes=['keyword']),\n                nodes.Text(' '),\n                classes=['modifier']\n            )\n            targets = data[keyword]\n            if type(targets) != list:\n                targets = [targets]\n            for i, target in enumerate(targets):\n                if i > 0:\n                    line += nodes.Text(', ')\n                line += nodes.inline(text=target)\n            result += line\n        return result\n\n    def _generate_function_signature_node(self, data: Dict, level: int) -> Element:\n        node_id = data['name']\n        if level >= 2:\n            node_id = f'{self.symbol}-{node_id}'\n        result = nodes.container(classes=['signature', f'sig{level}'], ids=[node_id])\n        first_line = nodes.container()\n        first_line += nodes.inline(text=data['name'], classes=['name'])\n        first_line += self._generate_type_parameters_node(data)\n        arguments = nodes.inline(classes=['arguments'])\n        arguments += nodes.inline(text='(', classes=['punct'])\n        if 'parameters' in data:\n            parameters = data['parameters']['all']\n            n_positional_parameters = data['parameters'].get('positional', 0)\n            n_named_parameters = data['parameters'].get('named', 0)\n            n_simple_parameters = len(parameters) - n_positional_parameters - n_named_parameters\n            brackets = \\\n                '[]' if n_positional_parameters > 0 else \\\n                '{}' if n_named_parameters > 0 else \\\n                ''\n            for i, param in enumerate(parameters):\n                if i > 0:\n                    arguments += nodes.inline(text=', ', classes=['punct'])\n                if i == n_simple_parameters:\n                    arguments += nodes.inline(text=brackets[0], classes=['punct'])\n                argument = nodes.inline(classes=['argument'])\n                if 'required' in param:\n                    argument += nodes.inline(text='required ', classes=['keyword'])\n                if 'type' in param:\n                    argument += nodes.Text(param['type'])\n                    argument += nodes.Text(' ')\n                argument += nodes.inline(text=param['name'], classes=['name'])\n                if 'default' in param:\n                    argument += nodes.inline(text=' = ', classes=['punct'])\n                    argument += nodes.inline(text=param['default'], classes=['default'])\n                arguments += argument\n            if brackets:\n                arguments += nodes.inline(text=brackets[1], classes=['punct'])\n        arguments += nodes.inline(text=')', classes=['punct'])\n        first_line += arguments\n        if 'returns' in data:\n            return_type = data['returns']\n            if return_type != 'void':\n                first_line += nodes.inline(text=' → ', classes=['punct'])\n                first_line += nodes.Text(return_type)\n        result += first_line\n        return result\n\n    def _generate_field_signature_node(self, data: Dict, level: int) -> Element:\n        node_id = data['name']\n        if level >= 2:\n            node_id = f'{self.symbol}-{node_id}'\n        result = nodes.container(classes=['signature', f'sig{level}'], ids=[node_id])\n        result += nodes.inline(text=data['name'], classes=['name'])\n        if data['kind'] == 'field':\n            arrow = ':'\n        elif data['kind'] == 'getter':\n            arrow = '←→' if data.get('has_setter') else '→'\n        elif data['kind'] == 'setter':\n            arrow = '←'\n        else:\n            raise self.error(f'Unexpected field type {data[\"kind\"]}')\n        result += nodes.inline(text=f' {arrow} ', classes=['punct'])\n        if 'type' in data:\n            field_type = data['type']\n        elif 'returns' in data:\n            field_type = data['returns']\n        elif 'parameters' in data:\n            field_type = data['parameters']['all'][0]['type']\n        else:\n            raise self.error(f'Unexpected field data: {data}')\n        result += nodes.Text(field_type)\n        return result\n\n    def _generate_type_parameters_node(self, data: Dict) -> Optional[Element]:\n        if 'typeParameters' not in data:\n            return None\n        result = nodes.inline(classes=['types'])\n        result += nodes.inline(text='<', classes=['punct'])\n        for i, type_param in enumerate(data['typeParameters']):\n            if i > 0:\n                result += nodes.inline(text=', ', classes=['punct'])\n            result += nodes.inline(text=type_param['name'], classes=['name'])\n            if 'extends' in type_param:\n                result += nodes.inline(text=' extends ', classes=['keyword'])\n                result += nodes.Text(type_param['extends'])\n        result += nodes.inline(text='>', classes=['punct'])\n        return result\n\n    def _generate_description(self, data: Dict, level: int) -> Optional[Element]:\n        if not data.get('description'):\n            return None\n        lines = self._augment_comment(data['description']).split('\\n')\n        result = nodes.container(classes=['description', f'doc{level}'])\n        self.state.nested_parse(lines, 0, result)\n        return result\n\n    def _generate_constructors_section(self, data: Dict, level: int) -> Optional[Element]:\n        constructors = self._select_class_members(data, ['constructor'])\n        if not constructors:\n            return None\n        # A section needs an id, otherwise Sphinx breaks\n        result = nodes.section(ids=['constructors'])\n        result += nodes.title(text='Constructors')\n        for constructor in constructors:\n            result += self._generate_node_for_declaration(constructor, level + 1)\n        return result\n\n    def _generate_methods_section(self, data: Dict, level: int) -> Optional[Element]:\n        methods = self._select_class_members(data, ['method'])\n        if not methods:\n            return None\n        # A section needs an id, otherwise Sphinx breaks\n        result = nodes.section(ids=['methods'])\n        result += nodes.title(text='Methods')\n        for method in methods:\n            result += self._generate_node_for_declaration(method, level + 1)\n        return result\n\n    def _generate_properties_section(self, data: Dict, level: int) -> Optional[Element]:\n        fields = self._select_class_members(data, ['field', 'getter', 'setter'])\n        if not fields:\n            return None\n        for i, field in enumerate(fields):\n            if field is None:\n                continue\n            if field['kind'] == 'setter':\n                name = field['name']\n                for j, field2 in enumerate(fields):\n                    if field2 is None:\n                        continue\n                    if field2['kind'] == 'getter' and field2['name'] == name:\n                        fields[j]['has_setter'] = True\n                        fields[i] = None\n        result = nodes.section(ids=['properties'])\n        result += nodes.title(text='Properties')\n        for i, field in enumerate(fields):\n            if field is not None:\n                result += self._generate_node_for_declaration(field, level + 1)\n        return result\n\n    def _select_class_members(self, data: Dict, kinds: List[str]) -> List[Dict]:\n        \"\"\"\n        Given the JSON object [data] which describes a single Dart object such\n        as class/mixin/etc, this method returns all entries in `data.members`\n        whose \"kind\" property is among the provided [kinds].\n        \"\"\"\n        filter_overrides = not self.env.config.dartdoc_show_overrides\n        result = []\n        for entry in data.get('members', []):\n            if entry['kind'] not in kinds:\n                continue\n            if filter_overrides:\n                annotations = [\n                    annotation['name']\n                    for annotation in entry.get('annotations', [])\n                ]\n                if '@override' in annotations:\n                    continue\n            result.append(entry)\n        return result\n\n    def _augment_comment(self, text: str) -> str:\n        rx_escape = re.compile(r'([<>`*])')\n\n        def escape(text: str) -> str:\n            return re.sub(rx_escape, r'\\\\\\1', text)\n\n        def count(char, text, j):\n            n = 0\n            while j < len(text):\n                if text[j] == char:\n                    n += 1\n                else:\n                    break\n                j += 1\n            return n\n\n        parts: List[str] = []\n        i = 0\n        i0 = 0\n        while i < len(text):\n            ch = text[i]\n            if ch == '`':\n                # Skip any backtick-escaped text\n                num_backticks = count('`', text, i)\n                i += num_backticks\n                assert num_backticks > 0\n                while i < len(text):\n                    if count('`', text, i) >= num_backticks:\n                        i += num_backticks\n                        break\n                    i += 1\n            elif ch == '[':\n                parts.append(text[i0:i])\n                num_brackets = count('[', text, i)\n                i += num_brackets\n                assert num_brackets > 0\n                start = i\n                while i < len(text):\n                    if count(']', text, i) >= num_brackets:\n                        i += num_brackets\n                        break\n                    i += 1\n                target = text[start:i - num_brackets]\n                i0 = i\n                if num_brackets >= 2:\n                    # Links of the form `[[NAME]]` are converted into `[NAME](URL)`. The\n                    # `NAME` must be listed beforehand within the directive's content.\n                    if target in self.links:\n                        url = self.links[target]\n                        parts.append(f'[{escape(target)}]({url})')\n                    else:\n                        raise self.error(\n                            f'Unexpected link {target}, please specify its '\n                            f'target URL within the content section of the '\n                            f'directive.'\n                        )\n                else:\n                    # Links of the form `[NAME]` are converted into \"{ref}`NAME`\", so that\n                    # they can be resolved later by the domain.\n                    if target in self.param_set:\n                        parts.append(f'{{param}}`{target}`')\n                    elif target in self.member_set:\n                        parts.append(f'{{ref}}`{target} <{self.symbol}-{target}>`')\n                    elif target in self.links:\n                        url = self.links[target]\n                        parts.append(f'[{escape(target)}]({url})')\n                    else:\n                        parts.append(f'{{ref}}`{escape(target)}`')\n            else:\n                i += 1\n        parts.append(text[i0:])\n        return ''.join(parts)\n\n\ndef ParamRole(name: str, rawtext: str, text: str, lineno: int, inliner: Inliner,\n              options: Dict[str, Any], content: List[str]) \\\n        -> Tuple[List[nodes.Node], List[nodes.system_message]]:\n    return [nodes.inline(text=text, classes=['param'])], []\n\n\nclass DartDomain(Domain):\n    \"\"\"\n    The domain for describing objects in Dart language.\n    \"\"\"\n    name = 'dart'\n    label = 'dart'\n\n    roles = {\n        'ref': XRefRole(),\n        'param': ParamRole,\n    }\n    directives = {\n        'dartdoc': DartdocDirective,\n    }\n    initial_data = {\n        # Dictionary of all API objects known. The dictionary is keyed by the\n        # package name at first level, then by the symbol name, and the values\n        # are dictionary of properties for that object.\n        'objects': {\n            # package: str\n            # -> symbol: str\n            #    -> object_data: Dict = {\n            #         'json': Dict,  # object's raw API data, in JSON format\n            #         'source': str,  # file name where the API data came from\n            #         'timestamp': float,  # last modified time of [source]\n            #         'docname': str,  # doc where the symbol is documented\n            #       }\n        },\n        # Dictionary that provides for each document name the references to\n        # all objects that are declared within that document.\n        'docs': {\n            # docname: str\n            # -> doc_data: Dict = {\n            #   'symbols': List[str], # names of symbols documented on the page\n            #   'package': str  # package declared for that page\n            # }\n        },\n        # The name of the package that should be used if a directive does not\n        # specify any.\n        'default_package': '',\n    }\n    data_version = 1\n\n    def merge_domaindata(self, docnames: List[str], other_data: Dict) -> None:\n        for package, package_data in other_data['objects'].items():\n            for symbol, object_data in package_data.items():\n                if object_data['docname'] in docnames:\n                    if package not in self.data['objects']:\n                        self.data['objects'][package] = {}\n                    self.data['objects'][package][symbol] = object_data\n        for docname, doc_data in other_data['docs'].items():\n            if docname in docnames:\n                self.data['docs'][docname] = doc_data\n\n    def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,\n                         target: str, node: pending_xref, contnode: Element) \\\n            -> List[Tuple[str, Element]]:\n        return []\n\n    def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,\n                     typ: str, target: str, node: pending_xref, contnode: Element\n                     ) \\\n            -> Optional[Element]:\n        target_id = target\n        if '-' in target:\n            target, suffix = target.split('-', 2)\n        symbol_data = None\n        for package, package_data in self.data['objects'].items():\n            if target in package_data:\n                symbol_data = package_data[target]\n                break\n        if not symbol_data:\n            return None\n        return make_refnode(\n            builder=builder,\n            fromdocname=fromdocname,\n            todocname=symbol_data['docname'],\n            targetid=target_id,\n            child=contnode,\n            title=None,\n        )\n\n\ndef copy_asset_files(app, exc):\n    assert __file__.endswith('dart_domain.py')\n    css_file = __file__[:-2] + 'css'\n    if not os.path.isfile(css_file):\n        raise FileNotFoundError(f'Missing file `{css_file}`')\n    if app.builder.format == 'html' and not exc:\n        static_dir = os.path.join(app.builder.outdir, '_static')\n        copy_asset_file(css_file, static_dir)\n\n\n# Emitted when the environment determines which source files have changed and should be re-read.\n# `added`, `changed` and `removed` are sets of docnames that the environment has determined. You\n# can return a list of docnames to re-read in addition to these.\n#\n# https://www.sphinx-doc.org/en/master/extdev/appapi.html#event-env-get-outdated\ndef on_env_get_outdated(_: Sphinx, env: BuildEnvironment, added: Set[str], changed: Set[str],\n                        removed: Set[str]) -> List[str]:\n    existing = added | changed | removed\n    modified = set()\n    for package, package_data in env.domaindata['dart']['objects'].items():\n        for symbol, record in package_data.items():\n            docname = record['docname']\n            if docname in existing or docname in modified:\n                continue\n            last_scan_time = record['timestamp']\n            last_modified_time = os.path.getmtime(record['filename'])\n            if last_scan_time < last_modified_time:\n                modified.add(docname)\n    return list(modified)\n\n\n# Emitted when all traces of a source file should be cleaned from the environment, that is, if the\n# source file is removed or before it is freshly read. This is for extensions that keep their own\n# caches in attributes of the environment.\n#\n# https://www.sphinx-doc.org/en/master/extdev/appapi.html#event-env-purge-doc\ndef on_env_purge_doc(_: Sphinx, env: BuildEnvironment, docname: str) -> None:\n    for package, package_data in env.domaindata['dart']['objects'].items():\n        symbols_to_remove = []\n        for symbol, record in package_data.items():\n            if record['docname'] == docname:\n                symbols_to_remove.append(symbol)\n        for symbol in symbols_to_remove:\n            del package_data[symbol]\n\n\n# Emitted after the environment has determined the list of all added and changed files and just\n# before it reads them. It allows extension authors to reorder the list of docnames (inplace)\n# before processing, or add more docnames that Sphinx did not consider changed.\n#\n# https://www.sphinx-doc.org/en/master/extdev/appapi.html#event-env-before-read-docs\ndef on_env_before_read_docs(_: Sphinx, __: BuildEnvironment, docnames: List[str]) -> None:\n    # This ensures that within each directory, the 'index.md' document is processed first.\n    def key(docname: str) -> str:\n        basename = os.path.basename(docname)\n        if docname == 'index':\n            return '_index'\n        return basename\n\n    docnames.sort(key=key)\n\n\ndef setup(app: Sphinx):\n    app.add_css_file('dart_domain.css')\n    app.add_config_value('dartdoc_root', '', 'env', str)\n    app.add_config_value('dartdoc_roots', {}, 'env', Dict[str, str])\n    app.add_config_value('dartdoc_show_overrides', False, 'env', bool)\n    app.add_domain(DartDomain)\n    app.connect('build-finished', copy_asset_files)\n    app.connect('env-get-outdated', on_env_get_outdated)\n    app.connect('env-purge-doc', on_env_purge_doc)\n    app.connect('env-before-read-docs', on_env_before_read_docs)\n    return {\n        'version': '1.0.0',\n        'parallel_read_safe': True,\n        'parallel_write_safe': True,\n    }\n"
  },
  {
    "path": "doc/_sphinx/extensions/flutter_app.css",
    "content": "\nbutton.flutter-app-button {\n  background: #e2a73c;\n  border: none;\n  border-radius: 6px;\n  box-shadow: 2px 2px 6px 0 black;\n  color: black;\n  cursor: pointer;\n  font-family: var(--font-sans);\n  font-size: 1.1em;\n  font-weight: bold;\n  line-height: 1em;\n  margin: 0 1em 1em 0;\n  min-height: 26pt;\n  min-width: 120pt;\n}\n\nbutton.flutter-app-button:hover {\n  background: #f3dc38;\n}\n\nbutton.flutter-app-button:active {\n  background: white;\n  left: 1px;\n  position: relative;\n  top: 1px;\n}\n\nbutton.flutter-app-button:after {\n  content: '\\f0da';\n  font-family: var(--font-awesome);\n  margin-left: 4px;\n  position: relative;\n  top: 1px;\n}\n\n.flutter-app-code {\n  display: none;\n}\n\n.flutter-app-code.active {\n  background: #282828;\n  box-shadow: 0 0 30px 0 #000;\n  box-sizing: border-box;\n  display: initial;\n  height: 80vh;\n  left: 50%;\n  overflow-y: scroll;\n  padding: 1em 2em;\n  position: absolute;\n  top: 50%;\n  transform: translate(-50%, -50%);\n  width: 80vw;\n}\n\n.flutter-app-code .filename {\n  font-family: var(--font-mono);\n  font-size: 1.3em;\n  font-weight: bold;\n  margin: 1.5em 0 -0.5em 0;\n}\n\n.flutter-app-code div.highlight span.linenos {\n  color: #444;\n  margin-right: 8pt;\n}\n\n#flutter-app-overlay {\n  background: repeating-linear-gradient(\n    -45deg,\n    #000000bb 0px,\n    #000000bb 5px,\n    #000000aa 5px,\n    #000000aa 10px\n  );\n  display: none;\n  height: 100vh;\n  left: 0;\n  position: fixed;\n  top: 0;\n  width: 100vw;\n  z-index: 10000;\n}\n\n#flutter-app-overlay.active {\n  display: initial;\n}\n\n#flutter-app-overlay.active iframe {\n  border: 1px solid #333;\n  box-shadow: 0px 0px 30px 0px #000;\n  display: none;\n  height: 80vh;\n  left: 50%;\n  position: absolute;\n  top: 50%;\n  transform: translate(-50%, -50%);\n  width: 80vw;\n}\n\n#flutter-app-overlay.active iframe.active {\n  display: initial;\n}\n\n#flutter-app-close-button {\n  background: white;\n  border: 1px solid black;\n  border-radius: 50%;\n  color: black;\n  cursor: pointer;\n  font-size: 20px;\n  height: 30px;\n  left: 90%;\n  margin-left: -15px;\n  margin-top: -15px;\n  position: absolute;\n  text-align: center;\n  top: 10%;\n  width: 30px;\n  z-index: 1;\n}\n\n#flutter-app-close-button:hover {\n  background: #e38f13;\n  color: white;\n}\n\n.flutter-app-iframe {\n  border: 1px solid #555;\n  display: block;\n  height: 350px;\n  width: 100%;\n}\n\n.flutter-app-infobox {\n  background: #282828;\n  border: 1px solid #555555;\n  border-radius: 6px;\n  float: right;\n  margin-left: 6pt;\n  max-width: 40%;\n  padding: 8px;\n}\n\n.flutter-app-infobox button.flutter-app-iframe {\n  height: 400px;\n}\n\n.flutter-app-infobox button.flutter-app-button {\n  float: right;\n  font-size: 0.85em;\n  margin: 6px 0 0 0;\n  min-height: 14pt;\n  min-width: 50pt;\n}\n\n.flutter-app-infobox p:last-child {\n  margin-bottom: 0;\n}\n"
  },
  {
    "path": "doc/_sphinx/extensions/flutter_app.js",
    "content": "'use strict';\n\n/// Create an overlay with an iframe, the iframe's source is [url]. This also\n/// creates an (x) button to hide the overlay.\nfunction run_flutter_app(url) {\n  let id = compute_iframe_id(url);\n  create_overlay();\n  if (!$('#' + id).length) {\n    $('#flutter-app-overlay').append($(\n      `<iframe id=\"${id}\" class=\"flutter-app\" src=\"${url}\"></iframe>`\n    ));\n  }\n  $('#flutter-app-overlay').addClass('active');\n  $('#' + id).addClass('active');\n}\n\nfunction open_code_listings(id) {\n  create_overlay();\n  if (!$('#flutter-app-overlay #' + id).length) {\n    $('#' + id).appendTo($('#flutter-app-overlay'));\n  }\n  $('#flutter-app-overlay').addClass('active');\n  $('#' + id).addClass('active');\n}\n\nfunction create_overlay() {\n  if (!$('#flutter-app-overlay').length) {\n    $('body').append($(`\n      <div id=\"flutter-app-overlay\">\n        <button id=\"flutter-app-close-button\" onclick=\"close_flutter_app()\">✖</button>\n      </div>`\n    ));\n  }\n}\n\n/// Handler for the (x) close button on an app iframe.\nfunction close_flutter_app() {\n  $('#flutter-app-overlay > iframe').removeClass('active');\n  $('#flutter-app-overlay > div').removeClass('active');\n  $('#flutter-app-overlay').removeClass('active');\n}\n\n/// Convert a URL such as '_static/app/tutorial1/index.html?page1' into a string\n/// that can be used as an id: 'app-tutorial1-index-html-page1'.\nfunction compute_iframe_id(url) {\n  if (url.startsWith('_static/')) {\n    url = url.substr(8);\n  }\n  let matches = url.matchAll(new RegExp('\\\\w+', 'g'));\n  return Array.from(matches, m => m[0]).join('-');\n}\n"
  },
  {
    "path": "doc/_sphinx/extensions/flutter_app.py",
    "content": "#!/usr/bin/env python\nimport glob\nimport os\nimport re\nimport shutil\nimport subprocess\nimport sys\nfrom docutils import nodes\nfrom docutils.parsers.rst import directives\nfrom sphinx.util.docutils import SphinxDirective\nfrom sphinx.util.logging import getLogger\n\n\n# ------------------------------------------------------------------------------\n# `.. flutter-app::` directive\n# ------------------------------------------------------------------------------\n\nclass FlutterAppDirective(SphinxDirective):\n    \"\"\"\n    Embed Flutter apps into documentation pages.\n\n    This extension allows inserting precompiled Flutter apps into the\n    generated documentation. The app to be inserted has to be configured for\n    compiling in 'web' mode.\n\n    Example of usage in Markdown:\n\n        ```{flutter-app}\n        :sources: ../../tetris-tutorial\n        :page: page3\n        :show: popup\n        ```\n\n    The following arguments are supported:\n      :sources: - the directory where the app is located, i.e. the directory\n        with the pubspec.yaml file of the app. The path should be relative to\n        the `doc/_sphinx` folder.\n\n      :page: - an additional parameter that will be appended to the URL of the\n        app. The app can retrieve this parameter by checking the property\n        `window.location.search` (where `window` is from `dart:html`), and then\n        display the content based on that. Thus, this parameter allows bundling\n        multiple separate Flutter widgets into one compiled app.\n        In addition, the \"code\" run mode will try to locate a file or a folder\n        with the matching name.\n\n      :subfolder: - optional parameter, needed if the app page is not located in the lib folder.\n      The value of the parameter should be the path from the lib folder to the page source file.\n\n      :show: - a list of one or more run modes, which could include \"widget\",\n        \"popup\", \"code\", and \"infobox\". Each of these modes produces a different\n        output:\n          \"widget\" - an iframe shown directly inside the docs page;\n          \"popup\" - a [Run] button which opens the app to (almost) fullscreen;\n          \"code\" - a [Code] button which opens a popup with the code that was\n              compiled.\n          \"infobox\" - the content will be displayed as an infobox floating on\n              the right-hand side of the page.\n\n      :width: - override the default width of an iframe in widget/infobox modes.\n\n      :height: - override the default height of an iframe in widget/infobox\n        modes.\n    \"\"\"\n    has_content = True\n    required_arguments = 0\n    optional_arguments = 0\n    option_spec = {\n        'sources': directives.unchanged,\n        'page': directives.unchanged,\n        'subfolder': directives.unchanged,\n        'show': directives.unchanged,\n        'width': directives.unchanged,\n        'height': directives.unchanged,\n    }\n    # Static list of targets that were already compiled during the build\n    COMPILED = []\n\n    def __init__(self, *args, **kwds):\n        super().__init__(*args, **kwds)\n        self.modes = None\n        self.logger = None\n        self.app_name = None\n        self.source_dir = None\n        self.source_build_dir = None\n        self.target_dir = None\n        self.html_dir = None\n\n    def run(self):\n        self.logger = getLogger('flutter-app')\n        self._process_show_option()\n        self._process_sources_option()\n        self.source_build_dir = os.path.join(self.source_dir, 'build', 'web')\n        self.app_name = self._get_app_name()\n        self.html_dir = '_static/apps/' + self.app_name\n        self.target_dir = os.path.abspath(\n            os.path.join('..', '_build', 'html', self.html_dir))\n        self._ensure_compiled()\n\n        page = self.options.get('page', '')\n        iframe_url = _doc_root() + self.html_dir + '/index.html?' + page\n        result = []\n        if 'widget' in self.modes:\n            iframe = IFrame(src=iframe_url, classes=['flutter-app-iframe'])\n            result.append(iframe)\n            styles = []\n            if self.options.get('width'):\n                width = self.options.get('width')\n                if width.isdigit():\n                    width += 'px'\n                styles.append(\"width: \" + width)\n            if self.options.get('height'):\n                height = self.options.get('height')\n                if height.isdigit():\n                    height += 'px'\n                styles.append(\"height: \" + height)\n            if styles:\n                iframe.attributes['style'] = '; '.join(styles)\n        if 'popup' in self.modes:\n            result.append(Button(\n                '',\n                nodes.Text('Run'),\n                classes=['flutter-app-button', 'popup'],\n                onclick=f'run_flutter_app(\"{iframe_url}\")',\n            ))\n        if 'code' in self.modes:\n            code_id = self.app_name + \"-source-\" + page\n            result.append(self._generate_code_listings(code_id))\n            result.append(Button(\n                '',\n                nodes.Text('Code'),\n                classes=['flutter-app-button', 'code'],\n                onclick=f'open_code_listings(\"{code_id}\")',\n            ))\n        if 'infobox' in self.modes:\n            self.state.nested_parse(self.content, 0, result)\n            result = [\n                nodes.container('', *result, classes=['flutter-app-infobox'])\n            ]\n        return result\n\n    def _process_show_option(self):\n        argument = self.options.get('show')\n        if argument:\n            values = argument.split()\n            for value in values:\n                if value not in ['widget', 'popup', 'code', 'infobox']:\n                    raise self.error('Invalid :show: value ' + value)\n            self.modes = values\n        else:\n            self.modes = ['widget']\n\n    def _process_sources_option(self):\n        argument = self.options.get('sources', '')\n        abspath = os.path.abspath(argument)\n        if not argument:\n            raise self.error('Missing required argument :sources:')\n        if not os.path.isdir(abspath):\n            raise self.error(\n                f'sources=`{abspath}` does not exist or is not a directory')\n        assert not abspath.endswith('/')\n        self.source_dir = abspath\n\n    def _get_app_name(self):\n        src = os.path.relpath(self.source_dir)\n        return '-'.join(word for word in re.split(r'\\W', src) if word)\n\n    def _ensure_compiled(self):\n        need_compiling = (\n            ('popup' in self.modes or 'widget' in self.modes) and\n            self.source_dir not in FlutterAppDirective.COMPILED\n        )\n        if not need_compiling:\n            return\n        self.logger.info('Compiling Flutter app [%s]' % self.app_name)\n        self._compile_source()\n        self._copy_compiled()\n        self._create_index_html()\n        self.logger.info('  + copied into ' + self.target_dir)\n        assert os.path.isfile(self.target_dir + '/main.dart.js')\n        assert os.path.isfile(self.target_dir + '/index.html')\n        FlutterAppDirective.COMPILED.append(self.source_dir)\n\n    def _compile_source(self):\n        flutter_cmd = 'flutter'\n        if sys.platform == 'win32':\n            flutter_cmd = 'flutter.bat'\n        try:\n            subprocess.run(\n                [flutter_cmd, 'build', 'web'],\n                stdout=subprocess.PIPE,\n                stderr=subprocess.STDOUT,\n                cwd=self.source_dir,\n                check=True,\n            )\n        except subprocess.CalledProcessError as e:\n            cmd = ' '.join(e.cmd)\n            raise self.error(\n                f'Command `{cmd}` returned with exit status {e.returncode}\\n' +\n                e.output.decode('utf-8'),\n            )\n\n    def _copy_compiled(self):\n        assert os.path.isdir(self.source_build_dir)\n        main_js = os.path.join(self.source_build_dir, 'main.dart.js')\n        assets_dir = os.path.join(self.source_build_dir, 'assets')\n        os.makedirs(self.target_dir, exist_ok=True)\n        shutil.copy2(main_js, self.target_dir)\n        if os.path.exists(assets_dir):\n            shutil.copytree(\n                src=assets_dir,\n                dst=os.path.join(self.target_dir, 'assets'),\n                dirs_exist_ok=True,\n            )\n\n    def _create_index_html(self):\n        target_file = os.path.join(self.target_dir, 'index.html')\n        with open(target_file, 'wt') as out:\n            out.write('<!DOCTYPE html>\\n')\n            out.write('<html>\\n<head>\\n')\n            out.write('<base href=\"%s%s/\">\\n' % (_doc_root(), self.html_dir))\n            out.write('<title>%s</title>\\n' % self.app_name)\n            out.write('<style>body { background: black; }</style>\\n')\n            out.write('</head>\\n<body>\\n')\n            out.write('<script src=\"main.dart.js\"></script>\\n')\n            out.write('</body>\\n</html>\\n')\n\n    def _generate_code_listings(self, code_id):\n        subfolder = self.options.get('subfolder', '')\n        if subfolder and not subfolder.endswith('/'):\n            subfolder += '/'\n        code_dir = self.source_dir + '/lib/' +  subfolder + self.options.get('page', '')\n        if os.path.isdir(code_dir):\n            files = glob.glob(code_dir + '/**', recursive=True)\n        elif os.path.isfile(code_dir + '.dart'):\n            files = [code_dir + '.dart']\n            code_dir += '/..'\n        else:\n            raise self.error(f'Cannot find source directory {code_dir} or '\n                             f'source file {code_dir}.dart')\n\n        result = nodes.container(classes=['flutter-app-code'], ids=[code_id])\n        for filename in sorted(files):\n            if os.path.isfile(filename):\n                simple_filename = os.path.relpath(filename, code_dir)\n                result += nodes.container(\n                    '', nodes.Text(simple_filename), classes=['filename']\n                )\n                with open(filename, 'rt') as f:\n                    self.state.nested_parse(\n                        ['``````{code-block} dart\\n:lineno-start: 1\\n'] +\n                        [line.rstrip() for line in f] +\n                        ['``````\\n'], 0, result)\n        return result\n\n\ndef _doc_root():\n    root = os.environ.get('PUBLISH_PATH', '')\n    if not root.startswith('/'):\n        root = '/' + root\n    if not root.endswith('/'):\n        root = root + '/'\n    return root\n\n\n# ------------------------------------------------------------------------------\n# Nodes\n# ------------------------------------------------------------------------------\n\nclass IFrame(nodes.Element, nodes.General):\n    def visit(self, node):\n        attrs = {'src': node.attributes['src']}\n        if 'style' in node.attributes:\n            attrs['style'] = node.attributes['style']\n        self.body.append(self.starttag(node, 'iframe', **attrs).strip())\n\n    def depart(self, _):\n        self.body.append('</iframe>')\n\n\nclass Button(nodes.Element, nodes.General):\n    def visit(self, node):\n        attrs = {}\n        if 'onclick' in node.attributes:\n            attrs['onclick'] = node.attributes['onclick']\n        self.body.append(self.starttag(node, 'button', **attrs).strip())\n\n    def depart(self, _):\n        self.body.append('</button>')\n\n\n# ------------------------------------------------------------------------------\n# Extension setup\n# ------------------------------------------------------------------------------\n\ndef setup(app):\n    base_dir = os.path.dirname(__file__)\n    target_dir = os.path.abspath('../_build/html/_static/')\n    os.makedirs(target_dir, exist_ok=True)\n    shutil.copy(os.path.join(base_dir, 'flutter_app.js'), target_dir)\n    shutil.copy(os.path.join(base_dir, 'flutter_app.css'), target_dir)\n\n    app.add_node(IFrame, html=(IFrame.visit, IFrame.depart))\n    app.add_node(Button, html=(Button.visit, Button.depart))\n    app.add_directive('flutter-app', FlutterAppDirective)\n    app.add_js_file('flutter_app.js')\n    app.add_css_file('flutter_app.css')\n    return {\n        'parallel_read_safe': False,\n        'parallel_write_safe': False,\n        'env_version': 1,\n    }\n"
  },
  {
    "path": "doc/_sphinx/extensions/package.css",
    "content": ".package {\n  display: flex;\n  margin-bottom: 12pt;\n}\n.package > p:first-child {\n  margin: 0 12pt 0 0;\n}\n.package > p:first-child > a {\n  background: #9b6814;\n  border-left: 4px solid #ffc43c;\n  box-shadow: 2px 2px 4px black;\n  color: #ffffff;\n  display: block;\n  font-family: var(--font-mono);\n  min-width: 110pt;\n  padding: 8pt 10pt;\n}\n.package > p:first-child > a:hover {\n  background: #d48f1d;\n  text-shadow: 1px 1px 1px black;\n}\n.package > div.container > p {\n  margin-bottom: 6pt;\n}\n.package > div.container > p:last-child {\n  margin-bottom: 0;\n}\n"
  },
  {
    "path": "doc/_sphinx/extensions/package.py",
    "content": "#!/usr/bin/env python\nimport os\nimport shutil\nfrom docutils import nodes\nfrom sphinx.util.docutils import SphinxDirective\n\n\nclass PackageDirective(SphinxDirective):\n    has_content = True\n    required_arguments = 1\n    optional_arguments = 0\n\n    def run(self):\n        pkg_name = self.arguments[0]\n        relative_path = self.find_package(pkg_name)\n        link = []\n        self.state.nested_parse([f\"[{pkg_name}]({relative_path})\"], 0, link)\n        content = []\n        self.state.nested_parse(self.content, 0, content)\n        return [\n            nodes.container('', link[0], nodes.container('', *content), classes=['package'])\n        ]\n\n    def find_package(self, pkg_name):\n        for root, dirs, files in os.walk('..'):\n            if '_build' in root:\n                continue\n            if os.path.split(root)[-1] == pkg_name:\n                if 'index.md' in files:\n                    return os.path.join(root, 'index.md')\n                elif pkg_name + '.md' in files:\n                    return os.path.join(root, pkg_name + '.md')\n                elif len(files) == 1:\n                    return os.path.join(root, files[0])\n                else:\n                    return root\n        raise self.error('Cannot find package `pkg_name`')\n\n\ndef setup(app):\n    base_dir = os.path.dirname(__file__)\n    target_dir = os.path.abspath('../_build/html/_static/')\n    os.makedirs(target_dir, exist_ok=True)\n    shutil.copy(os.path.join(base_dir, 'package.css'), target_dir)\n\n    app.add_directive('package', PackageDirective)\n    app.add_css_file('package.css')\n    return {\n        'parallel_read_safe': True,\n        'parallel_write_safe': True,\n        'env_version': 1,\n    }\n"
  },
  {
    "path": "doc/_sphinx/extensions/yarn_lexer.css",
    "content": "\n/* Single-line comment */\n.highlight-yarn span.c {\n  color: #505050;\n}\n\n/* Character name at the start of a line */\n.highlight-yarn span.nt {\n  color: #10d576;\n}\n\n/* Variable */\n.highlight-yarn span.nv {\n  color: #80a4da;\n}\n\n/* Hashtag */\n.highlight-yarn span.ch {\n  color: #1e6557;\n  font-style: italic;\n}\n\n/* Markup */\n.highlight-yarn span.sb {\n  color: #816417;\n}\n\n/* Built-in command */\n.highlight-yarn span.k {\n  color: #76e9ff;\n  font-weight: bold;\n}\n"
  },
  {
    "path": "doc/_sphinx/extensions/yarn_lexer.py",
    "content": "import os\nimport shutil\nfrom pygments.lexer import RegexLexer, bygroups, default, include, words\nfrom pygments.token import *\n\n\nclass YarnLexer(RegexLexer):\n    name = 'YarnSpinner'\n    aliases = ['yarn', 'jenny']\n    filenames = ['*.yarn']\n    mimetypes = ['text/yarn']\n\n    CONSTANTS = [\n        'Bool',\n        'Numeric',\n        'String',\n        'false',\n        'true',\n    ]\n    OPERATORS = [\n        'and',\n        'as',\n        'eq',\n        'ge',\n        'gt',\n        'gte',\n        'is',\n        'le',\n        'lt',\n        'lte',\n        'ne',\n        'neq',\n        'not',\n        'or',\n        'to',\n        'xor',\n    ]\n    BUILTIN_COMMANDS = [\n        'character',\n        'declare',\n        'else',\n        'elseif',\n        'embed',\n        'endif',\n        'if',\n        'jump',\n        'local',\n        'set',\n        'stop',\n        'visit',\n        'wait',\n    ]\n    BUILTIN_FUNCTIONS = [\n        'bool',\n        'ceil',\n        'dec',\n        'decimal',\n        'dice',\n        'floor',\n        'if',\n        'inc',\n        'int',\n        'number',\n        'plural',\n        'random',\n        'random_range',\n        'round',\n        'round_places',\n        'string',\n        'visit_count',\n        'visited',\n    ]\n\n    tokens = {\n        'root': [\n            include('<whitespace>'),\n            include('<commands>'),\n            (r'#[^\\n]*\\n', Comment.Hashbang),\n            (r'---+\\n', Punctuation, 'node_header'),\n            default('node_header'),\n        ],\n\n        '<whitespace>': [\n            (r'\\s+', Whitespace),\n            (r'//.*\\n', Comment),\n        ],\n\n        'node_header': [\n            include('<whitespace>'),\n            (r'---+\\n', Punctuation, 'node_body'),\n            (r'(title)(:)(.*?\\n)', bygroups(Name.Builtin, Punctuation, Name.Variable)),\n            (r'(\\w+)(:)(.*?\\n)', bygroups(Name, Punctuation, Text)),\n            default('node_body'),\n        ],\n        'node_body': [\n            (r'===+\\n', Punctuation, '#pop:2'),\n            include('<whitespace>'),\n            default('line_start'),\n        ],\n\n        'line_start': [\n            (r'\\s+', Whitespace),\n            (r'->', Punctuation),\n            (r'(\\w+)(\\s*:\\s*)', bygroups(Name.Tag, Punctuation)),\n            default('line_continue'),\n        ],\n        'line_continue': [\n            (r'\\n', Whitespace, '#pop:2'),\n            (r'\\s+', Whitespace),\n            (r'//[^\\n]*', Comment),\n            (r'\\\\[\\[\\]\\\\{}<>/:#]', String.Escape),\n            (r'\\\\\\n\\s*', String.Escape),\n            (r'\\\\.', Error),\n            (r'\\{', Punctuation, 'curly_expression'),\n            include('<commands>'),\n            include('<markup>'),\n            (r'#[^\\s]+', Comment.Hashbang),\n            (r'[<>/]', Text),\n            (r'[^\\n\\\\\\[\\]{}<>/#]+', Text),\n            (r'.', Text),  # just in case\n        ],\n\n        '<commands>': [\n            (r'<<', Punctuation, 'command_name'),\n        ],\n        'command_name': [\n            (words(BUILTIN_COMMANDS, suffix=r'\\b'), Keyword, 'command_body'),\n            (r'\\w+', Name.Class, 'command_custom'),\n        ],\n        'command_body': [\n            include('<whitespace>'),\n            (r'\\{', Punctuation, 'curly_expression'),\n            (r'>>', Punctuation, '#pop:2'),\n            (r'>', Text),\n            default('command_expression'),\n        ],\n        'command_custom': [\n            include('<whitespace>'),\n            (r'>>', Punctuation, '#pop:2'),\n            (r'\\{', Punctuation, 'curly_expression'),\n            (r'[^>{\\n]+', Text),\n            ('.', Text),\n        ],\n\n        '<expression>': [\n            (r'\\n', Error),\n            (r'\\s+', Whitespace),\n            (r'(//.*)(\\n)', bygroups(Comment, Error)),\n            (words(BUILTIN_FUNCTIONS, suffix=r'\\b'), Name.Builtin),\n            (words(CONSTANTS, suffix=r'\\b'), Name.Builtin),\n            (words(OPERATORS, suffix=r'\\b'), Operator),\n            (r'\\$\\w+', Name.Variable),\n            (r'([+\\-*/%><=]=?)', Operator),\n            (r'\\d+(?:\\.\\d+)?(?:[eE][+\\-]?\\d+)?', Number),\n            (r'[(),]|\\.\\.\\.', Punctuation),\n            (r'\"', String.Delimeter, 'string'),\n            (r'\\w+', Name.Function),\n            (r'.', Error),\n        ],\n        'command_expression': [\n            (r'>>', Punctuation, '#pop:3'),\n            include('<expression>'),\n        ],\n        'curly_expression': [\n            (r'\\}', Punctuation, '#pop'),\n            include('<expression>'),\n        ],\n        'string_expression': [\n            (r'\\}', String.Interpol, '#pop'),\n            include('<expression>'),\n        ],\n\n        '<markup>': [\n            (r'\\[', String.Backtick, 'markup_start'),\n        ],\n        'markup_start': [\n            (r'/?\\w+', String.Backtick, 'markup_body'),\n            (r'/?\\]', String.Backtick, '#pop'),\n        ],\n        'markup_body': [\n            (r'/?\\]', String.Backtick, '#pop:2'),\n            (r'[^/\\]]+', String.Backtick),\n        ],\n\n        'string': [\n            (r'\"', String.Delimeter, '#pop'),\n            (r'{', String.Interpol, 'string_expression'),\n            (r'[^\"{\\n\\\\]+', String),\n            (r'\\\\[nt\"{}]', String.Escape),\n            (r'\\n', Error),\n        ],\n    }\n\n\ndef setup(app):\n    base_dir = os.path.dirname(__file__)\n    target_dir = os.path.abspath('../_build/html/_static/')\n    os.makedirs(target_dir, exist_ok=True)\n    shutil.copy(os.path.join(base_dir, 'yarn_lexer.css'), target_dir)\n\n    app.add_css_file('yarn_lexer.css')\n    app.add_lexer('yarn', YarnLexer)\n    return {\n        'parallel_read_safe': True,\n        'parallel_write_safe': True,\n        'env_version': 1,\n    }\n"
  },
  {
    "path": "doc/_sphinx/kill-server.py",
    "content": "from psutil import process_iter\nfrom signal import SIGTERM # or SIGKILL\n\nfor proc in process_iter():\n    for conns in proc.connections(kind='inet'):\n        if conns.laddr.port == 8000:\n            proc.send_signal(SIGTERM) # or SIGKILL"
  },
  {
    "path": "doc/_sphinx/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\n\r\nset SOURCEDIR=..\r\nset BUILDDIR=..\\_build\r\nset SPHINXOPTS=-c .\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\nif \"%1\" == \"livehtml\" (\r\n\tgoto livehtml\t\r\n) else (\r\n\tgoto build\r\n)\r\n\r\ngoto end\r\n\r\n:livehtml\r\nsphinx-autobuild %SOURCEDIR% %BUILDDIR%\\html %SPHINXOPTS% %2 --ignore \"**/.*\" --ignore \"*build*\" --open-browser\r\n\r\n:build\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %2\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %2\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "doc/_sphinx/requirements.txt",
    "content": "linkify-it-py==2.0.2\nmyst-parser==2.0.0\nPygments==2.17.2\nSphinx==7.2.6\nsphinxcontrib-mermaid==0.9.2\nsphinxcontrib-jquery==4.1\nsphinx-autobuild==2021.3.14\nsphinx-copybutton==0.5.2\nJinja2==3.1.6\npsutil==5.9.7\n"
  },
  {
    "path": "doc/_sphinx/scripts/menu-expand.js",
    "content": "// Auto expand the first expandable node (\"flame\") when loaded.\nwindow.addEventListener('load', (_event) => {\n    expandFirstOnHome();\n});\n\n/**\n * This method expands the first expandable node on the home page.\n * \n * If the current page is not the home page, this method does nothing.\n */\n// When the path name ends with index.html or an empty string, it is home page.\nfunction expandFirstOnHome() {\n    const parts = location.pathname.split('/');\n    const lastPart = parts[parts.length - 1];\n    const isHomePage = (lastPart == '') || (lastPart == 'index.html');\n\n    if (isHomePage) {\n        // expand the first expandable node in the toctree\n        $('li.toctree-l1').has('ul').first().addClass('current');\n    }\n}\n"
  },
  {
    "path": "doc/_sphinx/scripts/versions.js",
    "content": "\n// Detect the doc version of the current page. This can be done by\n// looking at the URL, which is supposed to be of the form\n// `https://docs.flame-engine.org/${version}/...`. Thus, the first part in the\n// path is presumed to be the version.\nfunction getCurrentDocVersion() {\n  if (location.host === 'docs.flame-engine.org') {\n    const parts = location.pathname.split('/');\n    if (parts.length >= 2 && parts[0] === '') {\n      return parts[1];\n    }\n  }\n  return 'local';\n}\n\n// global constant - special versions from the docs\nconst specialVersions = ['main', 'local'];\n\n// Given a list of versions (as plain strings), return the latest version.\nfunction getLatestVersion(versions) {\n  return versions.filter(e => !specialVersions.includes(e))[0];\n}\n\n// Given a list of versions (as plain strings), convert them into HTML <A/>\n// links, so that they can be placed into the menu.\nfunction convertVersionsToHtmlLinks(versionsList, currentVersion) {\n  let out = '';\n  const latestVersion = getLatestVersion(versionsList);\n  for (let version of versionsList) {\n    version = version.trim();\n    if (version === '') continue;\n    let classes = 'btn btn-secondary topbarbtn';\n    if (version === currentVersion) {\n      classes += ' selected';\n    }\n    // Link to the 'latest/` path if it is the latest version.\n    if (version === latestVersion) {\n      out += `<a href=\"/latest/\">\n        <button class=\"${classes}\">\n          <i class=\"fa fa-code-branch\"></i> ${version} (latest)\n        </button>\n      </a>`;\n    } else {\n      out += `<a href=\"/${version}/\">\n        <button class=\"${classes}\">\n          <i class=\"fa fa-code-branch\"></i> ${version}\n        </button>\n      </a>`;\n    }\n  }\n  return out;\n}\n\nfunction maybeAddWarning(versions, currentVersion) {\n  const latestVersion = getLatestVersion(versions);\n  const nonWarningVersions = [...specialVersions, 'latest', latestVersion];\n  const showWarning = !nonWarningVersions.includes(currentVersion);\n  if (showWarning) {\n    $('#version-warning')\n      .find('.version').text(currentVersion).end()\n      .removeClass('hidden');\n  }\n}\n\nfunction buildVersionsMenu(data) {\n  const versions = data.split('\\n');\n  const currentVersion = getCurrentDocVersion();\n  const versionButtons = convertVersionsToHtmlLinks(versions, currentVersion);\n  $('div.versions-placeholder').append(`\n    <div id=\"versions-menu\" tabindex=\"-1\">\n      <div class=\"btn\">\n        <i class=\"fa fa-code-branch\"></i>\n        <span class=\"version-id\">${currentVersion}</span>\n      </div>\n      <div class=\"dropdown-buttons\">\n        <div class=\"header\">View documentation for version:</div>\n        ${versionButtons}\n      </div>\n    </div>\n  `);\n  $(\"#versions-menu\").on(\"click\", function() {\n    $(this).toggleClass(\"active\");\n  }).on(\"blur\", function() {\n    // A timeout ensures that `click` can propagate to child <A/> elements.\n    setTimeout(() => $(this).removeClass(\"active\"), 200);\n  });\n\n  maybeAddWarning(versions, currentVersion);\n}\n\n// Start loading the versions list as soon as possible, don't wait for DOM\nconst versionsRequest = $.get(\n    \"/versions.txt\"\n);\n\n// Now wait for DOM to finish loading\n$(function() {\n  // Lastly, wait for versions to finish loading too.\n  versionsRequest.then(buildVersionsMenu)\n    .fail(function() {\n      console.log(\"Failed to load versions.txt, using default version list\");\n      buildVersionsMenu(\"local\\nmain\\n1.0.0\\n\");\n    });\n});\n"
  },
  {
    "path": "doc/_sphinx/theme/copy-button.css",
    "content": "/* Custom CSS properties for sphinx_copybutton */\nbutton.copybtn {\n    top: 1.5em;\n    right: 0.15em;\n    color: #ffffff;\n    background-color: #484848;\n}\n\nbutton.copybtn:hover{\n    color: #484848;\n}\n"
  },
  {
    "path": "doc/_sphinx/theme/doctools.js",
    "content": "/*\n * Originally from:\n * https://github.com/sphinx-doc/sphinx/blob/2b42752219424cb09ba910b6f654145107e0387b/sphinx/themes/basic/static/doctools.js\n * -----------------------------------------------------------------------------\n * doctools.js\n * ~~~~~~~~~~~\n *\n * Base JavaScript utilities for all Sphinx HTML documentation.\n *\n * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.\n * :license: BSD, see LICENSE for details.\n * -----------------------------------------------------------------------------\n */\n\"use strict\";\n\nconst _ready = (callback) => {\n  if (document.readyState !== \"loading\") {\n    callback();\n  } else {\n    document.addEventListener(\"DOMContentLoaded\", callback);\n  }\n};\n\nconst BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([\n  \"TEXTAREA\",\n  \"INPUT\",\n  \"SELECT\",\n  \"BUTTON\",\n]);\n\n/**\n * highlight a given string on a node by wrapping it in\n * span elements with the given class name.\n */\nconst _highlightFlame = (node, addItems, text, className, index) => {\n  if (node.nodeType === Node.TEXT_NODE) {\n    const val = node.nodeValue;\n    const parent = node.parentNode;\n    const pos = val.toLowerCase().indexOf(text);\n    if (\n      pos >= 0 &&\n      !parent.classList.contains(className) &&\n      !parent.classList.contains(\"nohighlight\")\n    ) {\n      let span;\n\n      const closestNode = parent.closest(\"body, svg, foreignObject\");\n      const isInSVG = closestNode && closestNode.matches(\"svg\");\n      if (isInSVG) {\n        span = document.createElementNS(\"http://www.w3.org/2000/svg\", \"tspan\");\n      } else {\n        span = document.createElement(\"span\");\n        span.classList.add(className);\n        span.classList.add('i' + index);\n      }\n\n      span.appendChild(document.createTextNode(val.substr(pos, text.length)));\n      parent.insertBefore(\n        span,\n        parent.insertBefore(\n          document.createTextNode(val.substr(pos + text.length)),\n          node.nextSibling\n        )\n      );\n      node.nodeValue = val.substr(0, pos);\n\n      if (isInSVG) {\n        const rect = document.createElementNS(\n          \"http://www.w3.org/2000/svg\",\n          \"rect\"\n        );\n        const bbox = parent.getBBox();\n        rect.x.baseVal.value = bbox.x;\n        rect.y.baseVal.value = bbox.y;\n        rect.width.baseVal.value = bbox.width;\n        rect.height.baseVal.value = bbox.height;\n        rect.setAttribute(\"class\", className);\n        addItems.push({ parent: parent, target: rect });\n      }\n    }\n  } else if (node.matches && !node.matches(\"button, select, textarea\")) {\n    node.childNodes.forEach((el) => _highlightFlame(el, addItems, text, className, index));\n  }\n};\nconst _highlightTextFlame = (thisNode, text, className, index) => {\n  let addItems = [];\n  _highlightFlame(thisNode, addItems, text, className, index);\n  addItems.forEach((obj) =>\n    obj.parent.insertAdjacentElement(\"beforebegin\", obj.target)\n  );\n};\n\n/**\n * Small JavaScript module for the documentation.\n */\nconst DocumentationFlame = {\n  init: () => {\n    DocumentationFlame.highlightSearchWords();\n    DocumentationFlame.initDomainIndexTable();\n    DocumentationFlame.initOnKeyListeners();\n  },\n\n  /**\n   * i18n support\n   */\n  TRANSLATIONS: {},\n  PLURAL_EXPR: (n) => (n === 1 ? 0 : 1),\n  LOCALE: \"unknown\",\n\n  // gettext and ngettext don't access this so that the functions\n  // can safely bound to a different name (_ = DocumentationFlame.gettext)\n  gettext: (string) => {\n    const translated = DocumentationFlame.TRANSLATIONS[string];\n    switch (typeof translated) {\n      case \"undefined\":\n        return string; // no translation\n      case \"string\":\n        return translated; // translation exists\n      default:\n        return translated[0]; // (singular, plural) translation tuple exists\n    }\n  },\n\n  ngettext: (singular, plural, n) => {\n    const translated = DocumentationFlame.TRANSLATIONS[singular];\n    if (typeof translated !== \"undefined\")\n      return translated[DocumentationFlame.PLURAL_EXPR(n)];\n    return n === 1 ? singular : plural;\n  },\n\n  addTranslations: (catalog) => {\n    Object.assign(DocumentationFlame.TRANSLATIONS, catalog.messages);\n    DocumentationFlame.PLURAL_EXPR = new Function(\n      \"n\",\n      `return (${catalog.plural_expr})`\n    );\n    DocumentationFlame.LOCALE = catalog.locale;\n  },\n\n  /**\n   * highlight the search words provided in the url in the text\n   */\n  highlightSearchWords: () => {\n    const highlight =\n      new URLSearchParams(window.location.search).get(\"highlight\") || \"\";\n    const terms = highlight.toLowerCase().split(/\\s+/).filter(x => x);\n    if (terms.length === 0) return; // nothing to do\n\n    // There should never be more than one element matching \"div.body\"\n    const divBody = document.querySelectorAll(\"div.body\");\n    const body = divBody.length ? divBody[0] : document.querySelector(\"body\");\n    const hbox = $(\"#highlight-content\");\n    window.setTimeout(() => {\n      terms.forEach((term, index) => {\n        _highlightTextFlame(body, term, \"highlighted\", index);\n        hbox.append($('<span>' + term + '</span>').click(function(){\n          $(this).toggleClass(\"off\");\n          DocumentationFlame.toggleSearchWord(index);\n        }));\n      });\n    }, 10);\n\n    $(\"div.highlight-box\").show();\n    $(\"div.highlight-box button.close\").click(DocumentationFlame.hideSearchWords);\n    const searchBox = document.getElementById(\"searchbox\");\n    if (searchBox === null) return;\n    searchBox.appendChild(\n      document\n        .createRange()\n        .createContextualFragment(\n          '<p class=\"highlight-link\">' +\n            '<a href=\"javascript:DocumentationFlame.hideSearchWords()\">' +\n            DocumentationFlame.gettext(\"Hide Search Matches\") +\n            \"</a></p>\"\n        )\n    );\n  },\n\n  /**\n   * helper function to hide the search marks again\n   */\n  hideSearchWords: () => {\n    $(\"div.highlight-box\").fadeOut(300);\n    document\n      .querySelectorAll(\"#searchbox .highlight-link\")\n      .forEach((el) => el.remove());\n    document\n      .querySelectorAll(\"span.highlighted\")\n      .forEach((el) => el.classList.remove(\"highlighted\"));\n    const url = new URL(window.location);\n    url.searchParams.delete(\"highlight\");\n    window.history.replaceState({}, \"\", url);\n  },\n\n  /**\n   * helper function to focus on search bar\n   */\n  focusSearchBar: () => {\n    document.querySelectorAll(\"input[name=q]\")[0]?.focus();\n  },\n\n  toggleSearchWord : function(i) {\n    $('span.highlighted.i' + i).toggleClass('off');\n  },\n\n  /**\n   * Initialize the domain index toggle buttons\n   */\n  initDomainIndexTable: () => {\n    const toggler = (el) => {\n      const idNumber = el.id.substr(7);\n      const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`);\n      if (el.src.substr(-9) === \"minus.png\") {\n        el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`;\n        toggledRows.forEach((el) => (el.style.display = \"none\"));\n      } else {\n        el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`;\n        toggledRows.forEach((el) => (el.style.display = \"\"));\n      }\n    };\n\n    const togglerElements = document.querySelectorAll(\"img.toggler\");\n    togglerElements.forEach((el) =>\n      el.addEventListener(\"click\", (event) => toggler(event.currentTarget))\n    );\n    togglerElements.forEach((el) => (el.style.display = \"\"));\n    if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler);\n  },\n\n  initOnKeyListeners: () => {\n    // only install a listener if it is really needed\n    if (\n      !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS &&\n      !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS\n    )\n      return;\n\n    document.addEventListener(\"keydown\", (event) => {\n      if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; // bail for input elements\n      if (event.altKey || event.ctrlKey || event.metaKey) return; // bail with special keys\n\n      if (!event.shiftKey) {\n        switch (event.key) {\n          case \"ArrowLeft\":\n            if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break;\n\n            const prevLink = document.querySelector('link[rel=\"prev\"]');\n            if (prevLink && prevLink.href) {\n              window.location.href = prevLink.href;\n              event.preventDefault();\n            }\n            break;\n          case \"ArrowRight\":\n            if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break;\n\n            const nextLink = document.querySelector('link[rel=\"next\"]');\n            if (nextLink && nextLink.href) {\n              window.location.href = nextLink.href;\n              event.preventDefault();\n            }\n            break;\n          case \"Escape\":\n            if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break;\n            DocumentationFlame.hideSearchWords();\n            event.preventDefault();\n        }\n      }\n\n      // some keyboard layouts may need Shift to get /\n      switch (event.key) {\n        case \"/\":\n          if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break;\n          DocumentationFlame.focusSearchBar();\n          event.preventDefault();\n      }\n    });\n  },\n};\n\n// quick alias for translations\nconst _ = DocumentationFlame.gettext;\n\n_ready(DocumentationFlame.init);\n"
  },
  {
    "path": "doc/_sphinx/theme/flames.css",
    "content": "\n:root {\n  --document-width: 900px;\n  --document-x-margin: clamp(10px, 2.5vw, 30px);\n  --document-padding: min(40px, 4vw);\n  --font-sans: 'Ubuntu', Roboto, Helvetica Neue, Arial, sans-serif;\n  --font-mono: \"SF Mono\", Menlo, monospace;\n  --font-awesome: 'Font Awesome 5 Solid';\n  --highlight-color: 255, 233, 92;\n  --left-menu-width: clamp(250px, 25vw, 330px);\n  --right-menu-width: 240px;\n  --top-menu-height: 50px;\n  color-scheme: dark; /* makes scrollbars dark */\n}\n\n@media (max-width: 900px) {\n  :root {\n    --left-menu-width: min(320px, 80vw);\n  }\n}\n\nbody {\n  background-color: #383838;\n  color: #cccccc;\n  font-family: var(--font-sans);\n  font-size: 15px;\n  line-height: 1.5;\n  margin: 0;\n  padding: 0;\n  position: relative; /* needed by Scrollspy */\n  text-align: left;\n}\n\na {\n  color: #ff9500;\n  text-decoration: none;\n}\n\na:hover {\n  color: #ddbb99;\n}\n\na:visited {\n  color: #ffbc70;\n}\n\na:hover code {\n  color: white;\n}\n\na.reference.external {\n  word-wrap: break-word;\n}\n\na.reference.external::after {\n  content: \"\\f35d\";\n  font-family: var(--font-awesome);\n  font-size: 60%;\n  opacity: 60%;\n  padding-left: 2px;\n  position: relative;\n  top: -5px;\n}\n\ncode {\n  background-color: #181818;\n  border-radius: 3pt;\n  color: #8b9fb1;\n  font-family: var(--font-mono);\n  font-size: 85%;\n  padding: 2pt 3pt;\n}\n\ncode.xref {\n  background: none;\n  color: #ff9500;\n  padding: 0;\n}\n\na.reference code.xref { color: #ffbb3e; }\na.reference:hover code.xref { color: #ffffff; }\n\np {\n  margin-top: 0;\n}\n\nstrong {\n  color: white;\n}\n\n\ndiv.expander {\n  flex: 1;\n}\n\np + ul, p + ol {\n  margin-top: -0.5em;\n}\n\nul.simple p + ul, ol.simple p + ul {\n  margin-top: 0;\n}\n\nul ul.simple p {\n  margin-bottom: 1em;\n}\n\nkbd {\n  background-color: #000;\n  border-radius: 4pt;\n  color: #fff;\n  font-family: var(--font-mono);\n  font-size: 85%;\n  padding: 2pt 5pt;\n}\n\n/*----------------------------------------------------------------------------*\n * Top navigation bar\n *----------------------------------------------------------------------------*/\n\ndiv.top-bar {\n  align-items: center;\n  background: #303030;\n  box-shadow: 0px 3px 7px #00000066, 0 1px 2px #000000bf;\n  box-sizing: border-box;\n  display: flex;\n  height: var(--top-menu-height);\n  margin-left: auto;\n  margin-right: auto;\n  padding: 5px 15px;\n  position: fixed;\n  top: 0;\n  width: 100%;\n  z-index: 100;\n}\n\ndiv.top-bar .logo_image img {\n  height: calc(var(--top-menu-height) * 0.65);\n  position: relative;\n  top: 3px; /* correct for logo having slightly asymmetrical spacing */\n}\n\ndiv.top-bar .btn {\n  align-items: center;\n  background-color: transparent;\n  border: none;\n  border-radius: 0.25rem;\n  color: #ff9500;\n  cursor: pointer;\n  display: inline-flex;\n  font-size: 1rem;\n  font-weight: 400;\n  height: 40px;\n  justify-content: center;\n  transition: color .15s ease-in-out, background-color .15s ease-in-out;\n  user-select: none;\n  width: 40px;\n}\n\ndiv.top-bar .btn:hover {\n  background-color: #b7b7b740;\n}\n\ndiv.top-bar #github-button,\ndiv.top-bar #discord-button {\n  font-size: 1.4em;\n}\n\ndiv.top-bar #menu-button {\n  display: none;\n}\n\n@media (max-width: 900px) {\n  div.top-bar #menu-button {\n    display: inline-block;\n    margin-left: -8px;\n    margin-right: 8px;\n  }\n\n  div.top-bar #menu-button.active {\n    background-color: #8d7a39;\n  }\n}\n\n/*----------------------------------------------------------------------------*\n * Version menu\n *----------------------------------------------------------------------------*/\n\n#versions-menu {\n  position: relative;\n}\n\n#versions-menu > .btn {\n  padding: 0 8px;\n  width: auto;\n}\n\n#versions-menu.active > .btn {\n  background-color: #8d7a39;\n}\n\n#versions-menu > .btn > .version-id {\n  color: #ff9500;\n  font-weight: bold;\n  padding: 0 0 0 6px;\n}\n\n#versions-menu > .dropdown-buttons {\n  display: none;\n}\n\n#versions-menu.active > .dropdown-buttons {\n  background: #383838;\n  border-radius: 4px;\n  box-shadow: 1px 1px 4px black;\n  box-sizing: border-box;\n  display: flex;\n  flex-direction: column;\n  padding: 4px;\n  position: absolute;\n  right: 0;\n  top: calc(var(--top-menu-height) - 4px);\n  white-space: nowrap;\n}\n\n#versions-menu.active > .dropdown-buttons .header {\n  font-size: 80%;\n  font-style: italic;\n  min-width: 200px;\n  opacity: 0.6;\n}\n\n#versions-menu.active > .dropdown-buttons > a > button {\n  justify-content: flex-start;\n  min-width: 60px;\n  width: fill-available;\n  width: -moz-fill-available;\n  width: -webkit-fill-available;\n}\n\n#versions-menu.active > .dropdown-buttons > a > button > i.fa {\n  padding-right: 6px;\n}\n\n\n/*----------------------------------------------------------------------------*\n * Left sidebar area\n *----------------------------------------------------------------------------*/\n\ndiv.sidebar-left-bg {\n  background: #383838;\n  bottom: 0;\n  box-shadow: 2px 0 4px #000000a1;\n  contain: strict;\n  position: fixed;\n  top: 0;\n  width: var(--left-menu-width);\n  z-index: 50;\n}\n\ndiv.sidebar-left {\n  bottom: 0;\n  box-sizing: border-box;\n  overflow: hidden;\n  padding: 10px 0 0 12px;\n  position: fixed;\n  top: calc(var(--top-menu-height) + 10px);\n  width: var(--left-menu-width);\n  z-index: 60;\n}\n\ndiv.sidebar-left:hover {\n  overflow-y: auto;\n}\n\n/* On small displays the sidebar becomes hidden and can be toggled via a menu\n   button. */\n@media (max-width: 900px) {\n\n  div.sidebar-left-area {\n    position: absolute;\n    left: calc(0px - var(--left-menu-width));\n    transition: left 0.3s;\n  }\n\n  div.sidebar-left-area:not(.active) .sidebar-left-bg {\n    box-shadow: none;\n  }\n\n  div.sidebar-left-area.active {\n    left: 0;\n  }\n}\n\n#search-form {\n  position: relative;\n  margin: 0;\n  padding: 12px 12px 12px 0;\n}\n\n#search-form #search-input {\n  background: #2c2c2c;\n  border: none;\n  border-radius: 6px;\n  color: #ffe689;\n  height: calc(1.5em + 0.75rem + 2px);\n  line-height: 1.5;\n  opacity: 50%;\n  padding: 0.375rem 0.75rem 0.375rem 35px;\n  width: 100%;\n}\n\n#search-form #search-input:focus {\n  background: #303030;\n  border-bottom: none;\n  box-shadow: inset 0 0 5px 1px #1c1c1c;\n  opacity: 1.0;\n  outline: none;\n}\n\n#search-form i.icon.fa-search {\n  color: #a99a2f;\n  left: 9px;\n  position: absolute;\n  top: 21px;\n}\n\n#search-form:focus-within .form-control::placeholder,\n#search-form:focus-within i.icon.fa-search {\n  opacity: 50%;\n}\n\ndiv.nav-left {\n  margin-bottom: 70px;\n}\n\ndiv.nav-left ul {\n  display: none;\n  padding: 0 0 0 16px;\n}\n\ndiv.nav-left > ul {\n  display: block;\n}\n\ndiv.nav-left li {\n  list-style-type: none;\n  position: relative;\n  margin: 0;\n}\n\ndiv.nav-left li > a,\ndiv.nav-left li > div.submenu > a.reference {\n  border-radius: 5px 0 0 5px;\n  color: #ff9500;\n  display: block;\n  font-size: 13px;\n  margin-left: -5px;\n  padding: 2px 0 2px 5px;\n  text-decoration: none;\n}\n\ndiv.nav-left li a:hover {\n  background-color: #00000030;\n  color: #fff !important;\n}\n\ndiv.nav-left li > div.submenu > a.arrow {\n  border-radius: 5px;\n  cursor: pointer;\n  left: -20px;\n  position: absolute;\n  width: 20px;\n}\n\ndiv.nav-left li > div.submenu > a.arrow::before {\n  content: \"\\f0da\";\n  font-family: var(--font-awesome);\n  font-size: 80%;\n  padding-left: 9px;\n}\n\ndiv.nav-left li > a.current,\ndiv.nav-left li > div.submenu > a.reference.current {\n  background: #222;\n  color: #aaa;\n}\n\ndiv.nav-left li.current > div.submenu > a.arrow::before {\n  content: \"\\f0d7\";\n  padding-left: 6px;\n}\n\ndiv.nav-left li.current > ul {\n  display: block;\n}\n\n\n/*----------------------------------------------------------------------------*\n * Right sidebar area\n *----------------------------------------------------------------------------*/\n\ndiv.sidebar-right {\n  flex: 5 1 auto;\n  height: calc(100vh - 25px - var(--top-menu-height));\n  margin-right: -28px;\n  max-width: var(--right-menu-width);\n  position: sticky;\n  top: calc(25px + var(--top-menu-height));\n  overflow: hidden;\n}\n\ndiv.sidebar-right:hover {\n  overflow-y: auto;\n}\n\n@media (max-width: 1200px) {\n  div.sidebar-right {\n    display: none;\n  }\n}\n\ndiv.nav-right > #toc-local {\n  display: flex;\n  flex-direction: column;\n  margin: 0 0 0 6px;\n  max-width: var(--right-menu-width);\n  padding: 0 0 0 1rem;\n}\n\ndiv.nav-right > #toc-local > div.header {\n  font-weight: bold;\n  white-space: nowrap;\n}\n\ndiv.nav-right > #toc-local > a.list-group-item {\n  border: none;\n  border-left: 4px solid transparent;\n  color: #ff9500;\n  display: block;\n  font-size: 13px;\n  line-height: 1.2;\n  list-style-type: none;\n  padding: 4px 0 4px 6px;\n  text-decoration: none;\n  margin-left: -9px;\n}\n\ndiv.nav-right > #toc-local > a.list-group-item.level-2 {\n  padding-left: 15px;\n  padding-top: 0px;\n  font-size: 12px;\n}\n\ndiv.nav-right > #toc-local > a.list-group-item.active {\n  border-left-color: #4684b6;\n  color: #212529;\n}\n\ndiv.nav-right > #toc-local > a.list-group-item:hover {\n  color: white;\n  text-decoration: none;\n}\n\n\n/*----------------------------------------------------------------------------*\n * Main area\n *----------------------------------------------------------------------------*/\n\nbody > div.main-area {\n  display: flex;\n  margin-bottom: 0;\n  margin-left: var(--left-menu-width);\n  margin-right: 0;\n  margin-top: var(--top-menu-height);\n  min-height: calc(100vh - var(--top-menu-height));\n}\n\n@media (max-width: 900px){\n  body > div.main-area {\n    margin-left: 0;\n  }\n\n  body > div.main-area > div.document-wrapper {\n    margin: 0;\n  }\n\n  body > div.main-area > div.expander {\n    display: none;\n  }\n}\n\ndiv.document-wrapper {\n  flex: 25;\n  max-width: var(--document-width);\n  min-width: min(95vw, calc(var(--document-width) * 2/3));\n  margin-bottom: 25px;\n  margin-left: var(--document-x-margin);\n  margin-top: 25px;\n}\n\ndiv.warning {\n  background: #9b6814;\n  color: white;\n  border-radius: 2pt;\n  box-shadow: 2px 2px 3px #ffc43c;\n  padding: 16px;\n  margin-bottom: 16px;\n}\n\ndiv.warning .version {\n  font-style: italic;\n  text-decoration: dashed underline;\n}\n\n.hidden {\n  display: none;\n}\n\ndiv.document {\n  background-color: #282828;\n  color: #D6D6D6;\n  border-radius: 2pt;\n  box-shadow: 2px 2px 3px 0 #000;\n  padding: var(--document-padding);\n}\n\ndiv.document::after { /* clearfix */\n  content: '';\n  display: block;\n  clear: both;\n}\n\n\ndiv.copyright {\n  color: #cccccc;\n  font-size: 12px;\n  margin-top: 3px;\n  opacity: 0.5;\n}\n\n.copyright a {\n  color: #ff9500;\n  text-decoration: underline;\n}\ndiv.document h1 {\n  border-bottom: 1px solid #555;\n  color: #ccc;\n  font-size: 2em;\n  font-weight: bold;\n  margin: 0 0 0.6em 0;\n}\n\ndiv.document h2 {\n  color: #ddd;\n  font-size: 1.5em;\n  margin: 1.5em 0 .5em 0;\n}\n\ndiv.document h3 {\n  font-size: 1.25em;\n  margin: 1.3em 0 .5em 0;\n}\n\ndiv.document :is(h1, h2, h3, h4) > a.headerlink {\n  display: none;\n  font-size: 60%;\n  visibility: hidden;\n}\n\ndiv.document :is(h1, h2, h3, h4) > a.headerlink::after {\n  content: \"\\f0c1\";\n  font-family: var(--font-awesome);\n  left: -3px;\n  position: relative;\n  top: -2px;\n  visibility: visible;\n}\n\ndiv.document :is(h1, h2, h3, h4):hover > a.headerlink {\n  display: initial;\n}\n\ndiv.document :is(h1, h2, h3, h4) > code {\n  background: none;\n  color: inherit;\n  padding: 0;\n}\n\n\n/*----------------------------------------------------------------------------*\n * Previous/next buttons\n *----------------------------------------------------------------------------*/\n\n.prev-next-area {\n  display: flex;\n  padding: 20px 0;\n}\n\n.prev-next-area > a {\n  align-items: center;\n  color: #bbb;\n  display: flex;\n  padding: 10px;\n  max-width: 45%;\n  overflow-x: hidden;\n}\n\n.prev-next-area > a.left-prev {\n  border-radius: 100px 0 0 100px;\n  padding-left: 15px;\n}\n\n.prev-next-area > a.right-next {\n  border-radius: 0 100px 100px 0;\n  padding-right: 15px;\n}\n\n.prev-next-area > a:hover {\n  background: #323232;\n  box-shadow: 1px 1px 2px #000;\n}\n\n.prev-next-area > a > i {\n  font-size: 200%;\n  font-weight: bold;\n}\n\n.prev-next-area > a > .prev-next-info {\n  flex-direction: column;\n  margin: 0 0.5em;\n}\n\n.prev-next-area > a > .prev-next-info > p {\n  margin: 0 0.3em;\n  line-height: 1.3em;\n}\n\n.prev-next-area > a > .prev-next-info > p.prev-next-subtitle {\n  color: #ffffe8;\n}\n\n.prev-next-area > a:hover > .prev-next-info > p.prev-next-title {\n  color: #8CE4FF;\n}\n\n\n/*----------------------------------------------------------------------------*\n * Code\n *----------------------------------------------------------------------------*/\n\n div[class^=\"highlight-\"] {\n  position: relative;\n  overflow-x: auto;\n  padding: 2px;\n  padding-top: 11px;\n  margin-top: -11px;\n}\n\npre {\n  background: #202020;\n  border: none;\n  border-radius: 0.4em;\n  box-shadow: 0px 0px 3px black;\n  color: #888;\n  font-family: var(--font-mono);\n  font-size: 85%;\n  line-height: 125%;\n  margin: 0;\n  padding: 10px;\n  min-height: 25px;\n}\n\ndiv[class^=\"highlight-\"] pre:before {\n  background: black;\n  border-radius: 5px;\n  color: #58d0ff;\n  font-size: 80%;\n  padding: 1px 8px;\n  position: absolute;\n  right: 0;\n  top: 0;\n}\n\ndiv.highlight-shell pre:before {\n  content: \"shell\";\n  color: #666;\n}\ndiv.highlight-dart pre:before {\n  content: \"dart\";\n}\ndiv.highlight-yaml pre:before {\n  content: \"yaml\";\n  color: #5f5;\n}\ndiv.highlight-yarn pre:before {\n  content: \"yarn\";\n  color: #a6e22e;\n}\ndiv.highlight-text pre:before {\n  content: \"text\";\n  color: #666;\n}\nspan.keyword {\n  color: #ffffcA;\n}\n\n/*----------------------------------------------------------------------------*\n * Search elements\n *----------------------------------------------------------------------------*/\n\n/* On the search page the first header is H2 instead of H1 */\ndiv#search-results > h2 {\n  margin-top: 0;\n}\n\np.search-summary {\n    opacity: 35%;\n}\n\nspan.highlighted {\n  background: inherit; /* undo style from basic.css */\n}\n\nspan.highlighted:not(.off) {\n  background: #ffe000;\n  color: black;\n  padding: 2px 4px;\n  border-radius: 4px;\n}\n\n\ndiv.highlight-box {\n  background: black linear-gradient(rgba(var(--highlight-color), 100%),\n                                    rgba(var(--highlight-color), 85%),\n                                    rgba(var(--highlight-color), 50%));\n  border: 1px solid #5a441b;\n  border-style: none solid;\n  box-sizing: border-box;\n  display: flex;\n  height: 100%;\n  left: calc(var(--left-menu-width) + var(--document-x-margin));\n  margin: 0;\n  padding: 3px 8px;\n  position: absolute;\n  top: 0;\n}\n\n@media (max-width: 900px) {\n  div.highlight-box {\n    left: 54px;\n  }\n}\n\ndiv.highlight-box div.title {\n  color: rgba(0, 0, 0, 0.7);\n  font-size: 12px;\n  font-variant: small-caps;\n  line-height: 16px;\n  opacity: 0.5;\n}\n\ndiv.highlight-box div.content {\n  display: flex;\n}\n\ndiv.highlight-box div.content > span {\n  background: rgb(var(--highlight-color));\n  border: 1px solid rgba(0, 0, 0, 0.6);\n  border-radius: 6px;\n  color: #000;\n  cursor: pointer;\n  margin-right: 4px;\n  padding: 1px 6px 2px 6px;\n  user-select: none;\n}\n\ndiv.highlight-box div.content > span.off {\n  background: none;\n  border-style: dashed;\n}\n\ndiv.highlight-box button.close {\n  background: transparent;\n  border: none;\n  color: black;\n  cursor: pointer;\n  font-size: 20px;\n  margin: 0 0 0 20px;\n}\n\n\n/*----------------------------------------------------------------------------*\n * Admonitions\n *----------------------------------------------------------------------------*/\n\ndiv.admonition {\n  background: #333333;\n  border-left: 3px solid var(--admonition-border-color);\n  border-radius: 5px;\n  box-shadow: 1px 1px 4px black;\n  margin: 12pt 0;\n  padding: 0 0 6pt 0;\n  position: relative;\n}\n\ndiv.admonition > p.admonition-title {\n  background-color: var(--admonition-title-background-color);\n  border-radius: 5px 5px 0 0;\n  color: silver;\n  margin: 0 0 6pt 0;\n  padding: 4px 12px 4px 30px;\n}\n\ndiv.admonition > p.admonition-title:before {\n  color: var(--admonition-icon-color);\n  content: var(--admonition-icon);\n  font-family: var(--font-awesome);\n  left: 8px;\n  padding-right: 4pt;\n  position: absolute;\n}\n\ndiv.admonition > p {\n  margin: 0 12px 12px 30px;\n}\n\ndiv.admonition.warning {\n  --admonition-border-color: orange;\n  --admonition-icon: '\\f071';\n  --admonition-icon-color: gold;\n  --admonition-title-background-color: #a16820;\n}\n\ndiv.admonition.error {\n  --admonition-border-color: red;\n  --admonition-icon: '\\f188';\n  --admonition-icon-color: #ff7c7c;\n  --admonition-title-background-color: #460202;\n}\n\ndiv.admonition.note {\n  --admonition-border-color: #6eb1cc;\n  --admonition-icon: '\\f05a';\n  --admonition-icon-color: #ace3fa;\n  --admonition-title-background-color: #235179;\n}\n\ndiv.admonition.seealso {\n  --admonition-border-color: #54d452;\n  --admonition-icon: '\\f064';\n  --admonition-icon-color: #acfab6;\n  --admonition-title-background-color: #28513140;\n}\n\ndiv.admonition.admonition-deprecated {\n  --admonition-border-color: black;\n  --admonition-icon: '\\f1f8';\n  --admonition-icon-color: #555;\n  --admonition-title-background-color: #1c1c1c;\n}\n\n\npre, div[class*=\"highlight-\"] {\n  clear: none;\n}\n\nh2, h3, h4, h5, h6 {\n  clear: both;\n}\n\ntable.first-col-align-center tr td:first-child,\ntable.first-col-align-center tr th:first-child {\n  text-align: center;\n}\n\ntable.docutils td {\n  border-color: rgba(255, 255, 255, 0.125);\n  vertical-align: top;\n}\n\ntable.docutils th {\n  color: #ff9500;\n}\n\n/* This ensures that when navigating to a link within a page, the link\n   will be visible and not obscured by the top-page header. */\n:target::before {\n  content: '';\n  display: block;\n  height: calc(25px + var(--top-menu-height));\n  margin-top: calc(-25px - var(--top-menu-height));\n  visibility: hidden;\n}\n"
  },
  {
    "path": "doc/_sphinx/theme/layout.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n  <title>{{ title|striptags|e + \" &#8212; \"|safe + docstitle|e }}</title>\n\n  {%- block css %}\n  <!-- Stylesheets -->\n  {%- for css in css_files %}\n  {{ css_tag(css) }}\n  {%- endfor %}\n  {%- endblock %}\n\n  {%- block scripts %}\n  <!-- Scripts -->\n  {%- for js in script_files %}\n  {{ js_tag(js) }}\n  {%- endfor %}\n  <!--script src=\"searchindex.js\" defer></script-->\n  <script src=\"https://cdn.jsdelivr.net/npm/virtual-webgl\"></script>\n  {%- endblock %}\n\n  {%- block links %}\n  <!-- Links -->\n  {%- if pageurl %}\n  <link rel=\"canonical\" href=\"{{ pageurl|e }}\" />\n  {%- endif %}\n  <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n  <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n  <link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,400;0,700;1,400;1,700&display=swap\">\n  <link rel=\"preload\" as=\"font\" type=\"font/woff2\" crossorigin=\"\" href=\"{{ pathto('_static/fontawesome/fa-brands-400.woff2', 1) }}\">\n  <link rel=\"preload\" as=\"font\" type=\"font/woff2\" crossorigin=\"\" href=\"{{ pathto('_static/fontawesome/fa-solid-900.woff2', 1) }}\">\n  <link rel=\"stylesheet\" href=\"{{ pathto('_static/fontawesome/all.min.css', 1) }}\">\n  <link rel=\"shortcut icon\" href=\"{{ pathto('_static/favicon.ico', 1)|e }}\"/>\n  {%- if hasdoc('genindex') %}\n  <link rel=\"index\" title=\"{{ _('Index') }}\" href=\"{{ pathto('genindex') }}\" />\n  {%- endif %}\n  {%- if hasdoc('search') %}\n  <link rel=\"search\" title=\"{{ _('Search') }}\" href=\"{{ pathto('search') }}\" />\n  {%- endif %}\n  {%- if hasdoc('copyright') %}\n  <link rel=\"copyright\" title=\"{{ _('Copyright') }}\" href=\"{{ pathto('copyright') }}\" />\n  {%- endif %}\n  {%- if next %}\n  <link rel=\"next\" title=\"{{ next.title|striptags|e }}\" href=\"{{ next.link|e }}\" />\n  {%- endif %}\n  {%- if prev %}\n  <link rel=\"prev\" title=\"{{ prev.title|striptags|e }}\" href=\"{{ prev.link|e }}\" />\n  {%- endif %}\n  {%- endblock %}\n</head>\n\n<body data-spy=\"scroll\" data-target=\"#toc-local\" data-offset=\"80\">\n\n<div class=\"top-bar\">\n  <button id=\"menu-button\" class=\"btn\" title=\"Toggle menu\"><i class=\"fa fa-bars\"></i></button>\n  <script type=\"text/javascript\">\n    jQuery(function(){\n      $(\"#menu-button\").click(function() {\n        $(this).toggleClass(\"active\");\n        $(\".sidebar-left-area\").toggleClass(\"active\");\n      });\n\n      $('div.nav-left li > a:not(:only-child)').wrap('<div class=submenu></div>');\n      $('div.nav-left div.submenu').prepend('<a class=arrow>&nbsp;</a>');\n      $('div.nav-left div.submenu > a.arrow')\n        .click(function() {\n          $(this.parentElement.parentElement).toggleClass('current');\n        });\n\n      // This function ensures that when navigating to internal targets within the page (such as\n      // a section header), those targets will be visible to the user and not obscured by the menu\n      // bar at the top of the page.\n      function scrollToHashTarget() {\n        if (location.hash !== location.oldHash) {\n          window.scrollBy(0, -60);\n          location.oldHash = location.hash;\n        }\n      }\n      window.location.oldHash = '';\n      window.addEventListener('hashchange', scrollToHashTarget);\n      scrollToHashTarget();\n    });\n  </script>\n\n  <a href=\"{{pathto('index')}}\" class=\"logo_image\" aria-label=\"Navigate to home page\">\n    <img src=\"{{pathto('_static/logo_flame.png', 1)}}\" alt=\"Flame logo: a fiery symbol along with the FLAME wordmark.\">\n  </a>  \n  <div class=\"highlight-box\" role=\"alert\" style=\"display:none\">\n    <div>\n      <div class=\"title\">highlighted:</div>\n      <div class=\"content\" id=\"highlight-content\"></div>\n    </div>\n    <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Dismiss\">×</button>\n  </div>\n  <div class=\"expander\"></div>\n\n  <div class=\"versions-placeholder\"></div>\n  <a href=\"https://discord.com/invite/pxrBmy4\" id=\"discord-button\" class=\"btn\"\n     title=\"Blue Fire Discord server\">\n    <i class=\"fab fa-discord\"></i>\n  </a>\n  <a href=\"https://github.com/flame-engine/flame/\" id=\"github-button\" class=\"btn\"\n     title=\"GitHub repository\">\n    <i class=\"fab fa-github\"></i>\n  </a>\n</div>\n\n<div class=\"sidebar-left-area\">\n  <div class=\"sidebar-left-bg\"></div>\n  <div class=\"sidebar-left\">\n    <div class=\"searchbox\" role=\"search\">\n      <form id=\"search-form\" action=\"{{ pathto('search') }}\" method=\"get\">\n        <i class=\"icon fa fa-search\"></i>\n        <input type=\"search\" class=\"form-control\" id=\"search-input\" name=\"q\"\n               placeholder=\"Search the docs...\" autocomplete=\"off\" />\n      </form>\n    </div>\n    <div class=\"nav-left\" role=\"navigation\" aria-label=\"Main\">\n      {{ toctree(maxdepth=0, collapse=False, includehidden=True, titles_only=True) }}\n    </div>\n  </div>\n</div>\n\n<div class=\"main-area\">\n  <div class=\"document-wrapper\">\n    <div class=\"warning hidden\" id=\"version-warning\">\n      <p><strong>Warning:</strong> you are currently viewing the docs for an older\n      version <span class=\"version\"></span> of Flame.</p>\n      <p>Please <a href=\"/\">click here</a> to go see the documentation for the latest\n      released version.</p>\n    </div>\n    <div class=\"document\" role=\"main\">\n      {% block body %} {% endblock %}\n    </div>\n    <div class=\"copyright\">\n      The content on this page is licensed under the <a href=\"https://creativecommons.org/licenses/by/4.0/\">CC BY 4.0 License</a>,\n      and code samples under the <a href=\"https://raw.githubusercontent.com/flame-engine/flame/main/LICENSE\">MIT License</a>.\n    </div>\n    <div class=\"prev-next-area\">\n      {%- if prev %}\n      <a class=\"left-prev\" id=\"prev-link\" href=\"{{ prev.link|e }}\" title=\"previous page\">\n        <i class=\"fa fa-angle-left\"></i>\n        <div class=\"prev-next-info\">\n          <p class=\"prev-next-subtitle\">Previous:</p>\n          <p class=\"prev-next-title\">{{ prev.title|striptags|e }}</p>\n        </div>\n      </a>\n      {%- endif %}\n      <div class='expander'></div>\n      {%- if next %}\n      <a class=\"right-next\" id=\"next-link\" href=\"{{ next.link|e }}\" title=\"next page\">\n        <div class=\"prev-next-info\">\n          <p class=\"prev-next-subtitle\">Next:</p>\n          <p class=\"prev-next-title\">{{ next.title|striptags|e }}</p>\n        </div>\n        <i class=\"fa fa-angle-right\"></i>\n      </a>\n      {%- endif %}\n    </div>\n  </div>\n\n  <div class=\"sidebar-right\">\n    {# Local table of contents #}\n    <div class=\"nav-right\" role=\"navigation\" aria-label=\"table of contents\">\n      {{ get_local_toc() }}\n    </div>\n  </div>\n\n  <div class=\"expander\"></div>\n</div>\n\n{% block footer %}\n{% endblock %}\n\n</body>\n</html>\n"
  },
  {
    "path": "doc/_sphinx/theme/search.html",
    "content": "{#\nbasic/search.html\n~~~~~~~~~~~~~~~~~\n\nTemplate for the search page.\n\n:copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS.\n:license: BSD, see LICENSE for details.\n#}\n{%- extends \"layout.html\" %}\n{% set title = 'Search' %}\n\n{%- block scripts %}\n{{ super() }}\n<script type=\"text/javascript\" src=\"{{ pathto('_static/searchtools.js', 1) }}\"></script>\n<script type=\"text/javascript\" src=\"{{ pathto('_static/language_data.js', 1) }}\"></script>\n{%- endblock %}\n\n{% block footer %}\n<script type=\"text/javascript\">\n    jQuery(function() { Search.loadIndex(\"{{ pathto('searchindex.js', 1) }}\"); });\n</script>\n{# this is used when loading the search index using $.ajax fails,\nsuch as on Chrome for documents on localhost #}\n<script type=\"text/javascript\" id=\"searchindexloader\"></script>\n{{ super() }}\n{% endblock %}\n\n{% block body %}\n<noscript>\n  <div id=\"fallback\" class=\"alert alert-info\">\n    Please turn on JavaScript to enable the search functionality.\n  </div>\n</noscript>\n\n<div id=\"search-results\">\n</div>\n{% endblock %}\n"
  },
  {
    "path": "doc/_sphinx/theme/theme.conf",
    "content": "[theme]\ninherit = basic\nstyle = flames.css\npygments_style = sphinx\n\n[options]\nlogo =\ncollapse_navigation = True\nsticky_navigation = True\nnavigation_depth = 4\nincludehidden = True\ntitles_only =\n"
  },
  {
    "path": "doc/bridge_packages/bridge_packages.md",
    "content": "# Bridge Packages\n\n:::{package} flame_3d [WIP]\n\nUses Flutter GPU / Impeller low-level level access to provide an ergonomic and **very experimental**\n3D rendering engine on top of Flame.\n\n:::{package} flame_audio\n\nPlay multiple audio files simultaneously (bridge package for [AudioPlayers]).\n:::\n\n:::{package} flame_behaviors\n\nApply separation of concerns to game logic in the form of Entities and Behaviors.\n:::\n\n:::{package} flame_bloc\n\nA predictable state management library (bridge package for [Bloc]).\n:::\n\n:::{package} flame_console\n\nA terminal overlay for Flame games which allows developers to debug and interact\nwith their games.\n:::\n\n:::{package} flame_fire_atlas\n\nCreate texture atlases for games (bridge package for [FireAtlas]).\n:::\n\n:::{package} flame_forge2d\n\nA Box2D physics engine (bridge package for [Forge2D]).\n:::\n\n:::{package} flame_isolate\n\nUse isolates to offload heavy computations to another thread.\n:::\n\n:::{package} flame_lottie\n\nUse Lottie animations in Flame (bridge package for [Lottie]).\n:::\n\n:::{package} flame_network_assets\n\nFetch assets over the network.\n:::\n\n:::{package} flame_oxygen\n\nReplace FCS with the Oxygen Entity Component System.\n:::\n\n:::{package} flame_rive\n\nCreate interactive animations (bridge package for [Rive]).\n:::\n\n:::{package} flame_riverpod\n\nA reactive caching and data-binding framework (bridge package for [Riverpod]).\n:::\n\n:::{package} flame_spine\n\nUse Spine skeletal animations (bridge package for [Spine]).\n:::\n\n:::{package} flame_splash_screen\n\nAdd the \"Powered by Flame\" splash screen.\n:::\n\n:::{package} flame_svg\n\nDraw SVG files in Flutter (bridge package for [flutter_svg]).\n:::\n\n:::{package} flame_texturepacker\n\nLoad sprite sheets created by TexturePacker tool (bridge package for\n[TexturePacker]).\n:::\n\n:::{package} flame_tiled\n\n2D tilemap level editor (bridge package for [Tiled]).\n:::\n\n[AudioPlayers]: https://github.com/bluefireteam/audioplayers\n[Bloc]: https://github.com/felangel/bloc\n[FireAtlas]: https://github.com/flame-engine/fire-atlas\n[Forge2D]: https://github.com/flame-engine/forge2d\n[Lottie]: https://pub.dev/packages/lottie\n[Rive]: https://rive.app/\n[Riverpod]: https://github.com/rrousselGit/riverpod\n[Spine]: https://pub.dev/packages/spine_flutter\n[TexturePacker]: https://www.codeandweb.com/texturepacker\n[Tiled]: https://www.mapeditor.org/\n[flutter_svg]: https://github.com/dnfield/flutter_svg\n\n\n```{toctree}\n:hidden:\n\nflame_audio                 <flame_audio/flame_audio.md>\nflame_behaviors             <flame_behaviors/flame_behaviors.md>\nflame_bloc                  <flame_bloc/flame_bloc.md>\nflame_fire_atlas            <flame_fire_atlas/flame_fire_atlas.md>\nflame_forge2d               <flame_forge2d/flame_forge2d.md>\nflame_isolate               <flame_isolate/flame_isolate.md>\nflame_lottie                <flame_lottie/flame_lottie.md>\nflame_network_assets        <flame_network_assets/flame_network_assets.md>\nflame_oxygen                <flame_oxygen/flame_oxygen.md>\nflame_rive                  <flame_rive/flame_rive.md>\nflame_riverpod              <flame_riverpod/flame_riverpod.md>\nflame_splash_screen         <flame_splash_screen/flame_splash_screen.md>\nflame_spine                 <flame_spine/flame_spine.md>\nflame_svg                   <flame_svg/flame_svg.md>\nflame_texturepacker         <flame_texturepacker/flame_texturepacker.md>\nflame_tiled                 <flame_tiled/flame_tiled.md>\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_3d/basic_concepts.md",
    "content": "# Basic Concepts\n\nBefore delving into the details, let's explore some key concepts and terminology used in 3D\nrendering and how they apply to `flame_3d`.\n\n\n## Vertices, Surfaces, Meshes, and Models\n\nA **vertex** is a point in 3D space (think `Vector3`), with a few more properties such as a normal\nvector, texture coordinates, color and joint information.\n\nUsing vertices arranged in triangles, you can create a **surface**. A surface is simply a collection\nof triangles that share the same material.\n\nThen, you can group multiple surfaces into a **mesh** - a \"solid\" shape in 3D world. A mesh can be\nas simple as a cube or a sphere, and many pre-defined options are provided by Flame, such as `Cube`,\n`Sphere`, `Plane`, and `Cylinder`. You can also create custom meshes by providing your own vertices\nand triangle indices.\n\nFinally, a **model** is a collection of meshes, usually loaded from an external file. Models can\nalso contain animations, which can be played on the model class.\n\nTypically, for most games, unless you are working with basic shapes and polygons, all your\ncomponents will be `ModelComponent`s loaded from external model files. Currently, `flame_3d`\nsupports the formats `OBJ` (for very simple models, only vertices) and `GLTF` or `GLB` (for complex\nmodels, with custom materials and animations).\n\n\n## Component Hierarchy\n\nThe main building block of a 3D scene is the `Component3D`, which is the base class for all 3D:\n\n```{include} diagrams/flame_3d_components.md\n```\n\nInstead of extending `World` directly, your `FlameGame`'s world must be an instance `World3D`. You\ncan add normal Flame `Component`s to it, which will be rendered as usual, and you can also add\n`Component3D`s, which will be rendered in 3D space. You can also add `LightComponent`s to the\n`World3D` (note: as of now, they need to be added to the root of the world), which will add light\nsources to the scene. Light sources are not rendered themselves, just change how other\n`Component3D`s are rendered.\n\n"
  },
  {
    "path": "doc/bridge_packages/flame_3d/flame_3d.md",
    "content": "# flame_3d\n\n```{note}\n`flame_3d` is an extremely **experimental** package that is subject to many\nbreaking changes at any time and without any warning. We do not consider it to\nbe ready for production; but if you want to trail-blaze in a fresh, new cool\nspace, feel free to give it a try and help us improve it!\n```\n\n```{toctree}\nGetting Started <getting_started.md>\nBasic Concepts <basic_concepts.md>\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_3d/getting_started.md",
    "content": "# Getting Started\n\nThere are a few important things to note before you even start playing with this package.\n\nThe GPU-powered 3D support depends on the still experimental\n[Flutter GPU](https://github.com/flutter/flutter/wiki/Flutter-GPU), which in turn depends on\nImpeller. This means a few things:\n\n- Since **Flutter GPU** and **Impeller** are still experimental, this package is also considered\n  experimental and may have breaking changes at any time.\n- The only platforms that we have explicitly tested so far for support were Android, iOS, and macOS.\n- You need to enable Impeller, if not already enabled by default (see below).\n\n\n## Enabling Impeller\n\nThen, you need to enable Impeller, if not already enabled by default. For example, for macOS, add\nthe following to the generated `macos/runner/Info.plist` directory:\n\n```xml\n<dict>\n  ...\n  <key>FLTEnableImpeller</key>\n  <true/>\n  <key>FLTEnableFlutterGPU</key>\n  <true/>\n</dict>\n```\n\nAlternatively, you can run Flutter with this flag instead:\n\n```bash\nflutter run --enable-flutter-gpu\n```\n\n\n## Playground & Examples\n\nYou can find a \"playground\"-style example of using `flame_3d` in the `packages/flame_3d/example`\ndirectory. It contains multiple \"setups\" that you can switch using the built-in console commands.\n\nYou can also find three more complex examples below:\n\n- [Defend the Donut](https://github.com/flame-engine/defend_the_donut/) - a simple first-person\n  spaceship game involving a giant space donut.\n- [Collect the Donut](https://github.com/luanpotter/collect_the_donut) - a simple third-person game\n  where you control a low-poly rogue and can attack skeletons and collect donuts.\n- [Flutter & Friends 2025 Workshop](https://github.com/luanpotter/flutter_and_friends_slides) - our\n  talk at Flutter & Friends 2025, which includes a demo showcasing how to setup multiple camera\n  styles.\n"
  },
  {
    "path": "doc/bridge_packages/flame_audio/audio.md",
    "content": "# Audio\n\nPlaying audio is essential for most games, so we made it simple!\n\nFirst you have to add [flame_audio](https://github.com/flame-engine/flame_audio) to your dependency\nlist in your `pubspec.yaml` file:\n\n```yaml\ndependencies:\n  flame_audio: VERSION\n```\n\nThe latest version can be found on [pub.dev](https://pub.dev/packages/flame_audio/install).\n\nAfter installing the `flame_audio` package, you can add audio files in the assets section of your\n`pubspec.yaml` file. Make sure that the audio files exists in the paths that you provide.\n\nThe default directory for `FlameAudio` is `assets/audio` (which can be changed by providing your own\ninstance of `AudioCache`).\n\nFor the examples below, your `pubspec.yaml` file needs to contain something like this:\n\n```yaml\nflutter:\n  assets:\n    - assets/audio/explosion.mp3\n    - assets/audio/music.mp3\n```\n\nThen you have the following methods at your disposal:\n\n```dart\nimport 'package:flame_audio/flame_audio.dart';\n\n// For shorter reused audio clips, like sound effects\nFlameAudio.play('explosion.mp3');\n\n// For looping an audio file\nFlameAudio.loop('music.mp3');\n\n// For playing a longer audio file\nFlameAudio.playLongAudio('music.mp3');\n\n// For looping a longer audio file\nFlameAudio.loopLongAudio('music.mp3');\n\n// For background music that should be paused/played when the pausing/resuming\n// the game\nFlameAudio.bgm.play('music.mp3');\n```\n\nThe difference between the `play/loop` and `playLongAudio/loopLongAudio` is that `play/loop` makes\nuse of optimized features that allow sounds to be looped without gaps between their iterations, and\nalmost no drop on the game frame rate will happen. You should whenever possible, prefer the former\nmethods.\n\n`playLongAudio/loopLongAudio` allows for audios of any length to be played, but they do create frame\nrate drop, and the looped audio will have a small gap between iterations.\n\nYou can use [the `Bgm` class](bgm.md) (via `FlameAudio.bgm`) to play looping background music\ntracks. The `Bgm` class lets Flame automatically manage the pausing and resuming of background music\ntracks when the game is sent to background or comes back to the foreground.\n\nYou can use [the `AudioPool` class](audio_pool.md) if you want to fire quick sound effects in a very\nefficient manner. `AudioPool` will keep a pool of `AudioPlayer`s preloaded with a given sound, and\nallow you to play them very fast in quick succession.\n\nSome file formats that work across devices and that we recommend are: MP3, OGG and WAV.\n\nThis bridge library (flame_audio) uses [audioplayers](https://github.com/bluefireteam/audioplayers)\nin order to allow for playing multiple sounds simultaneously (crucial in a game). You can check the\nlink for a more in-depth explanation.\n\nBoth on `play` and `loop` you can pass an additional optional double parameter, the `volume`\n(defaults to `1.0`).\n\nBoth the `play` and `loop` methods return an instance of an `AudioPlayer` from the\n[audioplayers](https://github.com/bluefireteam/audioplayers) lib, that allows you to stop, pause and\nconfigure other parameters.\n\nIn fact you can always use `AudioPlayer`s directly to gain full control over how your audio is played\n-- the `FlameAudio` class is just a wrapper for common functionality.\n\n\n## Caching\n\nYou can pre-load your assets. Audios need to be stored in the memory the first time they\nare requested; therefore, the first time you play each mp3 you might get a delay. In order to\npre-load your audios, just use:\n\n```dart\nawait FlameAudio.audioCache.load('explosion.mp3');\n```\n\nYou can load all your audios in the beginning in your game's `onLoad` method so that they always\nplay smoothly. To load multiple audio files, use the `loadAll` method:\n\n```dart\nawait FlameAudio.audioCache.loadAll(['explosion.mp3', 'music.mp3']);\n```\n\nFinally, you can use the `clear` method to remove a file that has been loaded into the cache:\n\n```dart\nFlameAudio.audioCache.clear('explosion.mp3');\n```\n\nThere is also a `clearCache` method, that clears the whole cache.\n\nThis might be useful if, for instance, your game has multiple levels and each has a different\nset of sounds and music.\n"
  },
  {
    "path": "doc/bridge_packages/flame_audio/audio_pool.md",
    "content": "# AudioPool\n\nAn AudioPool is a provider of AudioPlayers that are pre-loaded with local assets to minimize audio\nplayback delays. This is particularly useful in fast-paced games where sound effects need to trigger\nquickly and potentially overlap with each other.\n\nA single AudioPool always plays the same sound, usually a quick sound effect that might need to be\nplayed repeatedly or simultaneously, such as:\n\n- Shooting sounds in a space shooter\n- Jump sounds in a platformer\n- Explosion effects\n- Collecting coins or items\n- Enemy hit sounds\n\n\n## How It Works\n\nAudioPool works by creating and pre-loading a pool of AudioPlayer instances that are all configured\nto play the same sound. When you need to play the sound:\n\n1. The pool gives you an available player from its collection\n2. If no player is available, a new player is created on demand\n3. When a sound finishes playing or is stopped manually, the player is returned to the pool for reuse,\n   unless the pool already has reached its maximum size limit, in which case the player is released\n\nThis approach significantly reduces latency compared to creating new AudioPlayer instances on demand,\nwhile also managing memory by limiting the maximum size of the pool.\n\n\n## Creating an AudioPool\n\nThere are multiple ways to create an AudioPool:\n\n\n### Using FlameAudio Helper\n\nThe simplest approach is to use the helper method in `FlameAudio`, which conveniently uses Flame's\nglobal audio cache:\n\n```dart\nimport 'package:flame_audio/flame_audio.dart';\n\nFuture<void> loadSounds() async {\n  // Create a pool with minimum 1 player and maximum 2 players\n  // This automatically uses Flame's global audio cache\n  AudioPool explosionSoundPool = await FlameAudio.createPool(\n    'explosion.mp3',\n    minPlayers: 1,\n    maxPlayers: 2,\n  );\n}\n```\n\n\n### Creating Directly with Source\n\nYou can also create an AudioPool by directly using the static factory methods:\n\n```dart\nimport 'package:audioplayers/audioplayers.dart';\nimport 'package:flame_audio/flame_audio.dart';\n\nFuture<void> loadSounds() async {\n  // Create a pool with a specific Source\n  AudioPool explosionSoundPool = await AudioPool.create(\n    source: AssetSource('explosion.mp3'),\n    minPlayers: 1,\n    maxPlayers: 2,\n    audioCache: FlameAudio.audioCache, // Optional\n  );\n}\n```\n\n\n### Creating from Asset Path\n\nFor convenience, you can create an AudioPool from just the asset path:\n\n```dart\nimport 'package:flame_audio/flame_audio.dart';\n\nFuture<void> loadSounds() async {\n  AudioPool explosionSoundPool = await AudioPool.createFromAsset(\n    path: 'explosion.mp3',\n    minPlayers: 1,\n    maxPlayers: 2,\n    audioCache: FlameAudio.audioCache, // Optional\n  );\n}\n```\n\nThe parameters are:\n\n- `source` or `path`: The audio source to play (either as a Source object or asset path)\n- `minPlayers`: The initial number of AudioPlayers to create and preload (default: 1)\n- `maxPlayers`: The maximum number of AudioPlayers that can be kept in the pool\n- `audioCache`: Optional AudioCache instance to use\n- `audioContext`: Optional audio context to be used by all players in the pool\n\n\n## Using an AudioPool\n\nOnce you've created an AudioPool, you can start playing sounds:\n\n```dart\n// Play the sound with default volume (1.0)\nfinal stopFunction = await audioPool.start();\n\n// Play the sound with custom volume\nfinal stopFunction = await audioPool.start(volume: 0.5);\n\n// Later, you can stop the sound if needed\nawait stopFunction();\n```\n\nThe `start()` method returns a `StopFunction` that you can call to stop the sound before it completes\nnaturally.\n\n\n## Managing the Pool\n\nAudioPool provides a `dispose()` method to release resources when you no longer need the pool:\n\n```dart\n// When you're done with the pool\nawait audioPool.dispose();\n```\n\n\n## Example Usage\n\nHere's a complete example showing how to use AudioPools in a Flame game:\n\n```dart\nimport 'package:flame/game.dart';\nimport 'package:flame_audio/flame_audio.dart';\n\nclass MyGame extends FlameGame {\n  late AudioPool laserSound;\n  late AudioPool explosionSound;\n\n  @override\n  Future<void> onLoad() async {\n    // Load sound effects into audio pools\n    laserSound = await FlameAudio.createPool(\n      'laser.mp3',\n      minPlayers: 3,\n      maxPlayers: 6,\n    );\n\n    explosionSound = await FlameAudio.createPool(\n      'explosion.mp3',\n      minPlayers: 2,\n      maxPlayers: 4,\n    );\n  }\n\n  void fireLaser() async {\n    // Play the laser sound effect - can be called rapidly\n    final stop = await laserSound.start();\n\n    // If you need to stop the sound early:\n    // await stop();\n  }\n\n  void enemyDestroyed() async {\n    // Play explosion sound effect\n    await explosionSound.start(volume: 0.7);\n  }\n\n  @override\n  Future<void> onRemove() async {\n    await super.onRemove();\n\n    // Clean up resources when the game component is removed\n    await laserSound.dispose();\n    await explosionSound.dispose();\n  }\n}\n```\n\nYou can also find the interactive example in [Flame Basic](https://examples.flame-engine.org/)\n"
  },
  {
    "path": "doc/bridge_packages/flame_audio/bgm.md",
    "content": "# Looping Background Music\n\nWith the `Bgm` class, you can manage looping of background music tracks with regards to application\n(or game) lifecycle state changes.\n\nWhen the application is terminated, or sent to background, `Bgm` will automatically pause\nthe currently playing music track. Similarly, when the application is resumed, `Bgm` will resume the\nbackground music. Manually pausing and resuming your tracks is also supported.\n\nFor this class to function properly, the observer must be registered by calling the following:\n\n```dart\nFlameAudio.bgm.initialize();\n```\n\n**IMPORTANT Note:** The `initialize` function must be called at a point in time where an instance of\nthe `WidgetsBinding` class already exists. Best practice is to put this call inside of your game's\n`onLoad` method`.\n\nIn cases where you're done with background music but still want to keep the application/game\nrunning, use the `dispose` function to remove the observer.\n\n```dart\nFlameAudio.bgm.dispose();\n```\n\nTo play a looping background music track, run:\n\n```dart\nimport 'package:flame_audio/flame_audio.dart';\n\nFlameAudio.bgm.play('adventure-track.mp3');\n```\n\nYou must have an appropriate folder structure and add the files to the `pubspec.yaml` file, as\nexplained in [Flame Audio documentation](audio.md).\n\n\n## Caching music files\n\nThe `Bgm` class will use the static instance of `FlameAudio` for storing cached\nmusic files by default.\n\nSo in order to pre-load music, you can use the same recommendations from the\n[Flame Audio documentation](audio.md).\n\nYou can optionally create your own `Bgm` instances with different backing `AudioCache`s,\nif you so desire.\n\n\n## Methods\n\n\n### Play\n\nThe `play` function takes in a `String` that should be a path that points to the location of the\nmusic file to be played (following the Flame Audio folder structure requirements).\n\nYou can pass an additional optional `double` parameter which is the `volume` (defaults to `1.0`).\n\nExamples:\n\n```dart\nFlameAudio.bgm.play('music/boss-fight/level-382.mp3');\n```\n\n```dart\nFlameAudio.bgm.play('music/world-map.mp3', volume: .25);\n```\n\n\n### Stop\n\nTo stop a currently playing background music track, just call `stop`.\n\n```dart\nFlameAudio.bgm.stop();\n```\n\n\n### Pause and Resume\n\nTo manually pause and resume background music you can use the `pause` and `resume` functions.\n\n`FlameAudio.bgm` automatically handles pausing and resuming the currently playing background music\ntrack. Manually `pausing` prevents the app/game from auto-resuming when focus is given back to the\napp/game.\n\n```dart\nFlameAudio.bgm.pause();\n```\n\n```dart\nFlameAudio.bgm.resume();\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_audio/flame_audio.md",
    "content": "# flame_audio\n\n```{toctree}\nGeneral audio    <audio.md>\nBackground music <bgm.md>\nAudioPool        <audio_pool.md>\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_behaviors/collision-detection.md",
    "content": "# Collision Detection 💥\n\nFlame comes with a powerful built-in [collision detection system](https://docs.flame-engine.org/latest/flame/collision_detection.html),\nbut this API is not strongly typed. Components always get the colliding component as a\n`PositionComponent` and developers need to manually check what type of class it is.\n\n`flame_behaviors` is all about enforcing a strongly typed API. It provides a special behavior\ncalled `CollisionBehavior` that describes the type of entity being targeted for collision. It\ndoes not, however, do any real collision detection. That is done by the\n`PropagatingCollisionBehavior`.\n\nThe `PropagatingCollisionBehavior` handles the collision detection by registering a hitbox on the\nparent entity. When that hitbox has a collision, the `PropagatingCollisionBehavior` checks if the\ncomponent that the parent entity is colliding with contains the target entity type specified in\n`CollisionBehavior`.\n\nThere are two main benefits of letting the `PropagatingCollisionBehavior` handle the collision detection,\nthe first and most important one is performance. By only registering collision callbacks on the\nentities themselves, the collision detection system does not have to go through any \"collidable\"\nbehaviors, for which there could be many per entity. We only do that now if we confirm a collision\nhas happened.\n\nThe second benefit is that it allows for [separation of concerns][separation_of_concerns].\nEach `CollisionBehavior` handles a specific collision use case and ensures that the developer does\nnot have to write a bunch of if statements in one big method to figure out what it is colliding\nwith.\n\nA good use case of this collisional behavior pattern can be seen in the `flame_behaviors`\n[example](https://github.com/flame-engine/flame/tree/main/packages/flame_behaviors/example)\n\n```dart\nclass MyEntityCollisionBehavior\n    extends CollisionBehavior<MyCollidingEntity, MyParentEntity> {\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    MyCollidingEntity other,\n  ) {\n    // We are starting colliding with MyCollidingEntity\n  }\n\n  @override\n  void onCollisionEnd(MyCollidingEntity other) {\n    // We stopped colliding with MyCollidingEntity\n  }\n}\n\nclass MyParentEntity extends Entity {\n  MyParentEntity()\n    : super(\n        behaviors: [\n          PropagatingCollisionBehavior(RectangleHitbox()),\n          MyEntityCollisionBehavior(),\n        ],\n      );\n   ...   \n}\n```\n\n[separation_of_concerns]: https://en.wikipedia.org/wiki/Separation_of_concerns\n"
  },
  {
    "path": "doc/bridge_packages/flame_behaviors/conventions/coding-conventions.md",
    "content": "# Coding Conventions\n\n> [!Note]\n> The following coding conventions are simply recommendations and are completely\n> optional. Feel free to use whatever coding conventions you prefer.\n\n\n## Entities\n\nEntities should not contain any behavioral logic, instead they should be composed of behaviors. This\nallows for more flexible and reusable code. Entities should not do any direct rendering, instead they\nshould add child components to handle the visualization of the entity.\n\n\n### Example of entities\n\n✅ **Good**\n\n```dart\nclass Player extends Entity {\n  Player() {\n    // Behaviors\n    add(JumpingBehavior());\n    add(AttackingBehavior());\n\n    // Components\n    add(SpriteComponent(...));\n  }\n}\n```\n\n❌ **Bad**\n\n```dart\nclass Player extends Entity {\n  void update(double dt) {\n    if (isJumping) {\n      // Jump logic\n    }\n    \n    if (isAttacking) {\n      // Attack logic\n    }\n  }\n\n  void render(Canvas canvas) {\n    // Render player\n    canvas.drawImage(...);\n  }\n}\n```\n\n\n## Behaviors\n\nBehaviors should only contain code related to the behavioral logic it describes. Behaviors should\nnever do any direct rendering.\n\nA behavior is allowed to have its own components for adding extra functionality related to the\nbehavior. For example, a behavior that makes an entity jump could have a `TimerComponent` to ensure\nthat the entity can only jump once every 0.5 seconds. And that behavior can also use a\n`KeyboardHandler` mixin to listen for the jump key to trigger the jump. Any logic that is not\nrelated to the behavior should not be in the behavior.\n\n\n### Example of behaviors\n\n✅ **Good**\n\n```dart\nclass JumpingBehavior extends Behavior<Entity> with KeyboardHandler {\n  bool isJumping = false;\n\n  @override\n  bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {\n    isJumping = keysPressed.contains(LogicalKeyboardKey.space);\n    return true;\n  }\n\n  @override\n  void update(double dt) {\n    if (isJumping) {\n      // Jump logic\n    }\n  }\n}\n```\n\n❌ **Bad**\n\n```dart\nclass JumpBehavior extends Behavior<Entity> with KeyboardHandler {\n  bool isJumping = false;\n  bool isAttacking = false;\n\n  @override\n  bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {\n    isJumping = keysPressed.contains(LogicalKeyboardKey.space);\n    isAttacking = keysPressed.contains(LogicalKeyboardKey.keyA);\n    return true;\n  }\n\n    \n  void update(double dt) {\n    if (isJumping) {\n      // Jump logic\n    }\n    if (isAttacking) {\n      // Attack logic\n    }\n  }\n    \n  void render(Canvas canvas) {\n    // Render something\n    canvas.drawImage(...);\n  }\n}\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_behaviors/conventions/naming-conventions.md",
    "content": "# Naming Conventions\n\n> [!Note]\n> The following naming conventions are simply recommendations and are completely\n> optional. Feel free to use whatever naming conventions you prefer.\n\n\n## Entities\n\n\n### Anatomy of entities\n\n`Type (name)`\n\n\n### Examples of entities\n\n✅ **Good**\n\n```dart\nclass Player extends Entity {}\n\nclass Enemy extends Entity {}\n\nclass Bullet extends Entity {}\n```\n\n❌ **Bad**\n\n```dart\nclass PlayerEntity extends Entity {}\n\nclass EnemyEntity extends Entity {}\n\nclass BulletEntity extends Entity {}\n```\n\n\n## Behaviors\n\n\n### Anatomy of behaviors\n\n`Verb (action)` + `Behavior`\n\n\n### Examples of behaviors\n\n✅ **Good**\n\n```dart\nclass JumpingBehavior extends Behavior<Entity> {}\n\nclass AttackingBehavior extends Behavior<Entity> {}\n```\n\n❌ **Bad**\n\n```dart\nclass JumpBehavior extends Behavior<Entity> {}\n\nclass AttackBehavior extends Behavior<Entity> {}\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_behaviors/event-behaviors.md",
    "content": "# Event Behaviors ⌨\n\nThe `flame_behaviors` package also provides event behaviors. These behaviors are a layer over the\nexisting Flame event mixins for components. These behaviors will trigger when the user interacts\nwith their parent entity. So these events are always relative to the parent entity.\n\n\n## TappableBehavior\n\nThe `TappableBehavior` allows developers to use the [tap events][flame_tap_docs] from Flame on\ntheir entities.\n\n```dart\nclass MyTappableBehavior extends TappableBehavior<MyEntity> {\n  @override\n  void onTapDown(TapDownEvent event) {\n    // Do something on tap down update event.\n  }\n}\n```\n\n\n## DraggableBehavior\n\nThe `DraggableBehavior` allows developers to use the [drag events][flame_drag_docs] from Flame on\ntheir entities.\n\n```dart\nclass MyDraggableBehavior extends DraggableBehavior<MyEntity> {\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    // Do something on drag update event.\n  }\n}\n```\n\n[flame_drag_docs]: https://docs.flame-engine.org/latest/flame/inputs/drag_events.html\n[flame_tap_docs]: https://docs.flame-engine.org/latest/flame/inputs/tap_events.html\n"
  },
  {
    "path": "doc/bridge_packages/flame_behaviors/flame_behaviors.md",
    "content": "# flame_behaviors\n\n```{toctree}\nGetting Started     <getting_started.md>\nEvent Behaviors     <event_behaviors.md>\nCollision Detection <collision_detection.md>\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_behaviors/getting_started.md",
    "content": "# Getting Started 🚀\n\n\n## Prerequisites 📝\n\nIn order to use Flame Behaviors you must have the [Flame package][flame_package_link] added to\nyour project.\n\n> **Note**: Flame Behaviors requires Flame `\">=1.10.0 <2.0.0\"`\n\n\n## Installing 🧑‍💻\n\nLet's start by adding the [`flame_behaviors`][flame_behaviors_package_link] package:\n\n```shell\n# 📦 Add the flame_behaviors package from pub.dev to your project\nflutter pub add flame_behaviors\n```\n\n\n## Entity\n\nThe entity is the building block of a game. It represents a visual game object that can hold\nmultiple `Behavior`s, which in turn define how the entity behaves.\n\n```dart\n// Define a custom entity by extending `Entity`.\nclass MyEntity extends Entity {\n  MyEntity() : super(behaviors: [MyBehavior()]);\n}\n```\n\n\n### Types of Entities\n\nThere are two types of entities in Flame Behaviors:\n\n- `Entity`: A generic entity that can be used to represent any game object.\n- `PositionedEntity`: An entity that has a position and size, it is based on the `PositionComponent`.\n\nThese entities use the `EntityMixin` which is a mixin that provides the basic functionality\nfor an entity.\n\nIf you want to turn any component into an entity, you can use this mixin. For instance, if you want\nto turn a `SpriteComponent` into an entity, you can do the following:\n\n```dart\nclass MySpriteEntity extends SpriteComponent with EntityMixin {\n  Future<void> onLoad() async {\n    // Add behaviors to the entity.\n    add(MyBehavior());\n  }\n}\n```\n\nYou can even turn a `FlameGame` into an entity:\n\n```dart\nclass MyGame extends FlameGame with EntityMixin {\n  Future<void> onLoad() async {\n    // Add behaviors to the entity.\n    add(MyGameBehavior());\n  }\n}\n```\n\n\n## Behavior\n\nA behavior is a component that defines how an entity behaves. It can be attached to any component\nthat uses the `EntityMixin` and handle a specific behavior for that entity. Behaviors can either\nbe generic for any entity or you can specify the specific type of entity that a behavior requires:\n\n```dart\n// Can be added to any type of Entity.\nclass MyGenericBehavior extends Behavior {\n  ...\n}\n\n// Can only be added to MyEntity and subclasses of it.\nclass MySpecificBehavior extends Behavior<MyEntity> {\n  ...\n}\n```\n\n\n### Behavior Composition\n\nEach behavior can have its own `Component`s for adding extra functionality related to the behavior.\nFor instance a `TimerComponent` can implement a time-based behavioral activity:\n\n```dart\nclass MyBehavior extends Behavior {\n  @override\n  Future<void> onLoad() async {\n    await add(TimerComponent(period: 5, repeat: true, onTick: _onTick));\n  }\n\n  void _onTick() {\n    // Do something every 5 seconds.\n  }\n}\n```\n\n> [!NOTE]\n> A `Behavior` is a non-visual component that describes how a visual component (Entity)\n> behaves therefore, a behavior can't have its own `Behavior`s.\n\n\n## What's Next\n\nThe following sections will show you how to use Flame Behaviors for common game development tasks:\n\n- [Handling Game Input](event-behaviors.md)\n- [Handling Collisions](collision-detection.md)\n\nFlame Behaviors also provides some conventions on how to name and organize your code:\n\n- [Naming Conventions](conventions/naming-conventions.md)\n- [Code Conventions](conventions/coding-conventions.md)\n\nTo learn more about how to use Flame Behaviors, check out our [article][article_link].\n\n[flame_package_link]: https://pub.dev/packages/flame\n[flame_behaviors_package_link]: https://pub.dev/packages/flame_behaviors\n[article_link]: https://verygood.ventures/blog/build-games-with-flame-behaviors\n"
  },
  {
    "path": "doc/bridge_packages/flame_bloc/bloc.md",
    "content": "# flame_bloc\n\n`flame_bloc` is a bridge library for using [Bloc](https://bloclibrary.dev/) in your Flame\ngame. `flame_bloc` offers a simple and natural (as in similar to flutter_bloc) way to use blocs and\ncubits inside a FlameGame. Bloc offers way to make game state changes predictable by regulating when\na game state change can occur and offers a single way to change game state throughout an entire\nGame.\n\nTo use it in your game you just need to add `flame_bloc` to your pubspec.yaml, as can be seen in the\n[Flame Bloc example](https://github.com/flame-engine/flame/tree/main/packages/flame_bloc/example)\nand in the pub.dev [installation instructions](https://pub.dev/packages/flame_bloc).\n\n\n## How to use\n\nLets assume we have a bloc that handles player inventory, first we need to make it available to our\ncomponents.\n\nWe can do that by using `FlameBlocProvider` component:\n\n```dart\nclass MyGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    await add(\n      FlameBlocProvider<PlayerInventoryBloc, PlayerInventoryState>(\n        create: () => PlayerInventoryBloc(),\n        children: [\n          Player(),\n          // ...\n        ],\n      ),\n    );\n  }\n}\n```\n\nWith the above changes, the `Player` component will now have access to our bloc.\n\nIf more than one bloc needs to be provided, `FlameMultiBlocProvider` can be used in a similar\nfashion:\n\n```dart\nclass MyGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    await add(\n      FlameMultiBlocProvider(\n        providers: [\n          FlameBlocProvider<PlayerInventoryBloc, PlayerInventoryState>(\n            create: () => PlayerInventoryBloc(),\n          ),\n          FlameBlocProvider<PlayerStatsBloc, PlayerStatsState>(\n            create: () => PlayerStatsBloc(),\n          ),\n        ],\n        children: [\n          Player(),\n          // ...\n        ],\n      ),\n    );\n  }\n}\n```\n\nListening to states changes at the component level can be done with two approaches:\n\nBy using `FlameBlocListener` component:\n\n```dart\nclass Player extends PositionComponent {\n  @override\n  Future<void> onLoad() async {\n    await add(\n      FlameBlocListener<PlayerInventoryBloc, PlayerInventoryState>(\n        listener: (state) {\n          updateGear(state);\n        },\n      ),\n    );\n  }\n}\n```\n\nOr by using `FlameBlocListenable` mixin:\n\n```dart\n\nclass Player extends PositionComponent\n    with FlameBlocListenable<PlayerInventoryBloc, PlayerInventoryState> {\n\n  @override\n  void onNewState(state) {\n    updateGear(state);\n  }\n}\n\n```\n\nIf all your component need is to simply access a bloc, the `FlameBlocReader` mixin can be applied to\na component:\n\n```dart\nclass Player extends PositionComponent\n    with FlameBlocReader<PlayerStatsBloc, PlayerStatsState> {\n\n  void takeHit() {\n    bloc.add(const PlayerDamaged());\n  }\n}\n\n```\n\nNote that one limitation of the mixin is that it can access only a single bloc.\n\n\n## Full Example\n\nYou can check an example\n[here](https://github.com/flame-engine/flame/tree/main/packages/flame_bloc/example).\n"
  },
  {
    "path": "doc/bridge_packages/flame_bloc/bloc_components.md",
    "content": "\n\n# Components\n\n\n## FlameBlocProvider\n\nFlameBlocProvider is a Component which creates and provides a bloc to its children.  \nThe bloc will only live while this component is alive. It is used as a dependency injection (DI)\nwidget so that a single instance of a bloc can be provided to multiple Components within a subtree.\n\nFlameBlocProvider should be used to create new blocs which will be made available to the rest of the\nsubtree.\n\n```dart\nFlameBlocProvider<BlocA, BlocAState>(\n  create: () => BlocA(),\n  children: [...]\n);\n```\n\nFlameBlocProvider can be used to provide an existing bloc to a new portion of the Component tree.\n\n```dart\nFlameBlocProvider<BlocA, BlocAState>.value(\n  value: blocA,\n  children: [...],\n);\n```\n\n\n## FlameMultiBlocProvider\n\nSimilar to FlameBlocProvider, but provides multiples blocs down to the component tree\n\n```dart\nFlameMultiBlocProvider(\n  providers: [\n    FlameBlocProvider<BlocA, BlocAState>(\n      create: () => BlocA(),\n    ),\n    FlameBlocProvider<BlocB, BlocBState>.value(\n      create: () => BlocB(),\n    ),\n    ],\n  children: [...],\n)\n```\n\n\n## FlameBlocListener\n\nFlameBlocListener is Component which can listen to changes in a Bloc state. It invokes\nthe `onNewState` in response to state changes in the bloc. For fine-grained control over when\nthe `onNewState` function is called an optional `listenWhen` can be provided. `listenWhen` takes the\nprevious bloc state and current bloc state and returns a boolean. If `listenWhen` returns\ntrue, `onNewState` will be called with `state`. If `listenWhen` returns false, `onNewState` will not\nbe called with `state`.\n\nalternatively you can use `FlameBlocListenable` mixin to listen state changes on Component.\n\n```dart\nFlameBlocListener<GameStatsBloc, GameStatsState>(\n  listenWhen: (previousState, newState) {\n      // return true/false to determine whether or not\n      // to call listener with state\n  },\n  onNewState: (state) {\n          // do stuff here based on state\n  },\n)\n```\n\n\n## FlameBlocListenable\n\nFlameBlocListenable is an alternative to FlameBlocListener to listen state changes.\n\n```dart\nclass ComponentA extends Component\n    with FlameBlocListenable<BlocA, BlocAState> {\n\n  @override\n  bool listenWhen(PlayerState previousState, PlayerState newState) {\n    // return true/false to determine whether or not\n    // to call listener with state\n  }\n\n  @override\n  void onNewState(PlayerState state) {\n    super.onNewState(state);\n    // do stuff here based on state\n  }\n}\n```\n\n\n## FlameBlocReader\n\nFlameBlocReader is mixin that allows you to read the current state of bloc on Component. It is\nUseful for components that needs to only read a bloc current state or to trigger an event on it. You\ncan have only one reader on Component\n\n\n```dart\n\nclass InventoryReader extends Component\n    with FlameBlocReader<InventoryCubit, InventoryState> {}\n\n    /// inside game\n    \n    final component = InventoryReader();\n    // reading current state\n    var state = component.bloc\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_bloc/flame_bloc.md",
    "content": "# flame_bloc\n\n```{toctree}\nOverview    <bloc.md>\nComponents    <bloc_components.md>\n```\n\n"
  },
  {
    "path": "doc/bridge_packages/flame_console/flame_console.md",
    "content": "# flame_console\n\nFlame Console is a terminal overlay for Flame games which allows developers to debug and interact\nwith their games.\n\nIt offers an overlay that can be plugged in to your `GameWidget` which when activated will show a\nterminal-like interface written with Flutter widgets where commands can be executed to see\ninformation about the running game and components, or perform actions.\n\nIt comes with a set of built-in commands, but it is also possible to add custom commands.\n\n\n## Usage\n\nFlame Console is an overlay, so to use it, you will need to register it in your game widget.\n\nThen, showing the overlay is up to you, below we see an example of a floating action button that will\nshow the console when pressed.\n\n```dart\n@override\nWidget build(BuildContext context) {\n  return Scaffold(\n    body: GameWidget(\n      game: _game,\n      overlayBuilderMap: {\n        'console': (BuildContext context, MyGame game) => FlameConsoleView(\n              game: game,\n              onClose: () {\n                _game.overlays.remove('console');\n              },\n            ),\n      },\n    ),\n    floatingActionButton: FloatingActionButton(\n      heroTag: 'console_button',\n      onPressed: () {\n        _game.overlays.add('console');\n      },\n      child: const Icon(Icons.developer_mode),\n    ),\n  );\n}\n```\n\n\n## Built-in commands\n\n- `help` - List available commands and their usage.\n- `ls` - List components.\n- `rm` - Remove components.\n- `debug` - Toggle debug mode on components.\n- `pause` - Pauses the game loop.\n- `resume` -Resumes the game loop.\n\n\n## Custom commands\n\n Custom commands can be created by extending the `FlameConsoleCommand` class and adding them to the\n the `customCommands` list in the `ConsoleView` widget.\n\n ```dart\nclass MyCustomCommand extends FlameConsoleCommand<MyGame> {\n  @override\n  String get name => 'my_command';\n\n  @override\n  String get description => 'Description of my command';\n\n  // The execute method should return a tuple where the first\n  // element is an error message (in case of failure), and the second\n  // element is the output of the command.\n  @override\n  (String?, String) execute(MyGame game, ArgResults args) {\n    // do something on the game\n    return (null, 'Hello World');\n  }\n}\n```\n\nThen when creating the `ConsoleView` widget, add the custom command to the `customCommands` list.\n\n```dart\nConsoleView(\n  game: game,\n  customCommands: [MyCustomCommand()],\n  onClose: () {\n    _game.overlays.remove('console');\n  },\n),\n```\n\n\n## Customizing the console UI\n\nThe console look and feel can also be customized. When creating the `ConsoleView` widget, there are\na couple of properties that can be used to customize it:\n\n- `containerBuilder`: It is used to created the decorated container where the history and the\ncommand input is displayed.\n- `cursorBuilder`: It is used to create the cursor widget.\n- `historyBuilder`: It is used to create the scrolling element of the history, by default a simple\n`SingleChildScrollView` is used.\n- `cursorColor`: The color of the cursor. Can be used when just wanting to change the color\nof the cursor.\n- `textStyle`: The text style of the console.\n\n"
  },
  {
    "path": "doc/bridge_packages/flame_fire_atlas/fire_atlas.md",
    "content": "# Flame fire atlas\n\nFlame fire atlas is a texture atlas lib for Flame. By using `flame_fire_atlas` one can access images\nand animations stored in a `.fa` texture atlas by referring to them by their named keys.\n\n\n## FireAtlas\n\nFireAtlas is a tool for handling texture atlases. Atlases can be created using the\n[Fire Atlas Editor](https://fire-atlas.flame-engine.org).\n\n\n### Creating Atlas\n\nTo create a texture atlas open [Fire Atlas Editor](https://fire-atlas.flame-engine.org).\n\nSelect new atlas and give the atlas a name, tile width, tile height and an image and press okay.\nThis will take you to the atlas editor.\n\nTo create a new `Sprite` in the atlas, select a portion and click the plus button on top left and\ngive the selection a name and then select type `Sprite` and press `Create Sprite`. You can now\nsee a preview in the right panel of editor.\n\nTo create a new `SpriteAnimation` in the atlas, select a portion and click the plus button on top\nleft and give the selection a name and then select type `Animation` and provide `frame count`\nand `steps times (in milliseconds)` and select the checkbox to loop the animation, and then\npress `Create Animation`. You can now see a preview of the animation in the right panel of the\neditor.\n\nOnce you are done with editing you can download the fire atlas file from top left with\nthe `download` icon button.\n\n\n## Texture atlas\n\nA [Texture atlas](https://en.wikipedia.org/wiki/Texture_atlas) is an image that contains data from\nseveral smaller images that have been packed together to reduce overall dimensions. With it, you\nreduce the number of images loaded and can speed up the loading time of the game.\n\n\n## Usage\n\nTo use the bridge library in your game you just need to add `flame_fire_atlas` to your pubspec.yaml,\nas can be seen in\nthe [Flame Fire Atlas example](https://github.com/flame-engine/flame/tree/main/packages/flame_fire_atlas/example)\nand in the pub.dev [installation instructions](https://pub.dev/packages/flame_fire_atlas).\n\nThen you have the following methods at your disposal:\n\n```dart\nimport 'package:flame_fire_atlas/flame_fire_atlas.dart';\n\n// Load the atlas from your assets\n// file at assets/atlas.fa\nfinal atlas = await FireAtlas.loadAsset('atlas.fa');\n\n//or when inside a game instance, the loadFireAtlas can be used:\n// file at assets/atlas.fa\nfinal atlas = await loadFireAtlas('atlas.fa');\n\n// Get a Sprite with the given key.\nFireAtlas.getSprite('sprite_name')\n\n// Get a SpriteAnimation with the given key.\nFireAtlas.getAnimation('animation_name')\n```\n\nTo use FireAtlas in your game, load the fire atlas file in an `onLoad` method, either in your game\nor a component. Then you can use `getSprite` and `getAnimation` to retrieve the mapped assets.\n\n```dart\nclass ExampleGame extends FlameGame {\n\n  late FireAtlas _atlas;\n\n  @override\n  Future<void> onLoad() async {\n    _atlas = await loadFireAtlas('atlas.fa');\n\n    add(\n      SpriteComponent(\n        size: Vector2(50, 50),\n        position: Vector2(0, 50),\n        sprite: _atlas.getSprite('sprite_name'),\n      ),\n    );\n\n    add(\n      SpriteAnimationComponent(\n        size: Vector2(150, 100),\n        position: Vector2(150, 100),\n        animation: _atlas.getAnimation('animation_name'),\n      ),\n    );\n  }\n\n}\n```\n\n\n## Full Example\n\nYou can check an example\n[here](https://github.com/flame-engine/flame/tree/main/packages/flame_fire_atlas/example).\n\n"
  },
  {
    "path": "doc/bridge_packages/flame_fire_atlas/flame_fire_atlas.md",
    "content": "# flame_fire_atlas\n\n```{toctree}\nOverview    <fire_atlas.md>\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_forge2d/flame_forge2d.md",
    "content": "# flame_forge2d\n\n```{toctree}\nOverview    <forge2d.md>\nJoints    <joints.md>\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_forge2d/forge2d.md",
    "content": "# Forge2D\n\nBlue Fire maintains a ported version of the Box2D physics engine and our\nversion is called Forge2D.\n\nIf you want to use Forge2D specifically for Flame you should use our bridge library\n[flame_forge2d](https://github.com/flame-engine/flame/tree/main/packages/flame_forge2d) and if you\njust want to use it in a Dart project you can use the\n[forge2d](https://github.com/flame-engine/forge2d) library directly.\n\nTo use it in your game you just need to add `flame_forge2d` to your\n`pubspec.yaml`, as can be seen in the [Forge2D\n[example](https://github.com/flame-engine/flame/tree/main/packages/flame_forge2d/example)\nand the pub. dev [installation\ninstructions](https://pub.dev/packages/flame_forge2d)](<https://pub.dev/packages/flame_forge2d>).\n\n\n## Forge2DGame\n\nIf you are going to use Forge2D in your project it can be a good idea to use the Forge2D-specific\n`FlameGame` class, `Forge2DGame`.\n\nIt is called `Forge2DGame` and supports both the special Forge2D components called `BodyComponents`\nas well as normal Flame components.\n\n`Forge2DGame` has a built-in `CameraComponent` and has a zoom level set to 10 by default, so your\ncomponents will be a lot bigger than in a normal Flame game. This is due to the speed limit in the\n`Forge2D` world, which you would hit very quickly if you are using it with `zoom = 1.0`. You can\neasily change the zoom level either by calling `super(zoom: yourZoom)` in your constructor or\ndoing `game.cameraComponent.viewfinder.zoom = yourZoom;` at a later stage.\n\nIf you are previously familiar with Box2D it can be good to know that the whole concept of the\nBox2d world is mapped to `world` in the `Forge2DGame` component and every `Body` that you want to\nuse as a component should be wrapped in a `BodyComponent`, and added to the `world` in your\n`Forge2DGame`.\n\nYou can have have non-physics-related components in your `Forge2DGame` world's component list along\nwith your physical entities. When the update is called, it will use the Forge2D physics engine to\nproperly update every `BodyComponent` and other components in the game will be updated according to\nthe normal `FlameGame` way.\n\nIn `Forge2DGame` the gravity is flipped compared to `Forge2D` to keep the same coordinate system as\nin Flame, so a positive y-axis in the gravity like `Vector2(0, 10)` would be pulling bodies\ndownwards, meanwhile, a negative y-axis would pull them upwards. The gravity can be set directly in\nthe constructor of the `Forge2DGame`.\n\nA simple `Forge2DGame` implementation example can be seen in the\n[examples folder](https://github.com/flame-engine/flame/tree/main/packages/flame_forge2d/example).\n\n\n## Forge2DWorld\n\nThe `Forge2DWorld` is a the world that all your [`BodyComponent`]s live in. In the `Forge2DGame`\nthere is a `Forge2DWorld` instance called `world` by default, which is where you should add your\n`BodyComponent`s.\n\nIf you want to swap between worlds you can create your own `Forge2DWorld` instance and assign it\nto the `Forge2DGame` instance's `world` property, `game.world = Forge2DWorld()`.\n\nIf you would like to re-use a world later and have it keep its physics state you have to make sure\nthat the bodies aren't destroyed when the world is removed from the game. You can do this by\nsetting `world.destroyOnRemove` to false, like `game.world.destroyOnRemove = false;`.\n\n\n## BodyComponent\n\nThe `BodyComponent` is a wrapper for the `Forge2D` body, which is the body that the physics engine\nis interacting with. To create a `BodyComponent` you can either:\n\n- override `createBody()` and create and return your created body;\n- use the default `createBody()` implementation by passing a `BodyDef` instance (and optionally a\nlist of `FixtureDef` instances) to the BodyComponent's constructor;\n- use the default `createBody()` implementation and assign a `BodyDef` instance to `this.bodyDef`,\nand optionally a list of `FixtureDef` instances to `this.fixtureDefs`.\n\nThe `BodyComponent` is by default having `renderBody = true`, since otherwise, it wouldn't show\nanything after you have created a `Body` and added the `BodyComponent` to the game. If you want to\nturn it off you can just set (or override) `renderBody` to false.\n\nJust like any other Flame component you can add children to the `BodyComponent`, which can be very\nuseful if you want to add for example animations or other components on top of your body.\n\nThe body that you create should be defined according to Flame's coordinate system,\nnot according to the coordinate system of Forge2D (where the Y-axis is flipped).\n\n:exclamation: In Forge2D you shouldn't add any bodies as children to other components,\nsince Forge2D doesn't have a concept of nested bodies.\nSo bodies should live on the top level in the physics world, `Forge2DGame.world`.\nSo instead of `add(Weapon()))`, `world.add(Weapon())` should be used (as below), and the `Player`\nshould also of course initially be added to the world.\n\n```dart\nclass Weapon extends BodyComponent  {\n  @override\n  void onLoad() {\n    ...\n  }\n}\n\nclass Player extends BodyComponent  {\n  @override\n  void onLoad() {\n    world.add(Weapon());\n  }\n}\n```\n\nLater you might want to add bullets coming from your weapon, these are added to the world in the\nsame sense, but if they are going to be moving very fast, make sure that you set `isBullet = true`\nto avoid some tunneling problems.\n\n\n## Contact callbacks\n\n`Forge2DGame` provides a simple out-of-the-box solution to propagate contact events.\n\nContact events occur whenever two `Fixture`s meet each other. These events allow listening when\nthese `Fixture`s begin to come in contact (`beginContact`) and cease being in contact\n(`endContact`).\n\nThere are multiple ways to listen to these events. One common way is to use the `ContactCallbacks`\nclass as a mixin in the `BodyComponent` where you are interested in these events.\n\n```dart\nclass Ball extends BodyComponent with ContactCallbacks {\n  ...\n  void beginContact(Object other, Contact contact) {\n    if (other is Wall) {\n      // Do something here.\n    }\n  }\n  ...\n}\n```\n\nFor the above to work, the `Ball`'s `body.userData` or contacting `fixture.userData` must be\nset to a `ContactCallback`. And if `Wall` is a `BodyComponent` it's `body.userData` or contacting\n`fixture.userData` must be set to `Wall`.\n\nIf `userData` is `null` the contact events are ignored, it is `null` by default.\n\nA convenient way of setting `userData` is to assign it when creating the body. For example:\n\n```dart\nclass Ball extends BodyComponent with ContactCallbacks {\n  ...\n\n  @override\n  Body createBody() {\n    ...\n    final bodyDef = BodyDef(\n      userData: this,\n    );\n    ...\n  }\n\n}\n```\n\nEvery time `Ball` and `Wall` begin to come in contact `beginContact` will be called, and once the\nfixtures cease being in contact, `endContact` will be called.\n\nAn implementation example can be seen in the [Flame Forge2D\nexample](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/bridge_libraries/flame_forge2d/utils/balls.dart).\n"
  },
  {
    "path": "doc/bridge_packages/flame_forge2d/joints.md",
    "content": "# Joints\n\nJoints are used to connect two different bodies together in various ways.\nThey help to simulate interactions between objects to create hinges, wheels, ropes, chains etc.\n\nOne `Body` in a joint may be of type `BodyType.static`. Joints between `BodyType.static` and/or\n`BodyType.kinematic` are allowed, but have no effect and use some processing time.\n\nTo construct a `Joint`, you need to create a corresponding subclass of `JointDef`and initialize it\nwith its parameters.\n\nTo register a `Joint` use `world.createJoint`and later use `world.destroyJoint` when you want to\nremove it.\n\n\n## Built-in joints\n\nCurrently, Forge2D supports the following joints:\n\n- [`ConstantVolumeJoint`](#constantvolumejoint)\n- [`DistanceJoint`](#distancejoint)\n- [`FrictionJoint`](#frictionjoint)\n- [`GearJoint`](#gearjoint)\n- [`MotorJoint`](#motorjoint)\n- [`MouseJoint`](#mousejoint)\n- [`PrismaticJoint`](#prismaticjoint)\n- [`PulleyJoint`](#pulleyjoint)\n- [`RevoluteJoint`](#revolutejoint)\n- [`RopeJoint`](#ropejoint)\n- [`WeldJoint`](#weldjoint)\n- WheelJoint\n\n\n### `ConstantVolumeJoint`\n\nThis type of joint connects a group of bodies together and maintains a constant volume within them.\nEssentially, it is a set of [`DistanceJoint`](#distancejoint)s, that connects all bodies one after\nanother.\n\nIt can for example be useful when simulating \"soft-bodies\".\n\n```dart\n  final constantVolumeJoint = ConstantVolumeJointDef()\n    ..frequencyHz = 10\n    ..dampingRatio = 0.8;\n\n  bodies.forEach((body) {\n    constantVolumeJoint.addBody(body);\n  });\n    \n  world.createJoint(ConstantVolumeJoint(world, constantVolumeJoint));\n```\n\n```{flutter-app}\n:sources: ../../examples\n:subfolder: stories/bridge_libraries/flame_forge2d/joints\n:page: constant_volume_joint\n:show: code popup\n```\n\n`ConstantVolumeJointDef` requires at least 3 bodies to be added using the `addBody` method. It also\nhas two optional parameters:\n\n- `frequencyHz`: This parameter sets the frequency of oscillation of the joint. If it is not set to\n0, the higher the value, the less springy each of the compound `DistantJoint`s are.\n\n- `dampingRatio`: This parameter defines how quickly the oscillation comes to rest. It ranges from\n0 to 1, where 0 means no damping and 1 indicates critical damping.\n\n\n### `DistanceJoint`\n\nA `DistanceJoint` constrains two points on two bodies to remain at a fixed distance from each other.\n\nYou can view this as a massless, rigid rod.\n\n```dart\nfinal distanceJointDef = DistanceJointDef()\n  ..initialize(firstBody, secondBody, firstBody.worldCenter, secondBody.worldCenter)\n  ..length = 10\n  ..frequencyHz = 3\n  ..dampingRatio = 0.2;\n\nworld.createJoint(DistanceJoint(distanceJointDef));\n```\n\n```{flutter-app}\n:sources: ../../examples\n:page: distance_joint\n:subfolder: stories/bridge_libraries/flame_forge2d/joints\n:show: code popup\n```\n\nTo create a `DistanceJointDef`, you can use the `initialize` method, which requires two bodies and a\nworld anchor point on each body. The definition uses local anchor points, allowing for a slight\nviolation of the constraint in the initial configuration. This is useful when saving and\nloading a game.\n\nThe `DistanceJointDef` has three optional parameters that you can set:\n\n- `length`: This parameter determines the distance between the two anchor points and must be greater\nthan 0. The default value is 1.\n\n- `frequencyHz`: This parameter sets the frequency of oscillation of the joint. If it is not set\nto 0, the higher the value, the less springy the joint becomes.\n\n- `dampingRatio`: This parameter defines how quickly the oscillation comes to rest. It ranges from\n0 to 1, where 0 means no damping and 1 indicates critical damping.\n\n```{warning}\nDo not use a zero or short length.\n```\n\n\n### `FrictionJoint`\n\nA `FrictionJoint` is used for simulating friction in a top-down game. It provides 2D translational\nfriction and angular friction.\n\nThe `FrictionJoint` isn't related to the friction that occurs when two shapes collide in the x-y plane\nof the screen. Instead, it's designed to simulate friction along the z-axis, which is perpendicular\nto the screen. The most common use-case for it is applying the friction force between a moving body\nand the game floor.\n\nThe `initialize` method of the `FrictionJointDef` method requires two bodies that will have friction\nforce applied to them, and an anchor.\n\nThe third parameter is the `anchor` point in the world coordinates where the friction force will be\napplied. In most cases, it would be the center of the first object. However, for more complex\nphysics interactions between bodies, you can set the `anchor` point to a specific location on one or\nboth of the bodies.\n\n```dart\nfinal frictionJointDef = FrictionJointDef()\n  ..initialize(ballBody, floorBody, ballBody.worldCenter)\n  ..maxForce = 50\n  ..maxTorque = 50;\n\n  world.createJoint(FrictionJoint(frictionJointDef));\n```\n\n```{flutter-app}\n:sources: ../../examples\n:page: friction_joint\n:subfolder: stories/bridge_libraries/flame_forge2d/joints\n:show: code popup\n```\n\nWhen creating a `FrictionJoint`, simulated friction can be applied via maximum force and torque\nvalues:\n\n- `maxForce`: the maximum translational friction which applied to the joined body. A higher value\n- simulates higher friction.\n\n- `maxTorque`: the maximum angular friction which may be applied to the joined body. A higher value\n- simulates higher friction.\n\nIn other words, the former simulates the friction, when the body is sliding and the latter simulates\nthe friction when the body is spinning.\n\n\n### `GearJoint`\n\nThe `GearJoint` is used to connect two joints together. Joints are required to be a\n[`RevoluteJoint`](#revolutejoint) or a [`PrismaticJoint`](#prismaticjoint) in any combination.\n\n```{warning}\nThe connected joints must attach a dynamic body to a static body. \nThe static body is expected to be a bodyA on those joints\n```\n\n```dart\nfinal gearJointDef = GearJointDef()\n  ..bodyA = firstJoint.bodyA\n  ..bodyB = secondJoint.bodyA\n  ..joint1 = firstJoint\n  ..joint2 = secondJoint\n  ..ratio = 1;\n\nworld.createJoint(GearJoint(gearJointDef));\n```\n\n```{flutter-app}\n:sources: ../../examples\n:page: gear_joint\n:subfolder: stories/bridge_libraries/flame_forge2d/joints\n:show: code popup\n```\n\n- `joint1`, `joint2`: Connected revolute or prismatic joints\n- `bodyA`, `bodyB`: Any bodies form the connected joints, as long as they are not the same body.\n- `ratio`: Gear ratio\n\nSimilarly to [`PulleyJoint`](#pulleyjoint), you can specify a gear ratio to bind the motions\ntogether:\n\n```text\ncoordinate1 + ratio * coordinate2 == constant \n```\n\nThe ratio can be negative or positive. If one joint is a `RevoluteJoint` and the other joint is a\n`PrismaticJoint`, then the ratio will have units of length or units of 1/length.\n\nSince the `GearJoint` depends on two other joints, if these are destroyed, the `GearJoint` needs to\nbe destroyed as well.\n\n```{warning}\nManually destroy the `GearJoint` if joint1 or joint2 is destroyed\n```\n\n\n### `MotorJoint`\n\nA `MotorJoint` is used to control the relative motion between two bodies. A typical usage is to\ncontrol the movement of a dynamic body with respect to the fixed point, for example to create\nanimations.\n\nA `MotorJoint` lets you control the motion of a body by specifying target position and rotation\noffsets. You can set the maximum motor force and torque that will be applied to reach the target\nposition and rotation. If the body is blocked, it will stop and the contact forces will be\nproportional the maximum motor force and torque.\n\n```dart\nfinal motorJointDef = MotorJointDef()\n  ..initialize(first, second)\n  ..maxTorque = 1000\n  ..maxForce = 1000\n  ..correctionFactor = 0.1;\n\n  world.createJoint(MotorJoint(motorJointDef));\n```\n\n```{flutter-app}\n:sources: ../../examples\n:page: motor_joint\n:subfolder: stories/bridge_libraries/flame_forge2d/joints\n:show: code popup\n```\n\nA `MotorJointDef` has three optional parameters:\n\n- `maxForce`: the maximum translational force which will be applied to the joined body to reach the\ntarget position.\n\n- `maxTorque`: the maximum angular force which will be applied to the joined body to reach the\ntarget rotation.\n\n- `correctionFactor`: position correction factor in range [0, 1]. It adjusts the joint's response to\ndeviation from target position. A higher value makes the joint respond faster, while a lower value\nmakes it respond slower. If the value is set too high, the joint may overcompensate and oscillate,\nbecoming unstable. If set too low, it may respond too slowly.\n  \nThe linear and angular offsets are the target distance and angle that the bodies should achieve\nrelative to each other's position and rotation. By default, the linear target will be the distance\nbetween the two body centers and the angular target will be the relative rotation of the bodies.\nUse the `setLinearOffset(Vector2)` and `setLinearOffset(double)` methods of the `MotorJoint` to set\nthe desired relative translation and rotate between the bodies.\n\nFor example, this code increments the angular offset of the joint every update cycle, causing the\nbody to rotate.\n\n```dart\n@override\nvoid update(double dt) {\n  super.update(dt);\n  \n  final angularOffset = joint.getAngularOffset() + motorSpeed * dt;\n  joint.setAngularOffset(angularOffset);\n}\n```\n\n\n### `MouseJoint`\n\nThe `MouseJoint` is used to manipulate bodies with the mouse. It attempts to drive a point on a body\ntowards the current position of the cursor. There is no restriction on rotation.\n\nThe `MouseJoint` definition has a target point, maximum force, frequency, and damping ratio. The\ntarget point initially coincides with the body's anchor point. The maximum force is used to prevent\nviolent reactions when multiple dynamic bodies interact. You can make this as large as you like.\nThe frequency and damping ratio are used to create a spring/damper effect similar to the distance\njoint.\n\n```{warning}\nMany users have tried to adapt the mouse joint for game play. Users often want\nto achieve precise positioning and instantaneous response. The mouse joint \ndoesn't work very well in that context. You may wish to consider using \nkinematic bodies instead.\n```\n\n```dart\nfinal mouseJointDef = MouseJointDef()\n  ..maxForce = 3000 * ballBody.mass * 10\n  ..dampingRatio = 1\n  ..frequencyHz = 5\n  ..target.setFrom(ballBody.position)\n  ..collideConnected = false\n  ..bodyA = groundBody\n  ..bodyB = ballBody;\n\n  mouseJoint = MouseJoint(mouseJointDef);\n  world.createJoint(mouseJoint);\n}\n```\n\n```{flutter-app}\n:sources: ../../examples\n:page: mouse_joint\n:subfolder: stories/bridge_libraries/flame_forge2d/joints\n:show: code popup\n```\n\n- `maxForce`: This parameter defines the maximum constraint force that can be exerted to move the\n  candidate body. Usually you will express as some multiple of the weight\n  (multiplier *mass* gravity).\n\n- `dampingRatio`: This parameter defines how quickly the oscillation comes to rest. It ranges from\n  0 to 1, where 0 means no damping and 1 indicates critical damping.\n\n- `frequencyHz`: This parameter defines the response speed of the body, i.e. how quickly it tries to\n  reach the target position\n\n- `target`: The initial world target point. This is assumed to coincide with the body anchor\n  initially.\n\n\n### `PrismaticJoint`\n\nThe `PrismaticJoint` provides a single degree of freedom, allowing for a relative translation of two\nbodies along an axis fixed in bodyA. Relative rotation is prevented.\n\n`PrismaticJointDef` requires defining a line of motion using an axis and an anchor point.\nThe definition uses local anchor points and a local axis so that the initial configuration\ncan violate the constraint slightly.\n\nThe joint translation is zero when the local anchor points coincide in world space.\nUsing local anchors and a local axis helps when saving and loading a game.\n\n```{warning}\nAt least one body should by dynamic with a non-fixed rotation.\n```\n\nThe `PrismaticJoint` definition is similar to the [`RevoluteJoint`](#revolutejoint) definition, but\ninstead of rotation, it uses translation.\n\n```dart\nfinal prismaticJointDef = PrismaticJointDef()\n  ..initialize(\n    dynamicBody,\n    groundBody,\n    dynamicBody.worldCenter,\n    Vector2(1, 0),\n  )\n```\n\n```{flutter-app}\n:sources: ../../examples\n:page: prismatic_joint\n:subfolder: stories/bridge_libraries/flame_forge2d/joints\n:show: code popup\n```\n\n- `b1`, `b2`: Bodies connected by the joint.\n- `anchor`: World anchor point, to put the axis through. Usually the center of the first body.\n- `axis`: World translation axis, along which the translation will be fixed.\n\nIn some cases you might wish to control the range of motion. For this, the `PrismaticJointDef` has\noptional parameters that allow you to simulate a joint limit and/or a motor.\n\n\n#### Prismatic Joint Limit\n\nYou can limit the relative rotation with a joint limit that specifies a lower and upper translation.\n\n```dart\njointDef\n  ..enableLimit = true\n  ..lowerTranslation = -20\n  ..upperTranslation = 20;\n```\n\n- `enableLimit`: Set to true to enable translation limits\n- `lowerTranslation`: The lower translation limit in meters\n- `upperTranslation`: The upper translation limit in meters\n\nYou change the limits after the joint was created with this method:\n\n```dart\nprismaticJoint.setLimits(-10, 10);\n```\n\n\n#### Prismatic Joint Motor\n\nYou can use a motor to drive the motion or to model joint friction. A maximum motor force is\nprovided so that infinite forces are not generated.\n\n```dart\njointDef\n  ..enableMotor = true\n  ..motorSpeed = 1\n  ..maxMotorForce = 100;\n```\n\n- `enableMotor`: Set to true to enable the motor\n- `motorSpeed`: The desired motor speed in radians per second\n- `maxMotorForce`: The maximum motor torque used to achieve the desired motor speed in N-m.\n\nYou change the motor's speed and force after the joint was created using these methods:\n\n```dart\nprismaticJoint.setMotorSpeed(2);\nprismaticJoint.setMaxMotorForce(200);\n```\n\nAlso, you can get the joint angle and speed using the following methods:\n\n```dart\nprismaticJoint.getJointTranslation();\nprismaticJoint.getJointSpeed();\n```\n\n\n### `PulleyJoint`\n\nA `PulleyJoint` is used to create an idealized pulley. The pulley connects two bodies to the ground\nand to each other. As one body goes up, the other goes down. The total length of the pulley rope is\nconserved according to the initial configuration:\n\n```text\nlength1 + length2 == constant\n```\n\nYou can supply a ratio that simulates a block and tackle. This causes one side of the pulley to\nextend faster than the other. At the same time the constraint force is smaller on one side than the\nother. You can use this to create a mechanical leverage.\n\n```text\nlength1 + ratio * length2 == constant\n```\n\nFor example, if the ratio is 2, then `length1` will vary at twice the rate of `length2`. Also the\nforce in the rope attached to the first body will have half the constraint force as the rope\nattached to the second body.\n\n```dart\nfinal pulleyJointDef = PulleyJointDef()\n  ..initialize(\n    firstBody,\n    secondBody,\n    firstPulley.worldCenter,\n    secondPulley.worldCenter,\n    firstBody.worldCenter,     \n    secondBody.worldCenter,\n    1,\n  );\n\nworld.createJoint(PulleyJoint(pulleyJointDef));\n```\n\n```{flutter-app}\n:sources: ../../examples\n:page: pulley_joint\n:subfolder: stories/bridge_libraries/flame_forge2d/joints\n:show: code popup\n```\n\nThe `initialize` method of `PulleyJointDef` requires two ground anchors, two dynamic bodies and\ntheir anchor points, and a pulley ratio.\n\n- `b1`, `b2`: Two dynamic bodies connected with the joint\n- `ga1`, `ga2`: Two ground anchors\n- `anchor1`, `anchor2`: Anchors on the dynamic bodies the joint will be attached to\n- `r`: Pulley ratio to simulate a block and tackle\n\n`PulleyJoint` also provides the current lengths:\n\n```dart\njoint.getCurrentLengthA()\njoint.getCurrentLengthB()\n```\n\n```{warning}\n`PulleyJoint` can get a bit troublesome by itself. They often work better when\ncombined with prismatic joints. You should also cover the anchor points \nwith static shapes to prevent one side from going to zero length.\n```\n\n\n### `RevoluteJoint`\n\nA `RevoluteJoint` forces two bodies to share a common anchor point, often called a hinge point.\nThe revolute joint has a single degree of freedom: the relative rotation of the two bodies.\n\nTo create a `RevoluteJoint`, provide two bodies and a common point to the `initialize` method.\nThe definition uses local anchor points so that the initial configuration can violate the\nconstraint slightly.\n\n```dart\nfinal jointDef = RevoluteJointDef()\n  ..initialize(firstBody, secondBody, firstBody.position);\nworld.createJoint(RevoluteJoint(jointDef));\n```\n\n```{flutter-app}\n:sources: ../../examples\n:page: revolute_joint\n:subfolder: stories/bridge_libraries/flame_forge2d/joints\n:show: code popup\n```\n\nIn some cases you might wish to control the joint angle. For this, the `RevoluteJointDef` has\noptional parameters that allow you to simulate a joint limit and/or a motor.\n\n\n#### Revolute Joint Limit\n\nYou can limit the relative rotation with a joint limit that specifies a lower and upper angle.\n\n```dart\njointDef\n  ..enableLimit = true\n  ..lowerAngle = 0\n  ..upperAngle = pi / 2;\n```\n\n- `enableLimit`: Set to true to enable angle limits\n- `lowerAngle`: The lower angle in radians\n- `upperAngle`: The upper angle in radians\n\nYou change the limits after the joint was created with this method:\n\n```dart\nrevoluteJoint.setLimits(0, pi);\n```\n\n\n#### Revolute Joint Motor\n\nYou can use a motor to drive the relative rotation about the shared point. A maximum motor torque is\nprovided so that infinite forces are not generated.\n\n```dart\njointDef\n  ..enableMotor = true\n  ..motorSpeed = 5\n  ..maxMotorTorque = 100;\n```\n\n- `enableMotor`: Set to true to enable the motor\n- `motorSpeed`: The desired motor speed in radians per second\n- `maxMotorTorque`: The maximum motor torque used to achieve the desired motor speed in N-m.\n\nYou change the motor's speed and torque after the joint was created using these methods:\n\n```dart\nrevoluteJoint.setMotorSpeed(2);\nrevoluteJoint.setMaxMotorTorque(200);\n```\n\nAlso, you can get the joint angle and speed using the following methods:\n\n```dart\nrevoluteJoint.jointAngle();\nrevoluteJoint.jointSpeed();\n```\n\n\n### `RopeJoint`\n\nA `RopeJoint` restricts the maximum distance between two points on two bodies.\n\n`RopeJointDef` requires two body anchor points and the maximum length.\n\n```dart\nfinal ropeJointDef = RopeJointDef()\n  ..bodyA = firstBody\n  ..localAnchorA.setFrom(firstBody.getLocalCenter())\n  ..bodyB = secondBody\n  ..localAnchorB.setFrom(secondBody.getLocalCenter())\n  ..maxLength = (secondBody.worldCenter - firstBody.worldCenter).length;\n\nworld.createJoint(RopeJoint(ropeJointDef));\n```\n\n```{flutter-app}\n:sources: ../../examples\n:page: rope_joint\n:subfolder: stories/bridge_libraries/flame_forge2d/joints\n:show: code popup\n```\n\n- `bodyA`, `bodyB`: Connected bodies\n- `localAnchorA`, `localAnchorB`: Optional parameter, anchor point relative to the body's origin.\n- `maxLength`: The maximum length of the rope. This must be larger than `linearSlop`, or the joint\nwill have no effect.\n\n```{warning}\nThe joint assumes that the maximum length doesn't change during simulation. \nSee `DistanceJoint` if you want to dynamically control length.\n```\n\n\n### `WeldJoint`\n\nA `WeldJoint` is used to restrict all relative motion between two bodies, effectively joining them\ntogether.\n\n`WeldJointDef` requires two bodies that will be connected, and a world anchor:\n\n```dart\nfinal weldJointDef = WeldJointDef()\n  ..initialize(bodyA, bodyB, anchor);\n\nworld.createJoint(WeldJoint(weldJointDef));\n```\n\n```{flutter-app}\n:sources: ../../examples\n:page: weld_joint\n:subfolder: stories/bridge_libraries/flame_forge2d/joints\n:show: code popup\n```\n\n- `bodyA`, `bodyB`: Two bodies that will be connected\n\n- `anchor`: Anchor point in world coordinates, at which two bodies will be welded together\n  to 0, the higher the value, the less springy the joint becomes.\n\n\n#### Breakable Bodies and WeldJoint\n\nSince the Forge2D constraint solver is iterative, joints are somewhat flexible. This means that the\nbodies connected by a WeldJoint may bend slightly. If you want to simulate a breakable body, it's\nbetter to create a single body with multiple fixtures. When the body breaks, you can destroy a\nfixture and recreate it on a new body instead of relying on a `WeldJoint`.\n"
  },
  {
    "path": "doc/bridge_packages/flame_isolate/flame_isolate.md",
    "content": "# flame_isolate\n\n```{toctree}\nOverview    <isolate.md>\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_isolate/isolate.md",
    "content": "# FlameIsolate\n\nThe power of [integral_isolates](https://pub.dev/packages/integral_isolates) neatly packaged in\n[flame_isolate](https://pub.dev/packages/flame_isolate) for your Flame game.\n\nIf you've ever used the [compute](https://api.flutter.dev/flutter/foundation/compute-constant.html)\nfunction before, you will feel right at home. This mixin allows you to run CPU-intensive code in an\nisolate.\n\nTo use it in your game you just need to add `flame_isolate` to your pubspec.yaml.\n\n\n## Usage\n\nJust add the mixin `FlameIsolate` to your component and start utilizing the power of an isolate as\nsimple as running the [compute](https://api.flutter.dev/flutter/foundation/compute-constant.html)\nfunction.\n\nExample:\n\n```dart\nclass MyGame extends FlameGame with FlameIsolate {\n  ...\n  @override\n  void update(double dt) {\n    if (shouldRecalculate) {\n      isolate(recalculateWorld, worldData).then(updateWorld);\n    }\n    ...\n  }\n  ...\n}\n```\n\n\n### Performance note\n\nKeep in mind that every component with `FlameIsolate` mixin that you create and add to your game\nwill create a new isolate. This means you will probably want to create a manager component to\nmanage a lot of \"dumber\" components. Think of it like ants, where the queen controls the worker\nants. If every individual worker ant got its own isolate, it would be a total waste of power,\nhence you would put it on the queen, which in turn tells all the worker ants what to do.\n\nA simple example of this can be found in the example application for the FlameIsolate package.\n\n\n### Backpressure Strategies\n\nBackpressure strategies are a way to cope with the job queue when job items are produced more\nrapidly than the isolate can handle them. This presents the problem of what to do with such a\ngrowing backlog of unhandled jobs. To mitigate this problem this library funnels all jobs through a\njob queue handler. Also known as `BackpressureStrategy`.\n\nThe ones currently supported are:\n\n- `NoBackPressureStrategy` that basically does not handle back pressure. It uses a FIFO stack for\nstoring a backlog of unhandled jobs.\n-`ReplaceBackpressureStrategy` that has a job queue with size one, and discards the queue upon\nadding a new job.\n- `DiscardNewBackPressureStrategy` that has a job queue with size one, and as long as the queue is\npopulated a new job will not be added.\n\nYou can specify a backpressure strategy by overriding the `backpressureStrategy` field. This will\ncreate the isolate with the specified strategy when the component is mounted.\n\n```dart\nclass MyGame extends FlameGame with FlameIsolate {\n  @override\n  BackpressureStrategy get backpressureStrategy => ReplaceBackpressureStrategy();\n  ...\n}\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_lottie/flame_lottie.md",
    "content": "# flame_lottie\n\nThis package allows you to load and add Lottie animations to your Flame game.\n\n\nThe native Lottie libraries (such as [lottie-android](https://github.com/airbnb/lottie-android))\nare maintained by **Airbnb**.\n\nThe Flutter package ``lottie``, on which this wrapper is based on, is by developed **xaha.dev** and\ncan be found on [pub.dev](https://pub.dev/packages/lottie).\n\n\n## Usage\n\nTo use it in your game you just need to add `flame_lottie` to your pubspec.yaml.\n\nSimply load the Lottie animation using the **loadLottie** method and\nthe [LottieBuilder](https://pub.dev/documentation/lottie/latest/lottie/LottieBuilder-class.html).\nIt allows all the various ways of loading a Lottie file:\n\n- [Lottie.asset](https://pub.dev/documentation/lottie/latest/lottie/Lottie/asset.html), for\nobtaining a Lottie file from an AssetBundle using a key.\n- [Lottie.network](https://pub.dev/documentation/lottie/latest/lottie/Lottie/network.html), for\nobtaining a lottie file from a URL.\n- [Lottie.file](https://pub.dev/documentation/lottie/latest/lottie/Lottie/file.html), for obtaining\n a lottie file from a File.\n- [Lottie.memory](https://pub.dev/documentation/lottie/latest/lottie/Lottie/memory.html), for\nobtaining a lottie file from a Uint8List.\n\n... and add it as `LottieComponent` to your Flame 🔥 game.\n\nExample:\n\n```dart\nclass MyGame extends FlameGame {\n  ...\n  @override\n  Future<void> onLoad() async {\n    final asset = Lottie.asset('assets/LottieLogo1.json');\n    final animation = await loadLottie(asset);\n    add(\n      LottieComponent(\n        animation,\n        repeating: true, // Continuously loop the animation.\n        size: Vector2.all(400),\n      ),\n    );\n  }\n  ...\n}\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_network_assets/flame_network_assets.md",
    "content": "# FlameNetworkAssets\n\n`FlameNetworkAssets` is a bridge package focused on providing a solution to fetch, and cache assets\nfrom the network.\n\nThe `FlameNetworkAssets` class provides an abstraction that should be extended in order to create\nasset specific handler.\n\nBy default, the package relies on the `http` package to make http requests, and `path_provider`\nto get the place to store the local cache, to use a different approach for those, use the\noptional arguments in the constructor.\n\nThis package bundles a specific asset handler class for images:\n\n```dart\nfinal networkAssets = FlameNetworkImages();\nfinal playerSprite = await networkAssets.load('https://url.com/image.png');\n```\n\nTo create a specific asset handler class, you just need to extend the `FlameNetworkAssets` class\nand define the `decodeAsset` and `encodeAsset` arguments:\n\n```dart\nclass FlameNetworkCustomAsset extends FlameNetworkAssets<CustomAsset> {\n  FlameNetworkImages({\n    super.get,\n    super.getAppDirectory,\n    super.cacheInMemory,\n    super.cacheInStorage,\n  }) : super(\n          decodeAsset: (bytes) => CustomAsset.decode(bytes),\n          encodeAsset: (CustomAsset asset) => asset.encode(),\n        );\n}\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_oxygen/flame_oxygen.md",
    "content": "# flame_oxygen\n\n[flame_oxygen](https://github.com/flame-engine/flame/tree/main/packages/flame_oxygen)\n\n```{toctree}\n:hidden:\n\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_rive/flame_rive.md",
    "content": "# flame_rive\n\n```{toctree}\nOverview    <rive.md>\n\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_rive/rive.md",
    "content": "# flame_rive\n\n`flame_rive` is a bridge library for using [rive](https://rive.app/) animations in your Flame game.\nRive is a real-time interactive design and animation tool and you use it to create animations.\n\nTo use a file created by Rive in your game you need to add `flame_rive` to your pubspec.yaml, as can\nbe seen in the\n[Flame Rive example](https://github.com/flame-engine/flame/tree/main/packages/flame_rive/example)\nand in the pub.dev [installation instructions](https://pub.dev/packages/flame_rive).\n\n\n## How to use it\n\nFirst, start with adding the `animation.riv` file to the assets folder. Then load the artboard of\nthe animation to the game using the `loadArtboard` method. After that, create the\n`StateMachine` from the artboard and pass it to the `RiveComponent`. The component will\nautomatically advance the state machine for you.\n\nInteractivity should be handled via [Data Binding](https://rive.app/docs/runtimes/data-binding)\ninstead of state machine inputs, as they are deprecated in Rive 0.14.x.\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: rive_example\n:show: widget code infobox\n:width: 200\n:height: 200\n```\n\n```dart\nclass RiveExampleGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    final file = await File.asset(\n      'assets/rewards.riv', \n      riveFactory: Factory.rive,\n    );\n\n    final artboard = await loadArtboard(file!);\n    final stateMachine = artboard.defaultStateMachine();\n\n    if (stateMachine != null) {\n      final viewModel = file.defaultArtboardViewModel(artboard);\n      if (viewModel != null) {\n        final viewModelInstance = viewModel.createDefaultInstance();\n        if (viewModelInstance != null) {\n          stateMachine.bindViewModelInstance(viewModelInstance);\n          final coinAmount =\n              viewModelInstance.viewModel('Coin')?.number('Item_Value');\n          coinAmount?.value = 100;\n        }\n      }\n    }\n\n    add(\n      RiveComponent(\n        artboard: artboard,\n        stateMachine: stateMachine,\n        size: Vector2.all(550),\n      ),\n    );\n  }\n}\n```\n\nYou can use the state machine to manage the state of animation via data binding.\nCheck out the example for more information.\n\n\n## Full Example\n\nYou can check an example\n[here](https://github.com/flame-engine/flame/tree/main/packages/flame_rive/example).\n"
  },
  {
    "path": "doc/bridge_packages/flame_riverpod/component.md",
    "content": "# Component\n\n\n## ComponentRef\n\n`ComponentRef` exposes Riverpod functionality to individual `Component`s, and is comparable to\n`flutter_riverpod`'s `WidgetRef`.\n\n\n## RiverpodComponentMixin\n\n`RiverpodComponentMixin` manages the lifecycle of listeners on behalf of individual `Component`s.\n\n`Component`s using this mixin must use `addToGameWidgetBuild` in their `onMount` method to add\nlisteners (e.g. `ref.watch` or `ref.listen`) *prior to* calling `super.onMount`, which manages the\nstaged listeners and disposes of them on the user's behalf inside `onRemove`.\n\n```dart\n\nclass RiverpodAwareTextComponent extends PositionComponent\n    with RiverpodComponentMixin {\n  late TextComponent textComponent;\n  int currentValue = 0;\n\n  @override\n  void onMount() {\n    addToGameWidgetBuild(() {\n      ref.listen(countingStreamProvider, (p0, p1) {\n        if (p1.hasValue) {\n          currentValue = p1.value!;\n          textComponent.text = '$currentValue';\n        }\n      });\n    });\n    super.onMount();\n    add(textComponent = TextComponent(position: position + Vector2(0, 27)));\n  }\n}\n\n```\n\n\n## RiverpodGameMixin\n\n`RiverpodGameMixin` provides listeners from all components to the build method of the\n`RiverpodAwareGameWidget`.\nThe `addToGameWidgetBuild` method is available in the `RiverpodGameMixin` as well,\nenabling you to access `ComponentRef` methods directly in your Game class.\n"
  },
  {
    "path": "doc/bridge_packages/flame_riverpod/flame_riverpod.md",
    "content": "# flame_riverpod\n\n```{toctree}\nOverview   <riverpod.md>\nComponent  <component.md>\nWidget     <widget.md>\n```\n\n"
  },
  {
    "path": "doc/bridge_packages/flame_riverpod/riverpod.md",
    "content": "# flame_riverpod\n\n\n## Riverpod\n\n[Riverpod](https://riverpod.dev/) is a reactive caching and data-binding\nframework for Dart & Flutter.\n\nIn `flutter_riverpod`, widgets can be configured to rebuild when the state\nof a provider changes.\n\nWhen using Flame, we are interacting with components, which are *not* Widgets.\n\n`flame_riverpod` provides the `RiverpodAwareGameWidget`, `RiverpodGameMixin`, and\n`RiverpodComponentMixin` to facilitate managing state from `Provider`s in your Flame Game.\n\n\n## Usage\n\nYou should use the `RiverpodAwareGameWidget` as your Flame `GameWidget`, the `RiverpodGameMixin`\nmixin on your game that extends `FlameGame`, and the `RiverpodComponentMixin` on any components\ninteracting with Riverpod providers.\n\nSubscriptions to a provider are managed in accordance with the lifecycle\nof a Flame Component: initialization occurs when a Component is mounted, and disposal\noccurs when a Component is removed.\n\nBy default, the `RiverpodAwareGameWidget` is rebuilt when\nRiverpod-aware (i.e. using the `RiverpodComponentMixin`) components are mounted and when they are\nremoved.\n\n```dart\n/// An excerpt from the Example. Check it out!\nclass RefExampleGame extends FlameGame with RiverpodGameMixin {\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    add(TextComponent(text: 'Flame'));\n    add(RiverpodAwareTextComponent());\n  }\n}\n\nclass RiverpodAwareTextComponent extends PositionComponent\n    with RiverpodComponentMixin {\n  late TextComponent textComponent;\n  int currentValue = 0;\n\n  @override\n  void onMount() {\n    addToGameWidgetBuild(() {\n      ref.listen(countingStreamProvider, (p0, p1) {\n        if (p1.hasValue) {\n          currentValue = p1.value!;\n          textComponent.text = '$currentValue';\n        }\n      });\n    });\n    super.onMount();\n    add(textComponent = TextComponent(position: position + Vector2(0, 27)));\n  }\n}\n\n```\n\nThe order of operations in `Component.onMount` is important. The `RiverpodComponentMixin`\ninteracts with `RiverpodGameMixin` (inside of `RiverpodComponentMixin.onMount`) to co-ordinate\nadding and removing listeners as the corresponding component is mounted and removed, respectively.\n"
  },
  {
    "path": "doc/bridge_packages/flame_riverpod/widget.md",
    "content": "# Widget\n\n\n## RiverpodAwareGameWidget\n\n`RiverpodAwareGameWidget` is a GameWidget with a `State` object of type\n`RiverpodAwareGameWidgetState`.\n\nThe required `GlobalKey` argument is used to provide `Component`s using `RiverpodComponentMixin`\naccess to `Provider`s via `RiverpodAwareGameWidgetState`.\n\n\n## RiverpodAwareGameWidgetState\n\n`RiverpodAwareGameWidgetState` performs the duties associated with the\n`ConsumerStatefulElement` in `flutter_riverpod` and `GameWidgetState` in `flame`.\n"
  },
  {
    "path": "doc/bridge_packages/flame_spine/flame_spine.md",
    "content": "# flame_spine\n\nThis package allows you to load and add Spine skeletal animations to your Flame game.\n\n\n## Usage\n\nTo use it in your game you just need to add `flame_spine` to your pubspec.yaml and your spine\nassets to your `assets/` directory, and you can add a `SpineComponent` to your `FlameGame`.\n\n```{note}\nRemember to call `await initSpineFlutter();` in your `main` method, or in `onLoad`.\n```\n\nExample:\n\n```dart\nvoid main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  await initSpineFlutter();\n  runApp(const GameWidget.controlled(gameFactory: SpineExample.new));\n}\n\nclass FlameSpineExample extends FlameGame {\n late final SpineComponent spineboy;\n\n @override\n Future<void> onLoad() async {\n  await initSpineFlutter();\n  // Load the Spineboy atlas and skeleton data from asset files\n  // and create a SpineComponent from them, scaled down and\n  // centered on the screen\n  spineboy = await SpineComponent.fromAssets(\n   atlasFile: 'assets/spine/spineboy.atlas',\n   skeletonFile: 'assets/spine/spineboy-pro.skel',\n   scale: Vector2(0.4, 0.4),\n   anchor: Anchor.center,\n   position: size / 2,\n  );\n\n  // Set the \"walk\" animation on track 0 in looping mode\n  spineboy.animationState.setAnimationByName(0, 'walk', true);\n  await add(spineboy);\n }\n\n @override\n void onDetach() {\n  // Dispose the native resources that have been loaded for spineboy.\n  spineboy.dispose();\n }\n}\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_splash_screen/flame_splash_screen.md",
    "content": "# flame_splash_screen\n\n![Showcase of the splash screen](https://raw.githubusercontent.com/flame-engine/flame_splash_screen/main/demogif.gif)\n\nStyle your flame game with a beautiful splash screen.\n\nflame_splash_screen is a very customizable splash screen package.\n\n```dart\nFlameSplashScreen(\n  theme: FlameSplashTheme.dark,\n  onFinish: (BuildContext context) => Navigator.pushNamed(context, '/your-game-initial-screen')\n)\n```\n\nCheck the [package's repo](https://github.com/flame-engine/flame_splash_screen) and the\n[pub page](https://pub.dev/packages/flame_splash_screen) for more details.\n"
  },
  {
    "path": "doc/bridge_packages/flame_svg/flame_svg.md",
    "content": "# flame_svg\n\n```{toctree}\nOverview    <svg.md>\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_svg/svg.md",
    "content": "# Flame SVG\n\nflame_svg provides a simple API for rendering SVG images in your game.\n\n\n## Installation\n\nSvg support is provided by the `flame_svg` bridge package, be sure to put it in your pubspec file\nto use it.\n\nIf you want to know more about the installation visit\n[flame_svg on pub.dev](https://pub.dev/packages/flame_svg/install).\n\n\n## How to use flame_svg\n\nTo use it just import the `Svg` class from `'package:flame_svg/flame_svg.dart'`, and use the\nfollowing snippet to render it on the canvas:\n\n```dart\nfinal svgInstance = await Svg.load('android.svg');\n\nfinal position = Vector2(100, 100);\nfinal size = Vector2(300, 300);\n\nsvgInstance.renderPosition(canvas, position, size);\n```\n\nor use the `SvgComponent` and add it to the component tree:\n\n```dart\nclass MyGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    final svgInstance = await Svg.load('android.svg');\n    final size = Vector2.all(100);\n    final position = Vector2.all(100);\n    final svgComponent = SvgComponent(\n      size: size,\n      position: position,\n      svg: svgInstance,\n    );\n\n    add(svgComponent);\n  }\n}\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_texturepacker/flame_texturepacker.md",
    "content": "# flame_texturepacker\n\n**flame_texturepacker**\nis a bridge package that allows you to load sprite sheets generated by [CodeAndWeb's TexturePacker]\ninto your Flame game.\n\nA sprite sheet (or texture atlas) is a larger image that packs many smaller sprites together.\nThis reduces the number of separate textures a game must load, which lowers overhead\nand can improve loading speed and rendering performance.\n\n\n## Installation\n\nAdd  *flame_texturepacker* to your project:\n\n```shell\nflutter pub add flame_texturepacker\n```\n\nThen import it in your Dart code:\n\n```dart\nimport 'package:flame_texturepacker/flame_texturepacker.dart';\n```\n\n\n## Usage\n\n\n### Loading from Assets\n\nFirst, add your ``.atlas`` data file and sprite sheet images to your assets directory and reference\nthem in your `pubspec.yaml`:\n\n```yaml\nflutter:\n  assets:\n    - assets/images/atlas_map.atlas\n    - assets/images/sprite_sheet1.png\n    - assets/images/sprite_sheet2.png\n    - assets/images/sprite_sheet3.png\n```\n\nThen load the atlas in your game:\n\n```dart\nclass MyGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    // Load the texture atlas\n    final atlas = await atlasFromAssets('atlas_map.atlas');\n    \n    // Use the atlas to get sprites\n    final sprite = atlas.findSpriteByName('robot_jump')!;\n    add(SpriteComponent(sprite: sprite));\n  }\n}\n```\n\n\n## TexturePackerAtlas\n\nThe `TexturePackerAtlas` class is the main interface for working with texture atlases. It provides\nseveral methods to query and retrieve sprites.\n\n\n### Finding Sprites\n\n**Find a single sprite by name:**\n\n```dart\nfinal jumpSprite = atlas.findSpriteByName('robot_jump')!;\nfinal fallSprite = atlas.findSpriteByName('robot_fall')!;\n```\n\n**Find a sprite by name and index:**\n\n```dart\nfinal sprite = atlas.findSpriteByNameIndex('robot_walk', 0);\n```\n\n**Find all sprites with the same name:**\n\nThis is particularly useful for creating animations from indexed sprites:\n\n```dart\n// Get all sprites with the name 'robot_walk'\n// (e.g., robot_walk_0, robot_walk_1, etc.)\nfinal walkingSprites = atlas.findSpritesByName('robot_walk');\n\n// Create an animation from the sprite list\nfinal walkingAnimation = SpriteAnimation.spriteList(\n  walkingSprites,\n  stepTime: 0.1,\n  loop: true,\n);\n\nadd(SpriteAnimationComponent(animation: walkingAnimation));\n```\n\n\n## Advanced Options\n\n\n### Whitelist Filtering\n\nTo optimize memory usage, you can specify a whitelist of sprite names to load. Only sprites whose\nnames contain any of the whitelist strings will be loaded:\n\n```dart\nfinal atlas = await TexturePackerAtlas.load(\n  'atlas_map.atlas',\n  whiteList: [ 'robot_walk' ]\n);\n```\n\nThis is particularly useful for large atlases where you only need a subset of sprites.\n\n\n### Original Size vs Packed Size\n\nTexturePacker can trim transparent pixels from sprites to save space. By default,\n`flame_texturepacker` uses the original (untrimmed) size. You can change this behavior:\n\n```dart\nfinal atlas = await TexturePackerAtlas.load(\n  'atlas_map.atlas',\n  useOriginalSize: false, // Use the trimmed/packed size instead\n);\n```\n\n\n### Custom Asset Prefix\n\nIf your ``.atlas`` data file is not stored in the default `images` directory:\n\n```dart\nfinal atlas = await atlasFromAssets(\n  'atlas_map.atlas',\n  assetsPrefix: 'custom_path',\n);\n```\n\n\n## Full Example\n\nA complete working example can be found in the [flame_texturepacker example] or\nin this [tutorial from CodeAndWeb].\n\n\n[flame_texturepacker example]: https://github.com/flame-engine/flame/tree/main/packages/flame_texturepacker/example\n[tutorial from CodeAndWeb]: https://www.codeandweb.com/texturepacker/tutorials/how-to-create-sprite-sheets-and-animations-with-flame-engine\n[CodeAndWeb's TexturePacker]: https://www.codeandweb.com/texturepacker\n"
  },
  {
    "path": "doc/bridge_packages/flame_tiled/flame_tiled.md",
    "content": "# flame_tiled\n\n**flame_tiled** is the bridge package that connects the flame game engine to [Tiled] maps by parsing\nTMX (XML) files and accessing the tiles, objects, and everything in there.\n\nTo use this,\n\n1. Create your own map by using [Tiled].\n2. Create a `TiledComponent` and add it to the component tree as follows:\n\n```dart\nfinal component = await TiledComponent.load(\n  'my_map.tmx',\n  Vector2.all(32),\n);\n\nadd(component);\n```\n\n\n## TiledComponent\n\nTiled is a free and open source, full-featured level and map editor for your platformer or\nRPG game. Currently we have an \"in progress\" implementation of a Tiled component. This API\nuses the lib [tiled.dart](https://github.com/flame-engine/tiled.dart) to parse map files and\nrender visible layers using the performant `SpriteBatch` for each layer.\n\nSupported map types include: Orthogonal, Isometric, Hexagonal, and Staggered.\n\nOrthogonal | Hexagonal             |  Isomorphic\n:--:|:-------------------------:|:-------------------------:\n![An example of an orthogonal map](../../images/orthogonal.png)|![An example of hexagonal map](../../images/pointy_hex_even.png) |  ![An example of isomorphic map](../../images/tile_stack_single_move.png)\n\nAn example of how to use the API can be found\n[here](https://github.com/flame-engine/flame/tree/main/packages/flame_tiled/example).\n\n\n### TileStack\n\nOnce a `TiledComponent` is loaded, you can select any column of (x,y) tiles in a `tileStack` to\nthen add animation. Removing the stack will not remove the tiles from the map.\n\n> **Note**: This currently only supports position based effects.\n\n```dart\nvoid onLoad() {\n  final stack = map.tileMap.tileStack(4, 0, named: {'floor_under'});\n  stack.add(\n    SequenceEffect(\n      [\n        MoveEffect.by(\n          Vector2(5, 0),\n          NoiseEffectController(duration: 1, frequency: 20),\n        ),\n        MoveEffect.by(Vector2.zero(), LinearEffectController(2)),\n      ],\n      repeatCount: 3,\n    )\n      ..onComplete = () => stack.removeFromParent(),\n  );\n  map.add(stack);\n}\n```\n\n\n### TileAtlas\n\nWhen a tilemap has multiple images (from multiple tilesets) `TiledComponent` uses a `TileAtlas` to\npack all those image into a single big image (a.k.a atlas). This helps in rendering the whole map in\na single draw call. But is there a limit on how big this atlas can be based on the target platform\nand hardware. As it is not possible to query this max size from Flame or Flutter as of now,\n`TiledComponent` limits the atlas to `4096x4096` for web and `8192x8192` for all other platforms.\n\nThese limits should work well for most cases. But in case you are sure that your target platform can\nsupport bigger atlas and want to override the limits used by `TiledComponent` you can do so by\npassing in the `atlasMaxX` and `atlasMaxX` values to `TiledComponent.load`.\n\nNOTE: This is not recommended as such huge sizes might not work with all hardware. Instead consider\nresizing the original tileset images so that when packed they fit with the limits.\n\n```dart\nfinal component = await TiledComponent.load(\n  'my_map.tmx',\n  Vector2.all(32),\n  atlasMaxX: 9216,\n  atlasMaxY: 9216,\n);\n\nadd(component);\n```\n\n\n## Limitations\n\n\n### Flip\n\n[Tiled] has a feature that allows you to flip a tile horizontally or vertically, or even rotate it.\n\n`flame_tiled` supports this but if you are using a large texture and have flipped tiles there will\nbe a drop in performance. If you want to ignore any flips in your tilemap you can set the\n`ignoreFlip` to false in the constructor.\n\n**Note**: A large texture in this context means one with multiple tilesets (or a huge tileset)\nwhere the sum of their dimensions are in the thousands.\n\n```dart\nfinal component = await TiledComponent.load(\n  'my_map.tmx',\n  Vector2.all(32),\n  ignoreFlip: true,\n);\n```\n\n\n### Clearing images cache\n\nIf you have called `Flame.images.clearCache()` you also need to call `TiledAtlas.clearCache()` to\nremove disposed images from the tiled cache. It might be useful if your next game map have completely\ndifferent tiles than the previous.\n\n[Tiled]: https://www.mapeditor.org/\n\n\n## Troubleshooting\n\n\n### My game shows \"lines\" and artifacts between the map tiles\n\nThis is caused by the imprecision found in float-pointing numbers in computer science.\n\nCheck this [Article](https://verygood.ventures/blog/solving-super-dashs-rendering-challenges-eliminating-ghost-lines-for-a-seamless-gaming-experience)\nto learn more about the issue and how it can be solved.\n\n```{toctree}\n:hidden:\n\nTiled  <tiled.md>\nLayers <layers.md>\n```\n"
  },
  {
    "path": "doc/bridge_packages/flame_tiled/layers.md",
    "content": "# Layers\n\nAt its simplest, layers can be retrieved from a Tilemap by invoking:\n\n```dart\ngetLayer<ObjectGroup>(\"myObjectGroupLayer\");\ngetLayer<ImageLayer>(\"myImageLayer\");\ngetLayer<TileLayer>(\"myTileLayer\");\ngetLayer<Group>(\"myGroupLayer\");\n```\n\nThese methods will either return the requested layer type or null if it does not exist.\n\n\n## Layer properties\n\nThe following Tiled properties are supported:\n\n- [x] Visible\n- [x] Opacity\n- [ ] Tint color\n- [x] Horizontal offset\n- [x] Vertical offset\n- [x] Parallax factor\n- [x] Custom properties\n\n\n## Tiles properties\n\n- Tiles can have custom properties accessible at `tile.properties`.\n- Tiles can have a custom `type` (or `class` starting in Tiled v1.9) accessible at `tile.type`.\n\n\n## Other features\n\nOther advanced features are not yet supported, but you can easily read the objects and other\nfeatures of the TMX and add custom behavior (eg regions for triggers and walking areas, custom\nanimated objects).\n\n\n## Full Example\n\nYou can check a working example\n[here](https://github.com/flame-engine/flame/tree/main/packages/flame_tiled/example).\n"
  },
  {
    "path": "doc/bridge_packages/flame_tiled/tiled.md",
    "content": "# Tiled\n\n[Tiled] is a great tool to design levels and maps.  From [Tiled]'s documentation:\n\n> Tiled is a 2D level editor that helps you develop the content of your game. Its\n> primary feature is to edit tile maps of various forms, but it also supports\n> free image placement as well as powerful ways to annotate your level with extra\n> information used by the game. Tiled focuses on general flexibility while trying\n> to stay intuitive.\n>\n> In terms of tile maps, it supports straight rectangular tile layers, but also\n> projected isometric, staggered isometric and staggered hexagonal layers. A\n> tileset can be either a single image containing many tiles, or it can be a\n> collection of individual images. In order to support certain depth faking\n> techniques, tiles and layers can be offset by a custom distance and their\n> rendering order can be configured.\n\n\n![Tiled Editor](../../images/TiledEditor.jpg)\n\n\nFlame provides a package ([flame_tiled]) that bundles a [dart] package which allows you to parse TMX\n(XML) files and access the tiles, objects, and everything in there.\n\nThe [dart] package provides a simple `Tiled` class and [flame_tiled] provides a component wrapper\n`TiledComponent`, for the map rendering, which renders the tiles on the screen and supports\nrotations and flips.\n\n\n## Tiled Editor\n\nYou can choose to download the [Tiled] map editor and create interactive maps that can be loaded\ninto your game.  At its core, the [Tiled] map editor creates a TMX file that can be parsed and used\nwithin your game.\n\n\n[dart]: https://pub.dev/packages/tiled\n[flame_tiled]: https://github.com/flame-engine/flame/tree/main/packages/flame_tiled\n[Tiled]: https://www.mapeditor.org/\n"
  },
  {
    "path": "doc/development/contributing.md",
    "content": "```{include} ../../CONTRIBUTING.md\n```\n"
  },
  {
    "path": "doc/development/development.md",
    "content": "# Development\n\n- [Contributing](contributing.md)\n- [Documentation](documentation.md)\n- [Style Guide](style_guide.md)\n- [Tests Guide](testing_guide.md)\n\n```{toctree}\n:hidden:\n\nContributing   <contributing.md>\nDocumentation  <documentation.md>\nStyle Guide    <style_guide.md>\nTests Guide    <testing_guide.md>\n```\n"
  },
  {
    "path": "doc/development/documentation.md",
    "content": "# Documentation Site\n\nFlame's documentation is written in **Markdown**. It is then rendered into HTML with the help of\nthe [Sphinx] engine and its [MyST] plugin. The rendered files are then manually (but with the help\nof a script) published to [flame-docs-site], where the site is served via [GitHub Pages].\n\n[Sphinx]: https://www.sphinx-doc.org/en/master/\n[MyST]: https://myst-parser.readthedocs.io/en/latest/\n[flame-docs-site]: https://github.com/flame-engine/flame-docs-site\n[GitHub Pages]: https://pages.github.com/\n\n\n## Markdown\n\nThe main documentation site is written in Markdown. We assume that you're already familiar with the\nbasics of the Markdown syntax (if not, there are plenty of guides on the Internet). Instead, this\nsection will focus on the Markdown extensions that are enabled in our build system.\n\n\n## Table of contents\n\nThe table of contents for the site must be created manually. This is done using special `{toctree}`\nblocks, one per each subdirectory:\n\n`````markdown\n```{toctree}\n:hidden:\n\nFirst Topic    <relative_path/to_topic1.md>\nSecond Topic   <topic2.md>\n```\n`````\n\nWhen adding new documents into the documentation site, make sure that they are mentioned in one of\nthe toctrees -- otherwise you will see a warning during the build that the document is orphaned.\n\n\n## Admonitions\n\nAdmonitions are emphasized blocks of text with a distinct appearance. They are created using the\ntriple-backticks syntax:\n\n`````markdown\n```{note}\nPlease note this very important caveat.\n```\n```{warning}\nDon't look down, or you will encounter an error.\n```\n```{error}\nI told you so.\n```\n```{seealso}\nAlso check out this cool thingy.\n```\n`````\n\n```{note}\nPlease note this very important caveat.\n```\n\n```{warning}\nDon't look down, or you will encounter an error.\n```\n\n```{error}\nI told you so.\n```\n\n```{seealso}\nAlso check out this cool thingy.\n```\n\n\n## Deprecations\n\nThe special `{deprecated}` block can be used to mark some part of documentation or syntax as being\ndeprecated. This block requires specifying the version when the deprecation has occurred\n\n`````markdown\n```{deprecated} v1.3.0\n\nPlease use this **other** thing instead.\n```\n`````\n\nWhich would be rendered like this:\n\n```{deprecated} v1.3.0\n\nPlease use this **other** thing instead.\n```\n\n\n## Live examples\n\nOur documentation site includes a custom-built **flutter-app** directive which allows creating\nFlutter widgets and embedding them alongside the overall documentation content.\n\nIn Markdown, the code for inserting an embed looks like this:\n\n`````markdown\n```{flutter-app}\n:sources: ../flame/examples\n:page: tap_events\n:show: widget code popup\n:width: 180\n:height: 160\n```\n``````\n\nHere's what the different options mean:\n\n- **sources**: specifies the name of the root directory where the Flutter code that you wish to run\n  is located. This directory must be a Flutter repository, and there must be a `pubspec.yaml` file\n  there. The path is considered relative to the `doc/_sphinx` directory.\n\n- **page**: a sub-path within the root directory given in `sources`. This option has two effects:\n  first, it is appended to the path of the html page of the widget, like so: `main.dart.html?$page`.\n  Secondly, the button to show the source code of the embed will display the code from the file or\n  directory with the name given by `page`.\n\n  The purpose of this option is to be able to bundle multiple examples into a single executable.\n  When using this option, the `main.dart` file of the app should route the execution to the proper\n  widget according to the `page` being passed.\n\n- **show**: contains a subset of modes: `widget`, `code`, `infobox`, and `popup`. The `widget` mode\n  creates an iframe with the embedded example, directly within the page. The `code` mode will show\n  a button that allows the user to see the code that produced this example. The `popup` mode also\n  shows a button, which displays the example in an overlay window. This is more suitable for\n  demoing larger apps. Using both \"widget\" and \"popup\" modes at the same time is not recommended.\n  Finally, the `infobox` mode will display the result in a floating window -- this mode is best\n  combined with `widget` and `code`.\n\n- **width**: an integer that defines the width of the embedded application.  If this is not defined,\n  the width will be 100%.\n\n- **height**: an integer that defines the height of the embedded application. If this is not\n  defined, the height will be 350px.\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: tap_events\n:show: widget code popup\n```\n\n\n## Standardization and Templates\n\nFor every section or package added to the documentation, naming conventions, directory structure,\nand standardized table of contents are important.  Every section and package must have a table of\ncontents or an entry in the parent markdown file to allow navigation from the left sidebar menu in\nlogical or alphabetical order. Additionally, naming conventions should be followed for organization,\nsuch as:\n\n- bridge_packages/package_name/package_name.md\n- documentation_section/documentation_section.md\n\n```{note}\nAvoid having spaces in the paths to the docs since that will keep you from\nbuilding the project due to\n[this bug](https://github.com/ipython/ipython/pull/13765).\n```\n\n\n## Building documentation locally\n\nBuilding the documentation site on your own computer is fairly simple. All you need is the\nfollowing:\n\n1. A working **Flutter** installation, accessible from the command line.\n\n2. **Melos** command-line tool, as per the [contributing] guide.\n\n3. A **Python** environment, with python version 3.8+ or higher. Having a dedicated python\n   virtual environment is recommended but not required.\n\n4. Install the remaining requirements using the command\n\n   ```shell\n   melos run doc-setup\n   ```\n\nOnce these prerequisites are met, you can build the documentation by using the built-in Melos\ntarget:\n\n```shell\nmelos doc-build\n```\n\nThe **melos doc-build** command here renders the documentation site into HTML. This command needs to\nbe re-run every time you make changes to any of the documents. Luckily, it is smart enough to only\nrebuild the documents that have changed since the previous run, so usually, a rebuild takes only a\nsecond or two.\n\nIf you want to automatically recompile the docs every time there is a change to one of the files\nyou can use the built-in Melos target below, which will also serve and open your default\nbrowser with the docs.\n\n```shell\nmelos doc-serve\n```\n\nWhen using the **melos doc-serve** command, the **melos doc-build** is only needed when\nthere are changes to the sphinx theme. This is because the serve command both automatically\ncompiles the docs on changes and also hosts them locally. The docs are served at\n`http://localhost:8000/` by default.\n\nThere are other make commands that you may find occasionally useful too:\n\n- **melos doc-clean** removes all cached generated files (in case the system gets stuck in a bad\nstate).\n- **melos doc-linkcheck** to check whether there are any broken links in the documentation.\n- **melos doc-kill** removes any orphaned TCP threads running on port 8000.\n\nThe generated html files will be in the `doc/_build/html` directory, you can view them directly\nby opening the file `doc/_build/html/index.html` in your browser. The only drawback is that the\nbrowser won't allow any dynamic content in a file opened from a local drive. The solution to this\nis to run **melos doc-serve**.\n\nIf you ever run the **melos doc-clean** command, the server will need to be restarted, because the\nclean command deletes the entire `html` directory.\n\n```{note}\nAvoid having spaces in the paths to the docs since that will keep you from\nbuilding the project due to\n[this bug](https://github.com/ipython/ipython/pull/13765).\n```\n\n\n[contributing]: contributing.md#environment-setup\n"
  },
  {
    "path": "doc/development/style_guide.md",
    "content": "# Flame Style Guide\n\nThis is a general style guide for writing code within Flame and adjacent projects. We strive to\nmaintain the code clean and readable -- both for the benefit of the users who will need to study\nthis code in order to understand how a particular feature works or debug something that is breaking,\nand for the benefit of the current and future maintainers.\n\nThis guide extends upon the official [Dart's style guide][effective dart]. Please make sure to read\nthat document first, as it is sure to improve your skill in Dart programming.\n\n\n## Code Formatting\n\nMost of the code formatting rules are enforced automatically via the linter. Run the following\ncommands to ensure the code is conformant and to fix any easy formatting problems:\n\n```shell\nflutter analyze\ndart format .\n```\n\n\n## Code Structure\n\n\n### Imports\n\n- If you're using an external symbol that's defined in multiple libraries, prefer importing the\n  smallest of them. For example, use `package:meta/meta.dart` to import annotations like\n  `@protected`, or `dart:ui` to import `Canvas`.\n\n- Never import `package:flutter/cupertino.dart` or `package:flutter/material.dart` -- prefer\n  a much smaller library `package:flutter/widgets.dart` if you're working with widgets.\n\n\n### Exports\n\n- Strongly prefer to have only one public class per file, and name the file after that class.\n  Having several private classes within the file is perfectly reasonable.\n\n- A possible exception to this rule is if the \"main\" class requires some small \"helper\" classes\n  that need to be public. Or if the file hosts multiple very small related classes.\n\n- The \"main\" class in a file should be located at the start of the file (right after the imports\n  section), so that it can be seen immediately upon opening the file. All other definitions,\n  including typedefs, helper classes and functions, should be moved below the main class.\n\n- If multiple public symbols are defined in a file, then they must be exported explicitly using\n  the `export ... show ...` statement. For example:\n\n  ```dart\n  export 'src/effects/provider_interfaces.dart'\n    show\n      AnchorProvider,\n      AngleProvider,\n      PositionProvider,\n      ScaleProvider,\n      SizeProvider;\n  ```\n\n\n### Assertions\n\nUse `assert`s to detect contract violations, or pre-condition/post-condition failures. Sometimes,\nhowever, using exceptions would be more appropriate. The following rules of thumb apply:\n\n- Use an assert with a clear error message to check for a condition that is in developers' control.\n  For example, when creating a component that takes an `opacity` level as an input, you should check\n  whether the value is in the range from 0 to 1. Consider also including the value itself into the\n  error message, to make it easier for the developer to debug the error:\n\n  ```dart\n  assert(0 <= opacity && opacity <= 1, 'The opacity value must be from 0 to 1: $opacity');\n  ```\n\n  Always use asserts as early as possible to detect possible violations. For example, check the\n  validity of `opacity` in the constructor/setter, instead of in the render function.\n\n  When adding such an assert, also include a test that checks that the assert triggers. This test\n  would verify that the component does not accept invalid input, and that the error message is what\n  you expect it to be.\n\n- Use an assert without an error message to check for a condition that cannot be triggered by the\n  developer through any means known to you. If such an assert does trigger, it would indicate a bug\n  in the Flame framework.\n\n  Such asserts serve as \"mini-tests\" directly in the code, and protect against future refactorings\n  that could create an erroneous internal state. It should not be possible to write a test that\n  would deliberately trigger such an assert.\n\n- Use an explicit if-check with an exception to test for a condition that may be outside of the\n  developer's control (i.e. it may depend on the environment or on user's input). When deciding\n  whether to use an assert or exception, consider the following question: is it possible for the\n  error condition to occur in production even after the developer has done extensive testing on\n  their side?\n\n\n### Class structure\n\n- Consider putting all class constructors at the top of the class. This makes it much easier to see\n  how the class ought to be used.\n\n- Try to make as much of your class' API private as possible, do not expose members \"just in case\".\n  This makes it much easier to modify/refactor the class later without it being a breaking change.\n\n  Remember to document all your public members! Documenting things is harder than it looks, and one\n  way to avoid the burden of documentation is to make as many variables private as possible.\n\n- If a class exposes a `List<X>` or `Vector2` property -- it is **NOT** an invitation to modify\n  them at will! Consider such properties as read-only, unless the documentation explicitly says that\n  they are allowed to be modified.\n\n- When a class becomes sufficiently big, consider adding *regions* inside it, which help with code\n  navigation and collapsing (note the lack of space after `//`):\n\n  ```dart\n  //#region Region description\n  ...\n  //#endregion\n  ```\n\n- If a class has a private member that needs to be exposed via a getter/setter, prefer the following\n  code structure:\n\n  ```dart\n  class MyClass {\n    MyClass();\n\n    ...\n    int _variable;\n    ...\n\n    /// Docs for both the getter and the setter.\n    int get variable => _variable;\n    set variable(int value) {\n      assert(value >= 0, 'variable must be non-negative: $value');\n      _variable = value;\n    }\n  }\n  ```\n\n  This would gather all private variables in a single block near the top of the class, allowing one\n  to quickly see what data the class has.\n\n\n## Documentation\n\n- Use dartdocs `///` to explain the meaning/purpose of a class, method, or a variable.\n\n- Use regular comments `//` to explain implementation details of a particular code fragment. That\n  is, these comments explain HOW something works.\n\n- Use markdown documentation in `doc/` folder to give the high-level overview of the functionality,\n  and especially how it fits into the overall Flame framework.\n\n\n### Dartdocs\n\n- Check the [Flutter Documentation Guide] -- it contains lots of great advice on writing good\n  documentation.\n  - However, disregard the advice about writing in a passive voice.\n\n- Class documentation should ideally start with the class name itself, and follow a pattern such as:\n\n  ```dart\n  /// [MyClass] is ...\n  /// [MyClass] serves as ...\n  /// [MyClass] does the following ...\n  ```\n\n  The reason for such convention is that often the documentation for a class becomes sufficiently\n  long, and it may not be immediately apparent when looking at the top of the doc what exactly is\n  being documented there.\n\n- Method documentation should start with a verb in the present simple tense, with the method name\n  as an implicit subject. Add a paragraph break after the first sentence. Try to think about what\n  could be unclear to the users of the method; and mention any pre- and post-conditions.\n  For example:\n\n  ```dart\n  /// Adds a new [child] into the container, and becomes the owner of that\n  /// child.\n  ///\n  /// The child will be disposed of when this container is destroyed.\n  /// It is an error to try to add a child that already belongs to another\n  /// container.\n  void addChild(T child) { ... }\n  ```\n\n  Avoid stating the obvious (or at least only the obvious).\n\n- Constructor documentation may follow either the style of a method (i.e. \"Creates ...\",\n  \"Constructs ...\"), or of the class but omitting the name of the class (i.e. \"Rectangular-shaped\n  component\"). Constructor documentation may be omitted if (1) it's the main constructor of the\n  class, and (2) all the parameters are obvious and coincide with the public members of the class.\n\n  **Do not** use macros to copy the class documentation into the constructor's dartdoc. Generally,\n  the class documentation answers the question \"what the class is\", whereas the constructor docs\n  answer \"how it will be constructed\".\n\n\n### Main docs\n\nThis refers to the docs on the main Flame Documentation website, the one you're reading right now.\nThe main documentation site serves as a place where people go to learn about various functionality\navailable in Flame. If you're adding a new class, then it must be documented both at the dartdocs\nlevel, and on the main docs site. The latter serves the purposes of discoverability of your class.\nWithout the docs site, your class might never be used (or at least used less than it could have\nbeen), because the developers would simply not know about it.\n\nWhen adding the documentation to the main docs site, consider also including an example directly\ninto the docs. This will make readers more excited about trying this new functionality.\n\nCheck the [Documentation] manual about how to work with the docs site.\n\nThe following style rules generally apply when writing documentation:\n\n- Maximum line length of 100 characters;\n- Prefer to define external links at the bottom of the document, so as to make reading the plain\n  text of the document easier;\n- Separate headers from the preceding content with 2 blank lines -- this makes it easier to see the\n  sections within the plain text.\n- Lists should start at the beginning of the line and sublists should be indented with 2 spaces.\n\n\n[effective dart]: https://dart.dev/guides/language/effective-dart\n[flutter documentation guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#user-content-documentation-dartdocs-javadocs-etc\n[documentation]: documentation.md\n"
  },
  {
    "path": "doc/development/testing_guide.md",
    "content": "# Writing tests\n\n- All new functionality must be tested, if at all possible. When fixing a bug, tests must be added\n  to ensure that this bug would not reappear in the future.\n\n- Run `melos run coverage` to execute all tests in the \"coverage\" mode. The results will be saved\n  in the `coverage/index.html` file, which can be opened in a browser. Try to achieve 100% coverage\n  for any new functionality added.\n\n- Every source file should have a corresponding test file, with the `_test` suffix. For example,\n  if you're making a `SpookyEffect` and the source file is `src/effects/spooky_effect.dart`, then\n  the test file should be `test/effects/spooky_effect_test.dart` mirroring the source directory.\n\n- The test file should contain a `main()` function with a single `group()` whose name matches the\n  name of the class being tested. If the source file contains multiple public classes, then each of\n  them should have its own group. For example:\n\n  ```dart\n  void main() {\n    group('SpookyEffect', () {\n      // tests here\n    });\n  }\n  ```\n\n- For a larger class, multiple groups can be created inside the top-level group, allowing to\n  navigate the test suite easier. The names of the nested groups should be capitalized.\n\n- The names of the individual tests should normally start with a lowercase.\n\n- Often, you would need to define multiple helper classes to run the tests. Such classes should be\n  private (start with an underscore), and placed at the end of the file. The reason for this is that\n  whenever some test breaks, the first thing one needs to do is to go into the test file and run all\n  the tests. Having the `main()` function at the top of the file makes this process much easier.\n\n\n## Types of tests\n\n\n### Simple tests\n\n```dart\ntest('the name of the test', () {\n  expect(...);\n});\n```\n\nThis is the simplest kind of test available, and also the fastest. Use these tests for checking\nsome classes/methods that can function in isolation from the rest of the Flame framework.\n\n\n### FlameGame tests\n\nIt is very common to want to have a `FlameGame` instance inside a test, so that you can add some\ncomponents to it and verify various behaviors. The following approach is recommended:\n\n```dart\ntestWithFlameGame('the name of the test', (game) async {\n  game.add(...);\n  await game.ready();\n\n  expect(...);\n});\n```\n\nHere the `game` instance that is passed to the test body is a fully initialized game that behaves\nas if it was mounted to a `GameWidget`. The `game.ready()` method waits until all the scheduled\ncomponents are loaded and mounted to the component tree.\n\nThe time within the `game` can be advanced with `game.update(dt)`.\n\nIf you need to have a custom game inside this test (say, a game with some mixin), then use\n\n```dart\ntestWithGame<_MyGame>(\n  'the name of the test',\n  _MyGame.new,\n  (game) async {\n    // test body...\n  },\n);\n```\n\n\n### Widget tests\n\nSometimes having a \"naked\" `FlameGame` is insufficient, and you want to have access to the Flutter\ninfrastructure as well. That is, to have a game mounted into a real `GameWidget` embedded into an\nactual Flutter framework. In such cases, use\n\n```dart\ntestWidgets('test name', (tester) async {\n  final game = _MyGame();\n  await tester.pumpWidget(GameWidget(game: game));\n  await tester.pump();\n  await tester.pump();\n\n  // At this point the game is fully initialized, and you can run your checks\n  // against it.\n  expect(...);\n\n  // Equivalent to game.update(0)\n  await tester.pump();\n\n  // Advances in-game time by 20 milliseconds\n  await tester.pump(const Duration(milliseconds: 20));\n});\n```\n\nThere are some additional methods available on the `tester` controller, for example in order to\nsimulate taps, or drags, or key presses.\n\n\n### Golden tests\n\nThese tests verify that things render as intended. The process of creating a golden test is\nsimple:\n\n1. Write the test, using the following template:\n\n   ```dart\n   testGolden(\n     'the name of the test',\n     (game) async {\n        // Set up the game by adding the necessary components\n        // You can add `expect()` checks here too, if you want to\n     },\n     size: Vector2(300, 200),\n     goldenFile: '.../_goldens/my_test_file.png',\n   );\n   ```\n\n   Here the `size` parameter determines the size of the game canvas and of the output image. The\n   `goldenFile` parameter is the name of the file where you want to store the \"golden\" results. This\n   should be a relative path to the `test/_goldens` directory, starting from your test file.\n\n2. Run\n\n   ```shell\n   flutter test --update-goldens\n   ```\n\n   this would create the golden file for the first time. Open the file to verify that it renders\n   exactly as you intended. If not, then delete the file and go back to step 1.\n\n3. Subsequent runs of `flutter test` will check whether the output of the golden test matches the\n   saved golden file. If not, Flutter will save the image-diff files into the `failures/` directory\n   where your test is located.\n\n```{note}\nAvoid using text in your golden tests -- it does not render reliably across\ndifferent platforms, due to font discrepancies and differences in\nanti-aliasing algorithms.\n```\n\n\n### Random tests\n\nThese are the tests that use a random number generator in order to construct a randomized input and\nthen check its correctness. Use as follows:\n\n```dart\ntestRandom('test name', (Random random) {\n  // Use [random] to generate random input\n});\n```\n\nYou can add `repeatCount: 1000` parameter to run this test the specified number of times, each one\nwith a different seed. It is useful to run a high `repeatCount` when developing the test, to ensure\nthat it doesn't break. However, when submitting the test to the main repository, avoid repeatCounts\nhigher than 10.\n\nIf the test breaks at some particular seed, then that seed will be shown in the test output. Add it\nas the `seed: NNN` parameter to your test, and you'll be able to run it for the same seed as long\nas you need until the test is fixed. Do not leave the `seed:` parameter when submitting your code,\nas it defeats the purpose of having the test randomized.\n"
  },
  {
    "path": "doc/flame/camera.md",
    "content": "# Camera & World\n\nIn most games the world is larger than what fits on screen at once. The camera controls which\nportion of the game world is visible and how it is projected onto the player's display, handling\npanning, zooming, and following characters. This is similar to how a\n[`Viewport`](https://api.flutter.dev/flutter/rendering/RenderViewport-class.html) in Flutter determines\nwhich part of a scrollable area is visible, but tailored for the free-form 2D coordinate space of\na game.\n\nExample of a simple game structure:\n\n```text\nFlameGame\n├── World\n│   ├── Player\n│   └── Enemy\n└── CameraComponent\n    ├── Viewfinder\n    │   ├── HudButton\n    │   └── FpsTextComponent\n    └── Viewport\n```\n\nIn order to understand how the `CameraComponent` works, imagine that your game\nworld is an entity that exists *somewhere* independently from your application.\nImagine that your game is merely a window through which you can look into that\nworld. That you can close that window at any moment, and the game world would\nstill be there. Or, on the contrary, you can open multiple windows that all look\nat the same world (or different worlds) at the same time.\n\nWith this mindset, we can now understand how the `CameraComponent` works.\n\nFirst, there is the [World](#world) class, which contains all components that are\ninside your game world. The `World` component can be mounted anywhere, for\nexample at the root of your game class, like the built-in `World` is.\n\nThen, a [CameraComponent](#cameracomponent) class that \"looks at\" the [World](#world). The\n`CameraComponent` has a [Viewport](#viewport) and a [Viewfinder](#viewfinder)\ninside of it, allowing both the flexibility of rendering the world at any place\non the screen, and also controlling the viewing location and angle. The\n`CameraComponent` also contains a [backdrop](#backdrop) component which is\nstatically rendered below the world.\n\n\n## World\n\nThis component should be used to host all other components that comprise your\ngame world. The main property of the `World` class is that it does not render\nthrough traditional means; instead it is rendered by one or more\n[CameraComponent](#cameracomponent)s to \"look at\" the world. In the `FlameGame` class there is\none `World` called `world` which is added by default and paired together with\nthe default `CameraComponent` called `camera`.\n\nA game can have multiple `World` instances that can be rendered either at the\nsame time, or at different times. For example, if you have two worlds A and B\nand a single camera, then switching that camera's target from A to B will\ninstantaneously switch the view to world B without having to unmount A and\nthen mount B.\n\nJust like with most `Component`s, children can be added to `World` by using the\n`children` argument in its constructor, or by using the `add` or `addAll`\nmethods.\n\nFor many games you want to extend the world and create your logic in there,\nsuch a game structure could look like this:\n\n```dart\nvoid main() {\n  runApp(GameWidget(FlameGame(world: MyWorld())));\n}\n\nclass MyWorld extends World {\n  @override\n  Future<void> onLoad() async {\n    // Load all the assets that are needed in this world\n    // and add components etc.\n  }\n}\n```\n\n\n## CameraComponent\n\nThis is a component through which a `World` is rendered. Multiple cameras can\nobserve the same world at the same time.\n\nThere is a default `CameraComponent` called `camera` on the `FlameGame` class\nwhich is paired together with the default `world`, so you don't need to create\nor add your own `CameraComponent` if your game doesn't need to.\n\nA `CameraComponent` has two other components inside: a [Viewport](#viewport) and a\n[Viewfinder](#viewfinder). Those components are always children of a camera.\n\nThe `FlameGame` class has a `camera` field in its constructor, so you can set\nwhat type of default camera that you want, like this camera with a\n[fixed resolution](#cameracomponent-with-fixed-resolution) for example:\n\n```dart\nvoid main() {\n  runApp(\n    GameWidget(\n      FlameGame(\n        camera: CameraComponent.withFixedResolution(\n          width: 800,\n          height: 600,\n        ),\n        world: MyWorld(),\n      ),\n    ),\n  );\n}\n```\n\nThere is also a static property `CameraComponent.currentCamera` and it returns\nthe camera object that currently performs rendering. This is needed only for\ncertain advanced use cases where the rendering of a component depends on the\ncamera settings. For example, some components may decide to skip rendering\nthemselves and their children if they are outside of the camera's viewport.\n\n\n### CameraComponent with fixed resolution\n\nThis named constructor will let you pretend that the user's device has a fixed resolution of your\nchoice. For example:\n\n```dart\nfinal camera = CameraComponent.withFixedResolution(\n  world: myWorldComponent,\n  width: 800,\n  height: 600,\n);\n```\n\nThis will create a camera with a viewport centered in the middle of the screen, taking as much space\nas possible while still maintaining the 4:3 (800x600) aspect ratio, and showing a game world region\nof size 800 x 600.\n\nA \"fixed resolution\" is very simple to work with, but it will underutilize the user's available\nscreen space, unless their device happens to have the same aspect ratio as your chosen dimensions.\n\n\n## Viewport\n\nThe `Viewport` is a window through which the `World` is seen. That window\nhas a certain size, shape, and position on the screen. There are multiple kinds\nof viewports available, and you can always implement your own.\n\nThe `Viewport` is a component, which means you can add other components to it.\nThese child components will be affected by the viewport's position, but not\nby its clip mask. Thus, if a viewport is a \"window\" into the game world, then\nits children are things that you can put on top of the window.\n\nAdding elements to the viewport is a convenient way to implement \"HUD\"\ncomponents.\n\nThe following viewports are available:\n\n- `MaxViewport` (default): this viewport expands to the maximum size allowed\n    by the game, i.e. it will be equal to the size of the game canvas.\n- `FixedResolutionViewport`: keeps the resolution and aspect ratio fixed, with black bars on the\n    sides if it doesn't match the aspect ratio.\n- `FixedSizeViewport`: a simple rectangular viewport with predefined size.\n- `FixedAspectRatioViewport`: a rectangular viewport which expands to fit\n    into the game canvas, but preserving its aspect ratio.\n- `CircularViewport`: a viewport in the shape of a circle, fixed size.\n\n\nIf you add children to the `Viewport` they will appear as static HUDs in front of the world.\n\n\n## Viewfinder\n\nThis part of the camera is responsible for knowing which location in the\nunderlying game world we are currently looking at. The `Viewfinder` also\ncontrols the zoom level, and the rotation angle of the view.\n\nThe `anchor` property of the viewfinder allows you to designate which point\ninside the viewport serves as a \"logical center\" of the camera. For example,\nin side-scrolling action games it is common to have the camera focused on the\nmain character who is displayed not in the center of the screen but closer to\nthe lower-left corner. This off-center position would be the \"logical center\"\nof the camera, controlled by the viewfinder's `anchor`.\n\nIf you add children to the `Viewfinder` they will appear in front of the world,\nbut behind the viewport and with the same transformations as are applied to the\nworld, so these components are not static.\n\nYou can also add behavioral components as children to the viewfinder, for\nexample [effects](effects.md) or other controllers. If you for example would add a\n`ScaleEffect` you would be able to achieve a smooth zoom in your game.\n\n\n## Backdrop\n\nTo add static components behind the world you can add them to the `backdrop`\ncomponent, or replace the `backdrop` component. This is for example useful if\nyou want to have a static `ParallaxComponent` that shows behind a world that\ncontains a player that can move around.\n\nExample:\n\n```dart\ncamera.backdrop.add(MyStaticBackground());\n```\n\nor\n\n```dart\ncamera.backdrop = MyStaticBackground();\n```\n\n\n## Camera controls\n\nThere are several ways to modify a camera's settings at runtime:\n\n1. Use camera functions such as `follow()`, `moveBy()` and `moveTo()`.\n   Under the hood, this approach uses the same effects/behaviors as in (2).\n\n2. Apply effects and/or behaviors to the camera's `Viewfinder` or `Viewport`.\n   The effects and behaviors are special kinds of components whose purpose is\n   to modify some property of a component over time.\n\n3. Do it manually. You can always override the `CameraComponent.update()`\n   method (or the same method on the viewfinder or viewport) and within it\n   change the viewfinder's position or zoom as you see fit. This approach may\n   be viable in some circumstances, but in general it is not recommended.\n\nThe `CameraComponent` has several methods for controlling its behavior:\n\n- `follow()` will force the camera to follow the provided target.\n   Optionally you can limit the maximum speed of movement of the camera, or\n   allow it to only move horizontally/vertically.\n\n- `stop()` will undo the effect of the previous call and stop the camera\n   at its current position.\n\n- `moveBy()` can be used to move the camera by the specified offset.\n   If the camera was already following another component or moving,\n   those behaviors would be automatically cancelled.\n\n- `moveTo()` can be used to move the camera to the designated point on\n   the world map. If the camera was already following another component or\n   moving towards another point, those behaviors would be automatically\n   cancelled.\n\n- `setBounds()` allows you to add limits to where the camera is allowed to go. These limits\n   are in the form of a `Shape`, which is commonly a rectangle, but can also be any other shape.\n\n\n### visibleWorldRect\n\nThe camera exposes property `visibleWorldRect`, which is a rect that describes the world's region\nwhich is currently visible through the camera. This region can be used in order to avoid rendering\ncomponents that are out of view, or updating objects that are far away from the player less\nfrequently.\n\nThe `visibleWorldRect` is a cached property, and it updates automatically whenever the camera\nmoves or the viewport changes its size.\n\n\n### canSee\n\nThe `CameraComponent` has a method called `canSee` which can be used to check\nif a component is visible from the camera point of view.\nThis is useful for example to cull components that are not in view.\n\n```dart\nif (!camera.canSee(component)) {\n   component.removeFromParent(); // Cull the component\n}\n```\n\n\n### Post processing\n\n[Post processing](rendering/post_processing.md) is a technique used in game development to apply visual\neffects to a component tree after it has been rendered. This can be added to the camera via the\n`postProcess` property.\n\n```dart\ncamera.postProcess = PostProcessGroup(\n  postProcesses: [\n    PostProcessSequentialGroup(\n      postProcesses: [\n        FireflyPostProcess(),\n        WaterPostProcess(),\n      ],\n    ),\n    ForegroundFogPostProcess(),\n  ],\n);\n```\n\nRead more about this on [Post processing](rendering/post_processing.md).\n"
  },
  {
    "path": "doc/flame/collision_detection.md",
    "content": "# Collision Detection\n\nAlmost every game needs to know when objects touch or overlap. Without collision detection a player\ncould walk through walls, bullets would pass through enemies, and coins could never be collected.\nFlame provides a built-in collision detection system so you can focus on what *happens* when objects\ncollide rather than writing the intersection math yourself.\n\nCollision detection is needed in most games to detect and act upon two components intersecting each\nother. For example, an arrow hitting an enemy or the player picking up a coin.\n\nIn most collision detection systems you use something called hitboxes to create more precise\nbounding boxes of your components. In Flame the hitboxes are areas of the component that can react\nto collisions and make [gesture input](inputs/gesture_input.md#gesturehitboxes) more accurate.\n\nThe collision detection system supports three different types of shapes that you can build hitboxes\nfrom, these shapes are Polygon, Rectangle and Circle. Multiple hitboxes can be added to a\ncomponent to form the area which can be used to either detect collisions or determine whether it\ncontains a point. The latter is very useful for accurate gesture detection. The collision\ndetection does not handle what should happen when two hitboxes collide, so it is up to the user\nto implement what will happen when for example two `PositionComponent`s have intersecting\nhitboxes.\n\nDo note that the built-in collision detection system does not take collisions between two hitboxes\nthat overshoot each other into account. This could happen when they either move very fast or\n`update` is called with a large delta time (for example if your app is not in the foreground).\nThis behavior is called tunneling.\n\n\n## Mixins\n\n\n### HasCollisionDetection\n\nIf you want to use collision detection in your game you have to add the `HasCollisionDetection`\nmixin to your game so that it can keep track of the components that can collide.\n\nExample:\n\n```dart\nclass MyGame extends FlameGame with HasCollisionDetection {\n  // ...\n}\n```\n\nNow when you add `ShapeHitbox`s to components that are then added to the game, they will\nautomatically be checked for collisions.\n\nYou can also add `HasCollisionDetection` directly to another `Component` instead\nof the `FlameGame`,\nfor example to the `World` that is used for the `CameraComponent`.\nIf that is done, hitboxes that are added in that component's tree will only be compared to other\nhitboxes in that subtree, which makes it possible to have several worlds with collision detection\nwithin one `FlameGame`.\n\nExample:\n\n```dart\nclass CollisionDetectionWorld extends World with HasCollisionDetection {}\n```\n\n```{note}\nHitboxes will only be connected to one collision detection system and that is\nthe closest parent that has the `HasCollisionDetection` mixin.\n```\n\n\n### CollisionCallbacks\n\nTo react to a collision you should add the `CollisionCallbacks` mixin to your component.\nExample:\n\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: collision_detection\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nclass MyCollidable extends PositionComponent with CollisionCallbacks {\n  @override\n  void onCollision(Set<Vector2> points, PositionComponent other) {\n    if (other is ScreenHitbox) {\n      //...\n    } else if (other is YourOtherComponent) {\n      //...\n    }\n  }\n\n  @override\n  void onCollisionEnd(PositionComponent other) {\n    if (other is ScreenHitbox) {\n      //...\n    } else if (other is YourOtherComponent) {\n      //...\n    }\n  }\n}\n```\n\nIn this example we use Dart's `is` keyword to check what kind of component we collided with.\nThe set of points is where the edges of the hitboxes intersect.\n\nNote that the `onCollision` method will be called on both `PositionComponent`s if they have both\nimplemented the `onCollision` method, and also on both hitboxes. The same goes for the\n`onCollisionStart` and `onCollisionEnd` methods, which are called when two components and hitboxes\nstart or stop colliding with each other.\n\nWhen a `PositionComponent` (and hitbox) starts to collide with another `PositionComponent`\nboth `onCollisionStart` and `onCollision` are called, so if you don't need to do something specific\nwhen a collision starts you only need to override `onCollision`, and vice versa.\n\nIf you want to check collisions with the screen edges, as we do in the example above, you can use\nthe predefined [ScreenHitbox](#screenhitbox) class.\n\nBy default all hitboxes are hollow, this means that one hitbox can be fully enclosed by another\nhitbox without triggering a collision. If you want to set your hitboxes to be solid you can set\n`isSolid = true`. A hollow hitbox inside of a solid hitbox will trigger a collision, but not the\nother way around. If there are no intersections with the edges on a solid hitbox the center\nposition is instead returned.\n\n\n### Collision order\n\nIf a `Hitbox` collides with more than one other `Hitbox` within a given time step, then\nthe `onCollision` callbacks will be called in an essentially random order. In some cases this can\nbe a problem, such as in a bouncing ball game where the trajectory of the ball can differ depending\non which other object was hit first. To help resolve this the `collisionsCompletedNotifier`\nlistener can be used - this triggers at the end of the collision detection process.\n\nAn example of how this might be used is to add a local variable in your `PositionComponent` to save\nthe other components with which it's colliding:\n`List<PositionComponent> collisionComponents = [];`. The `onCollision` callback is then used to\nsave all the other `PositionComponent`s to this list:\n\n```dart\n@override\nvoid onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {\n  collisionComponents.add(other);\n  super.onCollision(intersectionPoints, other);\n}\n\n```\n\nFinally, one adds a listener to the `onLoad` method of the `PositionComponent` to call a function\nwhich will resolve how the collisions should be dealt with:\n\n```dart\n(game as HasCollisionDetection)\n    .collisionDetection\n    .collisionsCompletedNotifier\n    .addListener(() {\n  resolveCollisions();\n});\n```\n\nThe list `collisionComponents` would need to be cleared in each call to `update`.\n\n\n## ShapeHitbox\n\nThe `ShapeHitbox`s are normal components, so you add them to the component that you want to add\nhitboxes to just like any other component:\n\n```dart\nclass MyComponent extends PositionComponent {\n  @override\n  void onLoad() {\n    add(RectangleHitbox());\n  }\n}\n```\n\nIf you don't add any arguments to the hitbox, like above, the hitbox will try to fill its parent as\nmuch as possible. Apart from having the hitboxes fill their parents, there are two ways to\ninitialize hitboxes. One is with the normal constructor where you define the hitbox by itself,\nwith a size and a position etc. The other way is to use the `relative` constructor which defines\nthe hitbox in relation to the size of its intended parent.\n\n\nIn some specific cases you might want to handle collisions only between hitboxes, without\npropagating `onCollision*` events to the hitbox's parent component. For example, a vehicle could\nhave a body hitbox to control collisions and side hitboxes to check the possibility to turn left\nor right.\nSo, colliding with a body hitbox means colliding with the component itself, whereas colliding with\na side hitbox does not mean a real collision and should not be propagated to hitbox's parent.\nFor this case you can set `triggersParentCollision` variable to `false`:\n\n```dart\nclass MyComponent extends PositionComponent {\n\n  late final MySpecialHitbox utilityHitbox;\n\n  @override\n  void onLoad() {\n    utilityHitbox = MySpecialHitbox();\n    add(utilityHitbox);\n  }\n\n  void update(double dt) {\n    if (utilityHitbox.isColliding) {\n      // do some specific things if hitbox is colliding\n    }\n  }\n// component's onCollision* functions, ignoring MySpecialHitbox collisions.\n}\n\nclass MySpecialHitbox extends RectangleHitbox {\n  MySpecialHitbox() {\n    triggersParentCollision = false;\n  }\n\n// hitbox specific onCollision* functions\n\n}\n```\n\nYou can read more about how the different shapes are defined in the\n[ShapeComponents](components/shape_components.md) section.\n\nRemember that you can add as many `ShapeHitbox`s as you want to your `PositionComponent` to make up\nmore complex areas. For example a snowman with a hat could be represented by three `CircleHitbox`s\nand two `RectangleHitbox`s as its hat.\n\nA hitbox can be used either for collision detection or for making gesture detection more accurate\non top of components, see more regarding the latter in the section about the\n[GestureHitboxes](inputs/gesture_input.md#gesturehitboxes) mixin.\n\n\n### CollisionType\n\nThe hitboxes have a field called `collisionType` which defines when a hitbox should collide with\nanother. Usually you want to set as many hitboxes as possible to `CollisionType.passive` to make\nthe collision detection more performant. By default the `CollisionType` is `active`.\n\nThe `CollisionType` enum contains the following values:\n\n- `active` collides with other `Hitbox`es of type active or passive\n- `passive` collides with other `Hitbox`es of type active\n- `inactive` will not collide with any other `Hitbox`es\n\nSo if you have hitboxes that you don't need to check collisions against each other you can mark\nthem as passive by setting `collisionType: CollisionType.passive` in the constructor,\nthis could for example be ground components or maybe your enemies don't need\nto check collisions between each other, then they could be marked as `passive` too.\n\nImagine a game where there are a lot of bullets, that can't collide with each other, flying towards\nthe player, then the player would be set to `CollisionType.active` and the bullets would be set to\n`CollisionType.passive`.\n\nThen we have the `inactive` type which simply doesn't get checked at all\nin the collision detection.\nThis could be used for example if you have components outside of the screen that you don't care\nabout at the moment but that might later come back in to view so they are not completely removed\nfrom the game.\n\nThese are just examples of how you could use these types, there will be a lot more use cases for\nthem so don't hesitate to use them even if your use case isn't listed here.\n\n\n### PolygonHitbox\n\nIt should be noted that if you want to use collision detection or `containsPoint` on the `Polygon`,\nthe polygon needs to be convex. So always use convex polygons or you will most likely run into\nproblems if you don't really know what you are doing.\n\nThe other hitbox shapes don't have any mandatory constructor, that is because they can have a\ndefault calculated from the size of the collidable that they are attached to, but since a\npolygon can be made in an infinite number of ways inside of a bounding box you have to add the\ndefinition in the constructor for this shape.\n\nThe `PolygonHitbox` has the same constructors as the [](components/shape_components.md#polygoncomponent),\nsee that section for documentation regarding those.\n\n\n### RectangleHitbox\n\nThe `RectangleHitbox` has the same constructors as the [](components/shape_components.md#rectanglecomponent),\nsee that section for documentation regarding those.\n\n\n### CircleHitbox\n\nThe `CircleHitbox` has the same constructors as the [](components/shape_components.md#circlecomponent),\nsee that section for documentation regarding those.\n\n\n## ScreenHitbox\n\n`ScreenHitbox` is a component which represents the edges of your viewport/screen. If you add a\n`ScreenHitbox` to your game your other components with hitboxes will be notified when they\ncollide with the edges. It doesn't take any arguments, it only depends on the `size` of the game\nthat it is added to. To add it you can just do `add(ScreenHitbox())` in your game, if you don't\nwant the `ScreenHitbox` itself to be notified when something collides with it. Since\n`ScreenHitbox` has the `CollisionCallbacks` mixin you can add your own `onCollisionCallback`,\n`onStartCollisionCallback` and `onEndCollisionCallback` functions to that object if needed.\n\n\n## CompositeHitbox\n\nIn the `CompositeHitbox` you can add multiple hitboxes so that\nthey emulate being one joined hitbox.\n\nIf you want to form a hat for example you might want\nto use two [](#rectanglehitbox)s to follow that\nhat's edges properly, then you can add those hitboxes to an instance of this class and react to\ncollisions to the whole hat, instead of for just each hitbox separately.\n\n\n## Broad phase\n\nIf your game field isn't huge and does not have a lot of collidable components - you don't have to\nworry about the broad phase system that is used, so if the standard implementation is performant\nenough for you, you probably don't have to read this section.\n\nA broad phase is the first step of collision detection where potential collisions are calculated.\nCalculating these potential collisions is faster than checking the intersections exactly,\nand it removes the need to check all hitboxes against each other and\ntherefore avoiding O(n²).\n\nThe broad phase produces a set of potential collisions (a set of\n`CollisionProspect`s). This set is then used to check the exact intersections between\nhitboxes (sometimes called \"narrow phase\").\n\nBy default, Flame's collision detection is using a sweep and prune broadphase step. If your game\nrequires another type of broadphase you can write your own broadphase by extending `Broadphase` and\nmanually setting the collision detection system that should be used.\n\nFor example, if you have implemented a broadphase built on a magic algorithm\ninstead of the standard sweep and prune, then you would do the following:\n\n```dart\nclass MyGame extends FlameGame with HasCollisionDetection {\n  MyGame() : super() {\n    collisionDetection =\n        StandardCollisionDetection(broadphase: MagicAlgorithmBroadphase());\n  }\n}\n```\n\n\n## Quad Tree broad phase\n\nIf your game field is large and the game contains a lot of collidable\ncomponents (more than a hundred), standard sweep and prune can\nbecome inefficient. If it does, you can try to use the quad tree broad phase.\n\nTo do this, add the `HasQuadTreeCollisionDetection` mixin to your game instead of\n`HasCollisionDetection` and call the `initializeCollisionDetection` function on game load:\n\n```dart\nclass MyGame extends FlameGame with HasQuadTreeCollisionDetection {\n  @override\n  void onLoad() {\n    initializeCollisionDetection(\n      mapDimensions: const Rect.fromLTWH(0, 0, mapWidth, mapHeight),\n      minimumDistance: 10,\n    );\n  }\n}\n```\n\nWhen calling `initializeCollisionDetection` you should pass it the correct map dimensions, to make\nthe quad tree algorithm to work properly. There are also additional parameters to make the system\nmore efficient:\n\n- `minimumDistance`: minimum distance between objects to consider them as possibly colliding.\n  If `null` - the check is disabled, it is default behavior\n- `maxObjects`: maximum objects count in one quadrant. Default to 25.\n- `maxDepth`: maximum nesting levels inside quadrant. Default to 10\n\nIf you use the quad tree system, you can make it even more efficient by implementing the\n`onComponentTypeCheck` function of the `CollisionCallbacks` mixin in your components.\nIt is useful if you need to prevent collisions of items of different types.\nThe result of the calculation is cached so\nyou should not check any dynamic parameters here, the function is intended to be used as a pure\ntype checker:\n\n```dart\nclass Bullet extends PositionComponent with CollisionCallbacks {\n\n  @override\n  bool onComponentTypeCheck(PositionComponent other) {\n    if (other is Player || other is Water) {\n      // do NOT collide with Player or Water\n      return false;\n    }\n    // Just return true if you're not interested in\n    // the parent's type check result. Or call super\n    // to override the result with the parent's result.\n    return super.onComponentTypeCheck(other);\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    // Removes the component when it comes in contact with a Brick.\n    // Neither Player nor Water would be passed to this function\n    // because these classes are filtered out by [onComponentTypeCheck]\n    // in an earlier stage.\n    if (other is Brick) {\n      removeFromParent();\n    }\n    super.onCollisionStart(intersectionPoints, other);\n  }\n}\n```\n\nAfter intensive gameplay a map could become over-clusterized with a lot of empty quadrants.\nRun `QuadTree.optimize()` to perform a cleanup of empty quadrants:\n\n```dart\nclass QuadTreeExample extends FlameGame\n        with HasQuadTreeCollisionDetection {\n\n  /// A function called when intensive gameplay session is over\n  /// It also might be scheduled, but no need to run it on every update.\n  /// Use right interval depending on your game circumstances\n  onGameIdle() {\n    (collisionDetection as QuadTreeCollisionDetection)\n            .quadBroadphase\n            .tree\n            .optimize();\n  }\n}\n\n```\n\n```{note}\nAlways experiment with different collision detection approaches\nand check how they perform on your game.\nIt is not unheard of that `QuadTreeBroadphase` is significantly \n_slower_ than the default.\nDon't assume that the more sophisticated approach is always faster.\n```\n\n\n## Ray casting and Ray tracing\n\nRay casting and ray tracing are methods for sending out rays from a point in your game and being\nable to see what these rays collide with and how they reflect after hitting something.\n\nFor all of the following methods, if there are any hitboxes that you wish to ignore,\nyou can add the `ignoreHitboxes` argument which is a list of the hitboxes\nthat you wish to disregard for the call.\nThis can be quite useful for example if you are casting rays from within a hitbox,\nwhich could be on your player or NPC;\nor if you don't want a ray to bounce off a `ScreenHitbox`.\n\n\n### Ray casting\n\nRay casting is the operation of casting out one or more rays from a point and see if they hit\nanything, in Flame's case, hitboxes.\n\nWe provide two methods for doing so, `raycast` and `raycastAll`. The first one just casts out\na single ray and gets back a result with information about what and where the ray hit, and some\nextra information like the distance, the normal and the reflection ray.\nThe second one, `raycastAll`,\nworks similarly but sends out multiple rays uniformly around the origin, or within an angle\ncentered at the origin.\n\nBy default, `raycast` and `raycastAll` scan for the nearest hit irrespective of\nhow far it lies from the ray origin.\nBut in some use cases, it might be interesting to find hits only within a certain\nrange. For such cases, an optional `maxDistance` can be provided.\n\nTo use the ray casting functionality you have to have the `HasCollisionDetection` mixin on your\ngame. After you have added that, you can call `collisionDetection.raycast(...)` on your game class,\nor with the `HasGameReference` mixin from other components as well.\n\nExample:\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: ray_cast\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nclass MyGame extends FlameGame with HasCollisionDetection {\n  @override\n  void update(double dt) {\n    super.update(dt);\n    final ray = Ray2(\n        origin: Vector2(0, 100),\n        direction: Vector2(1, 0),\n    );\n    final result = collisionDetection.raycast(ray);\n  }\n}\n```\n\nIn this example one can see that the `Ray2` class is being used, this class defines a ray from an\norigin position and a direction (which are both defined by `Vector2`s). This particular ray starts\nfrom `0, 100` and shoots a ray straight to the right.\n\nThe result from this operation will either be `null` if the ray didn't hit anything, or a\n`RaycastResult` which contains:\n\n- Which hitbox the ray hit\n- The intersection point of the collision\n- The reflection ray, i.e. how the ray would reflect on the hitbox that it hit\n- The normal of the collision, i.e. a vector perpendicular to the face of the hitbox that it hits\n\nIf you are concerned about performance you can pre-create a `RaycastResult` object that you send in\nto the method with the `out` argument, this will make it possible for the method to reuse this\nobject instead of creating a new one for each iteration. This can be good if you do a lot of\nray casting in your `update` methods.\n\n\n#### raycastAll\n\nSometimes you want to send out rays in all, or a limited range, of directions from an origin. This\ncan have a lot of applications, for example you could calculate the field of view of a player or\nenemy, or it can also be used to create light sources.\n\nExample:\n\n```dart\nclass MyGame extends FlameGame with HasCollisionDetection {\n  @override\n  void update(double dt) {\n    super.update(dt);\n    final origin = Vector2(200, 200);\n    final result = collisionDetection.raycastAll(\n      origin,\n      numberOfRays: 100,\n    );\n  }\n}\n```\n\nIn this example we would send out 100 rays from (200, 200) uniformly spread in all directions.\n\nIf you want to limit the directions you can use the `startAngle` and the `sweepAngle` arguments.\nWhere the `startAngle` (counting from straight up) is where the rays will start and then the rays\nwill end at `startAngle + sweepAngle`.\n\nIf you are concerned about performance you can re-use the `RaycastResult` objects that are created\nby the function by sending them in as a list with the `out` argument.\n\n\n### Ray tracing\n\nRay tracing is similar to ray casting, but instead of just checking what the ray hits you can\ncontinue to trace the ray and see what its reflection ray (the ray bouncing off the hitbox) will\nhit and then what that casted reflection ray's reflection ray will hit and so on, until you decide\nthat you have traced the ray for long enough. If you imagine how a pool ball would bounce on a pool\ntable for example, that information could be retrieved with the help of ray tracing.\n\nExample:\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: ray_trace\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nclass MyGame extends FlameGame with HasCollisionDetection {\n  @override\n  void update(double dt) {\n    super.update(dt);\n    final ray = Ray2(\n        origin: Vector2(0, 100),\n        direction: Vector2(1, 1)..normalize()\n    );\n    final results = collisionDetection.raytrace(\n      ray,\n      maxDepth: 100,\n    );\n    for (final result in results) {\n      if (result.intersectionPoint.distanceTo(ray.origin) > 300) {\n        break;\n      }\n    }\n  }\n}\n```\n\nIn the example above we send out a ray from (0, 100) diagonally down to the right\nand we say that we want it to bounce on at most 100 hitboxes,\nit doesn't necessarily have to get 100 results since at\nsome point one of the reflection rays might not hit a hitbox and then the method is done.\n\nThe method is lazy, which means that it will only do the calculations that you ask for, so you have\nto loop through the iterable that it returns to get the results, or do `toList()` to directly\ncalculate all the results.\n\nIn the for-loop it can be seen how this can be used, in that loop we check whether the current\nreflection rays intersection point (where the previous ray hit the hitbox) is further away than 300\npixels from the origin of the starting ray, and if it is we don't care about the rest\nof the results (and then they don't have to be calculated either).\n\nIf you are concerned about performance you can re-use the `RaycastResult` objects that are created\nby the function by sending them in as a list with the `out` argument.\n\n\n## Comparison to Forge2D\n\nIf you want to have a full-blown physics engine in your game we recommend that you use\nForge2D by adding\n[flame_forge2d](https://github.com/flame-engine/flame/tree/main/packages/flame_forge2d)\nas a dependency.\nBut if you have a simpler use-case and just want to check for collisions of components and improve\nthe accuracy of gestures, Flame's built-in collision detection will serve you very well.\n\nIf you have the following needs you should at least consider using [Forge2D](https://github.com/flame-engine/forge2d):\n\n- Realistic interacting forces\n- Particle systems that can interact with other bodies\n- Joints between bodies\n\nOn the other hand, it is a good idea to just use the Flame collision detection system if you only\nneed some of the following things (since it is simpler to not involve Forge2D):\n\n- The ability to act on some of your components colliding\n- The ability to act on your components colliding with the screen boundaries\n- Complex shapes to act as a hitbox for your component so that gestures will be more accurate\n- Hitboxes that can tell what part of a component that collided with something\n\n\n## Examples\n\n- [Collidable AnimationComponent](https://examples.flame-engine.org/#/Collision_Detection_Collidable_AnimationComponent)\n- [Circles](https://examples.flame-engine.org/#/Collision_Detection_Circles)\n- [Multiple shapes](https://examples.flame-engine.org/#/Collision_Detection_Multiple_shapes)\n- [More Examples](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/collision_detection)\n"
  },
  {
    "path": "doc/flame/components/components.md",
    "content": "# Components\n\nIn game development, a component is a self-contained unit that encapsulates a specific piece of game\nbehavior or visual. Flame uses the [Flame Component System](../game.md) (FCS) where every object in\nyour game (players, enemies, backgrounds, UI elements) is a component. This makes games easier to\nbuild and maintain because each piece of logic lives in its own class and components can be freely\ncomposed into a tree, much like\n[Flutter's widget tree](https://docs.flutter.dev/get-started/fundamentals/widgets).\n\n- [Position Component](position_component.md)\n- [Sprite Components](sprite_components.md)\n- [Parallax Component](parallax_component.md)\n- [Shape Components](shape_components.md)\n- [Utility Components](utility_components.md)\n\n```{include} ../diagrams/component.md\n```\n\nThis diagram might look intimidating, but don't worry, it is not as complex as it looks.\n\n\n## Component\n\nAll components inherit from the `Component` class and can have other `Component`s as children.\nThis is the base of what we call the Flame Component System, or FCS for short.\n\nChildren can be added either with the `add(Component c)` method or directly in the constructor.\n\nExample:\n\n```dart\nvoid main() {\n  final component1 = Component(children: [Component(), Component()]);\n  final component2 = Component();\n  component2.add(Component());\n  component2.addAll([Component(), Component()]);\n}\n```\n\nThe `Component()` here could of course be any subclass of `Component`.\n\nEvery `Component` has a few methods that you can optionally implement, which are used by the\n`FlameGame` class.\n\n\n### Component lifecycle\n\n```{include} ../diagrams/component_life_cycle.md\n```\n\nThe `onGameResize` method is called whenever the screen is resized, and also when this component\ngets added into the component tree, before the `onMount`.\n\nThe `onParentResize` method is similar: it is also called when the component is mounted into the\ncomponent tree, and also whenever the parent of the current component changes its size.\n\nThe `onRemove` method can be overridden to run code before the component is removed from the game.\nIt is only run once even if the component is removed both by using the parent's remove method and\nthe `Component` remove method.\n\nThe `onLoad` method can be overridden to run asynchronous initialization code for the component,\nlike loading an image for example. This method is executed before `onGameResize` and\n`onMount`. This method is guaranteed to execute only once during the lifetime of the component, so\nyou can think of it as an \"asynchronous constructor\".\n\nThe `onMount` method runs every time the component is mounted into a game tree. This means that\nyou should not initialize `late final` variables here, since this method might run several times\nthroughout the component's lifetime. This method will only run if the parent is already mounted.\nIf the parent is not mounted yet, then this method will wait in a queue (this will have no effect\non the rest of the game engine).\n\nThe `onChildrenChanged` method can be overridden if it's needed to detect changes in a parent's\nchildren. This method is called whenever a child is added to or removed from a parent (this includes\nif a child is changing its parent). Its parameters contain the target child and the type of\nchange it went through (`added` or `removed`).\n\nThe `onHotReload` method is called on every component in the tree when Flutter's hot reload is\ntriggered (debug mode only). Override this method to reload assets, recalculate cached values, or\nperform other actions in response to code changes during development. The notification propagates\nautomatically to all children that are loading or loaded, so you must call `super.onHotReload()`\nin your override:\n\n```dart\nclass MyComponent extends Component {\n  @override\n  void onHotReload() {\n    super.onHotReload();\n    // Re-read values that may have changed in source code.\n    _cachedValue = _computeExpensiveValue();\n  }\n}\n```\n\nA component's lifecycle state can be checked by a series of getters:\n\n- `isLoaded`: Returns a bool with the current loaded state.\n- `loaded`: Returns a future that will complete once the component has finished loading.\n- `isMounted`: Returns a bool with the current mounted state.\n- `mounted`: Returns a future that will complete once the component has finished mounting.\n- `isRemoved`: Returns a bool with the current removed state.\n- `removed`: Returns a future that will complete once the component has been removed.\n\n\n### Priority\n\nIn Flame every `Component` has the `int priority` property, which determines\nthat component's sorting order within its parent's children. This is sometimes referred to\nas `z-index` in other languages and frameworks. The higher the `priority` is set to, the\ncloser the component will appear on the screen, since it will be rendered on top of any components\nwith lower priority that were rendered before it.\n\nIf you add two components and set one of their priorities to 1 for example, then that component will\nbe rendered on top of the other component (if they overlap), because the default priority is 0.\n\nAll components take in `priority` as a named argument, so if you know the priority that you want\nyour component at compile time, then you can pass it in to the constructor.\n\nExample:\n\n```dart\nclass MyGame extends FlameGame {\n  @override\n  void onLoad() {\n    final myComponent = PositionComponent(priority: 5);\n    add(myComponent);\n  }\n}\n```\n\nTo update the priority of a component you have to set it to a new value, like\n`component.priority = 2`, and it will be updated in the current tick before the rendering stage.\n\nIn the following example we first initialize the component with priority 1, and then when the\nuser taps the component we change its priority to 2:\n\n```dart\nclass MyComponent extends PositionComponent with TapCallbacks {\n\n  MyComponent() : super(priority: 1);\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    priority = 2;\n  }\n}\n```\n\n\n### Composability of components\n\nSometimes it is useful to wrap other components inside of your component. For example by grouping\nvisual components through a hierarchy. You can do this by adding child components to any component,\nfor example `PositionComponent`.\n\nWhen you have child components on a component every time the parent is updated and rendered, all the\nchildren are rendered and updated with the same conditions.\n\nHere's an example where the visibility of two components is handled by a wrapper:\n\n```dart\nclass GameOverPanel extends PositionComponent {\n  bool visible = false;\n  final Image spriteImage;\n\n  GameOverPanel(this.spriteImage);\n\n  @override\n  void onLoad() {\n    // GameOverText is a Component\n    final gameOverText = GameOverText(spriteImage);\n    // GameOverRestart is a SpriteComponent\n    final gameOverButton = GameOverButton(spriteImage);\n\n    add(gameOverText);\n    add(gameOverButton);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    if (visible) {\n    } // If not visible none of the children will be rendered\n  }\n}\n```\n\nThere are two methods for adding child components to your component. First, you have methods\n`add()`, `addAll()`, and `addToParent()`, which can be used at any time during the game.\nTraditionally, children will be created and added from the component's `onLoad()` method, but it\nis also common to add new children during the course of the game.\n\nThe second method is to use the `children:` parameter in the component's constructor. This\napproach more closely resembles the standard Flutter API:\n\n```dart\nclass MyGame extends FlameGame {\n  @override\n  void onLoad() {\n    add(\n      PositionComponent(\n        position: Vector2(30, 0),\n        children: [\n          HighScoreDisplay(),\n          HitPointsDisplay(),\n          FpsComponent(),\n        ],\n      ),\n    );\n  }\n}\n```\n\nThe two approaches can be combined freely: the children specified within the constructor will be\nadded first, and then any additional child components after.\n\nNote that the children added via either method are only guaranteed to be available eventually:\nafter they are loaded and mounted. We can only assure that they will appear in the children list\nin the same order as they were scheduled for addition.\n\n\n### Access to the World from a Component\n\nIf a component that has a `World` as an ancestor and requires access to that `World` object, one\ncan use the `HasWorldReference` mixin.\n\nExample:\n\n```dart\nclass MyComponent extends Component with HasWorldReference<MyWorld>,\n    TapCallbacks {\n  @override\n  void onTapDown(TapDownEvent info) {\n    // world is of type MyWorld\n    world.add(AnotherComponent());\n  }\n}\n```\n\nIf you try to access `world` from a component that doesn't have a `World` ancestor of the\ncorrect type an assertion error will be thrown.\n\n\n### Ensuring a component has a given parent\n\nWhen a component needs to be added to a specific parent type, the `ParentIsA` mixin can be used\nto enforce a strongly typed parent.\n\nExample:\n\n```dart\nclass MyComponent extends Component with ParentIsA<MyParentComponent> {\n  @override\n  void onLoad() {\n    // parent is of type MyParentComponent\n    print(parent.myValue);\n  }\n}\n```\n\nIf you try to add `MyComponent` to a parent that is not `MyParentComponent`, an assertion error\nwill be thrown.\n\n\n### Ensuring a component has a given ancestor\n\nWhen a component needs to have a specific ancestor type somewhere in the component tree, the\n`HasAncestor` mixin can be used to enforce that relationship.\n\nThe mixin exposes the `ancestor` field that will be of the given type.\n\nExample:\n\n```dart\nclass MyComponent extends Component with HasAncestor<MyAncestorComponent> {\n  @override\n  void onLoad() {\n    // ancestor is of type MyAncestorComponent.\n    print(ancestor.myValue);\n  }\n}\n```\n\nIf you try to add `MyComponent` to a tree that does not contain `MyAncestorComponent`, an\nassertion error will be thrown.\n\n\n### Component Keys\n\nComponents can have an identification key that allows them to be retrieved from the component\ntree, from any point of the tree.\n\nTo register a component with a key, simply pass a key to the `key` argument on the component's\nconstructor:\n\n```dart\nfinal myComponent = Component(\n  key: ComponentKey.named('player'),\n);\n```\n\nThen, to retrieve it in a different point of the component tree:\n\n```dart\nflameGame.findByKey(ComponentKey.named('player'));\n```\n\nThere are two types of keys, `unique` and `named`. Unique keys are based on equality of the key\ninstance, meaning that:\n\n```dart\nfinal key = ComponentKey.unique();\nfinal key2 = key;\nprint(key == key2); // true\nprint(key == ComponentKey.unique()); // false\n```\n\nNamed ones are based on the name that it receives, so:\n\n```dart\nfinal key1 = ComponentKey.named('player');\nfinal key2 = ComponentKey.named('player');\nprint(key1 == key2); // true\n```\n\nWhen named keys are used, the `findByKeyName` helper can also be used to retrieve the component.\n\n\n```dart\nflameGame.findByKeyName('player');\n```\n\n\n### Querying child components\n\nThe children that have been added to a component live in a `QueryableOrderedSet` called\n`children`. To query for a specific type of components in the set, the `query<T>()` function can be\nused. By default `strictMode` is `false` in the children set, but if you set it to true, then the\nqueries will have to be registered with `children.register` before a query can be used.\n\nIf you know at compile time that you later will run a query of a specific type it is recommended to\nregister the query, no matter if the `strictMode` is set to `true` or `false`, since there are some\nperformance benefits to gain from it. The `register` call is usually done in `onLoad`.\n\nExample:\n\n```dart\n@override\nvoid onLoad() {\n  children.register<PositionComponent>();\n}\n```\n\nIn the example above a query is registered for `PositionComponent`s, and an example of how to\nquery the registered component type can be seen below.\n\n```dart\n@override\nvoid update(double dt) {\n  final allPositionComponents = children.query<PositionComponent>();\n}\n```\n\n\n### Querying components at a specific point on the screen\n\nThe method `componentsAtPoint()` allows you to check which components were rendered at some point\non the screen. The returned value is an iterable of components, but you can also obtain the\ncoordinates of the initial point in each component's local coordinate space by providing a writable\n`List<Vector2>` as a second parameter.\n\nThe iterable retrieves the components in the front-to-back order, i.e. first the components in\nthe front, followed by the components in the back.\n\nThis method can only return components that implement the method `containsLocalPoint()`. The\n`PositionComponent` (which is the base class for many components in Flame) provides such an\nimplementation. However, if you're defining a custom class that derives from `Component`, you'd have\nto implement the `containsLocalPoint()` method yourself.\n\nHere is an example of how `componentsAtPoint()` can be used:\n\n```dart\nvoid onDragUpdate(DragUpdateInfo info) {\n  game.componentsAtPoint(info.widget).forEach((component) {\n    if (component is DropTarget) {\n      component.highlight();\n    }\n  });\n}\n```\n\n\n### Visibility of components\n\nThe recommended way to hide or show a component is usually to add or remove it from the tree using\nthe `add` and `remove` methods.\n\nHowever, adding and removing components from the tree will trigger lifecycle steps for that\ncomponent (such as calling `onRemove` and `onMount`). It is also an asynchronous process and care\nneeds to be taken to ensure the component has finished removing before it is added again if you\nare removing and adding a component in quick succession.\n\n```dart\n/// Example of handling the removal and adding of a child component\n/// in quick succession\nvoid show() async {\n  // Need to await the [removed] future first, just in case the\n  // component is still in the process of being removed.\n  await myChildComponent.removed;\n  add(myChildComponent);\n}\n\nvoid hide() {\n  remove(myChildComponent);\n}\n```\n\nThese behaviors are not always desirable.\n\nAn alternative method to show and hide a component is to use the `HasVisibility` mixin, which may\nbe used on any class that inherits from `Component`. This mixin introduces the `isVisible` property.\nSimply set `isVisible` to `false` to hide the component, and `true` to show it again, without\nremoving it from the tree. This affects the visibility of the component and all its descendants\n(children).\n\n```dart\n/// Example that implements HasVisibility\nclass MyComponent extends PositionComponent with HasVisibility {}\n\n/// Usage of the isVisible property\nfinal myComponent = MyComponent();\nadd(myComponent);\n\nmyComponent.isVisible = false;\n```\n\nThe mixin only affects whether the component is rendered, and will not affect other behaviors.\n\n```{note}\nImportant! Even when the component is not visible, it is still in the tree and\nwill continue to receive calls to 'update' and all other lifecycle events. It\nwill still respond to input events, and will still interact with other\ncomponents, such as collision detection for example.\n```\n\nThe mixin works by preventing the `renderTree` method, therefore if `renderTree` is being\noverridden, a manual check for `isVisible` should be included to retain this functionality.\n\n```dart\nclass MyComponent extends PositionComponent with HasVisibility {\n\n  @override\n  void renderTree(Canvas canvas) {\n    // Check for visibility\n    if (isVisible) {\n      // Custom code here\n\n      // Continue rendering the tree\n      super.renderTree(canvas);\n    }\n  }\n}\n```\n\n\n### Render Contexts\n\nIf you want a parent component to pass render-specific properties down to its children tree, you\ncan override the `renderContext` property on the parent component. You can return a custom class\nthat inherits from `RenderContext`, and then use `findRenderContext` on the children while\nrendering. Render Contexts are stored as a stack and propagated whenever the render tree is\nnavigated for rendering.\n\nFor example:\n\n```dart\nclass IntContext extends ComponentRenderContext {\n  int value;\n\n  IntContext(this.value);\n}\n\nclass ParentWithContext extends Component {\n  @override\n  IntContext renderContext = IntContext(42);\n}\n\nclass ChildReadsContext extends Component {\n  @override\n  void render(Canvas canvas) {\n    final context = findRenderContext<IntContext>();\n    // context.value available\n  }\n}\n```\n\nEach component will have access to the context of any parent that is above it in the component\ntree. If multiple components add the contexts matching the selected type `T`, the \"closest\" one\nwill be returned (though typically you would create a unique context type for each component).\n\n\n## Effects\n\nFlame provides a set of effects that can be applied to a certain type of components. These effects\ncan be used to animate some properties of your components, like position or dimensions. You can\ncheck the [list of available effects](../effects/effects.md).\n\nExamples of the running effects can be found in the\n[effects examples directory](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/effects).\n\n```{toctree}\n:hidden:\n\nPosition Component       <position_component.md>\nSprite Components        <sprite_components.md>\nParallax Component       <parallax_component.md>\nShape Components         <shape_components.md>\nUtility Components       <utility_components.md>\n```\n"
  },
  {
    "path": "doc/flame/components/parallax_component.md",
    "content": "# ParallaxComponent\n\nParallax scrolling is a classic game development technique where background layers move at different\nspeeds to create an illusion of depth. Objects closer to the camera appear to move faster than those\nfar away. Just as when looking out a car window, nearby trees fly by while distant mountains barely\nmove. This effect makes 2D game worlds feel more immersive and is commonly used in side-scrollers,\nplatformers, and menu screens.\n\nThis `Component` can be used to render backgrounds with a depth feeling by drawing several\ntransparent images on top of each other, where each image or animation (`ParallaxRenderer`) is\nmoving with a different velocity.\n\nThe simplest `ParallaxComponent` is created like this:\n\n```dart\n@override\nFuture<void> onLoad() async {\n  final parallaxComponent = await loadParallaxComponent([\n    ParallaxImageData('bg.png'),\n    ParallaxImageData('trees.png'),\n  ]);\n  add(parallaxComponent);\n}\n```\n\nA ParallaxComponent can also \"load itself\" by implementing the `onLoad` method:\n\n```dart\nclass MyParallaxComponent extends ParallaxComponent<MyGame> {\n  @override\n  Future<void> onLoad() async {\n    parallax = await game.loadParallax([\n      ParallaxImageData('bg.png'),\n      ParallaxImageData('trees.png'),\n    ]);\n  }\n}\n\nclass MyGame extends FlameGame {\n  @override\n  void onLoad() {\n    add(MyParallaxComponent());\n  }\n}\n```\n\nThis creates a static background. If you want a moving parallax (which is the whole point of a\nparallax), you can do it in a few different ways depending on how fine-grained you want to set the\nsettings for each layer.\n\nThe simplest way is to set the named optional parameters `baseVelocity` and\n`velocityMultiplierDelta` in the `load` helper function. For example if you want to move your\nbackground images along the X-axis with a faster speed the \"closer\" the image is:\n\n```dart\n@override\nFuture<void> onLoad() async {\n  final parallaxComponent = await loadParallaxComponent(\n    _dataList,\n    baseVelocity: Vector2(20, 0),\n    velocityMultiplierDelta: Vector2(1.8, 1.0),\n  );\n}\n```\n\nYou can set the baseSpeed and layerDelta at any time, for example if your character jumps or your\ngame speeds up.\n\n```dart\n@override\nvoid onLoad() {\n  final parallax = parallaxComponent.parallax;\n  parallax.baseSpeed = Vector2(100, 0);\n  parallax.velocityMultiplierDelta = Vector2(2.0, 1.0);\n}\n```\n\nBy default, the images are aligned to the bottom left, repeated along the X-axis and scaled\nproportionally so that the image covers the height of the screen. If you want to change this\nbehavior, for example if you are not making a side-scrolling game, you can set the `repeat`,\n`alignment` and `fill` parameters for each `ParallaxRenderer` and add them to `ParallaxLayer`s that\nyou then pass in to the `ParallaxComponent`'s constructor.\n\nAdvanced example:\n\n```dart\nfinal images = [\n  loadParallaxImage(\n    'stars.jpg',\n    repeat: ImageRepeat.repeat,\n    alignment: Alignment.center,\n    fill: LayerFill.width,\n  ),\n  loadParallaxImage(\n    'planets.jpg',\n    repeat: ImageRepeat.repeatY,\n    alignment: Alignment.bottomLeft,\n    fill: LayerFill.none,\n  ),\n  loadParallaxImage(\n    'dust.jpg',\n    repeat: ImageRepeat.repeatX,\n    alignment: Alignment.topRight,\n    fill: LayerFill.height,\n  ),\n];\n\nfinal layers = images.map(\n  (image) => ParallaxLayer(\n    await image,\n    velocityMultiplier: images.indexOf(image) * 2.0,\n  )\n);\n\nfinal parallaxComponent = ParallaxComponent.fromParallax(\n  Parallax(\n    await Future.wait(layers),\n    baseVelocity: Vector2(50, 0),\n  ),\n);\n```\n\n- The stars image in this example will be repeatedly drawn in both axes, align in the center and be\n scaled to fill the screen width.\n- The planets image will be repeated in Y-axis, aligned to the bottom left of the screen and not be\n scaled.\n- The dust image will be repeated in X-axis, aligned to the top right and scaled to fill the screen\n height.\n\nOnce you are done setting up your `ParallaxComponent`, add it to the game like with any other\ncomponent (`game.add(parallaxComponent`).\nAlso, don't forget to add your images to the `pubspec.yaml` file as assets or they won't be found.\n\nThe `Parallax` file contains an extension of the game which adds `loadParallax`,\n`loadParallaxLayer`, `loadParallaxImage` and `loadParallaxAnimation` so that it automatically\nuses your game's image cache instead of the global one. The same goes for the `ParallaxComponent`\nfile, but that provides `loadParallaxComponent`.\n\nIf you want a fullscreen `ParallaxComponent` simply omit the `size` argument and it will take the\nsize of the game, it will also resize to fullscreen when the game changes size or orientation.\n\nFlame provides two kinds of `ParallaxRenderer`: `ParallaxImage` and `ParallaxAnimation`,\n`ParallaxImage` is a static image renderer and `ParallaxAnimation` is, as its name implies, an\nanimation and frame based renderer.\nIt is also possible to create custom renderers by extending the `ParallaxRenderer` class.\n\nThree example implementations can be found in the\n[examples directory](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/parallax).\n"
  },
  {
    "path": "doc/flame/components/position_component.md",
    "content": "# PositionComponent\n\nMost visible objects in a game need a position, size, and rotation. `PositionComponent` provides\nthese transform properties, making it the base class for nearly every visual element in Flame:\nsprites, animations, shapes, and your own custom components. It mirrors the concept of a\n[`Positioned`](https://api.flutter.dev/flutter/widgets/Positioned-class.html) widget in Flutter, but\nin a game-oriented coordinate system.\n\nThis class represents a positioned object on the screen, be it a floating rectangle, a rotating\nsprite, or anything else with position and size. It can also represent a group of positioned\ncomponents if children are added to it.\n\nThe base of the `PositionComponent` is that it has a `position`, `size`, `scale`, `angle` and\n`anchor` which transforms how the component is rendered.\n\n\n## Position\n\nThe `position` is just a `Vector2` which represents the position of the component's anchor in\nrelation to its parent; if the parent is a `FlameGame`, it is in relation to the viewport.\n\n\n## Size\n\nThe `size` of the component when the zoom level of the camera is 1.0 (no zoom, default).\nThe `size` is *not* in relation to the parent of the component.\n\n\n## Scale\n\nThe `scale` is how much the component and its children should be scaled. Since it is represented\nby a `Vector2`, you can scale in a uniform way by changing `x` and `y` with the same amount, or in a\nnon-uniform way, by changing `x` or `y` by different amounts.\n\n\n## Angle\n\nThe `angle` is the rotation angle around the anchor, represented as a double in radians. It is\nrelative to the parent's angle.\n\n\n## Native Angle\n\nThe `nativeAngle` is an angle in radians, measured clockwise, representing the default orientation\nof the component. It can be used to define the direction in which the component is facing when\n[angle](#angle) is zero.\n\nIt is especially helpful when making a sprite based component look at a specific target. If the\noriginal image of the sprite is not facing in the up/north direction, the calculated angle to make\nthe component look at the target will need some offset to make it look correct. For such cases,\n`nativeAngle` can be used to let the component know what direction the original image is facing.\n\nAn example could be a bullet image pointing in the east direction. In this case `nativeAngle` can\nbe set to pi/2 radians. Following are some common directions and their corresponding native\nangle values.\n\nDirection | Native Angle | In degrees\n----------|--------------|-------------\nUp/North  | 0            | 0\nDown/South| pi or -pi    | 180 or -180\nLeft/West | -pi/2        | -90\nRight/East| pi/2         | 90\n\n\n## Anchor\n\n```{flutter-app}\n:sources: ../../flame/examples\n:page: anchor\n:show: widget code infobox\nThis example shows effect of changing `anchor` point of parent\n(red) and child (blue) components. Tap on them to cycle through\nthe anchor points. Note that the local position of the child\ncomponent is (0, 0) at all times.\n```\n\nThe `anchor` is where on the component that the position and rotation should be defined from (the\ndefault is `Anchor.topLeft`). So if you have the anchor set as `Anchor.center` the component's\nposition on the screen will be in the center of the component and if an `angle` is applied, it is\nrotated around the anchor, so in this case around the center of the component. You can think of it\nas the point within the component by which Flame \"grabs\" it.\n\nWhen `position` or `absolutePosition` of a component is queried, the returned coordinates are that\nof the `anchor` of the component. In case you want to find the position of a specific anchor\npoint of a component which is not actually the `anchor` of that component, you can use the\n`positionOfAnchor` and `absolutePositionOfAnchor` methods.\n\n```dart\nfinal comp = PositionComponent(\n  size: Vector2.all(20),\n  anchor: Anchor.center,\n);\n\n// Returns (0,0)\nfinal p1 = component.position;\n\n// Returns (10, 10)\nfinal p2 = component.positionOfAnchor(Anchor.bottomRight);\n```\n\nA common pitfall when using `anchor` is confusing it as being the attachment point for children\ncomponents. For example, setting `anchor` to `Anchor.center` for a parent component does not mean\nthat the children components will be placed w.r.t the center of parent.\n\n```{note}\nLocal origin for a child component is always the top-left\ncorner of its parent component, irrespective of their\n`anchor` values.\n```\n\n\n## PositionComponent children\n\nAll children of the `PositionComponent` will be transformed in relation to the parent, which means\nthat the `position`, `angle` and `scale` will be relative to the parent's state.\nSo if you, for example, wanted to position a child in the center of the parent you would do this:\n\n```dart\n@override\nvoid onLoad() {\n  final parent = PositionComponent(\n    position: Vector2(100, 100),\n    size: Vector2(100, 100),\n  );\n  final child = PositionComponent(\n    position: parent.size / 2,\n    anchor: Anchor.center,\n  );\n  parent.add(child);\n}\n```\n\nRemember that most components that are rendered on the screen are `PositionComponent`s, so\nthis pattern can be used in for example [SpriteComponent](sprite_components.md#spritecomponent)\nand [SpriteAnimationComponent](sprite_components.md#spriteanimationcomponent) too.\n\n\n## Render PositionComponent\n\nWhen implementing the `render` method for a component that extends `PositionComponent` remember to\nrender from the top left corner (0.0). Your render method should not handle where on the screen your\ncomponent should be rendered. To handle where and how your component should be rendered use the\n`position`, `angle` and `anchor` properties and Flame will automatically handle the rest for you.\n\nIf you want to know where on the screen the bounding box of the component is you can use the\n`toRect` method.\n\nIn the event that you want to change the direction of your component's rendering, you can also use\n`flipHorizontally()` and `flipVertically()` to flip anything drawn to canvas during\n`render(Canvas canvas)`, around the anchor point. These methods are available on all\n`PositionComponent` objects, and are especially useful on `SpriteComponent` and\n`SpriteAnimationComponent`.\n\nIn case you want to flip a component around its center without having to change the anchor to\n`Anchor.center`, you can use `flipHorizontallyAroundCenter()` and `flipVerticallyAroundCenter()`.\n"
  },
  {
    "path": "doc/flame/components/shape_components.md",
    "content": "# ShapeComponents\n\nGeometric shapes are useful in many game scenarios: debug visualizations, procedurally generated\ngraphics, UI elements, or simple game objects that don't need sprite art. Flame's shape components\nlet you render polygons, rectangles, and circles as first-class components with all the transform\nproperties of `PositionComponent`. They also serve as the foundation for the\n[collision detection hitboxes](../collision_detection.md#shapehitbox).\n\n\nA `ShapeComponent` is the base class for representing a scalable geometrical shape. The shapes have\ndifferent ways of defining how they look, but they all have a size and angle that can be modified\nand the shape definition will scale or rotate the shape accordingly.\n\nThese shapes are meant as a tool for using geometrical shapes in a more general way than together\nwith the collision detection system, where you want to use the\n[ShapeHitbox](../collision_detection.md#shapehitbox)es.\n\n\n## PolygonComponent\n\nA `PolygonComponent` is created by giving it a list of points in the constructor, called vertices.\nThis list will be transformed into a polygon with a size, which can still be scaled and rotated.\n\nFor example, this would create a square going from (50, 50) to (100, 100), with its center in\n(75, 75):\n\n```dart\nvoid main() {\n  PolygonComponent([\n    Vector2(100, 100),\n    Vector2(100, 50),\n    Vector2(50, 50),\n    Vector2(50, 100),\n  ]);\n}\n```\n\nA `PolygonComponent` can also be created with a list of relative vertices, which are points defined\nin relation to the given size, most often the size of the intended parent.\n\nFor example you could create a diamond-shaped polygon like this:\n\n```dart\nvoid main() {\n  PolygonComponent.relative(\n    [\n      Vector2(0.0, -1.0), // Middle of top wall\n      Vector2(1.0, 0.0), // Middle of right wall\n      Vector2(0.0, 1.0), // Middle of bottom wall\n      Vector2(-1.0, 0.0), // Middle of left wall\n    ],\n    size: Vector2.all(100),\n  );\n}\n```\n\nThe vertices in the example define percentages of the length from the center to the edge of the\nscreen in both x and y axis, so for our first item in our list (`Vector2(0.0, -1.0)`) we are\npointing on the middle of the top wall of the bounding box, since the coordinate system here is\ndefined from\nthe center of the polygon.\n\n![An example of how to define a polygon shape](../../images/polygon_shape.png)\n\nIn the image you can see how the polygon shape formed by the purple arrows is defined by the red\narrows.\n\n\n## RectangleComponent\n\nA `RectangleComponent` is created very similarly to how a `PositionComponent` is created, since it\nalso has a bounding rectangle.\n\nSomething like this for example:\n\n```dart\nvoid main() {\n  RectangleComponent(\n    position: Vector2(10.0, 15.0),\n    size: Vector2.all(10),\n    angle: pi/2,\n    anchor: Anchor.center,\n  );\n}\n```\n\nDart also already has an excellent way to create rectangles and that class is called `Rect`, you\ncan create a Flame `RectangleComponent` from a `Rect` by using the\n`RectangleComponent.fromRect` factory, and just like when setting the vertices of the\n`PolygonComponent`, your rectangle will be sized\naccording to the `Rect` if you use this constructor.\n\nThe following would create a `RectangleComponent` with its top left corner in `(10, 10)` and a size\nof `(100, 50)`.\n\n```dart\nvoid main() {\n  RectangleComponent.fromRect(\n    Rect.fromLTWH(10, 10, 100, 50),\n  );\n}\n```\n\nYou can also create a `RectangleComponent` by defining a relation to the intended parent's size,\nyou can use the default constructor to build your rectangle from a position, size and angle. The\n`relation` is a vector defined in relation to the parent size, for example a `relation` that is\n`Vector2(0.5, 0.8)` would create a rectangle that is 50% of the width of the parent's size and\n80% of its height.\n\nIn the example below a `RectangleComponent` of size `(25.0, 30.0)` positioned at `(100, 100)` would\nbe created.\n\n```dart\nvoid main() {\n  RectangleComponent.relative(\n    Vector2(0.5, 1.0),\n    position: Vector2.all(100),\n    size: Vector2(50, 30),\n  );\n}\n```\n\nSince a square is a simplified version of a rectangle, there is also a constructor for creating a\nsquare `RectangleComponent`, the only difference is that the `size` argument is a `double` instead\nof a `Vector2`.\n\n```dart\nvoid main() {\n  RectangleComponent.square(\n    position: Vector2.all(100),\n    size: 200,\n  );\n}\n```\n\n\n## CircleComponent\n\nIf you know your circle's position and/or how long the radius is going to be from the start\nyou can use the optional arguments `radius` and `position` to set those.\n\nThe following would create a `CircleComponent` with its center in `(100, 100)` with a radius of 5,\nand therefore a size of `Vector2(10, 10)`.\n\n```dart\nvoid main() {\n  CircleComponent(radius: 5, position: Vector2.all(100), anchor: Anchor.center);\n}\n```\n\nWhen creating a `CircleComponent` with the `relative` constructor you can define how long the\nradius is in comparison to the shortest edge of the bounding box defined by `size`.\n\nThe following example would result in a `CircleComponent` that defines a circle with a radius of 40\n(a diameter of 80).\n\n```dart\nvoid main() {\n  CircleComponent.relative(0.8, size: Vector2.all(100));\n}\n```\n"
  },
  {
    "path": "doc/flame/components/sprite_components.md",
    "content": "# Sprite Components\n\nSprites are 2D images (or regions of images) that represent the visual appearance of game objects.\nThey are the most common way to display characters, items, backgrounds, and other visuals in 2D\ngames. Flame provides several sprite-based components that make it easy to load images, play\nanimations, and switch between visual states, all while benefiting from the transform properties\ninherited from `PositionComponent`.\n\n\n## SpriteComponent\n\nThe most commonly used implementation of `PositionComponent` is `SpriteComponent`, and it can be\ncreated with a `Sprite`:\n\n```dart\nimport 'package:flame/components/component.dart';\n\nclass MyGame extends FlameGame {\n  late final SpriteComponent player;\n\n  @override\n  Future<void> onLoad() async {\n    final sprite = await Sprite.load('player.png');\n    final size = Vector2.all(128.0);\n    final player = SpriteComponent(size: size, sprite: sprite);\n\n    // Vector2(0.0, 0.0) by default, can also be set in the constructor\n    player.position = Vector2(10, 20);\n\n    // 0 by default, can also be set in the constructor\n    player.angle = 0;\n\n    // Adds the component\n    add(player);\n  }\n}\n```\n\n\n## SpriteAnimationComponent\n\nThis class is used to represent a Component that has sprites that run in a single cyclic animation.\n\nThis will create a simple three frame animation using 3 different images:\n\n```dart\n@override\nFuture<void> onLoad() async {\n  final sprites = [0, 1, 2]\n      .map((i) => Sprite.load('player_$i.png'));\n  final animation = SpriteAnimation.spriteList(\n    await Future.wait(sprites),\n    stepTime: 0.01,\n  );\n  this.player = SpriteAnimationComponent(\n    animation: animation,\n    size: Vector2.all(64.0),\n  );\n}\n```\n\nIf you have a sprite sheet, you can use the `sequenced` constructor from the `SpriteAnimationData`\nclass (check more details on [Images > Animation](../rendering/images.md#animation)):\n\n```dart\n@override\nFuture<void> onLoad() async {\n  final size = Vector2.all(64.0);\n  final data = SpriteAnimationData.sequenced(\n    textureSize: size,\n    amount: 2,\n    stepTime: 0.1,\n  );\n  this.player = SpriteAnimationComponent.fromFrameData(\n    await images.load('player.png'),\n    data,\n  );\n}\n```\n\nAll animation components internally maintain a `SpriteAnimationTicker` which ticks the\n`SpriteAnimation`. This allows multiple components to share the same animation object.\n\nExample:\n\n```dart\nfinal sprites = [/*Your sprite list here*/];\nfinal animation = SpriteAnimation.spriteList(sprites, stepTime: 0.01);\n\nfinal animationTicker = SpriteAnimationTicker(animation);\n\n// or alternatively, you can ask the animation object to create one for you.\n\nfinal animationTicker = animation.createTicker(); // creates a new ticker\n\nanimationTicker.update(dt);\n```\n\nTo listen when the animation is done (when it reaches the last frame and is not looping) you can\nuse `animationTicker.completed`.\n\nExample:\n\n```dart\nawait animationTicker.completed;\n\ndoSomething();\n\n// or alternatively\n\nanimationTicker.completed.whenComplete(doSomething);\n```\n\nAdditionally, `SpriteAnimationTicker` also has the following optional event callbacks: `onStart`,\n`onFrame`, and `onComplete`. To listen to these events, you can do the following:\n\n```dart\nfinal animationTicker = SpriteAnimationTicker(animation)\n  ..onStart = () {\n    // Do something on start.\n  };\n\nfinal animationTicker = SpriteAnimationTicker(animation)\n  ..onComplete = () {\n    // Do something on completion.\n  };\n\nfinal animationTicker = SpriteAnimationTicker(animation)\n  ..onFrame = (index) {\n    if (index == 1) {\n      // Do something for the second frame.\n    }\n  };\n```\n\nTo reset the animation to the first frame when the component is removed, you can set\n`resetOnRemove` to `true`:\n\n```dart\nSpriteAnimationComponent(\n  animation: animation,\n  size: Vector2.all(64.0),\n  resetOnRemove: true,\n);\n```\n\n\n## SpriteAnimationGroupComponent\n\n`SpriteAnimationGroupComponent` is a simple wrapper around `SpriteAnimationComponent` which enables\nyour component to hold several animations and change the current playing animation at runtime. Since\nthis component is just a wrapper, the event listeners can be implemented as described in\n[SpriteAnimationComponent](#spriteanimationcomponent).\n\nIts use is very similar to the `SpriteAnimationComponent` but instead of being initialized with a\nsingle animation, this component receives a Map of a generic type `T` as key and a\n`SpriteAnimation` as value, and the current animation.\n\nExample:\n\n```dart\nenum RobotState {\n  idle,\n  running,\n}\n\nfinal running = await loadSpriteAnimation(/* omitted */);\nfinal idle = await loadSpriteAnimation(/* omitted */);\n\nfinal robot = SpriteAnimationGroupComponent<RobotState>(\n  animations: {\n    RobotState.running: running,\n    RobotState.idle: idle,\n  },\n  current: RobotState.idle,\n);\n\n// Changes current animation to \"running\"\nrobot.current = RobotState.running;\n```\n\nAs this component works with multiple `SpriteAnimation`s, naturally it needs an equal number of\nanimation tickers to make all those animations tick. Use `animationsTickers` getter to access a map\ncontaining tickers for each animation state. This can be useful if you want to register callbacks\nfor `onStart`, `onComplete` and `onFrame`.\n\nExample:\n\n```dart\nenum RobotState { idle, running, jump }\n\nfinal running = await loadSpriteAnimation(/* omitted */);\nfinal idle = await loadSpriteAnimation(/* omitted */);\n\nfinal robot = SpriteAnimationGroupComponent<RobotState>(\n  animations: {\n    RobotState.running: running,\n    RobotState.idle: idle,\n  },\n  current: RobotState.idle,\n);\n\nrobot.animationTickers?[RobotState.running]?.onStart = () {\n  // Do something on start of running animation.\n};\n\nrobot.animationTickers?[RobotState.jump]?.onStart = () {\n  // Do something on start of jump animation.\n};\n\nrobot.animationTickers?[RobotState.jump]?.onComplete = () {\n  // Do something on complete of jump animation.\n};\n\nrobot.animationTickers?[RobotState.idle]?.onFrame = (currentIndex) {\n  // Do something based on current frame index of idle animation.\n};\n```\n\n\n## SpriteGroupComponent\n\n`SpriteGroupComponent` is pretty similar to its animation counterpart, but especially for sprites.\n\nExample:\n\n```dart\nclass PlayerComponent extends SpriteGroupComponent<ButtonState>\n    with HasGameReference<SpriteGroupExample>, TapCallbacks {\n  @override\n  Future<void> onLoad() async {\n    final pressedSprite = await game.loadSprite(/* omitted */);\n    final unpressedSprite = await game.loadSprite(/* omitted */);\n\n    sprites = {\n      ButtonState.pressed: pressedSprite,\n      ButtonState.unpressed: unpressedSprite,\n    };\n\n    current = ButtonState.unpressed;\n  }\n\n  // tap methods handler omitted...\n}\n```\n\n\n## IconComponent\n\n`IconComponent` renders a Flutter `IconData` (such as `Icons.star`) as a Flame component. The icon\nis rasterized to an image once during `onLoad()` and then drawn each frame using\n`canvas.drawImageRect()` with the component's `Paint`. Because the icon is rendered as a cached\nimage rather than as text, all paint-based effects work out of the box, including `tint()`,\n`setOpacity()`, `ColorEffect`, `OpacityEffect`, `GlowEffect`, and custom `ColorFilter`s.\n\n\n### Basic usage\n\n```dart\nimport 'package:flame/components.dart';\nimport 'package:flutter/material.dart';\n\nclass MyGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    final star = IconComponent(\n      icon: Icons.star,\n      iconSize: 64,\n      position: Vector2(100, 100),\n    );\n    add(star);\n  }\n}\n```\n\n\n### Tinting and effects\n\nThe icon is rasterized in white, which allows you to tint it to any color using\n`HasPaint` methods:\n\n```dart\n// Tint the icon gold\nfinal star = IconComponent(\n  icon: Icons.star,\n  iconSize: 64,\n  position: Vector2(100, 100),\n)..tint(const Color(0xFFFFD700));\n\n// Set opacity\nstar.setOpacity(0.5);\n\n// Or use a custom paint\nfinal icon = IconComponent(\n  icon: Icons.favorite,\n  iconSize: 48,\n  paint: Paint()..colorFilter = const ColorFilter.mode(\n    Color(0xFFFF0000),\n    BlendMode.srcATop,\n  ),\n);\n```\n\n\n### Constructor parameters\n\n- `icon`: The `IconData` to render (e.g., `Icons.star`, `Icons.favorite`).\n- `iconSize`: The resolution at which the icon is rasterized (default `64`). This is independent\n  of the component's display `size`.\n- `size`: The display size of the component. Defaults to `Vector2.all(iconSize)` if not provided.\n- `paint`: Optional `Paint` for rendering effects.\n- All standard `PositionComponent` parameters (`position`, `scale`, `angle`, `anchor`, etc.).\n\n\n### Changing the icon at runtime\n\nBoth the `icon` and `iconSize` properties can be changed after creation. The component will\nautomatically re-rasterize the icon on the next frame:\n\n```dart\nfinal iconComponent = IconComponent(\n  icon: Icons.play_arrow,\n  iconSize: 64,\n);\n\n// Later, swap the icon\niconComponent.icon = Icons.pause;\n\n// Or change the rasterization resolution\niconComponent.iconSize = 128;\n```\n"
  },
  {
    "path": "doc/flame/components/utility_components.md",
    "content": "# Utility Components\n\nBeyond the core visual components, Flame provides several utility components that handle common\ngame development tasks: spawning objects over time, rendering tiled maps, clipping render areas,\nand bridging Flutter widgets into the game. These components save you from writing boilerplate so\nyou can focus on game-specific logic.\n\n\n## SpawnComponent\n\nThis component is a non-visual component that spawns other components inside of the parent of the\n`SpawnComponent`. It's great if you for example want to spawn enemies or power-ups randomly within\nan area.\n\nThe `SpawnComponent` takes a factory function that it uses to create new components and an area\nwhere the components should be spawned within (or along the edges of).\n\nFor the area, you can use the `Circle`, `Rectangle` or `Polygon` class, and if you want to only\nspawn components along the edges of the shape set the `within` argument to false (defaults to true).\n\nThis would for example spawn new components of the type `MyComponent` every 0.5 seconds randomly\nwithin the defined circle:\n\nThe component supports two types of factories. The `factory` returns a single component and the\n`multiFactory` returns a list of components that are added in a single step.\n\nThe factory functions take an `int` as an argument, which is the number of components that have\nbeen spawned, so if for example 4 components have been spawned already the 5th call of the factory\nmethod will be called with the `amount=4`, since the counting starts at 0 for the first call.\n\nThe `factory` with a single component is for backward compatibility, so you should use the\n`multiFactory` if in doubt. A single component `factory` will be wrapped internally to return a\nsingle item list and then used as the `multiFactory`.\n\nIf you only want to spawn a certain amount of components, you can use the `spawnCount` argument,\nand once the limit is reached the `SpawnComponent` will stop spawning and remove itself.\n\nBy default, the `SpawnComponent` will spawn components to its parent, but if you want to spawn\ncomponents to another component you can set the `target` argument. Remember that it should be a\n`Component` that has a size if you don't use the `area` or `selfPositioning` arguments.\n\n\n```dart\nSpawnComponent(\n  factory: (i) => MyComponent(size: Vector2(10, 20)),\n  period: 0.5,\n  area: Circle(Vector2(100, 200), 150),\n);\n```\n\nIf you don't want the spawning rate to be static, you can use the `SpawnComponent.periodRange`\nconstructor with the `minPeriod` and `maxPeriod` arguments instead.\nIn the following example the component would be spawned randomly within the circle and the time\nbetween each new spawned component is between 0.5 to 10 seconds.\n\n```dart\nSpawnComponent.periodRange(\n  factory: (i) => MyComponent(size: Vector2(10, 20)),\n  minPeriod: 0.5,\n  maxPeriod: 10,\n  area: Circle(Vector2(100, 200), 150),\n);\n```\n\nIf you want to set the position yourself within the `factory` function, you can set\n`selfPositioning = true` in the constructors and you will be able to set the positions yourself and\nignore the `area` argument.\n\n```dart\nSpawnComponent(\n  factory: (i) =>\n    MyComponent(position: Vector2(100, 200), size: Vector2(10, 20)),\n  selfPositioning: true,\n  period: 0.5,\n);\n```\n\n\n## SvgComponent\n\n**Note**: To use SVG with Flame, use the [`flame_svg`](https://github.com/flame-engine/flame_svg)\npackage.\n\nThis component uses an instance of `Svg` class to represent a Component that has an SVG that is\nrendered in the game:\n\n```dart\n@override\nFuture<void> onLoad() async {\n  final svg = await Svg.load('android.svg');\n  final android = SvgComponent.fromSvg(\n    svg,\n    position: Vector2.all(100),\n    size: Vector2.all(100),\n  );\n}\n```\n\n\n## IsometricTileMapComponent\n\nIsometric tile maps are commonly used in strategy, simulation, and RPG games to give a 2D map a\npseudo-3D perspective. This component allows you to render an isometric map based on a cartesian\nmatrix of blocks and an isometric tileset.\n\nA simple example on how to use it:\n\n```dart\n// Creates a tileset, the block ids are automatically assigned sequentially\n// starting at 0, from left to right and then top to bottom.\nfinal tilesetImage = await images.load('tileset.png');\nfinal tileset = SpriteSheet(image: tilesetImage, srcSize: Vector2.all(32));\n// Each element is a block id, -1 means nothing\nfinal matrix = [[0, 1, 0], [1, 0, 0], [1, 1, 1]];\nadd(IsometricTileMapComponent(tileset, matrix));\n```\n\nIt also provides methods for converting coordinates so you can handle clicks, hovers, render\nentities on top of tiles, add a selector, etc.\n\nYou can also specify the `tileHeight`, which is the vertical distance between the bottom and top\nplanes of each cuboid in your tile. Basically, it's the height of the front-most edge of your\ncuboid; normally it's half (default) or a quarter of the tile size. On the image below you can see\nthe height colored in the darker tone:\n\n![An example of how to determine the tileHeight](../../images/tile-height-example.png)\n\nThis is an example of what a quarter-length map looks like:\n\n![An example of a isometric map with selector](../../images/isometric.png)\n\nFlame's Example app contains a more in-depth example, featuring how to parse coordinates to make a\nselector. The\n[source code](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/isometric_tile_map_example.dart)\nis available on GitHub, and a\n[live version](https://examples.flame-engine.org/#/Rendering_Isometric_Tile_Map)\ncan be viewed in the browser.\n\n\n## NineTileBoxComponent\n\nA Nine Tile Box is a rectangle drawn using a grid sprite.\n\nThe grid sprite is a 3x3 grid with 9 blocks, representing the 4 corners, the 4 sides and the\nmiddle.\n\nThe corners are drawn at the same size, the sides are stretched on the side direction and the middle\nis expanded both ways.\n\nUsing this, you can get a box/rectangle that expands well to any sizes. This is useful for making\npanels, dialogs, borders.\n\nCheck the example app\n[nine_tile_box](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/nine_tile_box_example.dart)\nfor details on how to use it.\n\n\n## CustomPainterComponent\n\nA `CustomPainter` is a Flutter class used with the `CustomPaint` widget to render custom\nshapes inside a Flutter application.\n\nFlame provides a component that can render a `CustomPainter` called `CustomPainterComponent`. It\nreceives a custom painter and renders it on the game canvas.\n\nThis can be used for sharing custom rendering logic between your Flame game, and your Flutter\nwidgets.\n\nCheck the example app\n[custom_painter_component](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/widgets/custom_painter_example.dart)\nfor details on how to use it.\n\n\n## ComponentsNotifier\n\nMost of the time just accessing children and their attributes is enough to build the logic of\nyour game.\n\nBut sometimes, reactivity can help the developer to simplify and write better code, to help with\nthat Flame provides the `ComponentsNotifier`, which is an implementation of a\n`ChangeNotifier` that notifies listeners every time a component is added, removed or manually\nchanged.\n\nFor example, let's say that we want to show a game over text when the player's lives reach zero.\n\nTo make the component automatically report when new instances are added or removed, the `Notifier`\nmixin can be applied to the component class:\n\n```dart\nclass Player extends SpriteComponent with Notifier {}\n```\n\nThen to listen to changes on that component the `componentsNotifier` method from `FlameGame` can\nbe used:\n\n```dart\nclass MyGame extends FlameGame {\n  int lives = 2;\n\n  @override\n  void onLoad() {\n    final playerNotifier = componentsNotifier<Player>()\n        ..addListener(() {\n          final player = playerNotifier.single;\n          if (player == null) {\n            lives--;\n            if (lives == 0) {\n              add(GameOverComponent());\n            } else {\n              add(Player());\n            }\n          }\n        });\n  }\n}\n```\n\nA `Notifier` component can also manually notify its listeners that something changed. Let's expand\nthe example above to make a HUD component blink when the player has half of their health. In\norder to do so, we need the `Player` component to notify a change manually:\n\n```dart\nclass Player extends SpriteComponent with Notifier {\n  double health = 1;\n\n  void takeHit() {\n    health -= .1;\n    if (health == 0) {\n      removeFromParent();\n    } else if (health <= .5) {\n      notifyListeners();\n    }\n  }\n}\n```\n\nThen our hud component could look like:\n\n```dart\nclass Hud extends PositionComponent with HasGameReference {\n\n  @override\n  void onLoad() {\n    final playerNotifier = game.componentsNotifier<Player>()\n        ..addListener(() {\n          final player = playerNotifier.single;\n          if (player != null) {\n            if (player.health <= .5) {\n              add(BlinkEffect());\n            }\n          }\n        });\n  }\n}\n```\n\n`ComponentsNotifier`s can also come in handy to rebuild widgets when state changes inside a\n`FlameGame`, to help with that Flame provides a `ComponentsNotifierBuilder` widget.\n\nTo see an example of its use, check the\n[ComponentsNotifier example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/components/components_notifier_example.dart).\n\n\n## ClipComponent\n\nA `ClipComponent` is a component that will clip the canvas to its size and shape. This means that\nif the component itself or any child of the `ClipComponent` renders outside of the\n`ClipComponent`'s boundaries, the part that is not inside the area will not be shown.\n\nA `ClipComponent` receives a builder function that should return the `Shape` that will define the\nclipped area, based on its size.\n\nTo make it easier to use that component, there are three factories that offer common shapes:\n\n- `ClipComponent.rectangle`: Clips the area in the form of a rectangle based on its size.\n- `ClipComponent.circle`: Clips the area in the form of a circle based on its size.\n- `ClipComponent.polygon`:  Clips the area in the form of a polygon based on the points received\nin the constructor.\n\nCheck the example app\n[clip_component](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/components/clip_component_example.dart)\nfor details on how to use it.\n"
  },
  {
    "path": "doc/flame/diagrams/component.md",
    "content": "``` {mermaid}\n%%{init: { 'theme': 'dark' } }%%\ngraph TD\n    %% Config %%\n    classDef default fill:#282828,stroke:#F6BE00;\n    \n    %% Nodes %%\n    Component(Component)\n    Misc(\"\n        TimerComponent\n        ParticleComponent\n        SpriteBatchComponent\n    \")\n    Effects(\"Effects<br/>(See the effects section)\")\n    Game(Game)\n    FlameGame(FlameGame)\n    PositionComponent(PositionComponent)\n   \n    Sprites(\"\n        SpriteComponent\n        SpriteGroupComponent \n        SpriteAnimationComponent\n        SpriteAnimationGroupComponent\n        ParallaxComponent \n        IsoMetricTileMapComponent\n    \")\n    \n    HudMarginComponent(HudMarginComponent)\n    HudComponents(\"\n        HudButtonComponent\n        JoystickComponent\n    \")\n    \n    OtherPositionComponents(\"\n        ButtonComponent\n        CustomPainterComponent\n        ShapeComponent\n        SpriteButtonComponent\n        TextComponent\n        TextBoxComponent\n        NineTileBoxComponent\n    \")\n        \n    %% Flow %%\n    Component --> Misc\n    Component --> Effects\n    Component --> PositionComponent\n    Component --> FlameGame\n   \n    Game --> FlameGame\n    PositionComponent --> Sprites\n    PositionComponent --> HudMarginComponent\n    PositionComponent --> OtherPositionComponents\n    HudMarginComponent --> HudComponents\n```\n"
  },
  {
    "path": "doc/flame/diagrams/component_life_cycle.md",
    "content": "``` {mermaid}\n%%{init: { 'theme': 'dark' } }%%\n\n  graph TD\n\n   %% Node Color %%\n   classDef default fill:#282828,stroke:#F6BE00,stroke-width:2px;\n   classDef lightYellow fill:#523F00,stroke-width:2px;\n   classDef yellow fill:#F6BE00,color:#000000;\n   classDef green fill:#00523F,stroke:#F6BE00,stroke-width:2px;\n\n   %% Nodes  %%\n   x(Runs Each Tick)\n   y(Runs On Add & Resize):::lightYellow\n   z(Runs Once):::yellow\n   w(Runs On Hot Reload):::green\n\n```\n\n``` {mermaid}\n%%{init: { 'theme': 'dark' } }%%\n  graph LR\n\n   %% Node Color %%\n   classDef default fill:#282828,stroke:#F6BE00,stroke-width:2px;\n   classDef lightYellow fill:#523F00,stroke-width:2px;\n   classDef yellow fill:#F6BE00,color:#000000;\n   classDef green fill:#00523F,stroke:#F6BE00,stroke-width:2px;\n\n    %% Nodes %%\n\n    A(onLoad):::yellow\n    B(onGameResize):::lightYellow\n    C(onMount):::lightYellow\n    D(update)\n    E(render)\n    F(onRemove):::lightYellow\n    G(onHotReload):::green\n\n    %% Flow %%\n\n    A-->B\n    B-->C\n    C-->D\n    D-->E\n    E-->D\n    E-. If removed .->F\n    F-. If re-parented .->B\n    D-. If hot reloaded .->G\n    G-.->D\n\n```\n"
  },
  {
    "path": "doc/flame/diagrams/flame_3d_components.md",
    "content": "```{mermaid}\n%%{init: { 'theme': 'dark' } }%%\n\nflowchart TB\n    classDef default fill:#282828,stroke:#F6BE00;\n\n    %% base flame classes %%\n    World[\"<code>World</code><br /><span>(base Flame)<code>\"]\n    Component[\"<code>Component</code>\n        <span>(base Flame)<code>\"]\n\n    %% flame 3d classes %%\n    World3D[\"<code>World3D</code>\"]\n    Component3D[\"<code>Component3D</code>\"]\n    Object3D[\"<code>Object3D</code>\n        <span>has the renderCamera override for rendering</span>\"]\n    LightComponent[\"<code>LightComponent</code>\n        <span>exposes a <code>LightSource</code> to the <code>World3D</code></span>\"]\n    MeshComponent[\"<code>MeshComponent</code>\"]\n    Mesh[\"<code>Mesh</code>\"]\n    LightSource[\"<code>LightSource</code>\n        <span>light properties<br />(sans <code>transform</code>)</span>\"]\n    Resource[\"<code>Resource</code>\"]\n    Light[\"<code>Light</code>\n        <span><code>source</code> + <code>transform</code></span>\"]\n\n    World -.->|has| Component\n\n    World3D -->|is| World\n    World3D -.->|has| Component3D\n\n    Component3D -->|is| Component\n    Object3D -->|is| Component3D\n    LightComponent -->|is| Component3D\n    Light -.->|has| LightSource\n\n    MeshComponent -->|is| Object3D\n    MeshComponent -.->|has| Mesh\n    Mesh -->|is| Resource\n\n    LightComponent -.->|creates| Light\n    Light -->|is| Resource\n\n```\n"
  },
  {
    "path": "doc/flame/diagrams/flame_game_life_cycle.md",
    "content": "``` {mermaid}\n%%{init: { 'theme': 'dark' } }%%\n\n  graph TD\n\n   %% Node Color %%\n   classDef default fill:#282828,stroke:#F6BE00,stroke-width:2px;\n   classDef lightYellow fill:#523F00,stroke-width:2px;\n   classDef yellow fill:#F6BE00,color:#000000;\n   classDef green fill:#00523F,stroke:#F6BE00,stroke-width:2px;\n\n   %% Nodes  %%\n   x(Runs Each Tick)\n   y(Runs On Add & Resize):::lightYellow\n   z(Runs Once):::yellow\n   w(Runs On Hot Reload):::green\n\n```\n\n``` {mermaid}\n%%{init: { 'theme': 'dark' } }%%\n  graph LR\n\n   %% Node Color %%\n   classDef default fill:#282828,stroke:#F6BE00,stroke-width:2px;\n   classDef lightYellow fill:#523F00,stroke-width:2px;\n   classDef yellow fill:#F6BE00,color:#000000;\n   classDef green fill:#00523F,stroke:#F6BE00,stroke-width:2px;\n\n    %% Nodes %%\n\n    A(onGameResize):::lightYellow\n    B(onLoad):::yellow\n    C(onMount):::yellow\n    D(update)\n    E(render)\n    F(onRemove):::yellow\n    G(onHotReload):::green\n\n    %% Flow %%\n\n    A-->B\n    B-->C\n    C-->D\n    D-->E\n    E-->D\n    E-. If removed .->F\n    F-. If re-parented .->A\n    D-. If hot reloaded .->G\n    G-.->D\n\n```\n"
  },
  {
    "path": "doc/flame/diagrams/low_level_game_api.md",
    "content": "``` {mermaid}\n%%{init: { 'theme': 'dark' } }%%\n  graph TD  \n  \n    %% Node Color %%\n    classDef default fill:#282828,stroke:#F6BE00,stroke-width:2px;\n    classDef yellow fill:#F6BE00,color:#000;\n \n    %% Nodes  %%\n    \n    z(Abstract Class):::yellow\n    x(Normal Class)\n```\n\n``` {mermaid}\n%%{init: { 'theme': 'dark' } }%%\n  \n  graph BT  \n\n    %% Node Color %%\n    classDef default fill:#282828,stroke:#F6BE00,stroke-width:2px;\n    classDef yellow fill:#F6BE00,color:#000;\n \n    %% Nodes  %%\n    \n    A(OxygenGame)\n    B(Game):::yellow\n    C(FlameGame)\n    D(Component)\n    E(Other Components)\n    F(GameWidget)\n\n    %% Flow  %%\n\n    A-- Extends -->B\n    F-- Wants -->B\n\n    C-- Extends -->D\n    E-- Extends -->D\n\n    C-- With -->B\n ```\n"
  },
  {
    "path": "doc/flame/effects/anchor_effects.md",
    "content": "# Anchor Effects\n\nAnchor effects are used to change the anchor point of a component over time. The anchor point is\nthe point around which the component rotates and scales.\n\n\n## `AnchorByEffect`\n\nChanges the location of the target's anchor by the specified offset. This effect can also be created\nusing `AnchorEffect.by()`.\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: anchor_by_effect\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nfinal effect = AnchorByEffect(\n  Vector2(0.1, 0.1),\n  EffectController(speed: 1),\n);\n```\n\n\n## `AnchorToEffect`\n\nChanges the location of the target's anchor. This effect can also be created using\n`AnchorEffect.to()`.\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: anchor_to_effect\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nfinal effect = AnchorToEffect(\n  Anchor.center,\n  EffectController(speed: 1),\n);\n```\n"
  },
  {
    "path": "doc/flame/effects/color_effects.md",
    "content": "# Color Effects\n\nColor effects are used to change the color of a component over time. They can be used to tint a component,\nchange its opacity, or apply a color filter.\n\n\n## ColorEffect\n\nThis effect will change the base color of the paint, causing the rendered component to be tinted by\nthe provided color between a provided range.\n\nUsage example:\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: color_effect\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nfinal effect = ColorEffect(\n  const Color(0xFF00FF00),\n  EffectController(duration: 1.5),\n  opacityFrom: 0.2,\n  opacityTo: 0.8,\n);\n```\n\nThe `opacityFrom` and `opacityTo` arguments will determine \"how much\" of the color that will be\napplied to the component. In this example the effect will start with 20% and will go up to 80%.\n\n**Note:** Due to how this effect is implemented, and how Flutter's `ColorFilter` class works, this\neffect can't be mixed with other `ColorEffect`s, when more than one is added to the component, only\nthe last one will have effect.\n\n\n## `OpacityToEffect`\n\nThis effect will change the opacity of the target over time to the specified alpha-value.\nIt can only be applied to components that implement the `OpacityProvider`.\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: opacity_to_effect\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nfinal effect = OpacityEffect.to(\n  0.2,\n  EffectController(duration: 0.75),\n);\n```\n\nIf the component uses multiple paints, the effect can target one more more of those paints\nusing the `target` parameter. The `HasPaint` mixin implements `OpacityProvider` and exposes APIs\nto easily create providers for desired paintIds. For single paintId `opacityProviderOf` can be used\nand for multiple paintIds and `opacityProviderOfList` can be used.\n\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: opacity_effect_with_target\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nfinal effect = OpacityEffect.to(\n  0.2,\n  EffectController(duration: 0.75),\n  target: component.opacityProviderOfList(\n    paintIds: const [paintId1, paintId2],\n  ),\n);\n```\n\nThe opacity value of 0 corresponds to a fully transparent component, and the opacity value of 1 is\nfully opaque. Convenience constructors `OpacityEffect.fadeOut()` and `OpacityEffect.fadeIn()` will\nanimate the target into full transparency / full visibility respectively.\n\n\n## `OpacityByEffect`\n\nThis effect will change the opacity of the target relative to the specified alpha-value. For example,\nthe following effect will change the opacity of the target by `90%`:\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: opacity_by_effect\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nfinal effect = OpacityEffect.by(\n  0.9,\n  EffectController(duration: 0.75),\n);\n```\n\nCurrently this effect can only be applied to components that have a `HasPaint` mixin. If the\ntarget component uses multiple paints, the effect can target any individual color using the\n`paintId` parameter.\n\n\n## GlowEffect\n\n```{note}\nThis effect is currently experimental, and its API may change in the future.\n```\n\nThis effect will apply the glowing shade around target relative to the specified\n`glow-strength`. The color of shade will be targets paint color. For example, the following effect\nwill apply the glowing shade around target by strength of `10`:\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: glow_effect\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nfinal effect = GlowEffect(\n  10.0,\n  EffectController(duration: 3),\n);\n```\n\nCurrently this effect can only be applied to components that have a `HasPaint` mixin.\n\n\n## `HueToEffect`\n\nThis effect will change the hue of the target over time to the specified angle in radians.\nIt can only be applied to components that implement the `HueProvider`.\n\n```dart\nfinal effect = HueEffect.to(\n  pi / 2,\n  EffectController(duration: 3),\n);\n```\n\n\n## `HueByEffect`\n\nThis effect will rotate the hue of the target relative by the specified angle in radians.\nIt can only be applied to components that implement the `HueProvider`.\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: hue_effect\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nfinal effect = HueEffect.by(\n  2 * pi,\n  EffectController(duration: 3),\n);\n```\n\nBoth effects can target any component implementing `HueProvider`. The `HasPaint` mixin\nimplements `HueProvider` and handles the necessary `ColorFilter` updates automatically.\n\n> [!TIP]\n> **Performance Note**: `HueEffect` is extremely efficient because it modifies the `Paint`'s\n> `colorFilter` directly. If you have many components, prefer this effect over the `HueDecorator`,\n> which uses `saveLayer()` and has much higher overhead.\n"
  },
  {
    "path": "doc/flame/effects/combined_effect.md",
    "content": "# Combined Effect\n\nThis effect can be used to run multiple other effects simultaneously.\n\nThe combined effect can also be alternating (the sequence will first run forward, and then\nbackward); and also repeat a certain predetermined number of times, or infinitely.\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: combined_effect\n:show: widget code infobox\n:width: 450\n:height: 350\n```\n\n```dart\nfinal effect = CombinedEffect(\n  [\n    MoveEffect.by(Vector2(200, 0), EffectController(duration: 1)),\n    RotateEffect.by(tau / 4, EffectController(duration: 2)),\n    ScaleEffect.by(Vector2.all(1.5), EffectController(duration: 1)),\n  ],\n  alternate: true,\n  infinite: true,\n);\n```\n"
  },
  {
    "path": "doc/flame/effects/effect_controllers.md",
    "content": "# Effect controllers\n\nAn `EffectController` is an object that describes how the effect should evolve over time. If you\nthink of the initial value of the effect as 0% progress, and the final value as 100% progress, then\nthe job of the effect controller is to map from the \"physical\" time, measured in seconds, into the\n\"logical\" time, which changes from 0 to 1.\n\nThere are multiple effect controllers provided by the Flame framework:\n\n- [`EffectController`](#effectcontroller)\n- [`LinearEffectController`](#lineareffectcontroller)\n- [`ReverseLinearEffectController`](#reverselineareffectcontroller)\n- [`CurvedEffectController`](#curvedeffectcontroller)\n- [`ReverseCurvedEffectController`](#reversecurvedeffectcontroller)\n- [`PauseEffectController`](#pauseeffectcontroller)\n- [`RepeatedEffectController`](#repeatedeffectcontroller)\n- [`InfiniteEffectController`](#infiniteeffectcontroller)\n- [`SequenceEffectController`](#sequenceeffectcontroller)\n- [`SpeedEffectController`](#speedeffectcontroller)\n- [`DelayedEffectController`](#delayedeffectcontroller)\n- [`NoiseEffectController`](#noiseeffectcontroller)\n- [`RandomEffectController`](#randomeffectcontroller)\n- [`SineEffectController`](#sineeffectcontroller)\n- [`ZigzagEffectController`](#zigzageffectcontroller)\n\n\n## `EffectController`\n\nThe base `EffectController` class provides a factory constructor capable of creating a variety of\ncommon controllers. The syntax of the constructor is the following:\n\n```dart\nEffectController({\n    required double duration,\n    Curve curve = Curves.linear,\n    double? reverseDuration,\n    Curve? reverseCurve,\n    bool alternate = false,\n    double atMaxDuration = 0.0,\n    double atMinDuration = 0.0,\n    int? repeatCount,\n    bool infinite = false,\n    double startDelay = 0.0,\n    VoidCallback? onMax,\n    VoidCallback? onMin,\n});\n```\n\n- *`duration`*: the length of the main part of the effect, i.e. how long it should take to go\n  from 0 to 100%. This parameter cannot be negative, but can be zero. If this is the only parameter\n  specified then the effect will grow linearly over the `duration` seconds.\n\n- *`curve`*: if given, creates a non-linear effect that grows from 0 to 100% according to the\n  provided [curve](https://api.flutter.dev/flutter/animation/Curves-class.html).\n\n- *`reverseDuration`*: if provided, adds an additional step to the controller: after the effect\n  has grown from 0 to 100% over the `duration` seconds, it will then go backwards from 100% to 0\n  over the `reverseDuration` seconds. In addition, the effect will complete at progress level of 0\n  (normally the effect completes at progress 1).\n\n- *`reverseCurve`*: the curve to be used during the \"reverse\" step of the effect. If not given,\n  this will default to `curve.flipped`.\n\n- *`alternate`*: setting this to true is equivalent to specifying the `reverseDuration` equal\n  to the `duration`. If the `reverseDuration` is already set, this flag has no effect.\n\n- *`atMaxDuration`*: if non-zero, this inserts a pause after the effect reaches its max\n  progress and before the reverse stage. During this time the effect is kept at 100% progress. If\n  there is no reverse stage, then this will simply be a pause before the effect is marked as\n  completed.\n\n- *`atMinDuration`*: if non-zero, this inserts a pause after the reaches its lowest progress\n  (0) at the end of the reverse stage. During this time, the effect's progress is at 0%. If there\n  is no reverse stage, then this pause will still be inserted after the \"at-max\" pause if it's\n  present, or after the forward stage otherwise. In addition, the effect will now complete at\n  progress level of 0.\n\n- *`repeatCount`*: if greater than one, it will cause the effect to repeat itself the prescribed\n  number of times. Each iteration will consists of the forward stage, pause at max, reverse stage,\n  then pause at min (skipping those that were not specified).\n\n- *`infinite`*: if true, the effect will repeat infinitely and never reach completion. This is\n  equivalent to as if `repeatCount` was set to infinity.\n\n- *`startDelay`*: an additional wait time inserted before the beginning of the effect. This\n  wait time is executed only once, even if the effect is repeating. During this time the effect's\n  `.started` property returns false. The effect's `onStart()` callback will be executed at the end\n  of this waiting period.\n\n  Using this parameter is the simplest way to create a chain of effects that execute one after\n  another (or with an overlap).\n\n- *`onMax`*: callback function which will be invoked right after reaching its max progress and\n  before the optional pause and reverse stage.\n\n- *`onMin`*: callback function which will be invoked right after reaching its lowest progress\n  at the end of the reverse stage and before the optional pause and forward stage.\n\nThe effect controller returned by this factory constructor will be composited of multiple simpler\neffect controllers described further below. If this constructor proves to be too limited for your\nneeds, you can always create your own combination from the same building blocks.\n\nIn addition to the factory constructor, the `EffectController` class defines a number of properties\ncommon for all effect controllers. These properties are:\n\n- `.started`: true if the effect has already started. For most effect controllers this property\n  is always true. The only exception is the `DelayedEffectController` which returns false while the\n  effect is in the waiting stage.\n\n- `.completed`: becomes true when the effect controller finishes execution.\n\n- `.progress`: current value of the effect controller, a floating-point value from 0 to 1. This\n  variable is the main \"output\" value of an effect controller.\n\n- `.duration`: total duration of the effect, or `null` if the duration cannot be determined (for\n  example if the duration is random or infinite).\n\n\n## `LinearEffectController`\n\nThis is the simplest effect controller that grows linearly from 0 to 1 over the specified\n`duration`:\n\n```dart\nfinal controller = LinearEffectController(3);\n```\n\n\n## `ReverseLinearEffectController`\n\nSimilar to the `LinearEffectController`, but it goes in the opposite direction and grows linearly\nfrom 1 to 0 over the specified duration:\n\n```dart\nfinal controller = ReverseLinearEffectController(1);\n```\n\n\n## `CurvedEffectController`\n\nThis effect controller grows non-linearly from 0 to 1 over the specified `duration` and following\nthe provided `curve`:\n\n```dart\nfinal controller = CurvedEffectController(0.5, Curves.easeOut);\n```\n\n\n## `ReverseCurvedEffectController`\n\nSimilar to the `CurvedEffectController`, but the controller grows down from 1 to 0 following the\nprovided `curve`:\n\n```dart\nfinal controller = ReverseCurvedEffectController(0.5, Curves.bounceInOut);\n```\n\n\n## `PauseEffectController`\n\nThis effect controller keeps the progress at a constant value for the specified time duration.\nTypically, the `progress` would be either 0 or 1:\n\n```dart\nfinal controller = PauseEffectController(1.5, progress: 0);\n```\n\n\n## `RepeatedEffectController`\n\nThis is a composite effect controller. It takes another effect controller as a child, and repeats\nit multiple times, resetting before the start of each next cycle.\n\n```dart\nfinal controller = RepeatedEffectController(LinearEffectController(1), 10);\n```\n\nThe child effect controller cannot be infinite. If the child is random, then it will be\nre-initialized with new random values on each iteration.\n\n\n## `InfiniteEffectController`\n\nSimilar to the `RepeatedEffectController`, but repeats its child controller indefinitely.\n\n```dart\nfinal controller = InfiniteEffectController(LinearEffectController(1));\n```\n\n\n## `SequenceEffectController`\n\nExecutes a sequence of effect controllers, one after another. The list of controllers cannot be\nempty.\n\n```dart\nfinal controller = SequenceEffectController([\n  LinearEffectController(1),\n  PauseEffectController(0.2),\n  ReverseLinearEffectController(1),\n]);\n```\n\n\n## `SpeedEffectController`\n\nAlters the duration of its child effect controller so that the effect proceeds at the predefined\nspeed. The initial duration of the child EffectController is irrelevant. The child controller must\nbe the subclass of `DurationEffectController`.\n\nThe `SpeedEffectController` can only be applied to effects for which the notion of speed is\nwell-defined. Such effects must implement the `MeasurableEffect` interface. For example, the\nfollowing effects qualify:\n\n- [`MoveByEffect`](move_effects.md#movebyeffect)\n- [`MoveToEffect`](move_effects.md#movetoeffect)\n- [`MoveAlongPathEffect`](move_effects.md#movealongpatheffect)\n- [`RotateEffect.by`](rotate_effects.md#rotateeffectby)\n- [`RotateEffect.to`](rotate_effects.md#rotateeffectto)\n\nThe parameter `speed` is in units per second, where the notion of a \"unit\" depends on the target\neffect. For example, for move effects, they refer to the distance traveled; for rotation effects\nthe units are radians.\n\n```dart\nfinal speedController =\n    SpeedEffectController(LinearEffectController(0), speed: 1);\nfinal controller =\n    EffectController(speed: 1); // same as speedController\n```\n\n\n## `DelayedEffectController`\n\nEffect controller that executes its child controller after the prescribed `delay`. While the\ncontroller is executing the \"delay\" stage, the effect will be considered \"not started\", i.e. its\n`.started` property will be returning `false`.\n\n```dart\nfinal controller = DelayedEffectController(LinearEffectController(1), delay: 5);\n```\n\n\n## `NoiseEffectController`\n\nThis effect controller exhibits noisy behavior, i.e. it oscillates randomly around zero. Such effect\ncontroller can be used to implement a variety of shake effects.\n\n```dart\nfinal controller = NoiseEffectController(duration: 0.6, frequency: 10);\n```\n\n\n## `RandomEffectController`\n\nThis controller wraps another controller and makes its duration random. The actual value for the\nduration is re-generated upon each reset, which makes this controller particularly useful within\nrepeated contexts, such as [](#repeatedeffectcontroller) or [](#infiniteeffectcontroller).\n\n```dart\nfinal controller = RandomEffectController.uniform(\n  LinearEffectController(0),  // duration here is irrelevant\n  min: 0.5,\n  max: 1.5,\n);\n```\n\nThe user has the ability to control which `Random` source to use, as well as the exact distribution\nof the produced random durations. Two distributions, `.uniform` and `.exponential`, are included,\nany other can be implemented by the user.\n\n\n## `SineEffectController`\n\nAn effect controller that represents a single period of the sine function. Use this to create\nnatural-looking harmonic oscillations. Two perpendicular move effects governed by\n`SineEffectControllers` with different periods, will create a [Lissajous curve].\n\n```dart\nfinal controller = SineEffectController(period: 1);\n```\n\n\n## `ZigzagEffectController`\n\nSimple alternating effect controller. Over the course of one `period`, this controller will proceed\nlinearly from 0 to 1, then to -1, and then back to 0. Use this for oscillating effects where the\nstarting position should be the center of the oscillations, rather than the extreme (as provided\nby the standard alternating `EffectController`).\n\n```dart\nfinal controller = ZigzagEffectController(period: 2);\n```\n\n[Lissajous curve]: https://en.wikipedia.org/wiki/Lissajous_curve\n"
  },
  {
    "path": "doc/flame/effects/effects.md",
    "content": "# Effects\n\nIn game development, smoothly animating properties over time (moving a character, fading an element,\nscaling a power-up) is a constant need. Writing manual interpolation code in every `update` method\nis repetitive and error-prone. Effects provide a declarative way to describe these time-based\nchanges: you attach an effect to a component, and it automatically handles the animation, then\nremoves itself when finished.\n\nAn effect is a special component that can attach to another component in order to modify its\nproperties or appearance.\n\nFor example, suppose you are making a game with collectible power-up items. You want these power-ups\nto generate randomly around the map and then de-spawn after some time. Obviously, you could make a\nsprite component for the power-up and then place that component on the map, but we could do even\nbetter!\n\nLet's add a `ScaleEffect` to grow the item from 0 to 100% when the power-up first appears. Add\nanother infinitely repeating alternating `MoveEffect` in order to make the item move slightly up\nand down. Then add an `OpacityEffect` that will \"blink\" the item 3 times, this effect will have a\nbuilt-in delay of 30 seconds, or however long you want your power-up to stay in place. Lastly, add\na `RemoveEffect` that will automatically remove the item from the game tree after the specified\ntime (you probably want to time it right after the end of the `OpacityEffect`).\n\nAs you can see, with a few simple effects we have turned a simple lifeless sprite into a much more\ninteresting item. And what's more important, it didn't result in an increased code complexity: the\neffects, once added, will work automatically, and then self-remove from the game tree when\nfinished.\n\n\n## Overview\n\nThe function of an `Effect` is to effect a change over time in some component's property. In order\nto achieve that, the `Effect` must know the initial value of the property, the final value, and how\nit should progress over time. The initial value is usually determined by an effect automatically,\nthe final value is provided by the user explicitly, and progression over time is handled by\n[EffectControllers](effect_controllers.md).\n\n\n### Effect\n\nThe base `Effect` class is not usable on its own (it is abstract), but it provides some common\nfunctionality inherited by all other effects. This includes:\n\n- The ability to pause/resume the effect using `effect.pause()` and `effect.resume()`. You can\n  check whether the effect is currently paused using `effect.isPaused`.\n\n- Property `removeOnFinish` (which is true by default) will cause the effect component to be\n  removed from the game tree and garbage-collected once the effect completes. Set this to false\n  if you plan to reuse the effect after it is finished.\n\n- Optional user-provided `onComplete`, which will be invoked when the effect has just\n  completed its execution but before it is removed from the game.\n\n- A `completed` future that completes when the effect finishes.\n\n- The `reset()` method reverts the effect to its original state, allowing it to run once again.\n\nThere are multiple pre-built effects provided by Flame, and you can also\n[create your own](#creating-new-effects). The following effects are included:\n\n- [`MoveByEffect`](move_effects.md#movebyeffect)\n- [`MoveToEffect`](move_effects.md#movetoeffect)\n- [`MoveAlongPathEffect`](move_effects.md#movealongpatheffect)\n- [`RotateAroundEffect`](rotate_effects.md#rotatearoundeffect)\n- [`RotateEffect.by`](rotate_effects.md#rotateeffectby)\n- [`RotateEffect.to`](rotate_effects.md#rotateeffectto)\n- [`ScaleEffect.by`](scale_effects.md#scaleeffectby)\n- [`ScaleEffect.to`](scale_effects.md#scaleeffectto)\n- [`SizeEffect.by`](size_effects.md#sizeeffectby)\n- [`SizeEffect.to`](size_effects.md#sizeeffectto)\n- [`AnchorByEffect`](anchor_effects.md#anchorbyeffect)\n- [`AnchorToEffect`](anchor_effects.md#anchortoeffect)\n- [`OpacityToEffect`](color_effects.md#opacitytoeffect)\n- [`OpacityByEffect`](color_effects.md#opacitybyeffect)\n- [`ColorEffect`](color_effects.md#coloreffect)\n- [`SequenceEffect`](sequence_effect.md)\n- [`CombinedEffect`](combined_effect.md)\n- [`RemoveEffect`](remove_effect.md)\n- [`FunctionEffect`](function_effect.md)\n\n\n## Creating new effects\n\nAlthough Flame provides a wide array of built-in effects, eventually you may find them to be\ninsufficient. Luckily, creating new effects is very simple.\n\nEach effect extends the base `Effect` class, possibly via one of the more specialized abstract\nsubclasses such as `ComponentEffect<T>` or `Transform2DEffect`.\n\nThe `Effect` class' constructor requires an `EffectController` instance as an argument. In most\ncases you may want to pass that controller from your own constructor. Luckily, the effect controller\nencapsulates much of the complexity of an effect's implementation, so you don't need to worry about\nre-creating that functionality.\n\nLastly, you will need to implement a single method `apply(double progress)` that will be called at\neach update tick while the effect is active. In this method you are supposed to make changes to the\ntarget of your effect.\n\nIn addition, you may want to implement callbacks `onStart()` and `onFinish()` if there are any\nactions that must be taken when the effect starts or ends.\n\nWhen implementing the `apply()` method we recommend to use relative updates only. That is, change\nthe target property by incrementing/decrementing its current value, rather than directly setting\nthat property to a fixed value. This way multiple effects would be able to act on the same component\nwithout interfering with each other.\n\n\n## Effects vs Decorators\n\nWhile effects and decorators can sometimes achieve similar visual results (like changing opacity\nor color), they have different performance and visual characteristics:\n\n- **Effects** are fast and generally change a property on a single component. When applied to\n  a group, they affect each child individually.\n- **Decorators** are more powerful but slower. They use `saveLayer` to flatten a whole\n  component subtree into a single layer before applying an effect. This is essential for\n  correctly rendering composite objects with transparency or complex filters.\n\nSee the [Decorators documentation](../rendering/decorators.md) for a more detailed comparison.\n\n\n## See also\n\n- [Examples of various effects](https://examples.flame-engine.org/).\n\n```{toctree}\n:hidden:\n\nEffect Controllers        <effect_controllers.md>\nMove Effects              <move_effects.md>\nRotate Effects            <rotate_effects.md>\nScale Effects             <scale_effects.md>\nSize Effects              <size_effects.md>\nAnchor Effects            <anchor_effects.md>\nColor Effects             <color_effects.md>\nSequence Effect           <sequence_effect.md>\nCombined Effect           <combined_effect.md>\nRemove Effect             <remove_effect.md>\nFunction Effect           <function_effect.md>\n```\n"
  },
  {
    "path": "doc/flame/effects/function_effect.md",
    "content": "# Function Effect\n\nThe `FunctionEffect` class is a very generic Effect that allows you to do almost anything without\nhaving to define a new effect.\n\nIt runs a function that takes the target and the progress of the effect and then the user can\ndecide what to do with that input.\n\nThis could for example be used to make game state changes that happen over time, but that isn't\nnecessarily visual, like most other effects are.\n\nIn the following example we have a `PlayerState` enum that we want to change over time. We want to\nchange the state to `yawn` when the progress is over 50% and then back to `idle` when the progress\nis over 80%.\n\n```dart\nenum PlayerState {\n  idle,\n  yawn,\n}\n\nfinal effect = FunctionEffect<SpriteAnimationGroupComponent<PlayerState>>(\n  (target, progress) {\n    if (progress > 0.5) {\n      target.current = PlayerState.yawn;\n    } else if(progress > 0.8) {\n      target.current = PlayerState.idle;\n    }\n  },\n  EffectController(\n    duration: 10,\n    infinite: true,\n  ),\n);\n```\n"
  },
  {
    "path": "doc/flame/effects/move_effects.md",
    "content": "# Move Effects\n\nMove Effects are a special type of effects that modify the position of a component over time, if\nyou want to for example move your character from one point to another, make it jump, or follow a\npath, then you can use one of the predefined move effects.\n\n\n## `MoveByEffect`\n\nThis effect applies to a `PositionComponent` and shifts it by a prescribed `offset` amount. This\noffset is relative to the current position of the target:\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: move_by_effect\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nfinal effect = MoveByEffect(\n  Vector2(0, -10),\n  EffectController(duration: 0.5),\n);\n```\n\nIf the component is currently at `Vector2(250, 200)`, then at the end of the effect its position\nwill be `Vector2(250, 190)`.\n\nMultiple move effects can be applied to a component at the same time. The result will be the\nsuperposition of all the individual effects.\n\n\n## `MoveToEffect`\n\nThis effect moves a `PositionComponent` from its current position to the specified destination\npoint in a straight line.\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: move_to_effect\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nfinal effect = MoveToEffect(\n  Vector2(100, 500),\n  EffectController(duration: 3),\n);\n```\n\nIt is possible, but not recommended to attach multiple such effects to the same component.\n\n\n## `MoveAlongPathEffect`\n\nThis effect moves a `PositionComponent` along the specified path relative to the component's\ncurrent position. The path can have non-linear segments, but must be singly connected. It is\nrecommended to start a path at `Vector2.zero()` in order to avoid sudden jumps in the component's\nposition.\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: move_along_path_effect\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nfinal effect = MoveAlongPathEffect(\n  Path()..quadraticBezierTo(100, 0, 50, -50),\n  EffectController(duration: 1.5),\n);\n```\n\nAn optional flag `absolute: true` will declare the path within the effect as absolute. That is, the\ntarget will \"jump\" to the beginning of the path at start, and then follow that path as if it was a\ncurve drawn on the canvas.\n\nAnother flag `oriented: true` instructs the target not only move along the curve, but also rotate\nitself in the direction the curve is facing at each point. With this flag the effect becomes both\nthe move- and the rotate- effect at the same time.\n"
  },
  {
    "path": "doc/flame/effects/remove_effect.md",
    "content": "# Remove Effect\n\nThis is a simple effect that can be attached to a component causing it to be removed from the game\ntree after the specified delay has passed:\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: remove_effect\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n\n```dart\ncomponent.add(RemoveEffect(delay: 3.0));\n```\n"
  },
  {
    "path": "doc/flame/effects/rotate_effects.md",
    "content": "# Rotate Effects\n\nRotate effects are used to change the orientation of a component over time. They can be used to make\na component spin, turn towards a target, or rotate around a point. The rotation is specified in\nradians, and the effects can be applied to any component that has a rotation property, such as the\n`PositionComponent`.\n\n\n## `RotateEffect.by`\n\nRotates the target clockwise by the specified angle relative to its current orientation. The angle\nis in radians. For example, the following effect will rotate the target 90º (=[tau]/4 in radians)\nclockwise:\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: rotate_by_effect\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nfinal effect = RotateEffect.by(\n  tau/4,\n  EffectController(duration: 2),\n);\n```\n\n\n## `RotateEffect.to`\n\nRotates the target clockwise to the specified angle. For example, the following will rotate the\ntarget to look east (0º is north, 90º=[tau]/4 east, 180º=tau/2 south, and 270º=tau*3/4 west):\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: rotate_to_effect\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nfinal effect = RotateEffect.to(\n  tau/4,\n  EffectController(duration: 2),\n);\n```\n\n\n## `RotateAroundEffect`\n\nRotates the target clockwise by the specified angle relative to its current orientation around\nthe specified center. The angle is in radians. For example, the following effect will rotate the\ntarget 90º (=[tau]/4 in radians) clockwise around (100, 100).\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: rotate_around_effect\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nfinal effect = RotateAroundEffect(\n  tau/4,\n  center: Vector2(100, 100),\n  EffectController(duration: 2),\n);\n```\n\n[tau]: https://en.wikipedia.org/wiki/Tau_(mathematical_constant)\n"
  },
  {
    "path": "doc/flame/effects/scale_effects.md",
    "content": "# Scale Effects\n\nScale effects are used to change the scale of a component over time. They can be used to make a\ncomponent grow, shrink, or change its scale in a specific direction. The scale is specified as a\n`Vector2` value, where each x represents the width factor and y represents the height factor. The\neffects can be applied to any component that has a scale property, such as the `PositionComponent`.\nThe difference between size effects and scale effects is that size only changes the size of the\ntarget component, while scale changes the \"size\" of all children too.\n\n\n## `ScaleEffect.by`\n\nThis effect will change the target's scale by the specified amount. For example, this will cause\nthe component to grow 50% larger:\n\n ```{flutter-app}\n :sources: ../flame/examples\n :page: scale_by_effect\n :show: widget code infobox\n :width: 180\n :height: 160\n ```\n\n```dart\nfinal effect = ScaleEffect.by(\n  Vector2.all(1.5),\n  EffectController(duration: 0.3),\n);\n```\n\n\n## `ScaleEffect.to`\n\nThis effect works similar to `ScaleEffect.by`, but sets the absolute value of the target's scale.\n\n ```{flutter-app}\n :sources: ../flame/examples\n :page: scale_to_effect\n :show: widget code infobox\n :width: 180\n :height: 160\n ```\n\n```dart\nfinal effect = ScaleEffect.to(\n  Vector2.all(0.5),\n  EffectController(duration: 0.5),\n);\n```\n"
  },
  {
    "path": "doc/flame/effects/sequence_effect.md",
    "content": "# Sequence Effect\n\nThis effect can be used to run multiple other effects one after another. The constituent effects\nmay have different types.\n\nThe sequence effect can also be alternating (the sequence will first run forward, and then\nbackward); and also repeat a certain predetermined number of times, or infinitely.\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: sequence_effect\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nfinal effect = SequenceEffect([\n  ScaleEffect.by(\n    Vector2.all(1.5),\n    EffectController(\n      duration: 0.2,\n      alternate: true,\n    ),\n  ),\n  MoveEffect.by(\n    Vector2(30, -50),\n    EffectController(\n      duration: 0.5,\n    ),\n  ),\n  OpacityEffect.to(\n    0,\n    EffectController(\n      duration: 0.3,\n    ),\n  ),\n  RemoveEffect(),\n]);\n```\n"
  },
  {
    "path": "doc/flame/effects/size_effects.md",
    "content": "# Size Effects\n\nSize effects are used to change the size of a component over time. They can be used to make a\ncomponent grow, shrink, or change its size in a specific direction. The size is specified as a\n`Vector2` value, where x represents the width and y represents the height. The effects can be\napplied to any component that implements the `SizeProvider` interface, such as the\n`PositionComponent`. The difference between size effects and scale effects is that size only\nchanges the size of the target component, while scale changes the \"size\" of all children too.\n\n\n## `SizeEffect.by`\n\nThis effect will change the size of the target component, relative to its current size. For example,\nif the target has size `Vector2(100, 100)`, then after the following effect is applied and runs its\ncourse, the new size will be `Vector2(120, 50)`:\n\n ```{flutter-app}\n :sources: ../flame/examples\n :page: size_by_effect\n :show: widget code infobox\n :width: 180\n :height: 160\n ```\n\n```dart\nfinal effect = SizeEffect.by(\n   Vector2(-15, 30),\n   EffectController(duration: 1),\n);\n```\n\nThe size of a `PositionComponent` cannot be negative. If an effect attempts to set the size to a\nnegative value, the size will be clamped at zero.\n\nNote that for this effect to work, the target component must implement the `SizeProvider` interface\nand take its `size` into account when rendering. Only few of the built-in components implement this\nAPI, but you can always make your own component work with size effects by adding\n`implements SizeEffect` to the class declaration.\n\nAn alternative to `SizeEffect` is the `ScaleEffect`, which works more generally and scales both the\ntarget component and its children.\n\n\n## `SizeEffect.to`\n\nChanges the size of the target component to the specified size. Target size cannot be negative:\n\n\n ```{flutter-app}\n :sources: ../flame/examples\n :page: size_to_effect\n :show: widget code infobox\n :width: 180\n :height: 160\n ```\n\n```dart\nfinal effect = SizeEffect.to(\n  Vector2(90, 80),\n  EffectController(duration: 1),\n);\n```\n"
  },
  {
    "path": "doc/flame/examples/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    avoid_web_libraries_in_flutter: false\n"
  },
  {
    "path": "doc/flame/examples/lib/anchor.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/palette.dart';\n\nclass AnchorGame extends FlameGame {\n  final _parentAnchorText = TextComponent(position: Vector2.all(5));\n  final _childAnchorText = TextComponent(position: Vector2(5, 30));\n\n  late _AnchoredRectangle _redComponent;\n  late _AnchoredRectangle _blueComponent;\n\n  @override\n  Future<void> onLoad() async {\n    _redComponent = _AnchoredRectangle(\n      size: size / 4,\n      position: size / 2,\n      paint: BasicPalette.red.paint(),\n    );\n\n    _blueComponent = _AnchoredRectangle(\n      size: size / 8,\n      paint: BasicPalette.blue.paint(),\n    );\n\n    await _redComponent.addAll([\n      _blueComponent,\n      CircleComponent(radius: 2, anchor: Anchor.center),\n    ]);\n\n    await addAll([\n      _redComponent,\n      _parentAnchorText,\n      _childAnchorText,\n      CircleComponent(\n        radius: 4,\n        position: size / 2,\n        anchor: Anchor.center,\n      ),\n    ]);\n  }\n\n  @override\n  void update(double dt) {\n    _parentAnchorText.text = 'Parent: ${_redComponent.anchor}';\n    _childAnchorText.text = 'Child: ${_blueComponent.anchor}';\n    super.update(dt);\n  }\n}\n\nclass _AnchoredRectangle extends RectangleComponent with TapCallbacks {\n  _AnchoredRectangle({\n    super.position,\n    super.size,\n    super.paint,\n  });\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    var index = Anchor.values.indexOf(anchor) + 1;\n    if (index == Anchor.values.length) {\n      index = 0;\n    }\n    anchor = Anchor.values.elementAt(index);\n    super.onTapDown(event);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/anchor_by_effect.dart",
    "content": "import 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\n\nclass AnchorByEffectGame extends FlameGame {\n  bool reset = false;\n\n  @override\n  Future<void> onLoad() async {\n    final flower = Flower(\n      size: 60,\n      position: canvasSize / 2,\n      onTap: (flower) {\n        flower.add(\n          AnchorByEffect(\n            reset ? Vector2(-0.5, -0.5) : Vector2(0.5, 0.5),\n            EffectController(speed: 1),\n          ),\n        );\n        reset = !reset;\n      },\n    );\n    add(flower);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/anchor_to_effect.dart",
    "content": "import 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\n\nclass AnchorToEffectGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    final flower = Flower(\n      size: 60,\n      position: canvasSize / 2,\n      onTap: (flower) {\n        flower.add(\n          AnchorToEffect(\n            flower.anchor == Anchor.center ? Anchor.bottomLeft : Anchor.center,\n            EffectController(speed: 1),\n          ),\n        );\n      },\n    );\n    add(flower);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/collision_detection.dart",
    "content": "import 'package:doc_flame_examples/ember.dart';\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart' hide Image;\n\nclass CollisionDetectionGame extends FlameGame with HasCollisionDetection {\n  @override\n  Future<void> onLoad() async {\n    final emberPlayer = EmberPlayer(\n      position: Vector2(10, (size.y / 2) - 20),\n      size: Vector2.all(40),\n      onTap: (emberPlayer) {\n        emberPlayer.add(\n          MoveEffect.to(\n            Vector2(size.x - 40, (size.y / 2) - 20),\n            EffectController(\n              duration: 5,\n              reverseDuration: 5,\n              repeatCount: 1,\n              curve: Curves.easeOut,\n            ),\n          ),\n        );\n      },\n    );\n    add(emberPlayer);\n    add(RectangleCollidable(canvasSize / 2));\n  }\n}\n\nclass RectangleCollidable extends PositionComponent with CollisionCallbacks {\n  final _collisionStartColor = Colors.amber;\n  final _defaultColor = Colors.cyan;\n  late ShapeHitbox hitbox;\n\n  RectangleCollidable(Vector2 position)\n    : super(\n        position: position,\n        size: Vector2.all(50),\n        anchor: Anchor.center,\n      );\n\n  @override\n  Future<void> onLoad() async {\n    final defaultPaint = Paint()\n      ..color = _defaultColor\n      ..style = PaintingStyle.stroke;\n    hitbox = RectangleHitbox()\n      ..paint = defaultPaint\n      ..renderShape = true;\n    add(hitbox);\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n    hitbox.paint.color = _collisionStartColor;\n  }\n\n  @override\n  void onCollisionEnd(PositionComponent other) {\n    super.onCollisionEnd(other);\n    if (!isColliding) {\n      hitbox.paint.color = _defaultColor;\n    }\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/color_effect.dart",
    "content": "import 'package:doc_flame_examples/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/animation.dart';\nimport 'package:flutter/cupertino.dart';\n\nclass ColorEffectExample extends FlameGame {\n  bool reset = false;\n\n  @override\n  Future<void> onLoad() async {\n    final ember = EmberPlayer(\n      position: size / 2,\n      size: size / 4,\n      onTap: (ember) {\n        ember.add(\n          ColorEffect(\n            reset ? const Color(0xFF1039DB) : const Color(0xFF00FF00),\n            EffectController(duration: 1.5),\n            opacityTo: 0.6,\n          ),\n        );\n        reset = !reset;\n      },\n    )..anchor = Anchor.center;\n\n    add(ember);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/decorator_blur.dart",
    "content": "import 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/rendering.dart';\n\nclass DecoratorBlurGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    var step = 0;\n    add(\n      Flower(\n        size: 100,\n        position: canvasSize / 2,\n        onTap: (flower) {\n          final decorator = flower.decorator;\n          step++;\n          if (step == 1) {\n            decorator.addLast(PaintDecorator.blur(3.0));\n          } else if (step == 2) {\n            decorator.replaceLast(PaintDecorator.blur(5.0));\n          } else if (step == 3) {\n            decorator.replaceLast(PaintDecorator.blur(0.0, 20.0));\n          } else {\n            decorator.replaceLast(null);\n            step = 0;\n          }\n        },\n      )..onTapUp(),\n    );\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/decorator_grayscale.dart",
    "content": "import 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/rendering.dart';\n\nclass DecoratorGrayscaleGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    var step = 0;\n    add(\n      Flower(\n        size: 100,\n        position: canvasSize / 2,\n        onTap: (flower) {\n          final decorator = flower.decorator;\n          step++;\n          if (step == 1) {\n            decorator.addLast(PaintDecorator.grayscale());\n          } else if (step == 2) {\n            decorator.replaceLast(PaintDecorator.grayscale(opacity: 0.5));\n          } else if (step == 3) {\n            decorator.replaceLast(PaintDecorator.grayscale(opacity: 0.2));\n          } else if (step == 4) {\n            decorator.replaceLast(PaintDecorator.grayscale(opacity: 0.1));\n          } else {\n            decorator.removeLast();\n            step = 0;\n          }\n        },\n      )..onTapUp(),\n    );\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/decorator_hue.dart",
    "content": "import 'dart:math';\n\nimport 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/rendering.dart';\n\nclass DecoratorHueGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    var step = 0;\n    add(\n      Flower(\n        size: 100,\n        position: canvasSize / 2,\n        onTap: (flower) {\n          final decorator = flower.decorator;\n          step++;\n          if (step == 1) {\n            decorator.addLast(HueDecorator(hue: pi / 4));\n          } else if (step == 2) {\n            decorator.replaceLast(HueDecorator(hue: pi / 2));\n          } else if (step == 3) {\n            decorator.replaceLast(HueDecorator(hue: pi));\n          } else {\n            decorator.replaceLast(null);\n            step = 0;\n          }\n        },\n      )..onTapUp(),\n    );\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/decorator_rotate3d.dart",
    "content": "import 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/rendering.dart';\n\nclass DecoratorRotate3DGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    var step = 0;\n    final decorator = Rotate3DDecorator()\n      ..center = Vector2.all(50)\n      ..perspective = 0.01;\n    add(\n      Flower(\n        size: 100,\n        position: canvasSize / 2,\n        decorator: decorator,\n        onTap: (flower) {\n          step++;\n          if (step == 1) {\n            decorator.angleY = -0.8;\n          } else if (step == 2) {\n            decorator.angleX = 1.0;\n          } else if (step == 3) {\n            decorator.angleZ = 0.2;\n          } else if (step == 4) {\n            decorator.angleX = 10;\n          } else if (step == 5) {\n            decorator.angleY = 2;\n          } else {\n            decorator\n              ..angleX = 0\n              ..angleY = 0\n              ..angleZ = 0;\n            step = 0;\n          }\n        },\n      )..onTapUp(),\n    );\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/decorator_shadow3d.dart",
    "content": "import 'dart:ui';\n\nimport 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/rendering.dart';\n\nclass DecoratorShadowGame extends FlameGame {\n  @override\n  Color backgroundColor() => const Color(0xFFC7C7C7);\n\n  @override\n  Future<void> onLoad() async {\n    final decorator = Shadow3DDecorator(base: Vector2(0, 100));\n    var step = 0;\n    add(Grid());\n    add(\n      Flower(\n        size: 100,\n        position: canvasSize / 2,\n        decorator: decorator,\n        onTap: (flower) {\n          step++;\n          if (step == 1) {\n            decorator.xShift = 200;\n            decorator.opacity = 0.5;\n          } else if (step == 2) {\n            decorator.xShift = 400;\n            decorator.yScale = 3;\n            decorator.blur = 1;\n          } else if (step == 3) {\n            decorator.angle = 1.7;\n            decorator.blur = 2;\n          } else if (step == 4) {\n            decorator.ascent = 20;\n            decorator.angle = 1.7;\n            decorator.blur = 2;\n            flower.y -= 20;\n          } else {\n            decorator.ascent = 0;\n            decorator.xShift = 0;\n            decorator.yScale = 1;\n            decorator.angle = -1.4;\n            decorator.opacity = 0.8;\n            decorator.blur = 0;\n            flower.y += 20;\n            step = 0;\n          }\n        },\n      )..onTapUp(),\n    );\n  }\n}\n\nclass Grid extends Component {\n  final paint = Paint()\n    ..color = const Color(0xffa9a9a9)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 1;\n  @override\n  void render(Canvas canvas) {\n    for (var i = 0; i < 50; i++) {\n      canvas.drawLine(Offset(0, i * 25), Offset(500, i * 25), paint);\n      canvas.drawLine(Offset(i * 25, 0), Offset(i * 25, 500), paint);\n    }\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/decorator_tint.dart",
    "content": "import 'dart:ui';\n\nimport 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/rendering.dart';\n\nclass DecoratorTintGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    var step = 0;\n    add(\n      Flower(\n        size: 100,\n        position: canvasSize / 2,\n        onTap: (flower) {\n          final decorator = flower.decorator;\n          step++;\n          if (step == 1) {\n            decorator.addLast(PaintDecorator.tint(const Color(0x88FF0000)));\n          } else if (step == 2) {\n            decorator.replaceLast(PaintDecorator.tint(const Color(0x8800FF00)));\n          } else if (step == 3) {\n            decorator.replaceLast(PaintDecorator.tint(const Color(0x88000088)));\n          } else if (step == 4) {\n            decorator.replaceLast(PaintDecorator.tint(const Color(0x66FFFFFF)));\n          } else if (step == 5) {\n            decorator.replaceLast(PaintDecorator.tint(const Color(0xAA000000)));\n          } else {\n            decorator.removeLast();\n            step = 0;\n          }\n        },\n      )..onTapUp(),\n    );\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/drag_events.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flutter/rendering.dart';\n\nclass DragEventsGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    addAll([\n      DragTarget(),\n      Star(\n        n: 5,\n        radius1: 40,\n        radius2: 20,\n        sharpness: 0.2,\n        color: const Color(0xffbae5ad),\n        position: Vector2(70, 70),\n      ),\n      Star(\n        n: 3,\n        radius1: 50,\n        radius2: 40,\n        sharpness: 0.3,\n        color: const Color(0xff6ecbe5),\n        position: Vector2(70, 160),\n      ),\n      Star(\n        n: 12,\n        radius1: 10,\n        radius2: 75,\n        sharpness: 1.3,\n        color: const Color(0xfff6df6a),\n        position: Vector2(70, 270),\n      ),\n      Star(\n        n: 10,\n        radius1: 20,\n        radius2: 17,\n        sharpness: 0.85,\n        color: const Color(0xfff82a4b),\n        position: Vector2(110, 110),\n      ),\n    ]);\n  }\n}\n\n/// This component is the pink-ish rectangle in the center of the game window.\n/// It uses the [DragCallbacks] mixin in order to receive drag events.\nclass DragTarget extends PositionComponent with DragCallbacks {\n  DragTarget() : super(anchor: Anchor.center);\n\n  final _rectPaint = Paint()..color = const Color(0x88AC54BF);\n\n  /// We will store all current circles into this map, keyed by the `pointerId`\n  /// of the event that created the circle.\n  final Map<int, Trail> _trails = {};\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    this.size = size - Vector2(100, 75);\n    if (this.size.x < 100 || this.size.y < 100) {\n      this.size = size * 0.9;\n    }\n    position = size / 2;\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(size.toRect(), _rectPaint);\n  }\n\n  @override\n  void onDragStart(DragStartEvent event) {\n    super.onDragStart(event);\n    final trail = Trail(event.localPosition);\n    _trails[event.pointerId] = trail;\n    add(trail);\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    _trails[event.pointerId]!.addPoint(event.localEndPosition);\n  }\n\n  @override\n  void onDragEnd(DragEndEvent event) {\n    super.onDragEnd(event);\n    _trails.remove(event.pointerId)!.end();\n  }\n\n  @override\n  void onDragCancel(DragCancelEvent event) {\n    super.onDragCancel(event);\n    _trails.remove(event.pointerId)!.cancel();\n  }\n}\n\nclass Trail extends Component {\n  Trail(Vector2 origin)\n    : _paths = [Path()..moveTo(origin.x, origin.y)],\n      _opacities = [1],\n      _lastPoint = origin.clone(),\n      _color = HSLColor.fromAHSL(\n        1,\n        random.nextDouble() * 360,\n        1,\n        0.8,\n      ).toColor();\n\n  final List<Path> _paths;\n  final List<double> _opacities;\n  Color _color;\n  late final _linePaint = Paint()..style = PaintingStyle.stroke;\n  late final _circlePaint = Paint()..color = _color;\n  bool _released = false;\n  double _timer = 0;\n  final _vanishInterval = 0.03;\n  final Vector2 _lastPoint;\n\n  static final random = Random();\n  static const lineWidth = 10.0;\n\n  @override\n  void render(Canvas canvas) {\n    assert(_paths.length == _opacities.length);\n    for (var i = 0; i < _paths.length; i++) {\n      final path = _paths[i];\n      final opacity = _opacities[i];\n      if (opacity > 0) {\n        _linePaint.color = _color.withValues(alpha: opacity);\n        _linePaint.strokeWidth = lineWidth * opacity;\n        canvas.drawPath(path, _linePaint);\n      }\n    }\n    canvas.drawCircle(\n      _lastPoint.toOffset(),\n      (lineWidth - 2) * _opacities.last + 2,\n      _circlePaint,\n    );\n  }\n\n  @override\n  void update(double dt) {\n    assert(_paths.length == _opacities.length);\n    _timer += dt;\n    while (_timer > _vanishInterval) {\n      _timer -= _vanishInterval;\n      for (var i = 0; i < _paths.length; i++) {\n        _opacities[i] -= 0.01;\n        if (_opacities[i] <= 0) {\n          _paths[i].reset();\n        }\n      }\n      if (!_released) {\n        _paths.add(Path()..moveTo(_lastPoint.x, _lastPoint.y));\n        _opacities.add(1);\n      }\n    }\n    if (_opacities.last < 0) {\n      removeFromParent();\n    }\n  }\n\n  void addPoint(Vector2 point) {\n    if (!point.x.isNaN) {\n      for (final path in _paths) {\n        path.lineTo(point.x, point.y);\n      }\n      _lastPoint.setFrom(point);\n    }\n  }\n\n  void end() => _released = true;\n\n  void cancel() {\n    _released = true;\n    _color = const Color(0xFFFFFFFF);\n  }\n}\n\nclass Star extends PositionComponent with DragCallbacks {\n  Star({\n    required int n,\n    required double radius1,\n    required double radius2,\n    required double sharpness,\n    required this.color,\n    super.position,\n  }) {\n    _path = Path()..moveTo(radius1, 0);\n    for (var i = 0; i < n; i++) {\n      final p1 = Vector2(radius2, 0)..rotate(tau / n * (i + sharpness));\n      final p2 = Vector2(radius2, 0)..rotate(tau / n * (i + 1 - sharpness));\n      final p3 = Vector2(radius1, 0)..rotate(tau / n * (i + 1));\n      _path.cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);\n    }\n    _path.close();\n  }\n\n  final Color color;\n  final Paint _paint = Paint();\n  final Paint _borderPaint = Paint()\n    ..color = const Color(0xFFffffff)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 3;\n  final _shadowPaint = Paint()\n    ..color = const Color(0xFF000000)\n    ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0);\n  late final Path _path;\n\n  @override\n  bool containsLocalPoint(Vector2 point) {\n    return _path.contains(point.toOffset());\n  }\n\n  @override\n  void render(Canvas canvas) {\n    if (isDragged) {\n      _paint.color = color.withValues(alpha: 0.5);\n      canvas.drawPath(_path, _paint);\n      canvas.drawPath(_path, _borderPaint);\n    } else {\n      _paint.color = color.withValues(alpha: 1);\n      canvas.drawPath(_path, _shadowPaint);\n      canvas.drawPath(_path, _paint);\n    }\n  }\n\n  @override\n  void onDragStart(DragStartEvent event) {\n    super.onDragStart(event);\n    priority = 10;\n  }\n\n  @override\n  void onDragEnd(DragEndEvent event) {\n    super.onDragEnd(event);\n    priority = 0;\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    position += event.localDelta;\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/ember.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/flame.dart';\n\nclass EmberPlayer extends SpriteAnimationComponent with TapCallbacks {\n  EmberPlayer({\n    required super.size,\n    super.position,\n    void Function(EmberPlayer player)? onTap,\n  }) : _onTap = onTap,\n       super();\n\n  Vector2 velocity = Vector2(0, 0);\n  final void Function(EmberPlayer player)? _onTap;\n\n  @override\n  Future<void> onLoad() async {\n    animation = SpriteAnimation.fromFrameData(\n      await Flame.images.load('ember.png'),\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        textureSize: Vector2.all(16),\n        stepTime: 0.12,\n      ),\n    );\n\n    add(CircleHitbox());\n  }\n\n  @override\n  void update(double dt) {\n    position += velocity * dt;\n    super.update(dt);\n  }\n\n  @override\n  void onTapUp([TapUpEvent? event]) => _onTap?.call(this);\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/flower.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/rendering.dart';\n\nenum FlowerPaint { paintId1, paintId2, paintId3, paintId4, paintId5 }\n\nclass Flower extends PositionComponent\n    with TapCallbacks, HasPaint<FlowerPaint> {\n  Flower({\n    required double size,\n    void Function(Flower)? onTap,\n    Decorator? decorator,\n    super.position,\n  }) : _onTap = onTap,\n       super(size: Vector2.all(size), anchor: Anchor.center) {\n    this.decorator.addLast(decorator);\n    final radius = size * 0.38;\n    _paths.add(_makePath(radius * 1.4, 6, -0.05, 0.8));\n    _paths.add(_makePath(radius, 6, 0.25, 1.5));\n    _paths.add(_makePath(radius * 0.8, 6, 0.3, 1.4));\n    _paths.add(_makePath(radius * 0.55, 6, 0.2, 1.5));\n    _paths.add(_makePath(radius * 0.1, 12, 0.1, 6));\n\n    setPaint(FlowerPaint.paintId1, Paint()..color = const Color(0xff255910));\n    setPaint(FlowerPaint.paintId2, Paint()..color = const Color(0xffee3f3f));\n    setPaint(FlowerPaint.paintId3, Paint()..color = const Color(0xffffbd66));\n    setPaint(FlowerPaint.paintId4, Paint()..color = const Color(0xfff6f370));\n    setPaint(FlowerPaint.paintId5, Paint()..color = const Color(0xfffffff0));\n  }\n\n  final List<Path> _paths = [];\n  final void Function(Flower flower)? _onTap;\n\n  Path _makePath(double radius, int n, double sharpness, double f) {\n    final radius2 = radius * f;\n    final p0 = Vector2(radius, 0)..rotate(0);\n    final path = Path()..moveTo(p0.x, p0.y);\n    for (var i = 0; i < n; i++) {\n      final p1 = Vector2(radius2, 0)..rotate(tau / n * (i + sharpness));\n      final p2 = Vector2(radius2, 0)..rotate(tau / n * (i + 1 - sharpness));\n      final p3 = Vector2(radius, 0)..rotate(tau / n * (i + 1));\n      path.cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);\n    }\n    path.close();\n    return path.shift(Offset(width / 2, height / 2));\n  }\n\n  @override\n  void render(Canvas canvas) {\n    for (var i = 0; i < _paths.length; i++) {\n      canvas.drawPath(\n        _paths[i],\n        getPaint(FlowerPaint.values.elementAt(i)),\n      );\n    }\n  }\n\n  @override\n  void onTapUp([TapUpEvent? event]) => _onTap?.call(this);\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/glow_effect.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\n\nimport 'package:flutter/material.dart';\n\nclass GlowEffectExample extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    final paint = Paint()..color = const Color(0xff39FF14);\n\n    add(\n      CircleComponent(\n        radius: size.y / 4,\n        position: size / 2,\n        anchor: Anchor.center,\n        paint: paint,\n      )..add(\n        GlowEffect(\n          10.0,\n          EffectController(\n            duration: 2,\n            infinite: true,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/hue_effect.dart",
    "content": "import 'dart:math';\n\nimport 'package:doc_flame_examples/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\n\nclass HueEffectExample extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    final ember = EmberPlayer(\n      position: size / 2,\n      size: size / 4,\n      onTap: (ember) {\n        ember.add(\n          HueEffect.by(\n            2 * pi,\n            EffectController(duration: 3),\n          ),\n        );\n      },\n    )..anchor = Anchor.center;\n\n    add(ember);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/main.dart",
    "content": "import 'package:doc_flame_examples/anchor.dart';\nimport 'package:doc_flame_examples/anchor_by_effect.dart';\nimport 'package:doc_flame_examples/anchor_to_effect.dart';\nimport 'package:doc_flame_examples/collision_detection.dart';\nimport 'package:doc_flame_examples/color_effect.dart';\nimport 'package:doc_flame_examples/decorator_blur.dart';\nimport 'package:doc_flame_examples/decorator_grayscale.dart';\nimport 'package:doc_flame_examples/decorator_hue.dart';\nimport 'package:doc_flame_examples/decorator_rotate3d.dart';\nimport 'package:doc_flame_examples/decorator_shadow3d.dart';\nimport 'package:doc_flame_examples/decorator_tint.dart';\nimport 'package:doc_flame_examples/drag_events.dart';\nimport 'package:doc_flame_examples/glow_effect.dart';\nimport 'package:doc_flame_examples/hue_effect.dart';\nimport 'package:doc_flame_examples/move_along_path_effect.dart';\nimport 'package:doc_flame_examples/move_by_effect.dart';\nimport 'package:doc_flame_examples/move_to_effect.dart';\nimport 'package:doc_flame_examples/opacity_by_effect.dart';\nimport 'package:doc_flame_examples/opacity_effect_with_target.dart';\nimport 'package:doc_flame_examples/opacity_to_effect.dart';\nimport 'package:doc_flame_examples/pointer_events.dart';\nimport 'package:doc_flame_examples/post_process.dart';\nimport 'package:doc_flame_examples/ray_cast.dart';\nimport 'package:doc_flame_examples/ray_trace.dart';\nimport 'package:doc_flame_examples/remove_effect.dart';\nimport 'package:doc_flame_examples/rive_example.dart';\nimport 'package:doc_flame_examples/rotate_around_effect.dart';\nimport 'package:doc_flame_examples/rotate_by_effect.dart';\nimport 'package:doc_flame_examples/rotate_to_effect.dart';\nimport 'package:doc_flame_examples/router.dart';\nimport 'package:doc_flame_examples/scale_by_effect.dart';\nimport 'package:doc_flame_examples/scale_to_effect.dart';\nimport 'package:doc_flame_examples/sequence_effect.dart';\nimport 'package:doc_flame_examples/size_by_effect.dart';\nimport 'package:doc_flame_examples/size_to_effect.dart';\nimport 'package:doc_flame_examples/tap_events.dart';\nimport 'package:doc_flame_examples/time_scale.dart';\nimport 'package:doc_flame_examples/value_route.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:web/web.dart' as web;\n\nfinal routes = <String, Game Function()>{\n  'anchor': AnchorGame.new,\n  'anchor_by_effect': AnchorByEffectGame.new,\n  'anchor_to_effect': AnchorToEffectGame.new,\n  'collision_detection': CollisionDetectionGame.new,\n  'color_effect': ColorEffectExample.new,\n  'decorator_blur': DecoratorBlurGame.new,\n  'decorator_grayscale': DecoratorGrayscaleGame.new,\n  'decorator_hue': DecoratorHueGame.new,\n  'decorator_rotate3d': DecoratorRotate3DGame.new,\n  'decorator_shadow3d': DecoratorShadowGame.new,\n  'decorator_tint': DecoratorTintGame.new,\n  'drag_events': DragEventsGame.new,\n  'glow_effect': GlowEffectExample.new,\n  'hue_effect': HueEffectExample.new,\n  'move_along_path_effect': MoveAlongPathEffectGame.new,\n  'move_by_effect': MoveByEffectGame.new,\n  'move_to_effect': MoveToEffectGame.new,\n  'opacity_by_effect': OpacityByEffectGame.new,\n  'opacity_effect_with_target': OpacityEffectWithTargetGame.new,\n  'opacity_to_effect': OpacityToEffectGame.new,\n  'pointer_events': PointerEventsGame.new,\n  'post_process': PostProcessGame.new,\n  'ray_cast': RayCastExample.new,\n  'ray_trace': RayTraceExample.new,\n  'remove_effect': RemoveEffectGame.new,\n  'rive_example': RiveExampleGame.new,\n  'rotate_around_effect': RotateAroundEffectGame.new,\n  'rotate_by_effect': RotateByEffectGame.new,\n  'rotate_to_effect': RotateToEffectGame.new,\n  'router': RouterGame.new,\n  'scale_by_effect': ScaleByEffectGame.new,\n  'scale_to_effect': ScaleToEffectGame.new,\n  'sequence_effect': SequenceEffectGame.new,\n  'size_by_effect': SizeByEffectGame.new,\n  'size_to_effect': SizeToEffectGame.new,\n  'tap_events': TapEventsGame.new,\n  'time_scale': TimeScaleGame.new,\n  'value_route': ValueRouteExample.new,\n};\n\nvoid main() {\n  var page = web.window.location.search;\n  if (page.startsWith('?')) {\n    page = page.substring(1);\n  }\n  final game = routes[page]?.call();\n  if (game != null) {\n    runApp(GameWidget(game: game));\n  } else {\n    runApp(_IndexRoute(page: page));\n  }\n}\n\nclass _IndexRoute extends StatelessWidget {\n  final String page;\n\n  const _IndexRoute({\n    required this.page,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Directionality(\n      textDirection: TextDirection.ltr,\n      child: Column(\n        children: [\n          Text('Error: unknown page name \"$page\"'),\n          const Text('Select an option below:'),\n          ...routes.keys.map((route) {\n            return GestureDetector(\n              onTap: () => web.window.location.replace('/?$route'),\n              child: Text(route),\n            );\n          }),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/move_along_path_effect.dart",
    "content": "import 'dart:ui';\n\nimport 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\n\nclass MoveAlongPathEffectGame extends FlameGame {\n  bool reset = false;\n  @override\n  Future<void> onLoad() async {\n    final flower = Flower(\n      size: 60,\n      position: canvasSize / 2,\n      onTap: (flower) {\n        flower.add(\n          MoveAlongPathEffect(\n            reset\n                ? (Path()..quadraticBezierTo(-100, 0, -50, 50))\n                : (Path()..quadraticBezierTo(100, 0, 50, -50)),\n            EffectController(duration: 1.5),\n          ),\n        );\n        reset = !reset;\n      },\n    );\n    add(flower);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/move_by_effect.dart",
    "content": "import 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\n\nclass MoveByEffectGame extends FlameGame {\n  bool reset = false;\n  @override\n  Future<void> onLoad() async {\n    final flower = Flower(\n      size: 60,\n      position: canvasSize / 2,\n      onTap: (flower) {\n        flower.add(\n          MoveEffect.by(\n            reset ? Vector2(-30, -30) : Vector2(30, 30),\n            EffectController(duration: 1.0),\n          ),\n        );\n        reset = !reset;\n      },\n    );\n    add(flower);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/move_to_effect.dart",
    "content": "import 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\n\nclass MoveToEffectGame extends FlameGame {\n  bool reset = false;\n\n  @override\n  Future<void> onLoad() async {\n    final flower = Flower(\n      size: 60,\n      position: canvasSize / 2,\n      onTap: (flower) {\n        flower.add(\n          MoveEffect.to(\n            reset ? size / 2 : Vector2(30, 30),\n            EffectController(duration: 1.0),\n          ),\n        );\n        reset = !reset;\n      },\n    );\n    add(flower);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/opacity_by_effect.dart",
    "content": "import 'package:doc_flame_examples/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\n\nclass OpacityByEffectGame extends FlameGame {\n  bool reset = false;\n\n  @override\n  Future<void> onLoad() async {\n    final ember = EmberPlayer(\n      position: size / 2,\n      size: size / 4,\n      onTap: (ember) {\n        ember.add(\n          OpacityEffect.by(\n            reset ? 0.9 : -0.9,\n            EffectController(duration: 0.75),\n          ),\n        );\n        reset = !reset;\n      },\n    )..anchor = Anchor.center;\n\n    add(ember);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/opacity_effect_with_target.dart",
    "content": "import 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\n\nclass OpacityEffectWithTargetGame extends FlameGame {\n  bool reset = false;\n\n  // This reference needs to be stored because every new instance of\n  // OpacityProvider caches the opacity ratios at the time on creation.\n  late OpacityProvider _borderOpacityProvider;\n\n  @override\n  Future<void> onLoad() async {\n    final flower = Flower(\n      position: size / 2,\n      size: 60,\n      onTap: _onTap,\n    )..anchor = Anchor.center;\n\n    _borderOpacityProvider = flower.opacityProviderOfList(\n      paintIds: const [FlowerPaint.paintId1, FlowerPaint.paintId2],\n    );\n\n    add(flower);\n  }\n\n  void _onTap(Flower flower) {\n    if (reset = !reset) {\n      flower.add(\n        OpacityEffect.fadeIn(\n          EffectController(duration: 0.75),\n          target: _borderOpacityProvider,\n        ),\n      );\n    } else {\n      flower.add(\n        OpacityEffect.to(\n          0.2,\n          EffectController(duration: 0.75),\n          target: _borderOpacityProvider,\n        ),\n      );\n    }\n    reset = !reset;\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/opacity_to_effect.dart",
    "content": "import 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\n\nclass OpacityToEffectGame extends FlameGame {\n  bool reset = false;\n\n  @override\n  Future<void> onLoad() async {\n    final flower = Flower(\n      position: size / 2,\n      size: 60,\n      onTap: _onTap,\n    )..anchor = Anchor.center;\n\n    add(flower);\n  }\n\n  void _onTap(Flower flower) {\n    if (reset) {\n      flower.add(\n        OpacityEffect.fadeIn(\n          EffectController(duration: 0.75),\n        ),\n      );\n    } else {\n      flower.add(\n        OpacityEffect.to(\n          0.2,\n          EffectController(duration: 0.75),\n        ),\n      );\n    }\n    reset = !reset;\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/pointer_events.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/rendering.dart';\n\nclass PointerEventsGame extends FlameGame with TapCallbacks {\n  @override\n  Future<void> onLoad() async {\n    add(HoverTarget(Vector2(100, 200)));\n    add(HoverTarget(Vector2(300, 300)));\n    add(HoverTarget(Vector2(400, 50)));\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    add(HoverTarget(event.localPosition));\n  }\n}\n\nclass HoverTarget extends PositionComponent with HoverCallbacks {\n  static final Random _random = Random();\n\n  HoverTarget(Vector2 position)\n    : super(\n        position: position,\n        size: Vector2.all(50),\n        anchor: Anchor.center,\n      );\n\n  final _paint = Paint()\n    ..color = HSLColor.fromAHSL(\n      1,\n      _random.nextDouble() * 360,\n      1,\n      0.8,\n    ).toColor().withValues(alpha: 0.5);\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(size.toRect(), _paint);\n  }\n\n  @override\n  void onHoverEnter() {\n    _paint.color = _paint.color.withValues(alpha: 1);\n  }\n\n  @override\n  void onHoverExit() {\n    _paint.color = _paint.color.withValues(alpha: 0.5);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/post_process.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:doc_flame_examples/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/post_process.dart';\n\nclass PostProcessGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    world.add(\n      PostProcessComponent(\n        postProcess: PixelationPostProcess(),\n        position: Vector2(0, 0),\n        anchor: Anchor.center,\n        children: [\n          EmberPlayer(size: Vector2(100, 100)),\n        ],\n      ),\n    );\n  }\n}\n\nclass PixelationPostProcess extends PostProcess {\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    _fragmentProgram = await FragmentProgram.fromAsset(\n      'packages/flutter_shaders/shaders/pixelation.frag',\n    );\n  }\n\n  late final FragmentProgram _fragmentProgram;\n  late final FragmentShader _fragmentShader = _fragmentProgram.fragmentShader();\n\n  double _time = 0;\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    _time += dt;\n  }\n\n  late final myPaint = Paint()..shader = _fragmentShader;\n\n  @override\n  void postProcess(Vector2 size, Canvas canvas) {\n    final preRenderedSubtree = rasterizeSubtree();\n\n    _fragmentShader.setFloatUniforms((value) {\n      value\n        ..setVector(size / (20 * sin(_time)))\n        ..setVector(size);\n    });\n\n    _fragmentShader.setImageSampler(0, preRenderedSubtree);\n\n    canvas\n      ..save()\n      ..drawRect(Offset.zero & size.toSize(), myPaint)\n      ..restore();\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/ray_cast.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/material.dart';\n\nclass RayCastExample extends FlameGame with HasCollisionDetection {\n  final origin = Vector2(20, 20);\n\n  final direction = Vector2(1, 0);\n\n  final velocity = 60;\n  double get resetPosition => -canvasSize.y;\n\n  Paint paint = Paint()..color = Colors.red.withValues(alpha: 0.6);\n\n  RaycastResult<ShapeHitbox>? result;\n\n  @override\n  Future<void> onLoad() async {\n    final paint = BasicPalette.gray.paint()\n      ..style = PaintingStyle.stroke\n      ..strokeWidth = 2.0;\n\n    add(ScreenHitbox());\n\n    add(\n      CircleComponent(\n        position: canvasSize / 2,\n        radius: 30,\n        paint: paint,\n        children: [CircleHitbox()],\n      ),\n    );\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    final ray = Ray2(\n      origin: origin,\n      direction: direction,\n    );\n    result = collisionDetection.raycast(ray);\n\n    origin.y += velocity * dt;\n\n    if (origin.y > canvasSize.y) {\n      origin.y += resetPosition;\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n\n    if (result != null && result!.isActive) {\n      final originOffset = origin.toOffset();\n      final intersectionPoint = result!.intersectionPoint!.toOffset();\n      canvas.drawLine(\n        originOffset,\n        intersectionPoint,\n        paint,\n      );\n\n      canvas.drawCircle(originOffset, 10, paint);\n    }\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/ray_trace.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/material.dart';\n\nclass RayTraceExample extends FlameGame\n    with HasCollisionDetection, TapCallbacks {\n  Paint paint = Paint()..color = Colors.red.withValues(alpha: 0.6);\n  bool isClicked = false;\n\n  Vector2 get origin => canvasSize / 2;\n\n  RaycastResult<ShapeHitbox>? result;\n\n  final Ray2 _ray = Ray2.zero();\n\n  final boxPaint = BasicPalette.gray.paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 2.0;\n\n  final List<RaycastResult<ShapeHitbox>> results = [];\n\n  @override\n  Future<void> onLoad() async {\n    add(\n      CircleComponent(\n        radius: min(size.x, size.y) / 2,\n        paint: boxPaint,\n        children: [CircleHitbox()],\n      ),\n    );\n  }\n\n  var _timePassed = 0.0;\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    if (isClicked) {\n      _timePassed += dt;\n    }\n\n    result = collisionDetection.raycast(_ray);\n\n    _ray.origin.setFrom(origin);\n    _ray.direction\n      ..setValues(1, 1)\n      ..normalize();\n    collisionDetection\n        .raytrace(\n          _ray,\n          maxDepth: min((_timePassed * 8).ceil(), 1000),\n          out: results,\n        )\n        .toList();\n  }\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    var originOffset = origin.toOffset();\n    for (final result in results) {\n      if (!result.isActive) {\n        continue;\n      }\n      final intersectionPoint = result.intersectionPoint!.toOffset();\n      canvas.drawLine(\n        originOffset,\n        intersectionPoint,\n        paint,\n      );\n      originOffset = intersectionPoint;\n    }\n  }\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    if (!isClicked) {\n      isClicked = true;\n      return;\n    }\n    _timePassed = 0;\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/remove_effect.dart",
    "content": "import 'package:doc_flame_examples/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\n\nclass RemoveEffectGame extends FlameGame with TapCallbacks {\n  static const double delayTime = 3;\n  late EmberPlayer ember;\n  late TextComponent textComponent;\n  final RemoveEffect effect = RemoveEffect(delay: delayTime);\n\n  @override\n  Future<void> onLoad() async {\n    add(\n      ember = EmberPlayer(\n        position: size / 2,\n        size: Vector2(45, 40),\n      )..anchor = Anchor.center,\n    );\n    add(textComponent = TextComponent()..position = Vector2.all(5));\n  }\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    if (children.contains(ember)) {\n      ember.add(effect);\n    } else {\n      effect.reset();\n      add(ember);\n    }\n  }\n\n  @override\n  void update(double dt) {\n    textComponent.text = (effect.controller.progress * delayTime)\n        .toStringAsFixed(2);\n    super.update(dt);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/rive_example.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_rive/flame_rive.dart';\n\nclass RiveExampleGame extends FlameGame with TapCallbacks {\n  ViewModelInstanceNumber? coinInput;\n  ViewModelInstanceNumber? gemInput;\n\n  @override\n  Future<void> onLoad() async {\n    final file = await File.asset(\n      'assets/rewards.riv',\n      riveFactory: Factory.flutter,\n    ).then((file) => file!);\n\n    final artboard = await loadArtboard(file);\n    final stateMachine = artboard.defaultStateMachine();\n\n    if (stateMachine != null) {\n      final viewModel = file.defaultArtboardViewModel(artboard);\n      if (viewModel != null) {\n        final viewModelInstance = viewModel.createDefaultInstance();\n        if (viewModelInstance != null) {\n          stateMachine.bindViewModelInstance(viewModelInstance);\n          coinInput = viewModelInstance.viewModel('Coin')?.number('Item_Value');\n          gemInput = viewModelInstance.viewModel('Gem')?.number('Item_Value');\n        }\n      }\n    }\n\n    add(\n      RiveComponent(\n        artboard: artboard,\n        stateMachine: stateMachine,\n        size: canvasSize,\n      ),\n    );\n  }\n\n  @override\n  void onTapDown(_) {\n    if (coinInput != null) {\n      coinInput!.value = (coinInput!.value + 10) % 1001;\n    }\n    if (gemInput != null) {\n      gemInput!.value = (gemInput!.value + 1) % 1001;\n    }\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/rotate_around_effect.dart",
    "content": "import 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\n\nclass RotateAroundEffectGame extends FlameGame {\n  bool reset = false;\n\n  @override\n  Future<void> onLoad() async {\n    final flower = Flower(\n      size: 60,\n      position: canvasSize / 4,\n      onTap: (flower) {\n        flower.add(\n          RotateAroundEffect(\n            reset != reset ? tau : -tau,\n            center: canvasSize / 2,\n            EffectController(duration: 1),\n          ),\n        );\n        reset = !reset;\n      },\n    );\n    add(flower);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/rotate_by_effect.dart",
    "content": "import 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\n\nclass RotateByEffectGame extends FlameGame {\n  bool reset = false;\n\n  @override\n  Future<void> onLoad() async {\n    final flower = Flower(\n      size: 60,\n      position: canvasSize / 2,\n      onTap: (flower) {\n        flower.add(\n          RotateEffect.by(\n            reset ? tau / 4 : -tau / 4,\n            EffectController(duration: 2),\n          ),\n        );\n        reset = !reset;\n      },\n    );\n    add(flower);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/rotate_to_effect.dart",
    "content": "import 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\n\nclass RotateToEffectGame extends FlameGame {\n  bool reset = false;\n\n  @override\n  Future<void> onLoad() async {\n    final flower = Flower(\n      size: 60,\n      position: canvasSize / 2,\n      onTap: (flower) {\n        flower.add(\n          RotateEffect.to(\n            reset ? tau / 4 : -tau / 4,\n            EffectController(duration: 2),\n          ),\n        );\n        reset = !reset;\n      },\n    );\n    add(flower);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/router.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/rendering.dart';\nimport 'package:flutter/rendering.dart';\n\nclass RouterGame extends FlameGame {\n  late final RouterComponent router;\n\n  @override\n  Future<void> onLoad() async {\n    add(\n      router = RouterComponent(\n        routes: {\n          'home': Route(StartPage.new),\n          'level1': WorldRoute(Level1Page.new),\n          'level2': WorldRoute(Level2Page.new, maintainState: false),\n          'pause': PauseRoute(),\n        },\n        initialRoute: 'home',\n      ),\n    );\n  }\n}\n\nclass StartPage extends Component with HasGameReference<RouterGame> {\n  StartPage() {\n    addAll([\n      _logo = TextComponent(\n        text: 'Your Game',\n        textRenderer: TextPaint(\n          style: const TextStyle(\n            fontSize: 64,\n            color: Color(0xFFC8FFF5),\n            fontWeight: FontWeight.w800,\n          ),\n        ),\n        anchor: Anchor.center,\n      ),\n      _button1 = RoundedButton(\n        text: 'Level 1',\n        action: () => game.router.pushNamed('level1'),\n        color: const Color(0xffadde6c),\n        borderColor: const Color(0xffedffab),\n      ),\n      _button2 = RoundedButton(\n        text: 'Level 2',\n        action: () => game.router.pushNamed('level2'),\n        color: const Color(0xffdebe6c),\n        borderColor: const Color(0xfffff4c7),\n      ),\n    ]);\n  }\n\n  late final TextComponent _logo;\n  late final RoundedButton _button1;\n  late final RoundedButton _button2;\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    _logo.position = Vector2(size.x / 2, size.y / 3);\n    _button1.position = Vector2(size.x / 2, _logo.y + 80);\n    _button2.position = Vector2(size.x / 2, _logo.y + 140);\n  }\n}\n\nclass Background extends Component {\n  Background(this.color);\n  final Color color;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawColor(color, BlendMode.srcATop);\n  }\n}\n\nclass RoundedButton extends PositionComponent with TapCallbacks {\n  RoundedButton({\n    required this.text,\n    required this.action,\n    required Color color,\n    required Color borderColor,\n    super.position,\n    super.anchor = Anchor.center,\n  }) : _textDrawable = TextPaint(\n         style: const TextStyle(\n           fontSize: 20,\n           color: Color(0xFF000000),\n           fontWeight: FontWeight.w800,\n         ),\n       ).toTextPainter(text) {\n    size = Vector2(150, 40);\n    _textOffset = Offset(\n      (size.x - _textDrawable.width) / 2,\n      (size.y - _textDrawable.height) / 2,\n    );\n    _rrect = RRect.fromLTRBR(0, 0, size.x, size.y, Radius.circular(size.y / 2));\n    _bgPaint = Paint()..color = color;\n    _borderPaint = Paint()\n      ..style = PaintingStyle.stroke\n      ..strokeWidth = 2\n      ..color = borderColor;\n  }\n\n  final String text;\n  final void Function() action;\n  final TextPainter _textDrawable;\n  late final Offset _textOffset;\n  late final RRect _rrect;\n  late final Paint _borderPaint;\n  late final Paint _bgPaint;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(_rrect, _bgPaint);\n    canvas.drawRRect(_rrect, _borderPaint);\n    _textDrawable.paint(canvas, _textOffset);\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    scale = Vector2.all(1.05);\n  }\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    scale = Vector2.all(1.0);\n    action();\n  }\n\n  @override\n  void onTapCancel(TapCancelEvent event) {\n    scale = Vector2.all(1.0);\n  }\n}\n\nabstract class SimpleButton extends PositionComponent with TapCallbacks {\n  SimpleButton(this._iconPath, {super.position}) : super(size: Vector2.all(40));\n\n  final Paint _borderPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..color = const Color(0x66ffffff);\n  final Paint _iconPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..color = const Color(0xffaaaaaa)\n    ..strokeWidth = 7;\n  final Path _iconPath;\n\n  void action();\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(\n      RRect.fromRectAndRadius(size.toRect(), const Radius.circular(8)),\n      _borderPaint,\n    );\n    canvas.drawPath(_iconPath, _iconPaint);\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    _iconPaint.color = const Color(0xffffffff);\n  }\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    _iconPaint.color = const Color(0xffaaaaaa);\n    action();\n  }\n\n  @override\n  void onTapCancel(TapCancelEvent event) {\n    _iconPaint.color = const Color(0xffaaaaaa);\n  }\n}\n\nclass BackButton extends SimpleButton with HasGameReference<RouterGame> {\n  BackButton()\n    : super(\n        Path()\n          ..moveTo(22, 8)\n          ..lineTo(10, 20)\n          ..lineTo(22, 32)\n          ..moveTo(12, 20)\n          ..lineTo(34, 20),\n        position: Vector2.all(10),\n      );\n\n  @override\n  void action() => game.router.pop();\n}\n\nclass PauseButton extends SimpleButton with HasGameReference<RouterGame> {\n  PauseButton()\n    : super(\n        Path()\n          ..moveTo(14, 10)\n          ..lineTo(14, 30)\n          ..moveTo(26, 10)\n          ..lineTo(26, 30),\n        position: Vector2(60, 10),\n      );\n\n  bool isPaused = false;\n\n  @override\n  void action() {\n    if (isPaused) {\n      game.router.pop();\n    } else {\n      game.router.pushNamed('pause');\n    }\n    isPaused = !isPaused;\n  }\n}\n\nclass Level1Page extends DecoratedWorld with HasGameReference {\n  @override\n  Future<void> onLoad() async {\n    addAll([\n      Background(const Color(0xbb2a074f)),\n      Planet(\n        radius: 25,\n        color: const Color(0xfffff188),\n        children: [\n          Orbit(\n            radius: 110,\n            revolutionPeriod: 6,\n            planet: Planet(\n              radius: 10,\n              color: const Color(0xff54d7b1),\n              children: [\n                Orbit(\n                  radius: 25,\n                  revolutionPeriod: 5,\n                  planet: Planet(radius: 3, color: const Color(0xFFcccccc)),\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    ]);\n  }\n\n  final hudComponents = <Component>[];\n\n  @override\n  void onMount() {\n    hudComponents.addAll([\n      BackButton(),\n      PauseButton(),\n    ]);\n    game.camera.viewport.addAll(hudComponents);\n  }\n\n  @override\n  void onRemove() {\n    game.camera.viewport.removeAll(hudComponents);\n    super.onRemove();\n  }\n}\n\nclass Level2Page extends DecoratedWorld with HasGameReference {\n  @override\n  Future<void> onLoad() async {\n    addAll([\n      Background(const Color(0xff052b44)),\n      Planet(\n        radius: 30,\n        color: const Color(0xFFFFFFff),\n        children: [\n          Orbit(\n            radius: 60,\n            revolutionPeriod: 5,\n            planet: Planet(radius: 10, color: const Color(0xffc9ce0d)),\n          ),\n          Orbit(\n            radius: 110,\n            revolutionPeriod: 10,\n            planet: Planet(\n              radius: 14,\n              color: const Color(0xfff32727),\n              children: [\n                Orbit(\n                  radius: 26,\n                  revolutionPeriod: 3,\n                  planet: Planet(radius: 5, color: const Color(0xffffdb00)),\n                ),\n                Orbit(\n                  radius: 35,\n                  revolutionPeriod: 4,\n                  planet: Planet(radius: 3, color: const Color(0xffdc00ff)),\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    ]);\n  }\n\n  final hudComponents = <Component>[];\n\n  @override\n  void onMount() {\n    hudComponents.addAll([\n      BackButton(),\n      PauseButton(),\n    ]);\n    game.camera.viewport.addAll(hudComponents);\n  }\n\n  @override\n  void onRemove() {\n    game.camera.viewport.removeAll(hudComponents);\n    super.onRemove();\n  }\n}\n\nclass Planet extends PositionComponent {\n  Planet({\n    required this.radius,\n    required this.color,\n    super.position,\n    super.children,\n  }) : _paint = Paint()..color = color;\n\n  final double radius;\n  final Color color;\n  final Paint _paint;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawCircle(Offset.zero, radius, _paint);\n  }\n}\n\nclass Orbit extends PositionComponent {\n  Orbit({\n    required this.radius,\n    required this.planet,\n    required this.revolutionPeriod,\n    double initialAngle = 0,\n  }) : _paint = Paint()\n         ..style = PaintingStyle.stroke\n         ..color = const Color(0x888888aa),\n       _angle = initialAngle {\n    add(planet);\n  }\n\n  final double radius;\n  final double revolutionPeriod;\n  final Planet planet;\n  final Paint _paint;\n  double _angle;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawCircle(Offset.zero, radius, _paint);\n  }\n\n  @override\n  void update(double dt) {\n    _angle += dt / revolutionPeriod * tau;\n    planet.position = Vector2(radius, 0)..rotate(_angle);\n  }\n}\n\nclass PauseRoute extends Route {\n  PauseRoute() : super(PausePage.new, transparent: true);\n\n  @override\n  void onPush(Route? previousRoute) {\n    if (previousRoute is WorldRoute && previousRoute.world is DecoratedWorld) {\n      (previousRoute.world! as DecoratedWorld).timeScale = 0;\n      (previousRoute.world! as DecoratedWorld).decorator =\n          PaintDecorator.grayscale(opacity: 0.5)..addBlur(3.0);\n    }\n  }\n\n  @override\n  void onPop(Route nextRoute) {\n    if (nextRoute is WorldRoute && nextRoute.world is DecoratedWorld) {\n      (nextRoute.world! as DecoratedWorld).timeScale = 1;\n      (nextRoute.world! as DecoratedWorld).decorator = null;\n    }\n  }\n}\n\nclass PausePage extends Component\n    with TapCallbacks, HasGameReference<RouterGame> {\n  @override\n  Future<void> onLoad() async {\n    final game = findGame()!;\n    addAll([\n      TextComponent(\n        text: 'PAUSED',\n        position: game.canvasSize / 2,\n        anchor: Anchor.center,\n        children: [\n          ScaleEffect.to(\n            Vector2.all(1.1),\n            EffectController(\n              duration: 0.3,\n              alternate: true,\n              infinite: true,\n            ),\n          ),\n        ],\n      ),\n    ]);\n  }\n\n  @override\n  bool containsLocalPoint(Vector2 point) => true;\n\n  @override\n  void onTapUp(TapUpEvent event) => game.router.pop();\n}\n\nclass DecoratedWorld extends World with HasTimeScale {\n  PaintDecorator? decorator;\n\n  @override\n  void renderFromCamera(Canvas canvas) {\n    if (decorator == null) {\n      super.renderFromCamera(canvas);\n    } else {\n      decorator!.applyChain(super.renderFromCamera, canvas);\n    }\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/scale_by_effect.dart",
    "content": "import 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\n\nclass ScaleByEffectGame extends FlameGame {\n  bool reverse = false;\n  bool hold = false;\n  @override\n  Future<void> onLoad() async {\n    final flower = Flower(\n      size: 60,\n      position: canvasSize / 2,\n      onTap: (flower) {\n        if (hold) {\n          return;\n        }\n        hold = true;\n        flower.add(\n          ScaleEffect.by(\n            reverse ? Vector2.all(1 / 1.5) : Vector2.all(1.5),\n            EffectController(duration: 0.3),\n            onComplete: () => hold = false,\n          ),\n        );\n        reverse = !reverse;\n      },\n    );\n    add(flower);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/scale_to_effect.dart",
    "content": "import 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\n\nclass ScaleToEffectGame extends FlameGame {\n  bool reverse = false;\n  bool hold = false;\n  @override\n  Future<void> onLoad() async {\n    final flower = Flower(\n      size: 60,\n      position: canvasSize / 2,\n      onTap: (flower) {\n        if (hold) {\n          return;\n        }\n        hold = true;\n        flower.add(\n          ScaleEffect.to(\n            reverse ? Vector2.all(1) : Vector2.all(0.5),\n            EffectController(duration: 0.5),\n            onComplete: () => hold = false,\n          ),\n        );\n        reverse = !reverse;\n      },\n    );\n    add(flower);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/sequence_effect.dart",
    "content": "import 'package:doc_flame_examples/flower.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\n\nclass SequenceEffectGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    final flower = Flower(\n      size: 40,\n      position: canvasSize / 2,\n      onTap: (flower) {\n        flower.add(\n          SequenceEffect([\n            ScaleEffect.by(\n              Vector2.all(1.5),\n              EffectController(duration: 0.2, alternate: true),\n            ),\n            MoveEffect.by(\n              Vector2(30, -50),\n              EffectController(duration: 0.5),\n            ),\n            MoveEffect.by(\n              Vector2(-30, 50),\n              EffectController(duration: 0.5),\n            ),\n            ScaleEffect.by(\n              Vector2.all(0.75),\n              EffectController(duration: 0.2, alternate: true),\n            ),\n          ]),\n        );\n      },\n    );\n    add(flower);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/size_by_effect.dart",
    "content": "import 'package:doc_flame_examples/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\n\nclass SizeByEffectGame extends FlameGame {\n  bool reset = false;\n  @override\n  Future<void> onLoad() async {\n    final ember = EmberPlayer(\n      position: size / 2,\n      size: Vector2(45, 40),\n      onTap: (ember) {\n        ember.add(\n          SizeEffect.by(\n            reset ? Vector2(-15, 30) : Vector2(15, -30),\n            EffectController(duration: 0.75),\n          ),\n        );\n        reset = !reset;\n      },\n    )..anchor = Anchor.center;\n\n    add(ember);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/size_to_effect.dart",
    "content": "import 'package:doc_flame_examples/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\n\nclass SizeToEffectGame extends FlameGame {\n  bool reset = false;\n  @override\n  Future<void> onLoad() async {\n    final ember = EmberPlayer(\n      position: size / 2,\n      size: Vector2(45, 40),\n      onTap: (ember) {\n        ember.add(\n          SizeEffect.to(\n            reset ? Vector2(45, 40) : Vector2(90, 80),\n            EffectController(duration: 0.75),\n          ),\n        );\n        reset = !reset;\n      },\n    )..anchor = Anchor.center;\n\n    add(ember);\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/tap_events.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/rendering.dart';\n\nclass TapEventsGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    add(TapTarget());\n  }\n}\n\n/// This component is the tappable blue-ish rectangle in the center of the game.\n/// It uses the [TapCallbacks] mixin to receive tap events.\nclass TapTarget extends PositionComponent with TapCallbacks {\n  TapTarget() : super(anchor: Anchor.center);\n\n  final _paint = Paint()..color = const Color(0x448BA8FF);\n\n  /// We will store all current circles into this map, keyed by the `pointerId`\n  /// of the event that created the circle.\n  final Map<int, ExpandingCircle> _circles = {};\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    this.size = size - Vector2(100, 75);\n    if (this.size.x < 100 || this.size.y < 100) {\n      this.size = size * 0.9;\n    }\n    position = size / 2;\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(size.toRect(), _paint);\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    final circle = ExpandingCircle(event.localPosition);\n    _circles[event.pointerId] = circle;\n    add(circle);\n  }\n\n  @override\n  void onLongTapDown(TapDownEvent event) {\n    _circles[event.pointerId]!.accent();\n  }\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    _circles.remove(event.pointerId)!.release();\n  }\n\n  @override\n  void onTapCancel(TapCancelEvent event) {\n    _circles.remove(event.pointerId)!.cancel();\n  }\n}\n\nclass ExpandingCircle extends Component {\n  ExpandingCircle(this._center)\n    : _baseColor = HSLColor.fromAHSL(\n        1,\n        random.nextDouble() * 360,\n        1,\n        0.8,\n      ).toColor();\n\n  final Color _baseColor;\n  final Vector2 _center;\n  double _outerRadius = 0;\n  double _innerRadius = 0;\n  bool _released = false;\n  bool _cancelled = false;\n  late final _paint = Paint()\n    ..style = PaintingStyle.stroke\n    ..color = _baseColor;\n\n  /// \"Accent\" is thin white circle generated by `onLongTapDown`. We use\n  /// negative radius to indicate that the circle should not be drawn yet.\n  double _accentRadius = -1e10;\n  late final _accentPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 0\n    ..color = const Color(0xFFFFFFFF);\n\n  /// At this radius the circle will disappear.\n  static const maxRadius = 175;\n  static final random = Random();\n\n  double get radius => (_innerRadius + _outerRadius) / 2;\n\n  void release() => _released = true;\n  void cancel() => _cancelled = true;\n  void accent() => _accentRadius = 0;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawCircle(_center.toOffset(), radius, _paint);\n    if (_accentRadius >= 0) {\n      canvas.drawCircle(_center.toOffset(), _accentRadius, _accentPaint);\n    }\n  }\n\n  @override\n  void update(double dt) {\n    if (_cancelled) {\n      _innerRadius += dt * 100; // implosion\n    } else {\n      _outerRadius += dt * 20;\n      _innerRadius += dt * (_released ? 20 : 6);\n      _accentRadius += dt * 20;\n    }\n    if (radius >= maxRadius || _innerRadius > _outerRadius) {\n      removeFromParent();\n    } else {\n      final opacity = 1 - radius / maxRadius;\n      _paint.color = _baseColor.withValues(alpha: opacity);\n      _paint.strokeWidth = _outerRadius - _innerRadius;\n    }\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/time_scale.dart",
    "content": "import 'dart:async';\n\nimport 'package:doc_flame_examples/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\n\nclass TimeScaleGame extends FlameGame with HasTimeScale {\n  final _timeScales = [0.5, 1.0, 2.0];\n  var _index = 1;\n\n  @override\n  Future<void> onLoad() async {\n    await add(\n      EmberPlayer(\n        position: size / 2,\n        size: size / 4,\n        onTap: (p0) => timeScale = getNextTimeScale(),\n      ),\n    );\n    return super.onLoad();\n  }\n\n  double getNextTimeScale() {\n    ++_index;\n    if (_index >= _timeScales.length) {\n      _index = 0;\n    }\n    return _timeScales[_index];\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/lib/value_route.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:doc_flame_examples/router.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\n\nclass ValueRouteExample extends FlameGame {\n  late final RouterComponent router;\n\n  @override\n  Future<void> onLoad() async {\n    router = RouterComponent(\n      routes: {'home': Route(HomePage.new)},\n      initialRoute: 'home',\n    );\n    add(router);\n  }\n}\n\nclass HomePage extends Component with HasGameReference<ValueRouteExample> {\n  @override\n  Future<void> onLoad() async {\n    add(\n      RoundedButton(\n        text: 'Rate me',\n        position: game.size / 2,\n        action: () async {\n          final score = await game.router.pushAndWait(RateRoute());\n          firstChild<TextComponent>()!.text = 'Score: $score';\n        },\n        color: const Color(0xff758f9a),\n        borderColor: const Color(0xff60d5ff),\n      ),\n    );\n    add(\n      TextComponent(\n        text: 'Score: –',\n        anchor: Anchor.topCenter,\n        position: game.size / 2 + Vector2(0, 30),\n        scale: Vector2.all(0.7),\n      ),\n    );\n  }\n}\n\nclass RateRoute extends ValueRoute<int>\n    with HasGameReference<ValueRouteExample> {\n  RateRoute() : super(value: -1, transparent: true);\n\n  @override\n  Component build() {\n    final size = Vector2(250, 130);\n    const radius = 18.0;\n    final starGap = (size.x - 5 * 2 * radius) / 6;\n    return DialogBackground(\n      position: game.size / 2,\n      size: size,\n      children: [\n        RoundedButton(\n          text: 'Ok',\n          position: Vector2(size.x / 2, 100),\n          action: () {\n            completeWith(\n              descendants().where((c) => c is Star && c.active).length,\n            );\n          },\n          color: const Color(0xFFFFFFFF),\n          borderColor: const Color(0xFF000000),\n        ),\n        for (var i = 0; i < 5; i++)\n          Star(\n            value: i + 1,\n            radius: radius,\n            position: Vector2(starGap * (i + 1) + radius * (2 * i + 1), 40),\n          ),\n      ],\n    );\n  }\n}\n\nclass DialogBackground extends RectangleComponent with TapCallbacks {\n  DialogBackground({super.position, super.size, super.children})\n    : super(\n        anchor: Anchor.center,\n        paint: Paint()..color = const Color(0xee858585),\n      );\n}\n\nclass Star extends PositionComponent with TapCallbacks {\n  Star({required this.value, required this.radius, super.position})\n    : super(size: Vector2.all(2 * radius), anchor: Anchor.center);\n\n  final int value;\n  final double radius;\n  final Path path = Path();\n  final Paint borderPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..color = const Color(0xffffe395)\n    ..strokeWidth = 2;\n  final Paint fillPaint = Paint()..color = const Color(0xffffe395);\n  bool active = false;\n\n  @override\n  Future<void> onLoad() async {\n    path.moveTo(radius, 0);\n    for (var i = 0; i < 5; i++) {\n      path.lineTo(\n        radius + 0.6 * radius * sin(tau / 5 * (i + 0.5)),\n        radius - 0.6 * radius * cos(tau / 5 * (i + 0.5)),\n      );\n      path.lineTo(\n        radius + radius * sin(tau / 5 * (i + 1)),\n        radius - radius * cos(tau / 5 * (i + 1)),\n      );\n    }\n    path.close();\n  }\n\n  @override\n  void render(Canvas canvas) {\n    if (active) {\n      canvas.drawPath(path, fillPaint);\n    }\n    canvas.drawPath(path, borderPaint);\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    var on = true;\n    for (final star in parent!.children.whereType<Star>()) {\n      star.active = on;\n      if (star == this) {\n        on = false;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "doc/flame/examples/pubspec.yaml",
    "content": "name: doc_flame_examples\nresolution: workspace\ndescription: Documentation examples\nversion: 1.0.0\npublish_to: none\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_rive: ^1.11.0\n  flutter:\n    sdk: flutter\n  flutter_shaders:\n  web: ^1.1.0\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n\nflutter:\n  shaders:\n    - packages/flutter_shaders/shaders/pixelation.frag\n  assets:\n    - assets/\n    - assets/images/\n"
  },
  {
    "path": "doc/flame/examples/web/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"UTF-8\">\n  <meta content=\"IE=Edge\" http-equiv=\"X-UA-Compatible\">\n  <meta name=\"description\" content=\"Klondike tutorial\">\n  <base href=\"$FLUTTER_BASE_HREF\">\n\n  <!-- iOS meta tags & icons -->\n  <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n  <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\">\n  <meta name=\"apple-mobile-web-app-title\" content=\"Klondike\">\n\n  <title>Examples</title>\n</head>\n<body>\n  <script>\n    if ('serviceWorker' in navigator) {\n      window.addEventListener('load', function () {\n        navigator.serviceWorker.register('flutter_service_worker.js');\n      });\n    }\n  </script>\n  <script src=\"main.dart.js\" type=\"application/javascript\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "doc/flame/flame.md",
    "content": "# Flame\n\n- [Getting Started](../README.md)\n- [Game Widget](game_widget.md)\n- [The game class](game.md)\n- [Assets Structure](structure.md)\n- [Components](components/components.md)\n- [Inputs](inputs/inputs.md)\n- [Camera](camera.md)\n- [Effects](effects/effects.md)\n- [Collision Detection](collision_detection.md)\n- [Router](router.md)\n- [Platforms](platforms.md)\n- [Rendering](rendering/rendering.md)\n- [Layout](layout/layout.md)\n- [Overlays](overlays.md)\n- [Other](other/other.md)\n\n```{toctree}\n:hidden:\n\nGetting Started      <../README.md>\nGame Widget          <game_widget.md>\nThe game class       <game.md>\nAssets Structure     <structure.md>\nComponents           <components/components.md>\nInputs               <inputs/inputs.md>\nCamera               <camera.md>\nEffects              <effects/effects.md>\nCollision Detection  <collision_detection.md>\nRouter               <router.md>\nPlatforms            <platforms.md>\nRendering            <rendering/rendering.md>\nLayout               <layout/layout.md>\nOverlays             <overlays.md>\nOther                <other/other.md>\n```\n"
  },
  {
    "path": "doc/flame/game.md",
    "content": "# FlameGame\n\nEvery game needs a central object that owns the game loop, the continuous cycle of updating state\nand rendering frames that drives all real-time games. In Flame, `FlameGame` fills that role while\nalso serving as the root of the component tree. If you are familiar with Flutter, think of\n`FlameGame` as the equivalent of `MaterialApp`: the top-level entry point that everything else\nhangs off of.\n\nThe base of almost all Flame games is the `FlameGame` class. It is the root of your component\ntree. We refer to this component-based system as the Flame Component System (FCS). Throughout the\ndocumentation, FCS is used to reference this system.\n\nThe `FlameGame` class implements a `Component` based `Game`. It has a tree of components\nand calls the `update` and `render` methods of all components that have been added to the game.\n\nComponents can be added to the `FlameGame` directly in the constructor with the named `children`\nargument, or from anywhere else with the `add`/`addAll` methods. Most of the time however, you want\nto add your children to a `World`, the default world exists under `FlameGame.world` and you add\ncomponents to it just like you would to any other component.\n\nA simple `FlameGame` implementation that adds two components, one in `onLoad` and one directly in\nthe constructor can look like this:\n\n```dart\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart';\n\n/// A component that renders the crate sprite, with a 16 x 16 size.\nclass MyCrate extends SpriteComponent {\n  MyCrate() : super(size: Vector2.all(16));\n\n  @override\n  Future<void> onLoad() async {\n    sprite = await Sprite.load('crate.png');\n  }\n}\n\nclass MyWorld extends World {\n  @override\n  Future<void> onLoad() async {\n    await add(MyCrate());\n  }\n}\n\nvoid main() {\n  final myGame = FlameGame(world: MyWorld());\n  runApp(\n    GameWidget(game: myGame),\n  );\n}\n```\n\n\n## Custom World type\n\n`FlameGame` has a generic type parameter `W` that defaults to `World`. By specifying a custom world\ntype, the `world` getter on your game will return your specific world type directly, without needing\nto cast it.\n\nThis is useful when you have a custom `World` subclass and want to access its properties or methods\nfrom within your game class:\n\n```dart\nclass MyWorld extends World {\n  int score = 0;\n}\n\nclass MyGame extends FlameGame<MyWorld> {\n  MyGame() : super(world: MyWorld());\n\n  void incrementScore() {\n    // No cast needed, `world` is already typed as `MyWorld`.\n    world.score++;\n  }\n}\n```\n\nWhen using this generic parameter, you **must** pass a matching world instance to the `super`\nconstructor. If the generic type is specified but no world is provided, a runtime assertion error\nwill be thrown.\n\n```{note}\nIf you instantiate your game in a build method your game will be rebuilt every\ntime the Flutter tree gets rebuilt, which usually is more often than you'd like.\nTo avoid this, you can either create an instance of your game first and\nreference it within your widget structure or use the `GameWidget.controlled`\nconstructor.\n```\n\nTo remove components from the list on a `FlameGame` the `remove` or `removeAll` methods can be used.\nThe first can be used if you just want to remove one component, and the second can be used when you\nwant to remove a list of components. These methods exist on all `Component`s, including the `World`.\n\nThe `FlameGame` has a built-in `World` called `world` and a `CameraComponent` instance called\n`camera`, you can read more about those in the [Camera section](camera.md).\n\n\n## Game Loop\n\nThe `GameLoop` module is a simple abstraction of the game loop concept. Basically, most games are\nbuilt upon two methods:\n\n- The render method takes the canvas for drawing the current state of the game.\n- The update method receives the delta time in seconds since the last update and allows you to\n  move to the next state.\n\nThe `GameLoop` is used by all of Flame's `Game` implementations.\n\n\n## Resizing\n\nEvery time the game needs to be resized, for example when the orientation is changed, `FlameGame`\nwill call all of the `Component`'s `onGameResize` methods and it will also pass this information to\nthe camera and viewport.\n\nThe `FlameGame.camera` controls which point in the coordinate space that should be at the anchor of\nyour viewfinder, [0,0] is in the center (`Anchor.center`) of the viewport by default.\n\n\n## Lifecycle\n\nThe `FlameGame` lifecycle callbacks, `onLoad`, `render`, etc. are called in the following sequence:\n\n```{include} diagrams/flame_game_life_cycle.md\n```\n\nWhen a `FlameGame` is first added to a `GameWidget` the lifecycle methods `onGameResize`, `onLoad`\nand `onMount` will be called in that order. Then `update` and `render` are called in sequence for\nevery game tick. If the `FlameGame` is removed from the `GameWidget` then `onRemove` is called.\nIf the `FlameGame` is added to a new `GameWidget` the sequence repeats from `onGameResize`.\n\n```{note}\nThe order of `onGameResize` and `onLoad` are reversed from that of other\n`Component`s. This is to allow game element sizes to be calculated before\nresources are loaded or generated.\n```\n\nThe `onRemove` callback can be used to clean up children and cached data:\n\n```dart\n  @override\n  void onRemove() {\n    // Optional based on your game needs.\n    removeAll(children);\n    processLifecycleEvents();\n    Flame.images.clearCache();\n    Flame.assets.clearCache();\n    // Any other code that you want to run when the game is removed.\n  }\n```\n\n```{note}\nClean-up of children and resources in a `FlameGame` is not done automatically\nand must be explicitly added to the `onRemove` call.\n```\n\n\n### onHotReload\n\nWhen Flutter's hot reload is triggered (debug mode only), the `GameWidget` calls `onHotReload` on\nthe `FlameGame`, which automatically propagates the notification to every component in the tree that\nis loading or loaded. Override this method on any component to reload assets, refresh cached values,\nor react to source code changes during development:\n\n```dart\nclass MyGame extends FlameGame {\n  @override\n  void onHotReload() {\n    // Refresh game-level state affected by code changes.\n    super.onHotReload();\n  }\n}\n```\n\n```{note}\n`onHotReload` is only called in debug mode. Components that are still in the\nlifecycle queue (loading but not yet mounted) also receive the notification.\nAlways call `super.onHotReload()` so the event continues to propagate to\nchildren.\n```\n\n\n### dispose()\n\nAs a convenience, `FlameGame` provides a `dispose()` method that handles all of the common cleanup\nin a single call:\n\n```dart\n  game.dispose();\n```\n\nThis removes all children from the game (triggering `onRemove` on every component in the tree),\nprocesses all pending lifecycle events, and clears the `images` and `assets` caches.\n\nThe difference between `dispose()` and `onRemove` is that `dispose()` is a method you call\nexplicitly to perform cleanup, while `onRemove` is a lifecycle callback that is invoked\nautomatically when the game is removed from a `GameWidget`. You can use `dispose()` from within\n`onRemove`, or call it independently whenever you need to reset the game state.\n\n\n## Debug mode\n\nFlame's `FlameGame` class provides a variable called `debugMode`, which by default is `false`. It\ncan, however, be set to `true` to enable debug features for the components of the game. **Be aware**\nthat the value of this variable is passed through to its components when they are added to the\ngame, so if you change the `debugMode` at runtime, it will not affect already added components by\ndefault.\n\nTo read more about the `debugMode` on Flame, please refer to the [Debug Docs](other/debug.md)\n\n\n## Change background color\n\nTo change the background color of your `FlameGame` you have to override `backgroundColor()`.\n\nIn the following example, the background color is set to be fully transparent, so that you can see\nthe widgets that are behind the `GameWidget`. The default is opaque black.\n\n```dart\nclass MyGame extends FlameGame {\n  @override\n  Color backgroundColor() => const Color(0x00000000);\n}\n```\n\nNote that the background color can't change dynamically while the game is running, but you could\njust draw a background that covers the whole canvas if you would want it to change dynamically.\n\n\n## SingleGameInstance mixin\n\nAn optional mixin `SingleGameInstance` can be applied to your game if you are making a single-game\napplication. This is a common scenario when building games: there is a single full-screen\n`GameWidget` that hosts a single `Game` instance.\n\nAdding this mixin provides performance advantages in certain scenarios. In particular, a component's\n`onLoad` method is guaranteed to start when that component is added to its parent, even if the\nparent is not yet mounted itself. Consequently, `await`-ing on `parent.add(component)` is guaranteed\nto always finish loading the component.\n\nUsing this mixin is simple:\n\n```dart\nclass MyGame extends FlameGame with SingleGameInstance {\n  // ...\n}\n```\n\n\n## Low-level Game API\n\n```{include} diagrams/low_level_game_api.md\n```\n\nThe abstract `Game` class is a low-level API that can be used when you want to implement the\nfunctionality of how the game engine should be structured. `Game` does not implement any `update` or\n`render` function for example.\n\nThe class also has the lifecycle methods `onLoad`, `onMount` and `onRemove` in it, which are\ncalled from the `GameWidget` (or another parent) when the game is loaded + mounted, or removed.\n`onLoad` is only called the first time the class is added to a parent, but `onMount` (which is\ncalled after `onLoad`) is called every time it is added to a new parent. `onRemove` is called when\nthe class is removed from a parent.\n\n```{note}\nThe `Game` class allows for more freedom of how to implement things, but you\nare also missing out on all of the built-in features in Flame if you use it.\n```\n\nAn example of what a `Game` implementation could look like:\n\n```dart\nclass MyGameSubClass extends Game {\n  @override\n  void render(Canvas canvas) {\n    // ...\n  }\n\n  @override\n  void update(double dt) {\n    // ...\n  }\n}\n\nvoid main() {\n  final myGame = MyGameSubClass();\n  runApp(\n    GameWidget(\n      game: myGame,\n    )\n  );\n}\n```\n\n\n## Pause/Resuming/Stepping game execution\n\nA Flame `Game` can be paused and resumed in two ways:\n\n- With the use of the `pauseEngine` and `resumeEngine` methods.\n- By changing the `paused` attribute.\n\nWhen pausing a `Game`, the `GameLoop` is effectively paused, meaning that no updates or new renders\nwill happen until it is resumed.\n\nWhile the game is paused, it is possible to advance it frame by frame using the `stepEngine`\nmethod. It might not be very useful in the final game, but it can be very helpful for inspecting\ngame state step by step during the development cycle.\n\n\n### Backgrounding\n\nThe game will be automatically paused when the app is sent to the background,\nand resumed when it comes back to the foreground. This behavior can be disabled by setting\n`pauseWhenBackgrounded` to `false`.\n\n```dart\nclass MyGame extends FlameGame {\n  MyGame() {\n    pauseWhenBackgrounded = false;\n  }\n}\n```\n\nThis flag currently only works on Android and iOS.\n\n\n## HasPerformanceTracker mixin\n\nWhile optimizing a game, it can be useful to track the time it took for the game to update and\nrender each frame. This data can help in detecting areas of the code that are running hot. It can\nalso help\nin detecting visual areas of the game that are taking the most time to render.\n\nTo get the update and render times, just add the `HasPerformanceTracker` mixin to the game class.\n\n```dart\nclass MyGame extends FlameGame with HasPerformanceTracker {\n  // access `updateTime` and `renderTime` getters.\n}\n```\n"
  },
  {
    "path": "doc/flame/game_widget.md",
    "content": "# Game Widget\n\nThe `GameWidget` is the bridge between Flutter and Flame. Since Flame games are not Flutter widgets\nby themselves, the `GameWidget` wraps a `Game` instance and places it into the Flutter widget tree,\njust like any other [widget](https://docs.flutter.dev/get-started/fundamentals/widgets). This lets\nyou combine a full-screen game with Flutter UI elements (navigation bars, overlays, dialogs) or\nembed a game as only part of your app's layout.\n\n```{dartdoc}\n:package: flame\n:symbol: GameWidget\n:file: src/game/game_widget/game_widget.dart\n\n[ClipRect]: https://api.flutter.dev/flutter/widgets/ClipRect-class.html\n[FocusNode]: https://api.flutter.dev/flutter/widgets/FocusNode-class.html\n[RepaintBoundary]: https://api.flutter.dev/flutter/widgets/RepaintBoundary-class.html\n```\n\n\n## Hit Test Behavior\n\nThe `behavior` argument controls how the `GameWidget` participates in Flutter's hit testing. This\ndetermines whether pointer events (taps, drags, etc.) are absorbed by the game or allowed to pass\nthrough to widgets underneath it in the widget tree.\n\nThere are three possible values from Flutter's `HitTestBehavior`:\n\n- **`HitTestBehavior.opaque`** (default): The game absorbs all pointer events on its entire surface,\n  preventing any widgets behind it from receiving them. This is the classic behavior where the game\n  acts as a solid layer.\n\n- **`HitTestBehavior.deferToChild`**: The game only intercepts events at positions where a component\n  with event callbacks (e.g. `TapCallbacks`) exists. Events at positions with no interactive\n  components pass through to widgets behind the `GameWidget`. This is useful when layering a game on\n  top of Flutter UI and you want the underlying widgets to remain interactive in areas the game\n  doesn't need to handle.\n\n- **`HitTestBehavior.translucent`**: The game receives events where it has event-handling\n  components, but always allows widgets behind it to be hit-tested as well. Both the game and the\n  widgets behind it can receive the same event.\n\n\n### Allowing taps to pass through\n\nA common use case is placing a `GameWidget` on top of other Flutter widgets in a `Stack`. By\ndefault, the game will block all interaction with the widgets underneath. To let taps pass through\nto those widgets, set `behavior` to `HitTestBehavior.deferToChild`:\n\n```dart\nWidget build(BuildContext context) {\n  return Stack(\n    children: [\n      // Flutter widgets underneath\n      Center(\n        child: ElevatedButton(\n          onPressed: () => print('Button tapped!'),\n          child: const Text('Tap me'),\n        ),\n      ),\n      // Game on top, letting taps pass through\n      Positioned.fill(\n        child: GameWidget(\n          game: MyGame(),\n          behavior: HitTestBehavior.deferToChild,\n        ),\n      ),\n    ],\n  );\n}\n```\n\nIn this setup, tapping an area with no interactive game components will reach the `ElevatedButton`\nbehind the game. Tapping a game component that uses `TapCallbacks` will be handled by the game\ninstead.\n\n```{note}\nWhen using `deferToChild` or `translucent`, `FlameGame` determines whether a\nposition has an interactive component by traversing the component tree via\n`componentsAtPoint`. Games that directly extend the low-level `Game` class\nreport a hit on their entire surface by default; override\n`containsEventHandlerAt` to customize this.\n```\n"
  },
  {
    "path": "doc/flame/inputs/drag_events.md",
    "content": "# Drag Events\n\n**Drag events** occur when the user moves their finger across the screen of the device, or when they\nmove the mouse while holding its button down.\n\nMultiple drag events can occur at the same time, if the user is using multiple fingers. Such cases\nwill be handled correctly by Flame, and you can even keep track of the events by using their\n`pointerId` property.\n\nFor those components that you want to respond to drags, add the `DragCallbacks` mixin.\n\n- This mixin adds four overridable methods to your component: `onDragStart`, `onDragUpdate`,\n  `onDragEnd`, and `onDragCancel`. By default, these methods do nothing; they need to be overridden\n  in order to perform any function.\n- In addition, the component must implement the `containsLocalPoint()` method (already implemented\n  in `PositionComponent`, so most of the time you don't need to do anything here). This method\n  allows Flame to know whether the event occurred within the component or not.\n\n```dart\nclass MyComponent extends PositionComponent with DragCallbacks {\n  MyComponent() : super(size: Vector2(180, 120));\n\n   @override\n   void onDragStart(DragStartEvent event) {\n     // Do something in response to a drag event\n   }\n}\n```\n\n\n## Demo\n\nIn this example you can use drag gestures to either drag star-like shapes across the screen, or to\ndraw curves inside the magenta rectangle.\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: drag_events\n:show: widget code\n```\n\n\n## Drag anatomy\n\n\n### onDragStart\n\nThis is the first event that occurs in a drag sequence. Usually, the event will be delivered to the\ntopmost component at the point of touch with the `DragCallbacks` mixin. However, by setting the flag\n`event.continuePropagation` to true, you can allow the event to propagate to the components below.\n\nThe `DragStartEvent` object associated with this event will contain the coordinate of the point\nwhere the event has originated. This point is available in multiple coordinate system:\n`devicePosition` is given in the coordinate system of the entire device, `canvasPosition` is in the\ncoordinate system of the game widget, and `localPosition` provides the position in the component's\nlocal coordinate system.\n\nAny component that receives `onDragStart` will later be receiving `onDragUpdate` and `onDragEnd`\nevents as well.\n\n\n### onDragUpdate\n\nThis event is fired continuously as user drags their finger across the screen. It will not fire if\nthe user is holding their finger still.\n\nThe default implementation delivers this event to all the components that received the previous\n`onDragStart` with the same pointer id. If the point of touch is still within the component, then\n`event.localPosition` will give the position of that point in the local coordinate system. However,\nif the user moves their finger away from the component, the property `event.localPosition` will\nreturn a point whose coordinates are NaNs. Likewise, the `event.renderingTrace` in this case will be\nempty. However, the `canvasPosition` and `devicePosition` properties of the event will be valid.\n\nIn addition, the `DragUpdateEvent` will contain `delta`, the amount the finger has moved since\nthe previous `onDragUpdate`, or since the `onDragStart` if this is the first drag-update after\na drag-start.\n\nThe `event.timestamp` property measures the time elapsed since the beginning of the drag. It can be\nused, for example, to compute the speed of the movement.\n\n\n### onDragEnd\n\nThis event is fired when the user lifts their finger and thus stops the drag gesture. There is no\nposition associated with this event.\n\n\n### onDragCancel\n\nThe precise semantics when this event occurs is not clear, so we provide a default implementation\nwhich simply converts this event into an `onDragEnd`.\n\n\n## Mixins\n\n\n### DragCallbacks\n\nThe `DragCallbacks` mixin can be added to any `Component` in order for that component to start\nreceiving drag events.\n\nThis mixin adds methods `onDragStart`, `onDragUpdate`, `onDragEnd`, and `onDragCancel` to the\ncomponent, which by default don't do anything, but can be overridden to implement any real\nfunctionality.\n\nAnother crucial detail is that a component will only receive drag events that originate *within*\nthat component, as judged by the `containsLocalPoint()` function. The commonly-used\n`PositionComponent` class provides such an implementation based on its `size` property. Thus, if\nyour component derives from a `PositionComponent`, then make sure that you set its size correctly.\nIf, however, your component derives from the bare `Component`, then the `containsLocalPoint()`\nmethod must be implemented manually.\n\nIf your component is a part of a larger hierarchy, then it will only receive drag events if its\nancestors have all implemented the `containsLocalPoint` correctly.\n\n```dart\nclass MyComponent extends PositionComponent with DragCallbacks {\n  MyComponent({super.size});\n\n  final _paint = Paint();\n  bool _isDragged = false;\n\n  @override\n  void onDragStart(DragStartEvent event) => _isDragged = true;\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) => position += event.delta;\n\n  @override\n  void onDragEnd(DragEndEvent event) => _isDragged = false;\n\n  @override\n  void render(Canvas canvas) {\n    _paint.color = _isDragged? Colors.red : Colors.white;\n    canvas.drawRect(size.toRect(), _paint);\n  }\n}\n```\n"
  },
  {
    "path": "doc/flame/inputs/gesture_input.md",
    "content": "# Gesture Input\n\nThis is documentation for gesture inputs attached directly on the game class, most of the time you\nwant to detect input on your components instead, see for example the [TapCallbacks](tap_events.md)\nand [DragCallbacks](drag_events.md) for that.\n\nFor other input documents, see also:\n\n- [Keyboard Input](keyboard_input.md): for keystrokes\n- [Other Inputs](other_inputs.md): For joysticks, game pads, etc.\n\n\n## Intro\n\nInside `package:flame/gestures.dart` you can find a whole set of `mixin`s which can be included on\nyour game class instance to be able to receive touch input events. Below you can see the full list\nof these `mixin`s and its methods:\n\n\n## Touch and mouse detectors\n\n```text\n- TapDetector\n  - onTap\n  - onTapCancel\n  - onTapDown\n  - onLongTapDown\n  - onTapUp\n\n- SecondaryTapDetector\n  - onSecondaryTapDown\n  - onSecondaryTapUp\n  - onSecondaryTapCancel\n\n- TertiaryTapDetector\n  - onTertiaryTapDown\n  - onTertiaryTapUp\n  - onTertiaryTapCancel\n\n- DoubleTapDetector\n  - onDoubleTap\n\n- LongPressDetector\n  - onLongPress\n  - onLongPressStart\n  - onLongPressMoveUpdate\n  - onLongPressUp\n  - onLongPressEnd\n\n- VerticalDragDetector\n  - onVerticalDragDown\n  - onVerticalDragStart\n  - onVerticalDragUpdate\n  - onVerticalDragEnd\n  - onVerticalDragCancel\n\n- HorizontalDragDetector\n  - onHorizontalDragDown\n  - onHorizontalDragStart\n  - onHorizontalDragUpdate\n  - onHorizontalDragEnd\n  - onHorizontalDragCancel\n\n- ForcePressDetector\n  - onForcePressStart\n  - onForcePressPeak\n  - onForcePressUpdate\n  - onForcePressEnd\n\n- PanDetector\n  - onPanDown\n  - onPanStart\n  - onPanUpdate\n  - onPanEnd\n  - onPanCancel\n\n- ScaleDetector\n  - onScaleStart\n  - onScaleUpdate\n  - onScaleEnd\n\n- MultiTouchTapDetector\n  - onTap\n  - onTapCancel\n  - onTapDown\n  - onTapUp\n\n- MultiTouchDragDetector\n  - onReceiveDrag\n```\n\nMouse only events\n\n```text\n - MouseMovementDetector\n  - onMouseMove\n - ScrollDetector\n  - onScroll\n```\n\n\nIt is not possible to mix advanced detectors (`MultiTouch*`) with basic detectors of the same\nkind, since the advanced detectors will *always win the gesture arena* and the basic detectors will\nnever be triggered. So for example, you can't use both `MultiTouchTapDetector` and `PanDetector`\ntogether, since no events will be triggered for the latter (there is also an assertion for this).\n\nFlame's GestureApi is provided by Flutter's Gesture Widgets, including\n[GestureDetector widget](https://api.flutter.dev/flutter/widgets/GestureDetector-class.html),\n[RawGestureDetector widget](https://api.flutter.dev/flutter/widgets/RawGestureDetector-class.html)\nand [MouseRegion widget](https://api.flutter.dev/flutter/widgets/MouseRegion-class.html), you can\nalso read more about\n[Flutter's gesture system](https://api.flutter.dev/flutter/gestures/gestures-library.html).\n\n\n## PanDetector and ScaleDetector\n\nIf you add a `PanDetector` together with a `ScaleDetector` you will be prompted with a quite cryptic\nassertion from Flutter that says:\n\n```{note}\nHaving both a pan gesture recognizer and a scale gesture recognizer is\nredundant; scale is a superset of pan.\n\nJust use the scale gesture recognizer.\n```\n\nThis might seem strange, but `onScaleUpdate` is not only triggered when the scale should be changed,\nbut for all pan/drag events too. So if you need to use both of those detectors you'll have to handle\nboth of their logic inside `onScaleUpdate` (+`onScaleStart` and `onScaleEnd`).\n\nFor example you could do something like this if you want to move the camera on pan events and zoom\non scale events:\n\n```dart\n  void clampZoom() {\n    camera.viewfinder.zoom = camera.viewfinder.zoom.clamp(0.05, 3.0);\n  }\n\n  late double startZoom;\n\n  @override\n  void onScaleStart(_) {\n    startZoom = camera.viewfinder.zoom;\n  }\n\n  @override\n  void onScaleUpdate(ScaleUpdateInfo info) {\n    final currentScale = info.scale.global;\n    if (!currentScale.isIdentity()) {\n      camera.viewfinder.zoom = startZoom * currentScale.y;\n      clampZoom();\n    } else {\n      final zoom = camera.viewfinder.zoom;\n      final delta = (info.delta.global..negate()) / zoom;\n      camera.moveBy(delta);\n    }\n  }\n```\n\nIn the example above the pan events are handled with `info.delta` and the scale events with\n`info.scale`, although they are theoretically both from underlying scale events.\n\nThis can also be seen in the\n[zoom example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/camera_and_viewport/zoom_example.dart).\n\n\n## Mouse cursor\n\nIt is also possible to change the current mouse cursor displayed on the `GameWidget` region. To do\nso the following code can be used inside the `Game` class\n\n```dart\nmouseCursor.value = SystemMouseCursors.move;\n```\n\nTo already initialize the `GameWidget` with a custom cursor, the `mouseCursor` property can be used\n\n```dart\nGameWidget(\n  game: MouseCursorGame(),\n  mouseCursor: SystemMouseCursors.move,\n);\n```\n\n\n## Event coordinate system\n\nOn events that have positions, like for example `Tap*` or `Drag`, you will notice that the\n`eventPosition` attribute includes 2 fields: `global` and `widget`. Below you will find a brief\nexplanation about each of them.\n\n\n### global\n\nThe position where the event occurred considering the entire screen, same as\n`globalPosition` in Flutter's native events.\n\n\n### widget\n\nThe position where the event occurred relative to the `GameWidget` position and size, same as\n`localPosition` in Flutter's native events.\n\n\n## Example\n\n```dart\nclass MyGame extends FlameGame with TapDetector {\n  // Other methods omitted\n\n  @override\n  bool onTapDown(TapDownInfo info) {\n    print(\"Player tap down on ${info.eventPosition.widget}\");\n    return true;\n  }\n\n  @override\n  bool onTapUp(TapUpInfo info) {\n    print(\"Player tap up on ${info.eventPosition.widget}\");\n    return true;\n  }\n}\n```\n\nYou can also check more complete examples in the\n[input examples directory](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/input/).\n\n\n### GestureHitboxes\n\nThe `GestureHitboxes` mixin is used to more accurately recognize gestures on top of your\n`Component`s. Say that you have a fairly round rock as a `SpriteComponent` for example, then you\ndon't want to register input that is in the corner of the image where the rock is not displayed,\nsince a `PositionComponent` is rectangular by default. Then you can use the `GestureHitboxes` mixin\nto define a more accurate circle or polygon (or another shape) for which the input should be within\nfor the event to be registered on your component.\n\nYou can add new hitboxes to the component that has the `GestureHitboxes` mixin just like they are\nadded in the below `Collidable` example.\n\nMore information about how to define hitboxes can be found in the hitbox section of the\n[collision detection](../collision_detection.md#shapehitbox) docs.\n\nAn example of how to use it can be seen in the\n[gesture hitboxes example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/input/gesture_hitboxes_example.dart).\n"
  },
  {
    "path": "doc/flame/inputs/hardware_keyboard_detector.md",
    "content": "# HardwareKeyboardDetector\n\n```{note}\nMost of the time you will want to use the `KeyboardEvents` class or the\n`KeyboardHandler` mixin instead of this component.\n```\n\n```{dartdoc}\n:file: src/events/hardware_keyboard_detector.dart\n:symbol: HardwareKeyboardDetector\n:package: flame\n\n[HardwareKeyboard]: https://api.flutter.dev/flutter/services/HardwareKeyboard-class.html\n[KeyDownEvent]: https://api.flutter.dev/flutter/services/KeyDownEvent-class.html\n[KeyUpEvent]: https://api.flutter.dev/flutter/services/KeyUpEvent-class.html\n[KeyRepeatEvent]: https://api.flutter.dev/flutter/services/KeyRepeatEvent-class.html\n```\n"
  },
  {
    "path": "doc/flame/inputs/inputs.md",
    "content": "# Inputs\n\nGames are interactive by nature, so handling player input is essential. Flame provides input\nhandling that works on all platforms Flutter supports: touch on mobile, mouse and keyboard on\ndesktop, and\npointer events on the web. These APIs are designed as mixins that you add to your components, so\neach component can independently decide which input events it cares about. This is similar to how\nFlutter's [GestureDetector](https://api.flutter.dev/flutter/widgets/GestureDetector-class.html)\nworks, but adapted for Flame's component tree.\n\n- [Tap Events](tap_events.md)\n- [Drag Events](drag_events.md)\n- [Gesture Input](gesture_input.md)\n- [Keyboard Input](keyboard_input.md)\n- [Other Inputs and Helpers](other_inputs.md)\n- [Pointer Events](pointer_events.md)\n- [Hardware Keyboard Detector](hardware_keyboard_detector.md)\n\n```{toctree}\n:hidden:\n\nTap Events                <tap_events.md>\nDrag Events               <drag_events.md>\nGesture Input             <gesture_input.md>\nKeyboard Input            <keyboard_input.md>\nOther Inputs              <other_inputs.md>\nPointer Events            <pointer_events.md>\nHardwareKeyboardDetector  <hardware_keyboard_detector.md>\n```\n"
  },
  {
    "path": "doc/flame/inputs/keyboard_input.md",
    "content": "# Keyboard Input\n\nThis includes documentation for keyboard inputs.\n\nFor other input documents, see also:\n\n- [Gesture Input](gesture_input.md): for mouse and touch pointer gestures\n- [Other Inputs](other_inputs.md): For joysticks, game pads, etc.\n\n\n## Intro\n\nThe keyboard API on flame relies on the\n[Flutter's Focus widget](https://api.flutter.dev/flutter/widgets/Focus-class.html).\n\nTo customize focus behavior, see [Controlling focus](#controlling-focus).\n\nThere are two ways a game can react to key strokes; at the game level and at a component level.\nFor each we have a mixin that can me added to a `Game` or `Component` class.\n\n\n### Receive keyboard events in a game level\n\nTo make a `Game` sub class sensitive to key stroke, mix it with `KeyboardEvents`.\n\nAfter that, it will be possible to override an `onKeyEvent` method.\n\nThis method receives two parameters, first the\n[`KeyEvent`](https://api.flutter.dev/flutter/services/KeyEvent-class.html)\nthat triggers the callback in the first place. The second is a set of the currently pressed\n[`LogicalKeyboardKey`](https://api.flutter.dev/flutter/services/LogicalKeyboardKey-class.html).\n\nThe return value is a\n[`KeyEventResult`](https://api.flutter.dev/flutter/widgets/KeyEventResult.html).\n\n`KeyEventResult.handled` will tell the framework that the key stroke was resolved inside of Flame\nand skip any other keyboard handler widgets apart of `GameWidget`.\n\n`KeyEventResult.ignored` will tell the framework to keep testing this event in any other keyboard\nhandler widget apart of `GameWidget`. If the event is not resolved by any handler, the framework\nwill trigger `SystemSoundType.alert`.\n\n`KeyEventResult.skipRemainingHandlers` is very similar to `.ignored`, apart from the fact that will\nskip any other handler widget and will straight up play the alert sound.\n\nMinimal example:\n\n```dart\nclass MyGame extends FlameGame with KeyboardEvents {\n  // ...\n  @override\n  KeyEventResult onKeyEvent(\n    KeyEvent event,\n    Set<LogicalKeyboardKey> keysPressed,\n  ) {\n    final isKeyDown = event is KeyDownEvent;\n\n    final isSpace = keysPressed.contains(LogicalKeyboardKey.space);\n\n    if (isSpace && isKeyDown) {\n      if (keysPressed.contains(LogicalKeyboardKey.altLeft) ||\n          keysPressed.contains(LogicalKeyboardKey.altRight)) {\n        this.shootHarder();\n      } else {\n        this.shoot();\n      }\n      return KeyEventResult.handled;\n    }\n    return KeyEventResult.ignored;\n  }\n}\n```\n\n\n### Receive keyboard events in a component level\n\nTo receive keyboard events directly in components, there is the mixin `KeyboardHandler`.\n\nSimilarly to `TapCallbacks` and `DragCallbacks`, `KeyboardHandler` can be mixed into any subclass of\n`Component`.\n\nKeyboardHandlers must only be added to games that are mixed with `HasKeyboardHandlerComponents`.\n\n> ⚠️ Note: If `HasKeyboardHandlerComponents` is used, you must remove `KeyboardEvents`\n> from the game mixin list to avoid conflicts.\n\nAfter applying `KeyboardHandler`, it will be possible to override an `onKeyEvent` method.\n\nThis method receives two parameters. First the\n[`KeyEvent`](https://api.flutter.dev/flutter/services/KeyEvent-class.html)\nthat triggered the callback in the first place. The second is a set of the currently pressed\n[`LogicalKeyboardKey`](https://api.flutter.dev/flutter/services/LogicalKeyboardKey-class.html)s.\n\nThe returned value should be `true` to allow the continuous propagation of the key event among other\ncomponents. To not allow any other component to receive the event, return `false`.\n\nFlame also provides a default implementation called `KeyboardListenerComponent` which can be used\nto handle keyboard events. Like any other component, it can be added as a child to a `FlameGame`\nor another `Component`:\n\nFor example, imagine a `PositionComponent` which has methods to move on the X and Y axis,\nthen the following code could be used to bind those methods to key events:\n\n```dart\nadd(\n  KeyboardListenerComponent(\n    keyUp: {\n      LogicalKeyboardKey.keyA: (keysPressed) { ... },\n      LogicalKeyboardKey.keyD: (keysPressed) { ... },\n      LogicalKeyboardKey.keyW: (keysPressed) { ... },\n      LogicalKeyboardKey.keyS: (keysPressed) { ... },\n    },\n    keyDown: {\n      LogicalKeyboardKey.keyA: (keysPressed) { ... },\n      LogicalKeyboardKey.keyD: (keysPressed) { ... },\n      LogicalKeyboardKey.keyW: (keysPressed) { ... },\n      LogicalKeyboardKey.keyS: (keysPressed) { ... },\n    },\n  ),\n);\n```\n\n\n### Controlling focus\n\nOn the widget level, it is possible to use the\n[`FocusNode`](https://api.flutter.dev/flutter/widgets/FocusNode-class.html) API to control whether\nthe game is focused or not.\n\n`GameWidget` has an optional `focusNode` parameter that allow its focus to be controlled externally.\n\nBy default `GameWidget` has its `autofocus` set to true, which means it will get focused once it is\nmounted. To override that behavior, set `autofocus` to false.\n\nFor a more complete example, see the\n[keyboard input example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/input/keyboard_example.dart).\n"
  },
  {
    "path": "doc/flame/inputs/other_inputs.md",
    "content": "# Other Inputs and Helpers\n\nThis includes documentation for input methods besides keyboard and mouse.\n\nFor other input documents, see also:\n\n- [Gesture Input](gesture_input.md): for mouse and touch pointer gestures\n- [Keyboard Input](keyboard_input.md): for keystrokes\n\n\n## Joystick\n\nFlame provides a component capable of creating a virtual joystick for taking input for your game.\nTo use this feature, you need to create a `JoystickComponent`, configure it the way you want, and\nadd it to your game.\n\nCheck out the following example to get a better understanding:\n\n```dart\nclass MyGame extends FlameGame {\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    final image = await images.load('joystick.png');\n    final sheet = SpriteSheet.fromColumnsAndRows(\n      image: image,\n      columns: 6,\n      rows: 1,\n    );\n    final joystick = JoystickComponent(\n      knob: SpriteComponent(\n        sprite: sheet.getSpriteById(1),\n        size: Vector2.all(100),\n      ),\n      background: SpriteComponent(\n        sprite: sheet.getSpriteById(0),\n        size: Vector2.all(150),\n      ),\n      margin: const EdgeInsets.only(left: 40, bottom: 40),\n    );\n\n    final player = Player(joystick);\n    add(player);\n    add(joystick);\n  }\n}\n\nclass Player extends SpriteComponent with HasGameReference {\n  Player(this.joystick)\n    : super(\n        anchor: Anchor.center,\n        size: Vector2.all(100.0),\n      );\n\n  /// Pixels/s\n  double maxSpeed = 300.0;\n\n  final JoystickComponent joystick;\n\n  @override\n  Future<void> onLoad() async {\n    sprite = await game.loadSprite('layers/player.png');\n    position = game.size / 2;\n  }\n\n  @override\n  void update(double dt) {\n    if (joystick.direction != JoystickDirection.idle) {\n      position.add(joystick.relativeDelta  * maxSpeed * dt);\n      angle = joystick.delta.screenAngle();\n    }\n  }\n}\n```\n\nIn this example, we created the classes `MyGame` and `Player`.\n`MyGame` creates a joystick which is passed to the `Player` when the latter is created.\nIn the `Player` class we act upon the current state of the joystick.\n\nThe joystick has a few fields that change depending on what state it is in.\n\nFollowing fields should be used to know the state of the joystick:\n\n- `intensity`: The percentage [0.0, 1.0] that the knob is dragged from the epicenter to the edge of\n  the joystick (or `knobRadius` if that is set).\n- `delta`: The absolute amount (defined as a `Vector2`) that the knob is dragged from its epicenter.\n- `relativeDelta`: The percentage, presented as a `Vector2`, and direction that the knob is currently\n  pulled from its base position to a edge of the joystick.\n\nIf you want to create buttons to go with your joystick, check out\n[`HudButtonComponent`](#hudbuttoncomponent).\n\nFor the complete code on implementing the joystick, check out the\n[Joystick Example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/input/joystick_example.dart).\nYou can also view the\n[JoystickComponent in action](https://examples.flame-engine.org/#/Input_Joystick)\nto see a live example of the joystick input function integrated into a game.\n\nFor an additional challenge, explore the\n[Advanced Joystick Example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/input/joystick_advanced_example.dart).\nSee what else the advanced features can do in the\n[live demo](https://examples.flame-engine.org/#/Input_Joystick_Advanced).\n\n\n## HudButtonComponent\n\nA `HudButtonComponent` is a button that can be defined with margins to the edge of the `Viewport`\ninstead of with a position. It takes two `PositionComponent`s. `button` and `buttonDown`, the first\nis used for when the button is idle and the second is shown when the button is being pressed. The\nsecond one is optional if you don't want to change the look of the button when it is pressed, or if\nyou handle this through the `button` component.\n\nAs the name suggests this button is a hud by default, which means that it will be static on your\nscreen even if the camera for the game moves around. You can also use this component as a non-hud by\nsetting `hudButtonComponent.respectCamera = true;`.\n\nIf you want to act upon the button being pressed (which would be the common thing to do) and released,\nyou can either pass in callback functions as the `onPressed` and `onReleased` arguments, or you can\nextend the component and override `onTapDown`, `onTapUp` and/or `onTapCancel` and implement your\nlogic there.\n\n\n## SpriteButtonComponent\n\nA `SpriteButtonComponent` is a button that is defined by two `Sprite`s, one that represents\nwhen the button is pressed and one that represents when the button is released.\n\n\n## ButtonComponent\n\nA `ButtonComponent` is a button that is defined by two `PositionComponent`s, one that represents\nwhen the button is pressed and one that represents when the button is released. If you only want\nto use sprites for the button, use the [](#spritebuttoncomponent) instead, but this component can be\ngood to use if you for example want to have a `SpriteAnimationComponent` as a button, or anything\nelse which isn't a pure sprite.\n\n\n## Gamepad\n\nFlame has a dedicated plugin to support external game controllers (gamepads).\nFind more information in the [Gamepads repository](https://github.com/flame-engine/gamepads).\n\n\n## AdvancedButtonComponent\n\nThe `AdvancedButtonComponent` have separate states for each of the different pointer phases.\nThe skin can be customized for each state and each skin is represented by a `PositionComponent`.\n\nThese are the fields that can be used to customize the looks of the `AdvancedButtonComponent`:\n\n- `defaultSkin`: Component that will be displayed by default on the button.\n- `downSkin`: Component displayed when the button is clicked or tapped.\n- `hoverSkin`: Component displayed when the button is hovered. (desktop and web).\n- `defaultLabel`: Component shown on top of skins. Automatically aligned to center.\n- `disabledSkin`: Component displayed when button is disabled.\n- `disabledLabel`: Component shown on top of skins when button is disabled.\n\n\n## ToggleButtonComponent\n\nThe [ToggleButtonComponent] is an [AdvancedButtonComponent] that can switch between selected\nand not selected.\n\nIn addition to the already existing skins, the [ToggleButtonComponent] contains the following skins:\n\n- `defaultSelectedSkin`: The component to display when the button is selected.\n- `downAndSelectedSkin`: The component that is displayed when the selectable button is selected and\n  pressed.\n- `hoverAndSelectedSkin`: Hover on selectable and selected button (desktop and web).\n- `disabledAndSelectedSkin`: For when the button is selected and in the disabled state.\n- `defaultSelectedLabel`: Component shown on top of the skins when button is selected.\n\n\n## IgnoreEvents mixin\n\nIf you don't want a component subtree to receive events, you can use the `IgnoreEvents` mixin.\nOnce you have added this mixin you can turn off events to reach a component and its descendants by\nsetting `ignoreEvents = true` (default when the mixin is added), and then set it to `false` when you\nwant to receive events again.\n\nThis can be done for optimization purposes, since all events currently go through the whole\ncomponent tree.\n"
  },
  {
    "path": "doc/flame/inputs/pointer_events.md",
    "content": "# Pointer Events\n\n```{note}\nThis document describes the new events API. The old (legacy) approach,\nwhich is still supported, is described in [](gesture_input.md).\n```\n\n**Pointer events** are Flutter's generalized \"mouse-movement\"-type events (for desktop or web).\n\nIf you want to interact with mouse movement events within your component or game, you can use the\n`PointerMoveCallbacks` mixin.\n\nFor example:\n\n```dart\nclass MyComponent extends PositionComponent with PointerMoveCallbacks {\n  MyComponent() : super(size: Vector2(80, 60));\n\n  @override\n  void onPointerMove(PointerMoveEvent event) {\n    // Do something in response to the mouse move (e.g. update coordinates)\n  }\n}\n```\n\nThe mixin adds two overridable methods to your component:\n\n- `onPointerMove`: called when the mouse moves within the component\n- `onPointerMoveStop`: called once if the component was being hovered and the mouse leaves\n\nBy default, each of these methods does nothing, they need to be overridden in order to perform any\nfunction.\n\nIn addition, the component must implement the `containsLocalPoint()` method (already implemented in\n`PositionComponent`, so most of the time you don't need to do anything here). This method allows\nFlame to know whether the event occurred within the component or not.\n\nNote that only mouse events happening within your component will be proxied along. However,\n`onPointerMoveStop` will be fired once on the first mouse movement that leaves your component, so\nyou can handle any exit conditions there.\n\n\n## HoverCallbacks\n\nIf you want to specifically know if your component is being hovered or not, or if you want to hook\ninto hover enter and exit events, you can use a more dedicated mixin called `HoverCallbacks`.\n\nFor example:\n\n```dart\nclass MyComponent extends PositionComponent with HoverCallbacks {\n\n  MyComponent() : super(size: Vector2(80, 60));\n\n  @override\n  void update(double dt) {\n    // use `isHovered` to know if the component is being hovered\n  }\n\n  @override\n  void onHoverEnter() {\n    // Do something in response to the mouse entering the component\n  }\n\n  @override\n  void onHoverExit() {\n    // Do something in response to the mouse leaving the component\n  }\n}\n```\n\nNote that you can still listen to the \"raw\" onPointerMove methods for additional functionality, just\nmake sure to call the `super` version to enable the `HoverCallbacks` behavior.\n\n\n### Demo\n\nPlay with the demo below to see the pointer hover events in action.\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: pointer_events\n:show: widget code\n```\n"
  },
  {
    "path": "doc/flame/inputs/scale_events.md",
    "content": "# Scale Events\n\n**Scale events** occur when the user moves two fingers in a pinch in, or in a pinch out move.\nOnly one single scale gesture can occur at the same time.\n\nFor those components that you want to respond to scale events, add the `ScaleCallbacks` mixin.\n\n- This mixin adds three overridable methods to your component: `onScaleStart`, `onScaleUpdate`,\n  `onScaleEnd`. By default, these methods do nothing; they need to be overridden in order to\n  perform any function.\n- In addition, the component must implement the `containsLocalPoint()` method (already implemented\n  in `PositionComponent`, so most of the time you don't need to do anything here). This method\n  allows Flame to know whether the event occurred within the component or not.\n\n```dart\nclass MyComponent extends PositionComponent with ScaleCallbacks {\n  MyComponent() : super(size: Vector2(180, 120));\n\n   @override\n   void onScaleStart(ScaleStartEvent event) {\n     // Do something in response to a scale event\n   }\n}\n```\n\n\n## Scale anatomy\n\n\n### onScaleStart\n\nThis is the first event that occurs in a scale sequence. Usually, the event will be delivered to the\ntopmost component at the focal point (the point at the center of the line formed by the two fingers)\n with the `ScaleCallbacks` mixin. However, by setting the flag\n`event.continuePropagation` to true, you can allow the event to propagate to the components below.\n\nThe `ScaleStartEvent` object associated with this event will contain\nthe coordinate of the first focal point\nrecognized by the scale gesture recognizer. This point is available in multiple coordinate system:\n`devicePosition` is given in the coordinate system of the entire device, `canvasPosition` is in the\ncoordinate system of the game widget, and `localPosition` provides the position in the component's\nlocal coordinate system.\n\nAny component that receives `onScaleStart` will later be receiving `onScaleUpdate` and `onScaleEnd`\nevents as well.\n\n\n### onScaleUpdate\n\nThis event is fired continuously as user drags their finger across the screen. It will not fire if\nthe user is holding their finger still.\n\nThe default implementation delivers this event to all the components that received the previous\n`onScaleStart`. If the point of touch is still within the component, then\n`event.localPosition` will give the position of that point in the local coordinate system. However,\nif the user moves their finger away from the component, the property `event.localPosition` will\nreturn a point whose coordinates are NaNs. Likewise, the `event.renderingTrace` in this case will be\nempty. However, the `canvasPosition` and `devicePosition` properties of the event will be valid.\n\nIn addition, the `ScaleUpdateEvent` will contain `focalPointDelta` --\nthe amount the focal point has moved since the\nprevious `onScaleUpdate`, or since the `onScaleStart` if this is the first scale-update after a scale-\nstart.\n\nThe `event.timestamp` property measures the time elapsed since the beginning of the scale. It can be\nused, for example, to compute the speed of the movement.\n\nThe `event.rotation` property measures the angle of rotation in radians, between the line formed\nfrom the two fingers at the start, and the line formed when this event is called.\n\nThe `event.scale` property measures the ratio of length between the line formed\nfrom the two fingers at the start, and the line formed when this event is called.\n\n\n### onScaleEnd\n\nThis event is fired when the user lifts their finger and thus stops the scale gesture. There is no\nposition associated with this event.\n\n\n## Mixins\n\n\n### ScaleCallbacks\n\nThe `ScaleCallbacks` mixin can be added to any `Component` in order for that component to start\nreceiving scale events.\n\nThis mixin adds methods `onScaleStart`, `onScaleUpdate`, `onScaleEnd` to the\ncomponent, which by default don't do anything, but can be overridden to implement any real\nfunctionality.\n\nAnother crucial detail is that a component will only receive scale events that originate *within*\nthat component, as judged by the `containsLocalPoint()` function. The commonly-used\n`PositionComponent` class provides such an implementation based on its `size` property. Thus, if\nyour component derives from a `PositionComponent`, then make sure that you set its size correctly.\nIf, however, your component derives from the bare `Component`, then the `containsLocalPoint()`\nmethod must be implemented manually.\n\nIf your component is a part of a larger hierarchy, then it will only receive scale events if its\nancestors have all implemented the `containsLocalPoint` correctly.\n\n```dart\nclass ScaleOnlyRectangle extends RectangleComponent with ScaleCallbacks {\n  ScaleOnlyRectangle({\n    required Vector2 position,\n    required Vector2 size,\n    Color color = Colors.blue,\n    Anchor anchor = Anchor.center,\n  }) : super(\n         position: position,\n         size: size,\n         anchor: anchor,\n         paint: Paint()..color = color,\n       );\n\n  @override\n  Future<void> onLoad() async {\n    final text = TextComponent(\n      text: 'scale',\n      textRenderer: TextPaint(\n        style: const TextStyle(fontSize: 25, color: Colors.white),\n      ),\n      position: size / 2,\n      anchor: Anchor.center,\n    );\n    add(text);\n  }\n\n  bool isScaling = false;\n  double initialAngle = 0;\n  Vector2 initialScale = Vector2.all(1);\n  double lastScale = 1.0;\n\n  /// ScaleCallbacks overrides\n  @override\n  void onScaleStart(ScaleStartEvent event) {\n    super.onScaleStart(event);\n    isScaling = true;\n    initialAngle = angle;\n    initialScale = scale;\n    lastScale = 1.0;\n    debugPrint('Scale started at ${event.devicePosition}');\n  }\n\n  @override\n  void onScaleUpdate(ScaleUpdateEvent event) {\n    super.onScaleUpdate(event);\n    // scale rectangle size by pinch\n    angle = initialAngle + event.rotation;\n    // delta scale since last frame\n    if (lastScale == 0) {\n      return;\n    }\n    final scaleDelta = event.scale / lastScale;\n    lastScale = event.scale; // update for next frame\n\n    // apply delta gently\n    scale *= sqrt(scaleDelta);\n\n    // clamp\n    scale.clamp(Vector2.all(0.8), Vector2.all(3));\n  }\n\n  @override\n  void onScaleEnd(ScaleEndEvent event) {\n    super.onScaleEnd(event);\n    isScaling = false;\n    debugPrint('Scale ended with velocity ${event.velocity}');\n  }\n}\n\n```\n\n\n## Scale and drag gestures interactions\n\nA multi drag gesture can sometimes look exactly like a scale gesture.\nThis is the case for instance, if you try to move two components toward each other at the same time.\nIf you added both a component using ScaleCallbacks and\none using DragCallbacks (or one using both), this issue will arise.\nThe Scale gesture will win over the drag gesture\nand prevent your user to perform the multi drag gesture as they wanted. This is a limitation\nwith the current implementation that devs need to be aware of.\n"
  },
  {
    "path": "doc/flame/inputs/tap_events.md",
    "content": "# Tap Events\n\n```{note}\nThis document describes the new events API. The old (legacy) approach,\nwhich is still supported, is described in [](gesture_input.md).\n```\n\n**Tap events** are one of the most basic methods of interaction with a Flame game. These events\noccur when the user touches the screen with a finger, or clicks with a mouse, or taps with a stylus.\nA tap can be \"long\", but the finger isn't supposed to move during the gesture. Thus, touching the\nscreen, then moving the finger, and then releasing is not a tap but a drag. Similarly, clicking\na mouse button while the mouse is moving will also be registered as a drag.\n\nMultiple tap events can occur at the same time, especially if the user has multiple fingers. Such\ncases will be handled correctly by Flame, and you can even keep track of the events by using their\n`pointerId` property.\n\nFor those components that you want to respond to taps, add the `TapCallbacks` mixin.\n\n- This mixin adds four overridable methods to your component: `onTapDown`, `onTapUp`,\n  `onTapCancel`, and `onLongTapDown`. By default, each of these methods does nothing, they need\n  to be overridden in order to perform any function.\n- In addition, the component must implement the `containsLocalPoint()` method (already implemented\n  in `PositionComponent`, so most of the time you don't need to do anything here). This method\n  allows Flame to know whether the event occurred within the component or not.\n\n```dart\nclass MyComponent extends PositionComponent with TapCallbacks {\n  MyComponent() : super(size: Vector2(80, 60));\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    // Do something in response to a tap event\n  }\n}\n```\n\n\n## Tap anatomy\n\n\n### onTapDown\n\nEvery tap begins with a \"tap down\" event, which you receive via the `void onTapDown(TapDownEvent)`\nhandler. The event is delivered to the first component located at the point of touch that has the\n`TapCallbacks` mixin. Normally, the event then stops propagation. However, you can force the event\nto also be delivered to the components below by setting `event.continuePropagation` to true.\n\nThe `TapDownEvent` object that is passed to the event handler, contains the available information\nabout the event. For example, `event.localPosition` will contain the coordinate of the event in the\ncurrent component's local coordinate system, whereas `event.canvasPosition` is in the coordinate\nsystem of the entire game canvas.\n\nEvery component that received an `onTapDown` event will eventually receive either `onTapUp` or\n`onTapCancel` with the same `pointerId`.\n\n\n### onLongTapDown\n\nIf the user holds their finger down for some time, \"long tap\" will be triggered. This event invokes\nthe `void onLongTapDown(TapDownEvent)` handler on those components that previously received the\n`onTapDown` event.\n\nBy default, the `.longTapDelay` is set to 300 milliseconds, what may be different of the system\ndefault. You can change this value by setting the `TapConfig.longTapDelay` value.\nIt may also be useful for specific accessibility needs.\n\n\n### onTapUp\n\nThis event indicates successful completion of the tap sequence. It is guaranteed to only be\ndelivered to those components that previously received the `onTapDown` event with the same pointer\nid.\n\nThe `TapUpEvent` object passed to the event handler contains the information about the event, which\nincludes the coordinate of the event (i.e. where the user was touching the screen right before\nlifting their finger), and the event's `pointerId`.\n\nNote that the device coordinates of the tap-up event will be the same (or very close) to the device\ncoordinates of the corresponding tap-down event. However, the same cannot be said about the local\ncoordinates. If the component that you're tapping is moving (as they often tend to in games), then\nyou may find that the local tap-up coordinates are quite different from the local tap-down\ncoordinates.\n\nIn extreme case, when the component moves away from the point of touch, the `onTapUp` event will not\nbe generated at all: it will be replaced with `onTapCancel`. Note, however, that in this case the\n`onTapCancel` will be generated at the moment the user lifts or moves their finger, not at the\nmoment the component moves away from the point of touch.\n\n\n### onTapCancel\n\nThis event occurs when the tap fails to materialize. Most often, this will happen if the user moves\ntheir finger, which converts the gesture from \"tap\" into \"drag\". Less often, this may happen when\nthe component being tapped moves away from under the user's finger. Even more rarely, the\n`onTapCancel` occurs when another widget pops over the game widget, or when the device turns off,\nor similar situations.\n\nThe `TapCancelEvent` object contains only the `pointerId` of the previous `TapDownEvent` which is\nnow being canceled. There is no position associated with a tap-cancel.\n\n\n### Demo\n\nPlay with the demo below to see the tap events in action.\n\nThe blue-ish rectangle in the middle is the component that has the `TapCallbacks` mixin. Tapping\nthis component would create circles at the points of touch. Specifically, `onTapDown` event\nstarts making the circle. The thickness of the circle will be proportional to the duration of the\ntap: after `onTapUp` the circle's stroke width will no longer grow. There will be a thin white\nstripe at the moment the `onLongTapDown` fires. Lastly, the circle will implode and disappear if\nyou cause the `onTapCancel` event by moving the finger.\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: tap_events\n:show: widget code\n```\n\n\n## Mixins\n\nThis section describes in more details several mixins needed for tap event handling.\n\n\n### TapCallbacks\n\nThe `TapCallbacks` mixin can be added to any `Component` in order for that component to start\nreceiving tap events.\n\nThis mixin adds methods `onTapDown`, `onLongTapDown`, `onTapUp`, and `onTapCancel` to the component,\nwhich by default don't do anything, but can be overridden to implement any real functionality. There\nis no need to override all of them either: for example, you can override only `onTapUp` if you wish\nto respond to \"real\" taps only.\n\nAnother crucial detail is that a component will only receive tap events that occur *within* that\ncomponent, as judged by the `containsLocalPoint()` function. The commonly-used `PositionComponent`\nclass provides such an implementation based on its `size` property. Thus, if your component derives\nfrom a `PositionComponent`, then make sure that you set its size correctly. If, however, your\ncomponent derives from the bare `Component`, then the `containsLocalPoint()` method must be\nimplemented manually.\n\nIf your component is a part of a larger hierarchy, then it will only receive tap events if its\nparent has implemented the `containsLocalPoint` correctly.\n\n```dart\nclass MyComponent extends Component with TapCallbacks {\n  final _rect = const Rect.fromLTWH(0, 0, 100, 100);\n  final _paint = Paint();\n  bool _isPressed = false;\n\n  @override\n  bool containsLocalPoint(Vector2 point) => _rect.contains(point.toOffset());\n\n  @override\n  void onTapDown(TapDownEvent event) => _isPressed = true;\n\n  @override\n  void onTapUp(TapUpEvent event) => _isPressed = false;\n\n  @override\n  void onTapCancel(TapCancelEvent event) => _isPressed = false;\n\n  @override\n  void render(Canvas canvas) {\n    _paint.color = _isPressed? Colors.red : Colors.white;\n    canvas.drawRect(_rect, _paint);\n  }\n}\n```\n\n\n### SecondaryTapCallbacks\n\nIn addition to the primary tap events (i.e. left mouse button on desktop), Flame also supports\nsecondary tap events (i.e. right mouse button on desktop). To receive these events, add the\n`SecondaryTapCallbacks` mixin to your `PositionComponent`.\n\n```dart\nclass MyComponent extends PositionComponent with SecondaryTapCallbacks {\n  @override\n  void onSecondaryTapUp(SecondaryTapUpEvent event) {\n    /// Do something\n  }\n\n  @override\n  void onSecondaryTapCancel(SecondaryTapCancelEvent event) {\n    /// Do something\n  }\n\n  @override\n  void onSecondaryTapDown(SecondaryTapDownEvent event) {\n    /// Do something\n  }\n```\n\nYou can extend both `TapCallbacks` and `SecondaryTapCallbacks` in the same component to\nreceive both primary and secondary tap events.\n\n\n### DoubleTapCallbacks\n\nFlame also offers a mixin named `DoubleTapCallbacks` to receive a double-tap event from the\ncomponent. To start receiving double tap events in a component, add the\n`DoubleTapCallbacks` mixin to your `PositionComponent`.\n\n```dart\nclass MyComponent extends PositionComponent with DoubleTapCallbacks {\n  @override\n  void onDoubleTapUp(DoubleTapEvent event) {\n    /// Do something\n  }\n\n  @override\n  void onDoubleTapCancel(DoubleTapCancelEvent event) {\n    /// Do something\n  }\n\n  @override\n  void onDoubleTapDown(DoubleTapDownEvent event) {\n    /// Do something\n  }\n```\n\n\n## Migration\n\nIf you have an existing game that uses `Tappable`/`Draggable` mixins, then this section will\ndescribe how to transition to the new API described in this document. Here's what you need to do:\n\nTake all of your components that uses these mixins, and replace them with\n`TapCallbacks`/`DragCallbacks`.\nThe methods `onTapDown`, `onTapUp`, `onTapCancel` and `onLongTapDown` will need to be adjusted\nfor the new API:\n\n- The argument pair such as `(int pointerId, TapDownDetails details)` was replaced with a single\n  event object `TapDownEvent event`.\n- There is no return value anymore, but if you need to make a component to pass-through the taps\n  to the components below, then set `event.continuePropagation` to true. This is only needed for\n  `onTapDown` events; all other events will pass-through automatically.\n- If your component needs to know the coordinates of the point of touch, use\n  `event.localPosition` instead of computing it manually. Properties `event.canvasPosition` and\n  `event.devicePosition` are also available.\n- If the component is attached to a custom ancestor then make sure that ancestor also have the\n  correct size or implement `containsLocalPoint()`.\n"
  },
  {
    "path": "doc/flame/layout/align_component.md",
    "content": "# AlignComponent\n\n```{dartdoc}\n:package: flame\n:symbol: AlignComponent\n:file: src/layout/align_component.dart\n\n[Align]: https://api.flutter.dev/flutter/widgets/Align-class.html\n[Alignment]: https://api.flutter.dev/flutter/painting/Alignment-class.html\n```\n"
  },
  {
    "path": "doc/flame/layout/column_component.md",
    "content": "# ColumnComponent\n\n```{dartdoc}\n:package: flame\n:symbol: ColumnComponent\n:file: src/experimental/column_component.dart\n```\n"
  },
  {
    "path": "doc/flame/layout/expanded_component.md",
    "content": "# ExpandedComponent\n\n```{dartdoc}\n:package: flame\n:symbol: ExpandedComponent\n:file: src/experimental/expanded_component.dart\n```\n"
  },
  {
    "path": "doc/flame/layout/layout.md",
    "content": "# Layout\n\nPositioning game elements manually with pixel coordinates works for simple cases, but quickly\nbecomes tedious when building HUDs, menus, or any UI that needs to adapt to different screen\nsizes. Flame's layout components bring familiar concepts from Flutter's layout system (rows,\ncolumns, padding, alignment) into the game world, so you can arrange components declaratively\nrather than calculating positions by hand.\n\n- [Align Component](align_component.md)\n- [Row Component](row_component.md)\n- [Column Component](column_component.md)\n- [Expanded Component](expanded_component.md)\n- [Padding Component](padding_component.md)\n\n```{toctree}\n:hidden:\n\nAlignComponent     <align_component.md>\nRowComponent       <row_component.md>\nColumnComponent    <column_component.md>\nExpanded Component <expanded_component.md>\nPadding Component  <padding_component.md>\n```\n"
  },
  {
    "path": "doc/flame/layout/padding_component.md",
    "content": "# PaddingComponent\n\n```{dartdoc}\n:package: flame\n:symbol: PaddingComponent\n:file: src/experimental/padding_component.dart\n```\n"
  },
  {
    "path": "doc/flame/layout/row_component.md",
    "content": "# RowComponent\n\n```{dartdoc}\n:package: flame\n:symbol: RowComponent\n:file: src/experimental/row_component.dart\n```\n"
  },
  {
    "path": "doc/flame/other/debug.md",
    "content": "# Debug features\n\n\n## FlameGame features\n\nFlame provides some debugging features for the `FlameGame` class. These features are enabled when\nthe `debugMode` property is set to `true` (or overridden to be `true`).\nWhen `debugMode` is enabled, each `PositionComponent` will be rendered with their bounding size, and\nhave their positions written on the screen. This way, you can visually verify the components\nboundaries and positions.\n\nCheck out this [working example of the debugging features of the `FlameGame`](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/components/debug_example.dart).\n\n\n## Devtools extension\n\nIf you open the [Flutter DevTools](https://docs.flutter.dev/tools/devtools/overview), you will see a\nnew tab called \"Flame\". This tab will show you information about the current game, for example a\nvisualization of the component tree, the ability to play, pause and step the game, information\nabout the selected component, and more.\n\n\n## FPS\n\nThe FPS reported from Flame might be a bit lower than what is reported from for example the Flutter\nDevTools, depending on which platform you are targeting. The source of truth for how many FPS your\ngame is running in should be the FPS that we are reporting, since that is what our game loop is\nbound by.\n\n\n### FpsComponent\n\nThe `FpsComponent` can be added to anywhere in the component tree and will keep track of how many\nFPS that the game is currently rendering in. If you want to display this as text in the game, use\nthe [](#fpstextcomponent).\n\n\n### FpsTextComponent\n\nThe `FpsTextComponent` is simply a [TextComponent] that wraps an `FpsComponent`, since you most\ncommonly want to show the current FPS somewhere when the `FpsComponent` is used.\n\n\n[TextComponent]: ../rendering/text_rendering.md#textcomponent\n\n\n### ChildCounterComponent\n\n`ChildCounterComponent` is a component that renders the number of children of\ntype `T` from a component (`target`) every second.\nSo for example, the following will render the number of `SpriteAnimationComponent` that are\nchildren of the game `world`:\n\n```dart\nadd(\n  ChildCounterComponent<SpriteAnimationComponent>(\n    target: world,\n  ),\n);\n```\n\n\n### TimeTrackComponent\n\nThis component allows developers to track time spent inside their code. This can be useful for\nperformance debugging time spent in certain parts of the code.\n\nTo use it, add it to your game somewhere (since this is a debug feature, we advise to only add the\ncomponent in a debug build/flavor):\n\n```dart\nadd(TimeTrackComponent());\n```\n\nThen in the code section that you want to track time, do the following:\n\n```dart\nvoid update(double dt) {\n  TimeTrackComponent.start('MyComponent.update');\n  // ...\n  TimeTrackComponent.end('MyComponent.update');\n}\n```\n\nWith the calls above, the added `TimeTrackComponent` will render the elapsed time in\nmicroseconds.\n"
  },
  {
    "path": "doc/flame/other/other.md",
    "content": "# Other\n\nThis section covers additional tools and utilities that don't fit neatly into the other categories\nbut are still important for day-to-day game development: debugging helpers, performance profiling,\nFlutter widget integration, and general-purpose utility functions.\n\n- [Debugging](debug.md)\n- [Utils](util.md)\n- [Widgets](widgets.md)\n- [Performance](performance.md)\n\n```{toctree}\n:hidden:\n\nDebugging    <debug.md>\nUtils        <util.md>\nWidgets      <widgets.md>\nPerformance  <performance.md>\n```\n"
  },
  {
    "path": "doc/flame/other/performance.md",
    "content": "# Performance\n\nJust like any other game engine, Flame tries to be as efficient as possible without making the API\ntoo complex. But given its general purpose nature, Flame cannot make any assumption about the type of\ngame being made. This means game developers will always have some room for performance optimizations\nbased on how their game functions.\n\nOn the other hand, depending on the underlying hardware, there will always be some hard limit on what\ncan be achieved with Flame. But apart from the hardware limits, there are some common pitfalls that\nFlame users can run into, which can be easily avoided by following some simple steps. This section tries\nto cover some optimization tricks and ways to avoid the common performance pitfalls.\n\n```{note}\nDisclaimer: Each Flame project is very different from the others. As a result, solution\ndescribed here cannot guarantee to always produce a significant improvement in performance.\n```\n\n\n## Object creation per frame\n\nCreating objects of a class is very common in any kind of project/game. But object creation is a somewhat\ninvolved operation. Depending on the frequency and amount of objects that are being created, the application\ncan experience some slow down.\n\nIn games, this is something to be very careful of because games generally have a game loop that\nupdates as fast as possible, where each update is called a frame. Depending on the hardware, a\ngame can be updating 30, 60, 120 or even higher frames per second. This means if a new object is\ncreated in a frame, the game will end up creating as many number of objects as the frame count\nper second.\n\nFlame users generally tend to run into this problem when they override the `update` and `render`\nmethod of a `Component`. For example, in the following innocent looking code, a new `Vector2` and\na new `Paint` object is spawned every frame. But the data inside the objects is essentially the\nsame across all frames. Now imagine if there are 100 instances of `MyComponent` in a game running\nat 60 FPS. That would essentially mean 6000 (100 * 60) new instances of `Vector2` and `Paint`\neach will be created every second.\n\n```{note}\nIt is like buying a new computer every time you want to send an email or buying\na new pen every time you want to write something. Sure it gets the job done, but\nit is not very economically smart.\n```\n\n```dart\nclass MyComponent extends PositionComponent {\n  @override\n  void update(double dt) {\n    position += Vector2(10, 20) * dt;\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(size.toRect(), Paint());\n  }\n}\n```\n\nA better way of doing things would be something like as shown below. This code stores the required `Vector2`\nand `Paint` objects as class members and reuses them across all the update and render calls.\n\n```dart\nclass MyComponent extends PositionComponent {\n  final _direction = Vector2(10, 20);\n  final _paint = Paint();\n\n  @override\n  void update(double dt) {\n    position.setValues(\n      position.x + _direction.x * dt, \n      position.y + _direction.y * dt,\n    );\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(size.toRect(), _paint);\n  }\n}\n```\n\n```{note}\nTo summarize, avoid creating unnecessary objects in every frame. Even a seemingly\nsmall object can affect the performance if spawned in high volume.\n```\n\n\n## Unwanted collision checks\n\nFlame has a built-in collision detection system which can detect when any two `Hitbox`es intersect\nwith each other. In an ideal case, this system runs on every frame and checks for collision. It is\nalso smart enough to filter out only the possible collisions before performing the actual\nintersection checks.\n\nDespite this, it is safe to assume that the cost of collision detection will increase as the\nnumber of hitboxes increases. But in many games, the developers are not always interested in\ndetecting collision between every possible pair. For example, consider a simple game where players\ncan fire a `Bullet` component that has a hitbox. In such a game it is likely that the developers\nare not interested in detecting collision between any two bullets, but Flame will still perform\nthose collision checks.\n\nTo avoid this, you can set the `collisionType` for bullet component to `CollisionType.passive`. Doing\nso will cause Flame to completely skip any kind of collision check between all the passive hitboxes.\n\n```{note}\nThis does not mean bullet component in all games must always have a passive hitbox.\nIt is up to the developers to decide which hitboxes can be made passive based on\nthe rules of the game. For example, the Rogue Shooter game in Flame's examples uses\npassive hitbox for enemies instead of the bullets. \n```\n\n\n## Object Pooling\n\nAs mentioned in the \"Object creation per frame\" section, creating and destroying objects\nfrequently can impact performance. For components that are spawned and removed repeatedly\n(like bullets, particles, or enemies), object pooling is an effective optimization technique.\n\nObject pooling reuses objects instead of constantly creating and destroying them. Flame\nprovides the `ComponentPool` class to make object pooling easy and efficient.\n\n\n### ComponentPool\n\nThe `ComponentPool` class manages a pool of reusable components. It automatically handles\nthe component lifecycle: when a pooled component is removed from its parent, it is\nreturned to the pool for reuse.\n\n**Creating a pool:**\n\n```dart\nclass MyGame extends FlameGame {\n  late final ComponentPool<Bullet> bulletPool;\n\n  @override\n  Future<void> onLoad() async {\n    bulletPool = ComponentPool<Bullet>(\n      factory: () => Bullet(),\n      maxSize: 50,      // Maximum number of bullets to keep in the pool\n      initialSize: 10,  // Pre-create 10 bullets for immediate use\n    );\n  }\n}\n```\n\n**Acquiring components from the pool:**\n\nWhen you need a component, use `acquire()` to get one from the pool. If the pool is empty,\na new component will be created automatically using the factory function.\n\n```dart\nvoid spawnBullet(Vector2 position, Vector2 velocity) {\n  final bullet = bulletPool.acquire();\n  bullet.position.setFrom(position);\n  bullet.velocity.setFrom(velocity);\n  world.add(bullet);\n}\n```\n\n**Returning components to the pool:**\n\nComponents are returned to the pool **automatically** when they are removed from the game\ntree. Simply call `removeFromParent()` on the component. There is no manual release step.\n\n```dart\nclass Bullet extends SpriteComponent with CollisionCallbacks {\n  Vector2 velocity = Vector2.zero();\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    position.add(velocity * dt);\n\n    // Remove bullet if it goes off screen. Automatically returned to pool.\n    if (position.x < -100 || position.x > game.size.x + 100) {\n      removeFromParent();\n    }\n  }\n\n  @override\n  void onCollisionStart(Set<Vector2> points, PositionComponent other) {\n    super.onCollisionStart(points, other);\n    // Return to pool on collision. No manual release needed.\n    removeFromParent();\n  }\n\n  @override\n  void onMount() {\n    super.onMount();\n    // Reset visual/internal state here so the component is clean when reused.\n    // Caller-configured state (position, velocity) should NOT be reset here\n    // because it is set between acquire() and add().\n  }\n}\n```\n\n\n### Pool Management\n\n**Checking available components:**\n\nYou can check how many components are currently available in the pool:\n\n```dart\nprint('Available bullets: ${bulletPool.availableCount}');\n```\n\n**Clearing the pool:**\n\nIf you need to free up memory or reset the pool, you can clear all available components:\n\n```dart\nbulletPool.clear();\n```\n\n```{note}\nClearing only affects components currently in the pool. Components that are in use\n(acquired but not yet released) are not affected.\n```\n\n\n### Best Practices\n\n1. **No special mixin needed**: Any `Component` subclass can be pooled. Just pass a factory\n   function to `ComponentPool` and you're ready to go.\n\n2. **Use `onMount` to reset internal state**: Reset visual or internal properties (e.g.\n   animation frame, bounce phase) in `onMount()`. Do not reset caller-configured state\n   (like position or velocity) there, since those are set between `acquire()` and `add()`.\n\n3. **Just call `removeFromParent()`**: Components are returned to the pool automatically\n   when removed. There is no manual release method to call.\n\n4. **Set appropriate pool sizes**: Set `maxSize` based on your game's needs. Too small and\n   you'll create new objects frequently; too large and you'll waste memory.\n\n5. **Use `initialSize` for warm-up**: Set `initialSize` to pre-create commonly used\n   components, reducing frame drops during gameplay.\n\n6. **Pool behavior is LIFO**: The pool uses a stack (Last In, First Out) internally, meaning\n   the most recently returned component will be the next one acquired.\n"
  },
  {
    "path": "doc/flame/other/util.md",
    "content": "# Util\n\nOn this page you can find documentation for some utility classes and methods.\n\n\n## Device Class\n\n```{warning}\nMany methods in this class only work on mobile platforms (Android and iOS).\n\nUsing these methods on other platforms will not have any effect and you will\nget a warning printed on your console when running in debug mode.\n```\n\nThis class can be accessed from `Flame.device` and it has some methods that can be used to control\nthe state of the device, for instance you can change the screen orientation and set whether the\napplication should be fullscreen or not.\n\n\n### `Flame.device.fullScreen()`\n\nWhen called, this disables all `SystemUiOverlay` making the app full screen.\nWhen called in the main method, it makes your app full screen (no top nor bottom bars).\n\n**Note:** It has no effect when called on the web.\n\n\n### `Flame.device.setLandscape()`\n\nThis method sets the orientation of the whole application (effectively, also the game) to landscape\nand depending on operating system and device setting, should allow both left and right landscape\norientations. To set the app orientation to landscape on a specific direction, use either\n`Flame.device.setLandscapeLeftOnly` or `Flame.device.setLandscapeRightOnly`.\n\n**Note:** It has no effect when called on the web.\n\n\n### `Flame.device.setPortrait()`\n\nThis method sets the orientation of the whole application (effectively, also the game) to portrait\nand depending on operating system and device setting, it should allow for both up and down portrait\norientations. To set the app orientation to portrait for a specific direction, use either\n`Flame.device.setPortraitUpOnly` or `Flame.device.setPortraitDownOnly`.\n\n**Note:** It has no effect when called on the web.\n\n\n### `Flame.device.setOrientation()` and `Flame.device.setOrientations()`\n\nIf a finer control of the allowed orientations is required (without having to deal with\n`SystemChrome` directly), `setOrientation` (accepts a single `DeviceOrientation` as a parameter) and\n`setOrientations` (accepts a `List<DeviceOrientation>` for possible orientations) can be used.\n\n**Note:** It has no effect when called on the web.\n\n\n## Timer\n\nFlame provides a simple utility class to help you handle countdowns and timer state changes like\nevents.\n\nCountdown example:\n\n```dart\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass MyGame extends Game {\n  final TextPaint textPaint = TextPaint(\n    style: const TextStyle(color: Colors.white, fontSize: 20),\n  );\n\n  final countdown = Timer(2);\n\n  @override\n  void update(double dt) {\n    countdown.update(dt);\n    if (countdown.finished) {\n      // Prefer the timer callback, but this is better in some cases\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    textPaint.render(\n      canvas,\n      \"Countdown: ${countdown.current.toString()}\",\n      Vector2(10, 100),\n    );\n  }\n}\n\n```\n\nInterval example:\n\n```dart\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass MyGame extends Game {\n  final TextPaint textPaint = TextPaint(\n    style: const TextStyle(color: Colors.white, fontSize: 20),\n  );\n  Timer interval;\n\n  int elapsedSecs = 0;\n\n  MyGame() {\n    interval = Timer(\n      1,\n      onTick: () => elapsedSecs += 1,\n      repeat: true,\n    );\n  }\n\n  @override\n  void update(double dt) {\n    interval.update(dt);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    textPaint.render(canvas, \"Elapsed time: $elapsedSecs\", Vector2(10, 150));\n  }\n}\n\n```\n\n`Timer` instances can also be used inside a `FlameGame` game by using the `TimerComponent` class.\n\n`TimerComponent` example:\n\n```dart\nimport 'package:flame/timer.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\n\nclass MyFlameGame extends FlameGame {\n  MyFlameGame() {\n    add(\n      TimerComponent(\n        period: 10,\n        repeat: true,\n        onTick: () => print('10 seconds elapsed'),\n      )\n    );\n  }\n}\n```\n\n```{note}\nA `Timer` or `TimerComponent` can repeat indefinitely by providing the\n`repeat: true` argument or it can be repeated a certain number of\ntimes by using the `tickCount` argument together with `repeat: true`.\n```\n\n\n## Time Scale\n\nIn many games it is often desirable to create  slow-motion or fast-forward effects based on some in\ngame events. A very common approach to achieve these results is to manipulate the in game time or\ntick rate.\n\nTo make this manipulation easier, Flame provides a `HasTimeScale` mixin. This mixin can be attached\nto any Flame `Component` and exposes a simple get/set API for `timeScale`. The default value of\n`timeScale` is `1`, implying in-game time of the component is running at the same speed as real life\ntime. Setting it to `2` will make the component tick twice as fast and setting it to `0.5` will make\nit tick at half the speed as compared to real life time. This mixin also provides `pause` and `resume`\nmethods, which can be used instead of manually setting the timeScale to 0 and 1 respectively.\n\nSince `FlameGame` is a `Component` too, this mixin can be attached to the `FlameGame` as well. Doing\nso will allow controlling time scale for all the component of the game from a single place.\n\n```{note}\nHasTimeScale cannot control the movement of BodyComponent from flame_forge2d individually.\nIt is only useful if the whole Game or Forge2DWorld is to be time scaled.\n```\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: time_scale\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\n```dart\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\n\nclass MyFlameGame extends FlameGame with HasTimeScale {\n  void speedUp(){\n    timeScale = 2.0;\n  }\n\n  void slowDown(){\n    timeScale = 1.0;\n  }\n}\n```\n\n\n## Extensions\n\nFlame bundles a collection of utility extensions, these extensions are meant to help the developer\nwith shortcuts and conversion methods, here you can find the summary of those extensions.\n\nThey can all be imported from `package:flame/extensions.dart`\n\n\n### Canvas\n\nMethods:\n\n- `scaleVector`: Just like `canvas scale` method, but takes a `Vector2` as an argument.\n- `translateVector`: Just like `canvas translate` method, but takes a `Vector2` as an argument.\n- `renderPoint`: renders a single point on the canvas (mostly for debugging purposes).\n- `renderAt` and `renderRotated`: if you are directly rendering to the `Canvas`, you can use these\n  functions to easily manipulate coordinates to render things on the correct places. They change the\n  `Canvas` transformation matrix but reset afterwards.\n\n\n### Color\n\nMethods:\n\n- `darken`: Darken the shade of the color by an amount between 0 to 1.\n- `brighten`: Brighten the shade of the color by an amount between 0 to 1.\n\nFactories:\n\n- `ColorExtension.fromRGBHexString`: Parses an RGB color from a valid hex string (e.g. #1C1C1C).\n- `ColorExtension.fromARGBHexString`: Parses an ARGB color from a valid hex string (e.g. #FF1C1C1C).\n\n\n### Image\n\nMethods:\n\n- `pixelsInUint8`: Retrieves the pixel data as a `Uint8List`, in the `ImageByteFormat.rawRgba`\n pixel format, for the image.\n- `getBoundingRect`: Get the bounding rectangle of the `Image` as a `Rect`.\n- `size`: The size of an `Image` as `Vector2`.\n- `darken`: Darken each pixel of the `Image` by an amount between 0 to 1.\n- `brighten`: Brighten each pixel of the `Image` by an amount between 0 to 1.\n\n\n### Offset\n\nMethods;\n\n- `toVector2`; Creates an `Vector2` from the `Offset`.\n- `toSize`: Creates a `Size` from the `Offset`.\n- `toPoint`: Creates a `Point` from the `Offset`.\n- `toRect`: Creates a `Rect` starting in (0,0) and its bottom right corner is the [Offset].\n\n\n### Rect\n\nMethods:\n\n- `toOffset`: Creates an `Offset` from the `Rect`.\n- `toVector2`: Creates a `Vector2` starting in (0,0) and goes to the size of the `Rect`.\n- `containsPoint` Whether this `Rect` contains a `Vector2` point or not.\n- `intersectsSegment`; Whether the segment formed by two `Vector2`s intersects this `Rect`.\n- `intersectsLineSegment`: Whether the `LineSegment` intersects the `Rect`.\n- `toVertices`: Turns the four corners of the `Rect` into a list of `Vector2`.\n- `toFlameRectangle`: Converts this `Rect` into a Flame `Rectangle`.\n- `toMathRectangle`: Converts this `Rect` into a `math.Rectangle`.\n- `toGeometryRectangle`: Converts this `Rect` into a `Rectangle` from flame-geom.\n- `transform`: Transforms the `Rect` using a `Matrix4`.\n\nFactories:\n\n- `RectExtension.getBounds`: Construct a `Rect` that represents the bounds of a list of `Vector2`s.\n- `RectExtension.fromCenter`: Construct a `Rect` from a center point (using `Vector2`).\n\n\n### math.Rectangle\n\nMethods:\n\n- `toRect`: Converts this math `Rectangle` into an ui `Rect`.\n\n\n### Size\n\nMethods:\n\n- `toVector2`; Creates an `Vector2` from the `Size`.\n- `toOffset`: Creates a `Offset` from the `Size`.\n- `toPoint`: Creates a `Point` from the `Size`.\n- `toRect`: Creates a `Rect` starting in (0,0) with the size of `Size`.\n\n\n### Vector2\n\nThis class comes from the `vector_math` package and we have some useful extension methods on top of\nwhat is offered by that package.\n\nMethods:\n\n- `toOffset`: Creates a `Offset` from the `Vector2`.\n- `toPoint`: Creates a `Point` from the `Vector2`.\n- `toRect`: Creates a `Rect` starting in (0,0) with the size of `Vector2`.\n- `toPositionedRect`: Creates a `Rect` starting from [x, y] in the `Vector2` and has the size of\n  the `Vector2` argument.\n- `lerp`: Linearly interpolates the `Vector2` towards another Vector2.\n- `rotate`: Rotates the `Vector2` with an angle specified in radians, it rotates around the\n  optionally defined `Vector2`, otherwise around the center.\n- `scaleTo`: Changes the length of the `Vector2` to the length provided, without changing\n  direction.\n- `moveToTarget`: Smoothly moves a Vector2 in the target direction by a given distance.\n\nFactories:\n\n- `Vector2Extension.fromInts`: Create a `Vector2` with ints as input.\n\nOperators:\n\n- `&`: Combines two `Vector2`s to form a Rect, the origin should be on the left and the size on the\n  right.\n- `%`: Modulo/Remainder of x and y separately of two `Vector2`s.\n\n\n### Matrix4\n\nThis class comes from the `vector_math` package. We have created a few extension methods on top\nof what is already offered by `vector_math`.\n\nMethods:\n\n- `translate2`: Translate the `Matrix4` by the given `Vector2`.\n- `transform2`: Create a new `Vector2` by transforming the given `Vector2` using the `Matrix4`.\n- `transformed2`: Transform the input `Vector2` into the output `Vector2`.\n\nGetters:\n\n- `m11`: The first row and first column.\n- `m12`: The first row and second column.\n- `m13`: The first row and third column.\n- `m14`: The first row and fourth column.\n- `m21`: The second row and first column.\n- `m22`: The second row and second column.\n- `m23`: The second row and third column.\n- `m24`: The second row and fourth column.\n- `m31`: The third row and first column.\n- `m32`: The third row and second column.\n- `m33`: The third row and third column.\n- `m34`: The third row and fourth column.\n- `m41`: The fourth row and first column.\n- `m42`: The fourth row and second column.\n- `m43`: The fourth row and third column.\n- `m44`: The fourth row and fourth column.\n\nFactories:\n\n- `Matrix4Extension.scale`: Create a scaled `Matrix4`. Either by passing a `Vector4` or `Vector2`\n   as it's first argument, or by passing x y z doubles.\n"
  },
  {
    "path": "doc/flame/other/widgets.md",
    "content": "# Widgets\n\nOne advantage when developing games with Flutter is the ability to use Flutter's extensive toolset\nfor building UIs, Flame tries to expand on that by introducing widgets that are made with games in\nmind.\n\nHere you can find all the available widgets provided by Flame.\n\nYou can also see all the widgets showcased inside a\n[Dashbook](https://github.com/bluefireteam/dashbook) sandbox in the\n[widgets examples directory](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/widgets).\n\n\n## NineTileBoxWidget\n\nA Nine Tile Box is a rectangle drawn using a grid sprite.\n\nThe grid sprite is a 3x3 grid and with 9 blocks, representing the 4 corners, the 4 sides and the\nmiddle.\n\nThe corners are drawn at the same size, the sides are stretched on the side direction and the middle\nis expanded both ways.\n\nThe `NineTileBoxWidget` implements a `Container` using that standard. This pattern is also\nimplemented as a component in the `NineTileBoxComponent` so that you can add this feature directly\nto your `FlameGame`. To learn more, check the\n[NineTileBoxComponent docs](../components/utility_components.md#ninetileboxcomponent).\n\nHere you can find an example of how to use it (without using the `NineTileBoxComponent`):\n\n```dart\nimport 'package:flame/widgets';\n\nNineTileBoxWidget(\n    image: image, // dart:ui image instance\n    tileSize: 16, // The width/height of the tile on your grid image\n    destTileSize: 50, // The dimensions for the tile on canvas\n    child: SomeWidget(), // Any Flutter widget\n)\n```\n\n\n## SpriteButton\n\n`SpriteButton` is a simple widget that creates a button based on Flame sprites. This can be very\nuseful when trying to create non-default looking buttons. For example when it is easier for you to\nachieve your wanted look by drawing the button in a graphics editor, instead of making it directly\nin Flutter.\n\nHow to use it:\n\n```dart\nSpriteButton(\n    onPressed: () {\n      print('Pressed');\n    },\n    label: const Text('Sprite Button', style: const TextStyle(color: const Color(0xFF5D275D))),\n    sprite: _spriteButton,\n    pressedSprite: _pressedSprite,\n    // Optional, will be shown when onPressed in null.\n    disabledSprite: _disabledSprite,\n    height: _height,\n    width: _width,\n)\n```\n\n\n## SpriteWidget\n\n`SpriteWidget` is a widget used to display a [Sprite](../rendering/images.md#sprite) inside a widget\ntree.\n\nThis is how to use it:\n\n```dart\nSpriteWidget(\n    sprite: yourSprite,\n    anchor: Anchor.center,\n)\n```\n\n\n## SpriteAnimationWidget\n\n`SpriteAnimationWidget` is a widget used to display\n[SpriteAnimations](../rendering/images.md#animation) inside a widget tree.\n\nThis is how to use it:\n\n```dart\nSpriteAnimationWidget(\n    animation: _animation,\n    animationTicker: _animationTicker,\n    playing: true,\n    anchor: Anchor.center,\n)\n```\n"
  },
  {
    "path": "doc/flame/overlays.md",
    "content": "# Overlays\n\nGames often need to display Flutter widgets on top of the game canvas for things like pause menus,\nscore displays, or chat interfaces. Because Flame games live inside a Flutter widget tree, you can\nlayer any Flutter widget over the game surface. The Overlays API makes this especially convenient\nby letting you toggle named widget overlays on and off from within your game code.\n\nSince a Flame game can be wrapped in a widget, it is quite easy to use it alongside other Flutter\nwidgets in your tree. However, if you want to easily show widgets on top of your Flame game, like\nmessages, menu screens or something of that nature, you can use the Widgets Overlay API to make\nthings even easier.\n\n`Game.overlays` enables any Flutter widget to be shown on top of a game instance. This makes it very\neasy to create things like a pause menu or an inventory screen for example.\n\nThe feature can be used via the `game.overlays.add` and `game.overlays.remove` methods that mark an\noverlay to be shown or hidden, respectively, via a `String` argument that identifies the overlay.\nAfter that, you can map each overlay to their corresponding Widget in your `GameWidget` declaration\nby providing an `overlayBuilderMap`.\n\n```dart\n  // Inside your game:\n  final pauseOverlayIdentifier = 'PauseMenu';\n  final secondaryOverlayIdentifier = 'SecondaryMenu';\n\n  // Marks 'SecondaryMenu' to be rendered.\n  overlays.add(secondaryOverlayIdentifier, priority: 1);\n  // Marks 'PauseMenu' to be rendered. Priority = 0 by default \n  // which means the 'PauseMenu' will be displayed under the 'SecondaryMenu'\n  overlays.add(pauseOverlayIdentifier);\n  // Marks 'PauseMenu' to not be rendered. \n  overlays.remove(pauseOverlayIdentifier);\n  // Toggles the 'PauseMenu' overlay.\n  overlays.toggle(pauseOverlayIdentifier);\n```\n\n```dart\n// On the widget declaration\nfinal game = MyGame();\n\nWidget build(BuildContext context) {\n  return GameWidget(\n    game: game,\n    overlayBuilderMap: {\n      'PauseMenu': (BuildContext context, MyGame game) {\n        return Text('A pause menu');\n      },\n      'SecondaryMenu': (BuildContext context, MyGame game) {\n        return Text('A secondary menu');\n      },\n    },\n  );\n}\n```\n\nThe order of rendering for an overlay is determined by the order of the keys in the\n`overlayBuilderMap`.\n\nSee an [example of the Overlays feature](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/system/overlays_example.dart).\n"
  },
  {
    "path": "doc/flame/platforms.md",
    "content": "# Supported Platforms\n\nOne of Flame's biggest advantages is that it inherits Flutter's cross-platform reach. A single\ncodebase can produce games for phones, desktops, and the web. This section covers platform\nsupport details and shows how to deploy your finished game to popular hosting services.\n\nSince Flame runs on top of Flutter, its supported platforms depend on which platforms are\nsupported by Flutter.\n\nAt the moment, Flame supports web, mobile (Android and iOS) and desktop (Windows, macOS and Linux).\n\n\n## Flutter channels\n\nFlame keeps its support on the stable channel. The dev, beta and master channels should work,\nbut we don't support them. This means that issues happening outside the stable channel are not\na priority.\n\n\n## Deploy your game to GitHub Pages\n\nOne easy way to deploy your game online is to use [GitHub Pages](https://pages.github.com/).\nIt is a cool feature from GitHub, by which you can easily host web content from your repository.\n\nHere we will explain the easiest way to get your game hosted using GitHub pages.\n\nFirst, let's create the branch where your deployed files will live:\n\n```shell\ngit checkout -b gh-pages\n```\n\nThis branch can be created from `main` or any other place, it doesn't matter much. After you push\nthat branch go back to your `main` branch.\n\nNow you should add the [flutter-gh-pages](https://github.com/bluefireteam/flutter-gh-pages)\naction to your repository, you can do that by creating a file `gh-pages.yaml` under the folder\n`.github/workflows`.\n\n```yaml\nname: Gh-Pages\n\non:\n  push:\n    branches: [ main ]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n      - uses: subosito/flutter-action@v2\n      - uses: bluefireteam/flutter-gh-pages@v8\n        with:\n          baseHref: /NAME_OF_YOUR_REPOSITORY/\n          webRenderer: canvaskit\n```\n\nBe sure to change `NAME_OF_YOUR_REPOSITORY` to the name of your GitHub repository.\n\nNow, whenever you push something to the `main` branch, the action will run and update your\ndeployed game.\n\nThe game should be available at a URL like this:\n`https://YOUR_GITHUB_USERNAME.github.io/NAME_OF_YOUR_REPOSITORY/`\n\n\n## Deploy your game to itch.io\n\n1. Create a web build, either from your IDE or by running `flutter build web`\n(If it complains about `Missing index.html` run `flutter create . --platforms=web`)\n2. Go into `index.html` and remove the line that says `<base href=\"/\">`\n3. zip the `build/web` folder and upload to itch.io\n\n**Remember that it shouldn't be the `web` directory in your project's root, but in `build/web`!**\n\nIf you are submitting your game to a game jam, remember to make it public and submit it on the\ngame jam page too (many get confused by this).\n\nFurther instructions can be found on\n[itch.io](https://itch.io/docs/creators/html5#getting-started/zip-file).\n\n\n## Deploy your game to Cloudflare Pages\n\n```{note}\nAutomated deployment to Cloudflare Pages is only available for GitHub and GitLab\nrepositories.\n```\n\n[Cloudflare pages](https://pages.cloudflare.com/) is another interesting option to host your\nFlame game online.\n\nSetting up an automated deployment on it is super simple and can be achieved in a few steps:\n\nFirst, create your account on Cloudflare, and once you are logged in, use the `+ Add` button on\nthe top right corner to create your page project.\n\n![Cloudflare add menu screenshot](../images/add_button.png)\n\nNext follow the steps to connect your repository, you can choose between GitHub and GitLab.\n\nYou should then be presented with a screen to configure your project name, which should be\npre-filled with the name of your repository, and the production branch, which will also\nbe pre-filled with `main`.\n\nScrolling down you will see the build settings panel, which should look like this:\n\n![Cloudflare build settings screenshot](../images/build_form.png)\n\nLeave the `Framework preset` as `None` since Flutter is not supported out of the box.\n\nThen on the `Build command` field, enter the following command:\n\n```shell\nif cd flutter; then git pull && cd ..;else\ngit clone https://github.com/flutter/flutter.git; fi &&\n../flutter/bin/flutter doctor && ../flutter/bin/flutter clean &&\n../flutter/bin/flutter build web --release\n```\n\nIt should be entered as a single line, but below you can see it split into multiple lines for\nbetter readability:\n\n```shell\nif cd flutter; then\n  git pull && cd ..\nelse\n  git clone https://github.com/flutter/flutter.git\nfi\n../flutter/bin/flutter doctor\n../flutter/bin/flutter clean\n../flutter/bin/flutter build web --release\n```\n\nSome people might prefer to create a bash script in the root of their repository with the above\ncommands and use it instead of entering the commands directly in the field, so it is up to you.\n\nSet the Build output directory to `build/web`.\n\nIf needed use the advanced options to set environment variables.\n\nFinally, click on the `Save and Deploy` button to start the deployment and that is it. You should\nhave automation ready to deploy your game to Cloudflare Pages every time you push to your\nrepository.\n\n\n### Web support\n\nWhen using Flame on the web some methods may not work. For example `Flame.device.setOrientation` and\n`Flame.device.fullScreen` won't work on web, they can be called, but nothing will happen.\n\nAnother example: pre-caching audio using the `flame_audio` package also doesn't work due to\nAudioplayers not supporting it on web. This can be worked around by using the `http` package,\nand requesting a get to the audio file, that will make the browser cache the file producing the\nsame effect as on mobile.\n\nIf you want to create instances of `ui.Image` on the web you can use our\n`Flame.images.decodeImageFromPixels` method. This wraps the `decodeImageFromPixels` from the `ui`\nlibrary, but with support for the web platform. If the `runAsWeb` argument is set to `true` (by\ndefault it is set to `kIsWeb`) it will decode the image using an internal image method. When the\n`runAsWeb` is `false` it will use the `decodeImageFromPixels`, which is currently not supported on\nthe web.\n"
  },
  {
    "path": "doc/flame/rendering/decorators.md",
    "content": "# Decorators\n\n**Decorators** are classes that can encapsulate certain visual effects and then apply those visual\neffects to a sequence of canvas drawing operations. Decorators are not [Component]s, but they can\nbe applied to components either manually or via the [HasDecorator] mixin. Likewise, decorators are\nnot [Effect]s, although they can be used to implement certain `Effect`s.\n\nThere are a certain number of decorators available in Flame, and it is simple to add one's own if\nnecessary. We are planning to add shader-based decorators once Flutter fully supports them on the\nweb.\n\n\n## Performance considerations\n\nApplying a Decorator to a component can have a significant performance overhead, especially when\nit involves `canvas.saveLayer()`.\n\n- **Decorators**: Use `canvas.saveLayer()` by default to isolate rendering and apply\n  filters. This requires off-screen buffer allocation and GPU context switches. This is\n  computationally expensive but essential for correct visual composition of complex\n  objects (see below).\n- **Effects** (e.g., `OpacityEffect`, `ColorEffect`): Modify the component's properties\n  or `Paint` directly. These are extremely fast and hardware-accelerated, but they apply\n  to each child individually.\n\n\n### Decorators vs Effects: Visual Composition\n\nThe key difference lies in how they handle composite objects (components with multiple\noverlapping children):\n\n1. **Effects (Individual Blend)**: If you apply an `OpacityEffect` to a parent component,\n   Flame will render each child with that opacity. If children overlap, you will see\n   through them to the background and to other children, creating a \"double-exposure\"\n   look.\n2. **Decorators (Group Blend)**: Because decorators use `saveLayer`, they render the\n   entire subtree into a flat buffer first, and then apply the effect to that\n   buffer. This results in a uniform appearance where overlaps are not visible,\n   making the group look like a single solid object.\n\n**Recommendation**:\n\n- Use **Effects** for simple property animations and high-performance color shifts on\n  large numbers of units.\n- Use **Decorators** for advanced post-processing (blurs, tints) and when you need\n  to treat a group of components as a single visual unit.\n\n\n## Flame built-in decorators\n\n\n### PaintDecorator.blur\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: decorator_blur\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\nThis decorator applies a Gaussian blur to the underlying component. The amount of blur can be\ndifferent in the X and Y direction, though this is not very common.\n\n```dart\nfinal decorator = PaintDecorator.blur(3.0);\n```\n\nPossible uses:\n\n- soft shadows;\n- \"out-of-focus\" objects in the distance or very close to the camera;\n- motion blur effects;\n- deemphasize/obscure content when showing a popup dialog;\n- blurred vision when the character is drunk.\n\n\n### PaintDecorator.grayscale\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: decorator_grayscale\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\nThis decorator converts the underlying image into the shades of grey, as if it was a\nblack-and-white photograph. In addition, you can make the image semi-transparent to the desired\nlevel of `opacity`.\n\n```dart\nfinal decorator = PaintDecorator.grayscale(opacity: 0.5);\n```\n\nPossible uses:\n\n- apply to an NPC to turn them into stone, or into a ghost!\n- apply to a scene to indicate that it is a memory of the past;\n- black-and-white photos.\n\n\n### PaintDecorator.tint\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: decorator_tint\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\nThis decorator *tints* the underlying image with the specified color, as if watching it through a\ncolored glass. It is recommended that the `color` used by this decorator was semi-transparent, so\nthat you can see the details of the image below.\n\n```dart\nfinal decorator = PaintDecorator.tint(const Color(0xAAFF0000);\n```\n\nPossible uses:\n\n- NPCs affected by certain types of magic;\n- items/characters in the shadows can be tinted black;\n- tint the scene red to show bloodlust, or that the character is low on health;\n- tint green to show that the character is poisoned or sick;\n- tint the scene deep blue during the night time;\n\n\n### Rotate3DDecorator\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: decorator_rotate3d\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\nThis decorator applies a 3D rotation to the underlying component. You can specify the angles of the\nrotation, as well as the pivot point and the amount of perspective distortion to apply.\n\nThe decorator also supplies the `isFlipped` property, which allows you to determine whether the\ncomponent is currently being viewed from the front side or from the back. This is useful if you want\nto draw a component whose appearance is different in the front and in the back.\n\n```dart\nfinal decorator = Rotate3DDecorator(\n  center: component.center,\n  angleX: rotationAngle,\n  perspective: 0.002,\n);\n```\n\nPossible uses:\n\n- a card that can be flipped over;\n- pages in a book;\n- transitions between app routes;\n- 3d falling particles such as snowflakes or leaves.\n\n\n### Shadow3DDecorator\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: decorator_shadow3d\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\nThis decorator renders a shadow underneath the component, as if the component was a 3D object\nstanding on a plane. This effect works best for games that use isometric camera projection.\n\nThe shadow produced by this generator is quite flexible: you can control its angle, length, opacity,\nblur, etc. For a full description of what properties this decorator has and their meaning, see the\nclass documentation.\n\n```dart\nfinal decorator = Shadow3DDecorator(\n  base: Vector2(100, 150),\n  angle: -1.4,\n  xShift: 200,\n  yScale: 1.5,\n  opacity: 0.5,\n  blur: 1.5,\n);\n```\n\nThe primary purpose of this decorator is to add shadows on the ground to your components. The main\nlimitation is that the shadows are flat and cannot interact with the environment. For example, this\ndecorator cannot handle shadows that fall onto walls or other vertical structures.\n\n\n### HueDecorator\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: decorator_hue\n:show: widget code infobox\n:width: 180\n:height: 160\n```\n\nThis decorator shifts the hue of the underlying component by the specified angle in radians.\n\n```dart\nfinal decorator = HueDecorator(hue: tau / 4);\n```\n\nPossible uses:\n\n- alternative color schemes for enemies (\"palette swapping\");\n- environmental changes (e.g., world turning purple/surreal);\n- power-up indicators.\n\n\n## Using decorators\n\n\n### HasDecorator mixin\n\nThis `Component` mixin adds the `decorator` property, which is initially `null`. If you set this\nproperty to an actual `Decorator` object, then that decorator will apply its visual effect during\nthe rendering of the component. In order to remove this visual effect, simply set the `decorator`\nproperty back to `null`.\n\n\n### PositionComponent\n\n`PositionComponent` (and all the derived classes) already has a `decorator` property, so for these\ncomponents the `HasDecorator` mixin is not needed.\n\nIn fact, the `PositionComponent` uses its decorator in order to properly position the component on\nthe screen. Thus, any new decorators that you'd want to apply to the `PositionComponent` will need\nto be chained (see the [Multiple decorators](#multiple-decorators) section below).\n\nIt is also possible to replace the root decorator of the `PositionComponent`, if you want to create\nan alternative logic for how the component shall be positioned on the screen.\n\n\n### Multiple decorators\n\nIt is possible to apply several decorators simultaneously to the same component: the `Decorator`\nclass supports chaining. That is, if you have an existing decorator on a component and you want to\nadd another one, then you can call `component.decorator.addLast(newDecorator)`. This will add\nthe new decorator at the end of the existing chain. The method `removeLast()` can remove that\ndecorator later.\n\nSeveral decorators can be chain that way. For example, if `A` is an initial decorator, then\n`A.addLast(B)` can be followed by either `A.addLast(C)` or `B.addLast(C)`, and in both cases the\nchain `A -> B -> C` will be created. In practice, it means that the entire chain can be manipulated\nfrom its root, which usually is `component.decorator`.\n\n\n[Component]: ../components/components.md#component\n[Effect]: ../effects/effects.md\n[HasDecorator]: #hasdecorator-mixin\n"
  },
  {
    "path": "doc/flame/rendering/images.md",
    "content": "# Images\n\nTo start off you must have an appropriate folder structure and add the files to the `pubspec.yaml`\nfile, like this:\n\n```yaml\nflutter:\n  assets:\n    - assets/images/player.png\n    - assets/images/enemy.png\n```\n\nImages can be in any format supported by Flutter, which include: JPEG, WebP, PNG, GIF, animated GIF,\nanimated WebP, BMP, and WBMP. Other formats would require additional libraries. For example, SVG\nimages can be loaded via the `flame_svg` library.\n\n\n## Loading images\n\nFlame bundles an utility class called `Images` that allows you to easily load and cache images from\nthe assets directory into memory.\n\nFlutter has a handful of types related to images, and converting everything properly from a local\nasset to an `Image` that can be drawn on Canvas is a bit convoluted. This class allows you to obtain\nan `Image` that can be drawn on the `Canvas` using the `drawImageRect` method.\n\nIt automatically caches any image loaded by filename, so you can safely call it many times.\n\nThe methods for loading and clearing the cache are: `load`, `loadAll`, `clear` and `clearCache`.\nThey return `Future`s for loading the images. These futures must be awaited for before the images\ncan be used in any way. If you do not want to await these futures right away, you can initiate\nmultiple `load()` operations and then await for all of them at once using `Images.ready()` method.\n\nTo synchronously retrieve a previously cached image, the `fromCache` method can be used. If an image\nwith that key was not previously loaded, it will throw an exception.\n\nTo add an already loaded image to the cache, the `add` method can be used and you can set the key\nthat the image should have in the cache. You can retrieve all the keys in the cache using the `keys`\ngetter.\n\nYou can also use `ImageExtension.fromPixels()` to dynamically create an image during the game.\n\nFor `clear` and `clearCache`, do note that `dispose` is called for each removed image from the\ncache, so make sure that you don't use the image afterwards.\n\n\n### Standalone usage\n\nIt can manually be used by instantiating it:\n\n```dart\nimport 'package:flame/cache.dart';\nfinal imagesLoader = Images();\nImage image = await imagesLoader.load('yourImage.png');\n```\n\nBut Flame also offers two ways of using this class without instantiating it yourself.\n\n\n### Flame.images\n\nThere is a singleton, provided by the `Flame` class, that can be used as a global image cache.\n\nExample:\n\n```dart\nimport 'package:flame/flame.dart';\nimport 'package:flame/sprite.dart';\n\n// inside an async context\nImage image = await Flame.images.load('player.png');\n\nfinal playerSprite = Sprite(image);\n```\n\n\n### Game.images\n\nThe `Game` class offers some utility methods for handling images loading too. It bundles an instance\nof the `Images` class, that can be used to load image assets to be used during the game. The game\nwill automatically free the cache when the game widget is removed from the widget tree.\n\nThe `onLoad` method from the `Game` class is a great place for the initial assets to be loaded.\n\nExample:\n\n```dart\nclass MyGame extends Game {\n\n  Sprite player;\n\n  @override\n  Future<void> onLoad() async {\n    // Note that you could also use Sprite.load for this.\n    final playerImage = await images.load('player.png');\n    player = Sprite(playerImage);\n  }\n}\n```\n\nLoaded assets can also be retrieved while the game is running by `images.fromCache`, for example:\n\n```dart\nclass MyGame extends Game {\n\n  // attributes omitted\n\n  @override\n  Future<void> onLoad() async {\n    // other loads omitted\n    await images.load('bullet.png');\n  }\n\n  void shoot() {\n    // This is just an example, in your game you probably don't want to\n    // instantiate new [Sprite] objects every time you shoot.\n    final bulletSprite = Sprite(images.fromCache('bullet.png'));\n    _bullets.add(bulletSprite);\n  }\n}\n```\n\n\n## Loading images over the network\n\nThe Flame core package doesn't offer a built in method to loading images from the network.\n\nThe reason for that is that Flutter/Dart does not have a built in http client, which requires\na package to be used and since there are a couple of packages available out there, we refrain\nfrom forcing the user to use a specific package.\n\nWith that said, it is quite simple to load images from the network once a http client package\nis chosen by the user. The following snippet shows how an `Image` can be fetched from the web\nusing the [http](https://pub.dev/packages/http) package.\n\n```dart\nimport 'package:http/http.dart' as http;\nimport 'package:flutter/painting.dart';\n\nfinal response = await http.get('https://url.com/image.png');\nfinal image = await decodeImageFromList(response.bytes);\n```\n\n```{note}\nCheck [`flame_network_assets`](https://pub.dev/packages/flame_network_assets)\nfor a ready to use network assets solution that provides a built in cache.\n```\n\n\n## Sprite\n\nFlame offers a `Sprite` class that represents an image, or a region of an image.\n\nYou can create a `Sprite` by providing it an `Image` and coordinates that defines the piece of the\nimage that that sprite represents.\n\nFor example, this will create a sprite representing the whole image of the file passed:\n\n```dart\nfinal image = await images.load('player.png');\nSprite player = Sprite(image);\n```\n\nYou can also specify the coordinates in the original image where the sprite is located. This allows\nyou to use sprite sheets and reduce the number of images in memory, for example:\n\n```dart\nfinal image = await images.load('player.png');\nfinal playerFrame = Sprite(\n  image,\n  srcPosition: Vector2(32.0, 0),\n  srcSize: Vector2(16.0, 16.0),\n);\n```\n\nThe default values are `(0.0, 0.0)` for `srcPosition` and `null` for `srcSize` (meaning it will use\nthe full width/height of the source image).\n\nThe `Sprite` class has a render method, that allows you to render the sprite onto a `Canvas`:\n\n```dart\nfinal image = await images.load('block.png');\nSprite block = Sprite(image);\n\n// in your render method\nblock.render(canvas, 16.0, 16.0); //canvas, width, height\n```\n\nYou must pass the size to the render method, and the image will be resized accordingly.\n\nAll render methods from the `Sprite` class can receive a `Paint` instance as the optional named\nparameter `overridePaint` that parameter will override the current `Sprite` paint instance for that\nrender call.\n\n`Sprite`s can also be used as widgets, to do so just use `SpriteWidget` class.\nHere is a complete\n[example using sprite as widgets](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/widgets/sprite_widget_example.dart).\n\n\n### Sprite Bleeding\n\nIn some cases when rendering sprites next to each other, when the edges of the sprites are touching,\nyou may see a rendering artifact called \"ghost lines\" between them.\n\nThis happens especially when the sprites are positioned in coordinates that are not whole numbers,\nor when scaling is applied to the canvas.\n\nThose lines appear because floating-point numbers aren't 100% accurate in computer science. Due\nto rounding errors, even though the sprites are supposed to be touching, they are not rendered that\nway.\n\nOne way to avoid this is to use a technique called \"bleeding\", which consists of adding a very small\nmargin to the edges of the sprites, so that when they are rendered, they will overlap a bit and thus\navoid rendering the ghost lines.\n\nFlame provides a way to do this by using the `bleed` parameter in the `Sprite` render method. This\nis a double value that represents the amount of bleeding to be applied to the edges of the sprite.\n\nFor example, if you do:\n\n```dart\nfinal image = await images.load('player.png');\nfinal playerFrame = Sprite(\n  image,\n  srcPosition: Vector2(32.0, 0),\n  srcSize: Vector2(16.0, 16.0),\n);\nplayerFrame.render(canvas, 16.0, 16.0, bleed: 1.0);\n```\n\nThe sprite will be rendered with a bleed amount of 1.0, meaning that it will have\na value of 1 pixels added to each edge of the sprite.\n\nFor users of the `SpriteComponent`, using the bleeding feature is also quite simple, it is just\na matter of passing a value to the `bleed` attribute in the component constructor:\n\n```dart\nfinal sprite = Sprite(...);\n\nfinal spriteComponent = SpriteComponent(\n  sprite: sprite,\n  size: Vector2.all(16.0),\n  bleed: 1.0, // bleed value\n);\n```\n\nNote that the amount of the bleed value depends on the size of the sprite, so a bleed value of 1.0\nmight not make much difference for a sprite of 100x100.\n\n\n### Sprite Rasterization\n\nRasterizing a sprite is the process of extracting the selected area of the image from that sprite,\nstoring it in memory, and returning a new Sprite that contains that rasterized image.\n\nThat can be used for a variety of reasons, one of the most useful ones is to avoid texture leaking\nwhen using a sprite sheet.\n\nTexture leaking can happen for the same reason as in the issue explained above (floating point\nrounding errors), and it causes parts outside of a sprite selection to also be rendered.\n\nExtracting the sprite selection and rasterizing it before rendering is a way to avoid this issue,\nsince it then renders an image that only contains the selected area.\n\nExample of using a `RasterSpriteComponent`:\n\n```dart\nfinal sprite = await Sprite.load('flame.png');\nfinal rasterSpriteComponent = RasterSpriteComponent(\n  sprite: sprite,\n  size: Vector2.all(16.0),\n);\n```\n\nWhen using the `RasterSpriteComponent`, it will automatically rasterize the sprite when it is\nloaded.\n\nIf you need to rasterize a sprite manually, you can use the `Sprite.rasterize` method:\n\n```dart\nfinal image = await images.load('player.png');\nfinal playerFrame = Sprite(\n  image,\n  srcPosition: Vector2(32.0, 0),\n  srcSize: Vector2(16.0, 16.0),\n);\n\nfinal rasterizedSprite = await playerFrame.rasterize();\n```\n\nBy default, the `rasterize` method will use `Flame.images` to cache the rasterized image,\nauto generating a key based on the sprite's source position and size. If you want to use a custom\nkey for the rasterized image, or use a different cache object, you can pass it as an optional\nparameter:\n\n```dart\nfinal rasterizedSprite = await playerFrame.rasterize(\n  cacheKey: 'custom_key_for_rasterized_image',\n  images: Images(),\n);\n```\n\n\n## SpriteBatch\n\nIf you have a sprite sheet (also called an image atlas, which is an image with smaller images\ninside), and would like to render it effectively - `SpriteBatch` handles that job for you.\n\nGive it the filename of the image, and then add rectangles which describes various part of the\nimage, in addition to transforms (position, scale and rotation) and optional colors.\n\nYou render it with a `Canvas` and an optional `Paint`, `BlendMode` and `CullRect`.\n\nA `SpriteBatchComponent` is also available for your convenience.\n\nSee how to use it in the\n[SpriteBatch examples](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/sprites/sprite_batch_example.dart)\n\n\n## ImageComposition\n\nIn some cases you may want to merge multiple images into a single image; this is called\n[Compositing](https://en.wikipedia.org/wiki/Compositing). This can be useful for example when\nworking with the [SpriteBatch](#spritebatch) API to optimize your drawing calls.\n\nFor such use cases Flame comes with the `ImageComposition` class. This allows you to add multiple\nimages, each at their own position, onto a new image:\n\n```dart\nfinal composition = ImageComposition()\n  ..add(image1, Vector2(0, 0))\n  ..add(image2, Vector2(64, 0));\n  ..add(image3,\n    Vector2(128, 0),\n    source: Rect.fromLTWH(32, 32, 64, 64),\n  );\n  \nImage image = await composition.compose();\nImage imageSync = composition.composeSync();\n```\n\nAs you can see, two versions of composing image are available. Use `ImageComposition.compose()` for\nthe async approach. Or use the new `ImageComposition.composeSync()` function to rasterize the\nimage into GPU context using the benefits of the `Picture.toImageSync` function.\n\n**Note:** Composing images is expensive, we do not recommend you run this every tick as it affect\nthe performance badly. Instead we recommend to have your compositions pre-rendered so you can just\nreuse the output image.\n\n\n## Animation\n\nThe Animation class helps you create a cyclic animation of sprites.\n\nYou can create it by passing a list of equally sized sprites and the stepTime (that is, how many\nseconds it takes to move to the next frame):\n\n```dart\nfinal a = SpriteAnimationTicker(SpriteAnimation.spriteList(sprites, stepTime: 0.02));\n```\n\nAfter the animation is created, you need to call its `update` method and render the current frame's\nsprite on your game instance.\n\nExample:\n\n```dart\nclass MyGame extends Game {\n  SpriteAnimationTicker a;\n\n  MyGame() {\n    a = SpriteAnimationTicker(SpriteAnimation(...));\n  }\n\n  void update(double dt) {\n    a.update(dt);\n  }\n\n  void render(Canvas c) {\n    a.getSprite().render(c);\n  }\n}\n```\n\nA better alternative to generate a list of sprites is to use the `fromFrameData` constructor:\n\n```dart\nconst amountOfFrames = 8;\nfinal a = SpriteAnimation.fromFrameData(\n    imageInstance,\n    SpriteAnimationFrame.sequenced(\n      amount: amountOfFrames,\n      textureSize: Vector2(16.0, 16.0),\n      stepTime: 0.1,\n    ),\n);\n```\n\nThis constructor makes creating an `Animation` very easy when using sprite sheets.\n\nIn the constructor you pass an image instance and the frame data, which contains some parameters\nthat can be used to describe the animation. Check the documentation on the constructors available on\nthe `SpriteAnimationFrameData` class to see all the parameters.\n\nIf you use Aseprite for your animations, Flame does provide some support for Aseprite animation's\nJSON data. To use this feature you will need to export the Sprite Sheet's JSON data, and use\nsomething like the following snippet:\n\n```dart\nfinal image = await images.load('chopper.png');\nfinal jsonData = await assets.readJson('chopper.json');\nfinal animation = SpriteAnimation.fromAsepriteData(image, jsonData);\n```\n\n**Note:** trimmed sprite sheets are not supported by flame, so if you export your sprite sheet this\nway, it will have the trimmed size, not the sprite original size.\n\nAnimations, after created, have an update and render method; the latter renders the current frame,\nand the former ticks the internal clock to update the frames.\n\nAnimations are normally used inside `SpriteAnimationComponent`s, but custom components with several\nAnimations can be created as well.\n\nTo learn more, check out the full example code of\n[using animations as widgets](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/widgets/sprite_animation_widget_example.dart).\n\n\n## SpriteSheet\n\nSprite sheets are big images with several frames of the same sprite on it and is a very good way to\norganize and store your animations. Flame provides a very simple utility class to deal with\nSpriteSheets, using which you can load your sprite sheet image and extract animations from it as\nwell. Following is a simple example of how to use it:\n\n```dart\nimport 'package:flame/sprite.dart';\n\nfinal spriteSheet = SpriteSheet(\n  image: imageInstance,\n  srcSize: Vector2.all(16.0),\n);\n\nfinal animation = spriteSheet.createAnimation(0, stepTime: 0.1);\n```\n\nNow you can use the animation directly or use it in an animation component.\n\nYou can also create a custom animation by retrieving individual `SpriteAnimationFrameData` using\neither `SpriteSheet.createFrameData` or `SpriteSheet.createFrameDataFromId`:\n\n```dart\nfinal animation = SpriteAnimation.fromFrameData(\n  imageInstance, \n  SpriteAnimationData([\n    spriteSheet.createFrameDataFromId(1, stepTime: 0.1), // by id\n    spriteSheet.createFrameData(2, 3, stepTime: 0.3), // row, column\n    spriteSheet.createFrameDataFromId(4, stepTime: 0.1), // by id\n  ]),\n);\n```\n\nIf you don't need any kind of animation and instead only want an instance of a `Sprite` on the\n`SpriteSheet` you can use the `getSprite` or `getSpriteById` methods:\n\n```dart\nspriteSheet.getSpriteById(2); // by id\nspriteSheet.getSprite(0, 0); // row, column\n```\n\nSee a full example of the [`SpriteSheet` class](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/sprites/sprite_sheet_example.dart)\nfor more details on how to work with it.\n\n\n## HasAutoBatchedChildren\n\nFlame introduces automatic sprite batching for improved rendering performance via the\n`HasAutoBatchedChildren` mixin. This mixin enables groups of sprite components to be rendered in a\nsingle draw call per atlas, significantly reducing rendering overhead and improving performance,\nespecially when managing many similar sprites.\n\n\n### Purpose\n\nThe `HasAutoBatchedChildren` mixin is designed for scenarios where you have a group of sprite or\nanimation components (such as enemies, bullets, or particles) that share the same atlas image. By\nbatching their rendering, Flame minimizes the number of draw calls, which is a major performance\nbottleneck in graphics applications.\n\n\n### When to Use\n\nUse this mixin when you have many `SpriteComponent` or `SpriteAnimationComponent` children that:\n\n- Use the same atlas image\n- Have uniform scale\n- Do not require custom decorators or snapshot caching\n- Do not have complex paint effects\n\nThis is ideal for groups of similar objects, such as enemy waves or particle systems.\n\n\n### How to Use\n\nTo enable batching, simply add the mixin to your group component:\n\n```dart\nimport 'package:flame/components.dart';\nimport 'package:flame/src/components/mixins/has_auto_batched_children.dart';\n\nclass EnemyGroup extends PositionComponent with HasAutoBatchedChildren {\n  // Add SpriteComponent or SpriteAnimationComponent children\n}\n```\n\nYou can toggle batching at runtime:\n\n```dart\nfinal group = EnemyGroup();\ngroup.batchingEnabled = false; // falls back to individual rendering\n```\n\nThe mixin works by intercepting per-child rendering (`renderChild`) and post-children rendering\nhooks (`afterChildrenRendered`), accumulating eligible children for batch rendering and flushing\nbatches at priority boundaries to preserve correct render order.\n\n\n### Example\n\n```dart\nclass BulletGroup extends PositionComponent with HasAutoBatchedChildren {\n  // Add SpriteComponent children representing bullets\n}\n\n// Add bullets to the group\nbulletGroup.add(BulletSpriteComponent(...));\n```\n\n\n#### Rogue Shooter Example\n\nSee the [Rogue Shooter game example](https://examples.flame-engine.org/#/Sample_Games_Rogue_Shooter)\nfor a real-world usage of this mixin.\n"
  },
  {
    "path": "doc/flame/rendering/layers.md",
    "content": "# Layers and Snapshots\n\nLayers and snapshots share some common features, including the ability to pre-render and cache\nobjects for improved performance. However, they also have unique features which make them better\nsuited for different use-cases.\n\n`Snapshot` is a mixin that can be added to any `PositionComponent`. Use this for:\n\n- Mixing in to existing game objects (that are `PositionComponents`).\n- Caching game objects, such as sprites, that are complex to render.\n- Drawing the same object many times without rendering it each time.\n- Capturing an image snapshot to save as a screenshot (for example).\n\n`Layer` is a class. Use or extend this class for:\n\n- Structuring your game with logical layers (e.g. UI, foreground, main, background).\n- Grouping objects to form a complex scene, and then caching it (e.g. a background layer).\n- Processor support. Layers allow user-defined processors to run pre- and post- render.\n\n\n## Layers\n\nLayers allow you to group rendering by context, as well as allow you to pre-render things. This\nenables, for example, rendering parts of your game that don't change much in memory, like a\nbackground. By doing this, you'll free processing power for more dynamic content that needs to be\nrendered every game tick.\n\nThere are two types of layers on Flame:\n\n- `DynamicLayer`: For things that are moving or changing.\n- `PreRenderedLayer`: For things that are static.\n\n\n### DynamicLayer\n\nDynamic layers are layers that are rendered every time that they are drawn on the canvas. As the\nname suggests, it is meant for dynamic content and is most useful for grouping rendering of objects\nthat have the same context.\n\nUsage example:\n\n```dart\nclass GameLayer extends DynamicLayer {\n  final MyGame game;\n\n  GameLayer(this.game);\n\n  @override\n  void drawLayer() {\n    game.playerSprite.render(\n      canvas,\n      position: game.playerPosition,\n    );\n    game.enemySprite.render(\n      canvas,\n      position: game.enemyPosition,\n    );\n  }\n}\n\nclass MyGame extends Game {\n  // Other methods omitted...\n\n  @override\n  void render(Canvas canvas) {\n    gameLayer.render(canvas); // x and y can be provided as optional position arguments\n  }\n}\n```\n\n\n### PreRenderedLayer\n\nPre-rendered layers are rendered only once, cached in memory and then just\nreplicated on the game canvas afterwards. They are useful for caching content that doesn't change\nduring the game, like a background for example.\n\nUsage example:\n\n```dart\nclass BackgroundLayer extends PreRenderedLayer {\n  final Sprite sprite;\n\n  BackgroundLayer(this.sprite);\n\n  @override\n  void drawLayer() {\n    sprite.render(\n      canvas,\n      position: Vector2(50, 200),\n    );\n  }\n}\n\nclass MyGame extends Game {\n  // Other methods omitted...\n\n  @override\n  void render(Canvas canvas) {\n    // x and y can be provided as optional position arguments.\n    backgroundLayer.render(canvas);\n  }\n}\n```\n\n\n### Layer Processors\n\nFlame also provides a way to add processors on your layer, which are ways to add effects on the\nentire layer. At the moment, out of the box, only the `ShadowProcessor` is available, this processor\nrenders a back drop shadow on your layer.\n\nTo add processors to your layer, just add them to the layer `preProcessors` or `postProcessors`\nlist, like so:\n\n```dart\n// Works the same for both DynamicLayer and PreRenderedLayer\nclass BackgroundLayer extends PreRenderedLayer {\n  final Sprite sprite;\n\n  BackgroundLayer(this.sprite) {\n    preProcessors.add(ShadowProcessor());\n  }\n\n  @override\n  void drawLayer() { /* omitted */ }\n\n  // ...\n```\n\nCustom processors can be created by extending the `LayerProcessor` class.\n\nSee [a working example of layers](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/layers_example.dart).\n\n\n## Snapshots\n\nSnapshots are an alternative to layers. The `Snapshot` mixin can be applied to any `PositionComponent`.\n\n```dart\nclass SnapshotComponent extends PositionComponent with Snapshot {}\n\nclass MyGame extends FlameGame {\n  late final SnapshotComponent root;\n\n  @override\n  Future<void> onLoad() async {\n    // Add a snapshot component.\n    root = SnapshotComponent();\n    add(root);\n  }\n}\n```\n\n\n### Render as a snapshot\n\nSetting `renderSnapshot` to `true` (the default) on a snapshot-enabled component behaves similarly\nto a `PreRenderedLayer`. The component is rendered only once, cached in memory and then just\nreplicated on the game canvas afterwards. This is useful for caching content that doesn't change\nduring the game, like a background.\n\n```dart\nclass SnapshotComponent extends PositionComponent with Snapshot {}\n\nclass MyGame extends FlameGame {\n  late final SnapshotComponent root;\n  late final SpriteComponent background1;\n  late final SpriteComponent background2;\n\n  @override\n  Future<void> onLoad() async {\n    // Add a snapshot component.\n    root = SnapshotComponent();\n    add(root);\n\n    // Add some children.\n    final background1Sprite = Sprite(await images.load('background1.png'));\n    background1 = SpriteComponent(sprite: background1Sprite);\n    root.add(background1);\n\n    final background2Sprite = Sprite(await images.load('background2.png'));\n    background2 = SpriteComponent(sprite: background2Sprite);\n    root.add(background2);\n\n    // root will now render once (itself and all its children) and then cache\n    // the result. On subsequent render calls, root itself, nor any of its\n    // children, will be rendered. The snapshot will be used instead for\n    // improved performance.\n  }\n}\n```\n\n\n#### Regenerating a snapshot\n\nA snapshot-enabled component will generate a snapshot of its entire tree, including its children.\nIf any of the children change (for example, their position changes, or they are animated), call\n`takeSnapshot` to update the cached snapshot. If they are changing very frequently, it's best not\nto use a `Snapshot` because there will be no performance benefit.\n\nA component rendering a snapshot can still be transformed without incurring any performance cost.\nOnce a snapshot has been taken, the component may still be scaled, moved and rotated. However, if\nthe content of the component changes (what it is rendering) then the snapshot must be regenerated\nby calling `takeSnapshot`.\n\n\n### Taking a snapshot\n\nA snapshot-enabled component can be used to generate a snapshot at any time, even if\n`renderSnapshot` is set to false. This is useful for taking screen-grabs or any other purpose when\nit may be useful to have a static snapshot of all or part of your game.\n\nA snapshot is always generated with no transform applied - i.e. as if the snapshot-enabled\ncomponent is at position (0,0) and has no scale or rotation applied.\n\nA snapshot is saved as a `Picture`, but it can be converted to an `Image` using `snapshotToImage`.\n\n```dart\nclass SnapshotComponent extends PositionComponent with Snapshot {}\n\nclass MyGame extends FlameGame {\n  late final SnapshotComponent root;\n\n  @override\n  Future<void> onLoad() async {\n    // Add a snapshot component, but don't use its render mode.\n    root = SnapshotComponent()..renderSnapshot = false;\n    add(root);\n\n    // Other code omitted.\n  }\n\n  // Call something like this to take an image snapshot at any time.\n  void takeSnapshot() {\n    root.takeSnapshot();\n    final image = root.snapshotToImage(200, 200);\n  }\n}\n```\n\n\n### Snapshots that are cropped or off-center\n\nSometimes your snapshot `Image` may appear cropped, or not in the position you expected.\n\nThis is because the contents of a `Picture` can be positioned anywhere with respect to the origin,\nbut when it is converted to an `Image`, the image always starts from `0,0`. This means that\nanything with a -ve position will be cropped.\n\nThe best way to deal with this is to ensure that your `Snapshot` component is always at position\n`0,0` with respect to your game and you never move it. This means that the image will usually\ncontain what you expect it to.\n\nHowever, this is not always possible. To move (or rotate, or scale etc) the snapshot before\nconverting it to an image, pass a transformation matrix to `snapshotToImage` like so:\n\n```dart\n// Call something like this to take an image snapshot at any time.\nvoid takeSnapshot() {\n  // Prepare a matrix to move the snapshot by 200,50.\n  final matrix = Matrix4.identity()..translate(200.0,50.0);\n\n  root.takeSnapshot();\n  final image = root.snapshotToImage(200, 200, transform: matrix);\n}\n```\n"
  },
  {
    "path": "doc/flame/rendering/palette.md",
    "content": "# Palette\n\nThroughout your game you are going to need to use colors in lots of places. There are two classes on\n`dart:ui` that can be used, `Color` and `Paint`.\n\nThe `Color` class represents a ARGB color in a hexadecimal integer\nformat. So to create a `Color` instance, you just need to pass the color as an integer in the ARGB\nformat.\n\n<!--- cSpell:ignore AARRGGBB -->\nYou can use Dart's hexadecimal notation to make it really easy; for instance: `0xFF00FF00` is fully\nopaque green (the \"mask\" would be `0xAARRGGBB`).\n\n**Note**: The first two hexadecimal digits are for\nthe alpha channel (transparency), unlike on regular (non-A) RGB. The max(FF = 255) for the two first\ndigits means fully opaque, and the min (00 = 0) means fully transparent.\n\nIn the Material Flutter package there is a `Colors` class that provides common colors as constants:\n\n```dart\nimport 'package:flutter/material.dart' show Colors;\n\nconst black = Colors.black;\n```\n\nSome more complex methods might also take a `Paint` object, which is a more complete structure that\nallows you to configure aspects related to stroke, colors, filters and blends.\nHowever, normally when using even the more complex APIs, you just want an instance of a `Paint`\nobject representing just a single simple plain solid color.\n\n**Note:** we don't recommend that you create a new `Paint` object every time you need a specific\n`Paint`, since it could potentially lead to a lot of unnecessary objects being created. A better way\nis to either define the `Paint` object somewhere and re-use it (however, do note that the `Paint`\nclass is mutable, unlike `Color`), or to use the `Palette` class to define all the colors that you\nwant to use in your game.\n\nYou can create such an object like this:\n\n```dart\nPaint green = Paint()..color = const Color(0xFF00FF00);\n```\n\nTo help you with this and also keep your game's color palette consistent, Flame adds the `Palette`\nclass. You can use it to easily access both `Color`s and `Paint`s where needed and also define\nthe colors your game use as constants, so that you don't get those mixed up.\n\nThe `BasicPalette` class is an example of what a palette can look like, and adds black and white as\ncolors. So you can access black or white directly from the `BasicPalette`; for example,\nusing `color`:\n\n```dart\nTextConfig regular = TextConfig(color: BasicPalette.white.color);\n```\n\nOr using `paint`:\n\n```dart\ncanvas.drawRect(rect, BasicPalette.black.paint);\n```\n\nHowever, the idea is that you can create your own palette, following the `BasicPalette` example, and\nadd the color palette/scheme of your game. Then you will be able to statically access any color in\nyour components and classes. Below is an example of a `Palette` implementation, from the [example\ngame BGUG](https://github.com/bluefireteam/bgug/blob/master/lib/palette.dart):\n\n```dart\nimport 'dart:ui';\n\nimport 'package:flame/palette.dart';\n\nclass Palette {\n  static PaletteEntry white = BasicPalette.white;\n\n  static PaletteEntry toastBackground = PaletteEntry(Color(0xFFAC3232));\n  static PaletteEntry toastText = PaletteEntry(Color(0xFFDA9A00));\n\n  static PaletteEntry grey = PaletteEntry(Color(0xFF404040));\n  static PaletteEntry green = PaletteEntry(Color(0xFF54a286));\n}\n```\n\nA `PaletteEntry` is a `const` class that holds information of a color and it has the following\nmembers:\n\n- `color`: returns the `Color` specified\n- `paint`: creates a new `Paint` with the color specified. `Paint` is a non-`const` class, so this\n  method actually creates a brand new instance every time it's called. It's safe to cascade\n  mutations to this.\n"
  },
  {
    "path": "doc/flame/rendering/particles.md",
    "content": "# Particles\n\nFlame offers a basic, yet robust and extendable particle system. The core concept of this system is\nthe `Particle` class, which is very similar in its behavior to the `ParticleSystemComponent`.\n\nThe most basic usage of a `Particle` with `FlameGame` would look as in the following:\n\n```dart\nimport 'package:flame/components.dart';\n\n// ...\n\ngame.add(\n  // Wrapping a Particle with ParticleSystemComponent\n  // which maps Component lifecycle hooks to Particle ones\n  // and embeds a trigger for removing the component.\n  ParticleSystemComponent(\n    particle: CircleParticle(),\n  ),\n);\n```\n\nWhen using `Particle` with a custom `Game` implementation, please ensure that both the `update` and\n`render` methods are called during each game loop tick.\n\nMain approaches to implement desired particle effects:\n\n- Composition of existing behaviors.\n- Use behavior chaining (just a syntactic sugar of the first one).\n- Using `ComputedParticle`.\n\nComposition works in a similar fashion to those of Flutter widgets by defining the effect from top\nto bottom. Chaining allows to express the same composition trees more fluently by defining behaviors\nfrom bottom to top. Computed particles in their turn fully delegate implementation of the behavior\nto your code. Any of the approaches could be used in conjunction with existing behaviors where\nneeded.\n\n```dart\nRandom rnd = Random();\n\nVector2 randomVector2() => (Vector2.random(rnd) - Vector2.random(rnd)) * 200;\n\n// Composition.\n//\n// Defining a particle effect as a set of nested behaviors from top to bottom,\n// one within another:\n//\n// ParticleSystemComponent\n//   > ComposedParticle\n//     > AcceleratedParticle\n//       > CircleParticle\ngame.add(\n  ParticleSystemComponent(\n    particle: Particle.generate(\n      count: 10,\n      generator: (i) => AcceleratedParticle(\n        acceleration: randomVector2(),\n        child: CircleParticle(\n          paint: Paint()..color = Colors.red,\n        ),\n      ),\n    ),\n  ),\n);\n\n// Chaining.\n//\n// Expresses the same behavior as above, but with a more fluent API.\n// Only Particles with SingleChildParticle mixin can be used as chainable behaviors.\ngame.add(\n  ParticleSystemComponent(\n    particle: Particle.generate(\n      count: 10,\n      generator: (i) => pt.CircleParticle(paint: Paint()..color = Colors.red)\n    )\n  )\n);\n\n// Computed Particle.\n//\n// All the behaviors are defined explicitly. Offers greater flexibility\n// compared to built-in behaviors.\ngame.add(\n  ParticleSystemComponent(\n      particle: Particle.generate(\n        count: 10,\n        generator: (i) {\n          Vector2 position = Vector2.zero();\n          Vector2 speed = Vector2.zero();\n          final acceleration = randomVector2();\n          final paint = Paint()..color = Colors.red;\n\n          return ComputedParticle(\n            renderer: (canvas, _) {\n              speed += acceleration;\n              position += speed;\n              canvas.drawCircle(Offset(position.x, position.y), 1, paint);\n            }\n        );\n      }\n    )\n  )\n);\n```\n\nSee more [examples of how to use built-in particles in various\ncombinations](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/particles_example.dart).\n\n\n## Lifecycle\n\nA behavior common to all `Particle`s is that all of them accept a `lifespan` argument. This value is\nused to make the `ParticleSystemComponent` remove itself once its internal `Particle` has reached\nthe end of its life. Time within the `Particle` itself is tracked using the Flame `Timer` class. It\ncan be configured with a `double`, represented in seconds (with microsecond precision) by passing\nit into the corresponding `Particle` constructor.\n\n```dart\nParticle(lifespan: .2); // will live for 200ms.\nParticle(lifespan: 4); // will live for 4s.\n```\n\nIt is also possible to reset a `Particle`'s lifespan by using the `setLifespan` method, which also\naccepts a `double` of seconds.\n\n```dart\nfinal particle = Particle(lifespan: 2);\n\n// ... after some time.\nparticle.setLifespan(2) // will live for another 2s.\n```\n\nDuring its lifetime, a `Particle` tracks the time it was alive and exposes it through the `progress`\ngetter, which returns a value between `0.0` and `1.0`. This value can be used in a similar fashion\nas the `value` property of the `AnimationController` class in Flutter.\n\n```dart\nfinal particle = Particle(lifespan: 2.0);\n\ngame.add(ParticleSystemComponent(particle: particle));\n\n// Will print values from 0 to 1 with step of .1: 0, 0.1, 0.2 ... 0.9, 1.0.\nTimer.periodic(duration * .1, () => print(particle.progress));\n```\n\nThe `lifespan` is passed down to all the descendants of a given `Particle`, if it supports any of\nthe nesting behaviors.\n\n\n## Built-in particles\n\nFlame ships with a few built-in `Particle` behaviors:\n\n- The `TranslatedParticle` translates its `child` by given `Vector2`\n- The `MovingParticle` moves its `child` between two predefined `Vector2`, supports `Curve`\n- The `AcceleratedParticle` allows basic physics based effects, like gravitation or speed dampening\n- The `CircleParticle` renders circles of all shapes and sizes\n- The `SpriteParticle` renders Flame `Sprite` within a `Particle` effect\n- The `ImageParticle` renders *dart:ui* `Image` within a `Particle` effect\n- The `ComponentParticle` renders Flame `Component` within a `Particle` effect\n- The `FlareParticle` renders Flare animation within a `Particle` effect\n\nSee more [examples of how to use built-in Particle behaviors\ntogether](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/particles_example.dart).\nAll the implementations are available in the [particles folder on the\nFlame repository.](https://github.com/flame-engine/flame/tree/main/packages/flame/lib/src/particles)\n\n\n## TranslatedParticle\n\nSimply translates the underlying `Particle` to a specified `Vector2` within the rendering `Canvas`.\nDoes not change or alter its position, consider using `MovingParticle` or `AcceleratedParticle`\nwhere change of position is required. Same effect could be achieved by translating the `Canvas`\nlayer.\n\n```dart\ngame.add(\n  ParticleSystemComponent(\n    particle: TranslatedParticle(\n      // Will translate the child Particle effect to the center of game canvas.\n      offset: game.size / 2,\n      child: Particle(),\n    ),\n  ),\n);\n```\n\n\n## MovingParticle\n\nMoves the child `Particle` between the `from` and `to` `Vector2`s during its lifespan. Supports\n`Curve` via `CurvedParticle`.\n\n```dart\ngame.add(\n  ParticleSystemComponent(\n    particle: MovingParticle(\n      // Will move from corner to corner of the game canvas.\n      from: Vector2.zero(),\n      to: game.size,\n      child: CircleParticle(\n        radius: 2.0,\n        paint: Paint()..color = Colors.red,\n      ),\n    ),\n  ),\n);\n```\n\n\n## AcceleratedParticle\n\nA basic physics particle which allows you to specify its initial `position`, `speed` and\n`acceleration` and lets the `update` cycle do the rest. All three are specified as `Vector2`s, which\nyou can think of as vectors. It works especially well for physics-based \"bursts\", but it is not\nlimited to that. Unit of the `Vector2` value is *logical px/s*. So a speed of `Vector2(0, 100)` will\nmove a child `Particle` by 100 logical pixels of the device every second of game time.\n\n```dart\nfinal rnd = Random();\nVector2 randomVector2() => (Vector2.random(rnd) - Vector2.random(rnd)) * 100;\n\ngame.add(\n  ParticleSystemComponent(\n    particle: AcceleratedParticle(\n      // Will fire off in the center of game canvas\n      position: game.canvasSize/2,\n      // With random initial speed of Vector2(-100..100, 0..-100)\n      speed: Vector2(rnd.nextDouble() * 200 - 100, -rnd.nextDouble() * 100),\n      // Accelerating downwards, simulating \"gravity\"\n      // speed: Vector2(0, 100),\n      child: CircleParticle(\n        radius: 2.0,\n        paint: Paint()..color = Colors.red,\n      ),\n    ),\n  ),\n);\n```\n\n\n## CircleParticle\n\nA `Particle` which renders a circle with given `Paint` at the zero offset of passed `Canvas`. Use in\nconjunction with `TranslatedParticle`, `MovingParticle` or `AcceleratedParticle` in order to achieve\ndesired positioning.\n\n```dart\ngame.add(\n  ParticleSystemComponent(\n    particle: CircleParticle(\n      radius: game.size.x / 2,\n      paint: Paint()..color = Colors.red.withValues(alpha: .5),\n    ),\n  ),\n);\n```\n\n\n## SpriteParticle\n\nAllows you to embed a `Sprite` into your particle effects.\n\n```dart\ngame.add(\n  ParticleSystemComponent(\n    particle: SpriteParticle(\n      sprite: Sprite('sprite.png'),\n      size: Vector2(64, 64),\n    ),\n  ),\n);\n```\n\n\n## ImageParticle\n\nRenders given `dart:ui` image within the particle tree.\n\n```dart\n// During game initialization\nawait Flame.images.loadAll(const [\n  'image.png',\n]);\n\n// ...\n\n// Somewhere during the game loop\nfinal image = await Flame.images.load('image.png');\n\ngame.add(\n  ParticleSystemComponent(\n    particle: ImageParticle(\n      size: Vector2.all(24),\n      image: image,\n    );\n  ),\n);\n```\n\n\n## ScalingParticle\n\nScales the child `Particle` between `1` and `to` during its lifespan.\n\n```dart\ngame.add(\n  ParticleSystemComponent(\n    particle: ScalingParticle(\n      lifespan: 2,\n      to: 0,\n      curve: Curves.easeIn,\n      child: CircleParticle(\n        radius: 2.0,\n        paint: Paint()..color = Colors.red,\n      )\n    );\n  ),\n);\n```\n\n\n## SpriteAnimationParticle\n\nA `Particle` which embeds a `SpriteAnimation`.\nBy default, aligns the `SpriteAnimation`'s `stepTime` so that\nit's fully played during the `Particle` lifespan. It's possible to override this behavior with the\n`alignAnimationTime` argument.\n\n```dart\nfinal spriteSheet = SpriteSheet(\n  image: yourSpriteSheetImage,\n  srcSize: Vector2.all(16.0),\n);\n\ngame.add(\n  ParticleSystemComponent(\n    particle: SpriteAnimationParticle(\n      animation: spriteSheet.createAnimation(0, stepTime: 0.1),\n    );\n  ),\n);\n```\n\n\n## ComponentParticle\n\nThis `Particle` allows you to embed a `Component` within the particle effects. The `Component` could\nhave its own `update` lifecycle and could be reused across different effect trees. If the only thing\nyou need is to add some dynamics to an instance of a certain `Component`, please consider adding it\nto the `game` directly, without the `Particle` in the middle.\n\n```dart\nfinal longLivingRect = RectComponent();\n\ngame.add(\n  ParticleSystemComponent(\n    particle: ComponentParticle(\n      component: longLivingRect\n    );\n  ),\n);\n\nclass RectComponent extends Component {\n  void render(Canvas c) {\n    c.drawRect(\n      Rect.fromCenter(center: Offset.zero, width: 100, height: 100),\n      Paint()..color = Colors.red\n    );\n  }\n\n  void update(double dt) {\n    /// Will be called by parent [Particle]\n  }\n}\n```\n\n\n## ComputedParticle\n\nA `Particle` which could help you when:\n\n- Default behavior is not enough\n- Complex effects optimization\n- Custom easings\n\nWhen created, it delegates all the rendering to a supplied `ParticleRenderDelegate` which is called\non each frame to perform necessary computations and render something to the `Canvas`.\n\n```dart\ngame.add(\n  ParticleSystemComponent(\n    // Renders a circle which gradually changes its color and size during the\n    // particle lifespan.\n    particle: ComputedParticle(\n      renderer: (canvas, particle) => canvas.drawCircle(\n        Offset.zero,\n        particle.progress * 10,\n        Paint()\n          ..color = Color.lerp(\n            Colors.red,\n            Colors.blue,\n            particle.progress,\n          ),\n      ),\n    ),\n  ),\n)\n```\n\n\n## Nesting behavior\n\nFlame's implementation of particles follows the same pattern of extreme composition as Flutter\nwidgets. That is achieved by encapsulating small pieces of behavior in every particle and then\nnesting these behaviors together to achieve the desired visual effect.\n\nTwo entities that allow `Particle`s to nest each other are: `SingleChildParticle` mixin and\n`ComposedParticle` class.\n\nA `SingleChildParticle` may help you with creating `Particles` with a custom behavior. For example,\nrandomly positioning its child during each frame:\n\nThe `SingleChildParticle` may help you with creating `Particles` with a custom behavior.\n\nFor example, randomly positioning it's child during each frame:\n\n```dart\nvar rnd = Random();\n\nclass GlitchParticle extends Particle with SingleChildParticle {\n  Particle child;\n\n  GlitchParticle({\n    required this.child,\n    super.lifespan,\n  });\n\n  @override\n  render(Canvas canvas)  {\n    canvas.save();\n    canvas.translate(rnd.nextDouble() * 100, rnd.nextDouble() * 100);\n\n    // Will also render the child\n    super.render();\n\n    canvas.restore();\n  }\n}\n```\n\nThe `ComposedParticle` could be used either as a standalone or within an existing `Particle` tree.\n"
  },
  {
    "path": "doc/flame/rendering/post_processing.md",
    "content": "# Post Processing and Shaders\n\nPost processing is a technique used in game development to apply visual effects to a component tree\nafter it has been rendered. Once a frame is rendered, either directly or rasterized into an\nimage, post processing can modify or enhance the visuals.\n\nPost processing leverages fragment shaders to create dynamic visual effects such as blur, bloom,\ncolor grading, distortion, and lighting adjustments.\n\nIn Flame, the post processing system is modular and flexible, allowing developers to:\n\n- Define custom post processes by sub-classing the abstract class `PostProcess`.\n- Apply a single post process effect or chain multiple effects using groups.\n- Manage effects globally with the `CameraComponent` or locally with `PostProcessComponent`.\n\n\n## Key Components of the Post Processing System\n\n- **`PostProcess`**: Abstract base class for defining custom post-processing effects. Implement\n  your effect logic in its `postProcess` method.\n\n- **`PostProcessComponent`**: Applies a post process specifically to its children, enabling\n  localized effects.\n\n- **`CameraComponent`**: Applies post processes globally to the entire scene or world.\n\n- **`PostProcessGroup`**: Applies multiple post processes in parallel, useful when effects can be\n  applied independently.\n\n- **`PostProcessSequentialGroup`**: Applies post processes sequentially, where each process uses\n  the output of the previous one.\n\n\n## PostProcessComponent\n\n```{dartdoc}\n:package: flame\n:symbol: PostProcessComponent\n:file: src/post_process/post_process_component.dart\n```\n\n\n## Creating a Custom Post Process\n\nTo implement a custom post process:\n\n1. Subclass `PostProcess`.\n2. Override the `postProcess` method, implementing your rendering logic with `renderSubtree` or\n   `rasterizeSubtree`.\n3. Optionally, implement `onLoad` and `update` methods for managing resources and updating effects\n   each frame.\n\nThis system makes it easy to add creative and useful visual effects to your Flame game.\n\n\n## Example: pixelation\n\nHere’s an example of creating a pixelation effect using a fragment shader:\n\n```dart\nclass PostProcessGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    world.add(\n      PostProcessComponent(\n        postProcess: PixelationPostProcess(),\n        anchor: Anchor.center,\n        children: [\n          EmberPlayer(size: Vector2(100, 100)),\n        ],\n      ),\n    );\n  }\n}\n\nclass PixelationPostProcess extends PostProcess {\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    _fragmentProgram = await FragmentProgram.fromAsset(\n      'packages/flutter_shaders/shaders/pixelation.frag',\n    );\n  }\n\n  late final FragmentProgram _fragmentProgram;\n  late final FragmentShader _fragmentShader = _fragmentProgram.fragmentShader();\n\n  double _time = 0;\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    _time += dt;\n  }\n\n  late final myPaint = Paint()..shader = _fragmentShader;\n\n  @override\n  void postProcess(Vector2 size, Canvas canvas) {\n    final preRenderedSubtree = rasterizeSubtree();\n\n    _fragmentShader.setFloatUniforms((value) {\n      value\n        ..setVector(size / (20 * sin(_time)))\n        ..setVector(size);\n    });\n\n    _fragmentShader.setImageSampler(0, preRenderedSubtree);\n\n    canvas\n      ..save()\n      ..drawRect(Offset.zero & size.toSize(), myPaint)\n      ..restore();\n  }\n}\n\n```\n\nIn this example:\n\n- A fragment shader (`pixelation.frag`) is loaded and used to apply a pixelation effect.\n\n- The `rasterizeSubtree` method captures the component tree rendering as a texture, which the\n  shader uses to generate the pixelated output.\n\n- The effect dynamically changes over time, creating an animated pixelation effect.\n\nThis example demonstrates how straightforward it is to add visual effects to your Flame game using\nthe post-processing system.\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: post_process\n:show: widget code infobox\n:width: 180\n:height: 180\n```\n\nThe pixelation shader file:\n\n```glsl\n#version 460 core\n\nprecision highp float;\n\n#include <flutter/runtime_effect.glsl>\n\nuniform vec2 uPixels;\nuniform vec2 uSize;\nuniform sampler2D uTexture;\n\nout vec4 fragColor;\n\nvoid main() {\n  vec2 uv = FlutterFragCoord().xy / uSize;\n  vec2 puv = round(uv * uPixels) / uPixels;\n  fragColor = texture(uTexture, puv);\n}\n```\n\n\n## Advanced Example: Crystal Ball\n\nFor a more advanced use case of post processing, check out the\n[Crystal Ball example](https://examples.flame-engine.org/), which demonstrates camera-level post\nprocessing and chaining multiple effects using `PostProcessSequentialGroup`.\n\n![Crystal Ball Example](../images/crystal_ball.png)\n\nHere's how multiple post-processing effects are combined on a camera:\n\n```dart\nclass CrystalBallGame extends FlameGame<CrystalBallGameWorld> {\n\n  CrystalBallGame() : super(\n          camera: CameraComponent.withFixedResolution(\n            width: kCameraSize.x,\n            height: kCameraSize.y,\n          ),\n          world: CrystalBallGameWorld(),\n        ) {\n    camera.postProcess = PostProcessGroup(\n      postProcesses: [\n        PostProcessSequentialGroup(\n          postProcesses: [\n            FireflyPostProcess(),\n            WaterPostProcess(),\n          ],\n        ),\n        ForegroundFogPostProcess(),\n      ],\n    );\n  }\n}\n```\n\nIn this code:\n\n- The camera applies a `PostProcessGroup` containing multiple effects.\n- `PostProcessSequentialGroup` chains two effects (`FireflyPostProcess` and `WaterPostProcess`)\n  sequentially.\n- An additional parallel effect (`ForegroundFogPostProcess`) is applied alongside the sequential\n  group.\n\nYou can explore the source code [on GitHub](https://github.com/flame-engine/flame/tree/main/examples/games/crystal_ball).\n"
  },
  {
    "path": "doc/flame/rendering/rendering.md",
    "content": "# Rendering\n\nRendering is how your game draws everything the player sees: sprites, text, particle effects, and\ncustom shapes. Flame builds on Flutter's\n[Canvas](https://api.flutter.dev/flutter/dart-ui/Canvas-class.html) API and adds game-oriented utilities\nfor loading sprite sheets, animating frames, rendering text with bitmap fonts, applying visual\ndecorators, and running post-processing shaders.\n\n- [Post Processing and Shaders](post_processing.md)\n- [Colors and Palette](palette.md)\n- [Decorators](decorators.md)\n- [Images, Sprites and Animations](images.md)\n- [Layers](layers.md)\n- [Particles](particles.md)\n- [Text Rendering](text_rendering.md)\n\n```{toctree}\n:hidden:\n\nPost Processing and Shaders     <post_processing.md>\nColors and Palette              <palette.md>\nDecorators                      <decorators.md>\nImages, Sprites and Animations  <images.md>\nLayers and Snapshots            <layers.md>\nParticles                       <particles.md>\nText Rendering                  <text_rendering.md>\n```\n"
  },
  {
    "path": "doc/flame/rendering/text_rendering.md",
    "content": "# Text Rendering\n\nFlame has some dedicated classes to help you render text.\n\n\n## Text Components\n\nThe simplest way to render text with Flame is to leverage one of the provided text-rendering\ncomponents:\n\n- `TextComponent` for rendering a single line of text\n- `TextBoxComponent` for bounding multi-line text within a sized box, including the possibility of a\ntyping effect. You can use the `newLineNotifier` to be notified when a new line is added. Use the\n`onComplete` callback to execute a function when the text is completely printed.\n- `ScrollTextBoxComponent` enhances the functionality of `TextBoxComponent` by adding vertical\nscrolling capability when the text exceeds the boundaries of the enclosing box.\n\n\nAll components are showcased in [this example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/text_example.dart).\n\n\n### TextComponent\n\n`TextComponent` is a simple component that renders a single line of text.\n\nSimple usage:\n\n```dart\nclass MyGame extends FlameGame {\n  @override\n  void onLoad() {\n    add(\n      TextComponent(\n        text: 'Hello, Flame',\n        position: Vector2.all(16.0),\n      ),\n    );\n  }\n}\n```\n\nIn order to configure aspects of the rendering like font family, size, color, etc, you need to\nprovide (or amend) a `TextRenderer` with such information; while you can read more details about\nthis interface below, the simplest implementation you can use is the `TextPaint`, which takes a\nFlutter `TextStyle`:\n\n```dart\nfinal regular = TextPaint(\n  style: TextStyle(\n    fontSize: 48.0,\n    color: BasicPalette.white.color,\n  ),\n);\n\nclass MyGame extends FlameGame {\n  @override\n  void onLoad() {\n    add(\n      TextComponent(\n        text: 'Hello, Flame',\n        textRenderer: regular,\n        anchor: Anchor.topCenter,\n        position: Vector2(size.width / 2, 32.0),\n      ),\n    );\n  }\n}\n```\n\nYou can find all the options under [TextComponent's\nAPI](https://pub.dev/documentation/flame/latest/components/TextComponent-class.html).\n\n\n### TextBoxComponent\n\n`TextBoxComponent` is very similar to `TextComponent`, but as its name suggest it is used to render\ntext inside a bounding box, creating line breaks according to the provided box size.\n\nYou can decide if the box should grow as the text is written or if it should be static by the\n`growingBox` variable in the `TextBoxConfig`. A static box could either have a fixed size (setting\nthe `size` property of the `TextBoxComponent`), or to automatically shrink to fit the text content.\n\nIn addition, the `align` property allows you to control the horizontal and vertical alignment of\nthe text content. For example, setting `align` to `Anchor.center` will center the text within its\nbounding box both vertically and horizontally.\n\nIf you want to change the margins of the box use the `margins` variable in the `TextBoxConfig`.\n\nFinally, if you want to simulate a \"typing\" effect, by showing each character of the string one by\none as if being typed in real-time, you can provide the `boxConfig.timePerChar` parameter.\n\nTo control the typing effect, call `skip` to show the entire text at once, and `resetAnimation` to\nreset the typing effect back to the beginning without having to recreate the component. Do note\nthat `skip` sets `boxConfig.timePerChar` to `0` so when attempting to replay the typing effect\nafter calling `skip`, make sure to re-set the `boxConfig.timePerChar` right before or after\ncalling `resetAnimation`.\n\nExample usage:\n\n```dart\nclass MyTextBox extends TextBoxComponent {\n  MyTextBox(String text) : super(\n    text: text,\n    textRenderer: tiny,\n    boxConfig: TextBoxConfig(timePerChar: 0.05),\n  );\n\n  final bgPaint = Paint()..color = Color(0xFFFF00FF);\n  final borderPaint = Paint()..color = Color(0xFF000000)..style = PaintingStyle.stroke;\n\n  @override\n  void render(Canvas canvas) {\n    Rect rect = Rect.fromLTWH(0, 0, width, height);\n    canvas.drawRect(rect, bgPaint);\n    canvas.drawRect(rect.deflate(boxConfig.margin), borderPaint);\n    super.render(canvas);\n  }\n}\n```\n\n\nYou can find all the options under [TextBoxComponent's\nAPI](https://pub.dev/documentation/flame/latest/components/TextBoxComponent-class.html).\n\n\n### ScrollTextBoxComponent\n\nThe `ScrollTextBoxComponent` is an advanced version of the `TextBoxComponent`,\ndesigned for displaying scrollable text within a defined area.\nThis component is particularly useful for creating interfaces where large amounts of text\nneed to be presented in a constrained space, such as dialogues or information panels.\n\nNote that the `align` property of `TextBoxComponent` is not available.\n\n\nExample usage:\n\n\n```dart\nclass MyScrollableText extends ScrollTextBoxComponent {\n  MyScrollableText(Vector2 frameSize, String text) : super(\n    size: frameSize,\n    text: text,\n    textRenderer: regular, \n    boxConfig: TextBoxConfig(timePerChar: 0.05),\n  );\n}\n```\n\n\n### TextElementComponent\n\nIf you want to render an arbitrary TextElement, ranging from a single InlineTextElement to a\nformatted DocumentRoot, you can use the `TextElementComponent`.\n\nA simple example is to create a DocumentRoot to render a sequence of block elements (think of an\nHTML \"div\") containing rich text:\n\n```dart\n  final document = DocumentRoot([\n    HeaderNode.simple('1984', level: 1),\n    ParagraphNode.simple(\n      'Anything could be true. The so-called laws of nature were nonsense.',\n    ),\n    // ...\n  ]);\n  final element = TextElementComponent.fromDocument(\n    document: document,\n    position: Vector2(100, 50),\n    size: Vector2(400, 200),\n  );\n```\n\nNote that the size can be specified in two ways; either via:\n\n- the size property common to all `PositionComponents`; or\n- the width/height included within the `DocumentStyle` applied.\n\nAn example applying a style to the document (which can include the size but other parameters as\nwell):\n\n```dart\n  final style = DocumentStyle(\n    width: 400,\n    height: 200,\n    padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),\n    background: BackgroundStyle(\n      color: const Color(0xFF4E322E),\n      borderColor: const Color(0xFF000000),\n      borderWidth: 2.0,\n    ),\n  );\n  final document = DocumentRoot([ ... ]);\n  final element = TextElementComponent.fromDocument(\n    document: document,\n    style: style,\n    position: Vector2(100, 50),\n  );\n```\n\nSee a more elaborate [example of rich-text, formatted\ntext blocks rendering](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/rich_text_example.dart).\n\nFor more details about the underlying mechanics of the text rendering pipeline, see \"Text Elements,\nText Nodes, and Text Styles\" below.\n\n\n### Flame Markdown\n\nIn order to more easily create rich-text-based DocumentRoots, from simple strings with bold/italics\nto complete structured documents, Flame provides the `flame_markdown` bridge package that connects\nthe `markdown` library with Flame's text rendering infrastructure.\n\nJust use the `FlameMarkdown` helper class and the `toDocument` method to convert a markdown string\ninto a DocumentRoot (which can then be used to create a `TextElementComponent`):\n\n```dart\nimport 'package:flame/text.dart';\nimport 'package:flame_markdown/flame_markdown.dart';\n\n// ...\nfinal component = await TextElementComponent.fromDocument(\n  document: FlameMarkdown.toDocument(\n    '# Header\\n'\n    '\\n'\n    'This is a **bold** text, and this is *italic*.\\n'\n    '\\n'\n    'This is a second paragraph.\\n',\n  ),\n  style: ...,\n  position: ...,\n  size: ...,\n);\n```\n\n\n## Infrastructure\n\nIf you are not using the Flame Component System, want to understand the infrastructure behind text\nrendering, want to customize fonts and styles used, or want to create your own custom renderers,\nthis section is for you.\n\n- `TextRenderer`: renderers know \"how\" to render text; in essence they contain the style information\n  to render any string\n- `TextElement`: an element is formatted, \"laid-out\" piece of text, include the string (\"what\") and\n  the style (\"how\")\n\nThe following diagram showcases the class and inheritance structure of the text rendering pipeline:\n\n```{mermaid}\n%%{init: { 'theme': 'dark' } }%%\nclassDiagram\n    %% renderers\n    note for TextRenderer \"This just the style (how).\n    It knows how to take a text string and create a TextElement.\n    `render` is just a helper to `format(text).render(...)`. Same for `getLineMetrics`.\"\n    class TextRenderer {\n        TextElement format(String text)\n        LineMetrics getLineMetrics(String text)\n        void render(Canvas canvas, String text, ...)\n    }\n    class TextPaint\n    class SpriteFontRenderer\n    class DebugTextRenderer\n    \n    %% elements\n    class TextElement {\n        LineMetrics metrics\n        render(Canvas canvas, ...)\n    }\n    class TextPainterTextElement\n        \n    TextRenderer --> TextPaint\n    TextRenderer --> SpriteFontRenderer\n    TextRenderer --> DebugTextRenderer\n\n    TextRenderer *-- TextElement\n    TextPaint *-- TextPainterTextElement\n    SpriteFontRenderer *-- SpriteFontTextElement\n\n    note for TextElement \"This is the text (what) and the style (how);\n    laid out and ready to render.\"\n    TextElement --> TextPainterTextElement\n    TextElement --> SpriteFontTextElement\n    TextElement --> Others\n```\n\n\n### TextRenderer\n\n`TextRenderer` is the abstract class used by Flame to render text. Implementations of `TextRenderer`\nmust include the information about the \"how\" the text is rendered. Font style, size, color, etc. It\nshould be able to combine that information with a given string of text, via the `format` method, to\ngenerate a `TextElement`.\n\nFlame provides two concrete implementations:\n\n- `TextPaint`: most used, uses Flutter `TextPainter` to render regular text\n- `SpriteFontRenderer`: uses a `SpriteFont` (a sprite sheet-based font) to render bitmap text\n- `DebugTextRenderer`: only intended to be used for Golden Tests\n\nBut you can also provide your own if you want to extend to other customized forms of text rendering.\n\nThe main job of a `TextRenderer` is to format a string of text into a `TextElement`, that then can\nbe rendered onto the screen:\n\n```dart\nfinal textElement = textRenderer.format(\"Flame is awesome\")\ntextElement.render(...) \n```\n\nHowever the renderer provides a helper method to directly create the element and render it:\n\n```dart\ntextRenderer.render(\n  canvas,\n  'Flame is awesome',\n  Vector2(10, 10),\n  anchor: Anchor.topCenter,\n);\n```\n\n\n#### TextPaint\n\n`TextPaint` is the built-in implementation of text rendering in Flame. It is based on top of\nFlutter's `TextPainter` class (hence the name), and it can be configured by the style class\n`TextStyle`, which contains all typographical information required to render text; i.e., font size\nand color, font family, etc.\n\nOutside of the style you can also optionally provide one extra parameter which is the\n`textDirection` (but that is typically already set to `ltr` or left-to-right).\n\nExample usage:\n\n```dart\nconst TextPaint textPaint = TextPaint(\n  style: TextStyle(\n    fontSize: 48.0,\n    fontFamily: 'Awesome Font',\n  ),\n);\n```\n\nNote: there are several packages that contain the class `TextStyle`. We export the right one (from\nFlutter) via the `text` module:\n\n```dart\nimport 'package:flame/text.dart';\n```\n\nBut if you want to import it explicitly, make sure that you import it from\n`package:flutter/painting.dart` (or from material or widgets). If you also need to import `dart:ui`,\nyou might need to hide its version of `TextStyle`, since that module contains a different class with\nthe same name:\n\n```dart\nimport 'package:flutter/painting.dart';\nimport 'dart:ui' hide TextStyle;\n```\n\nFollowing are some common properties of `TextStyle`(see the [full\nlist of `TextStyle` properties](https://api.flutter.dev/flutter/painting/TextStyle-class.html)):\n\n- `fontFamily`: a commonly available font, like Arial (default), or a custom font added in your\n pubspec (see [how to add a custom font](https://docs.flutter.dev/cookbook/design/fonts)).\n- `fontSize`: font size, in pts (default `24.0`).\n- `height`: height of text line, as a multiple of font size (default `null`).\n- `color`: the color, as a `ui.Color` (default white).\n\nFor more information regarding colors and how to create them, see the [Colors and\nPalette](palette.md) guide.\n\n\n#### SpriteFontRenderer\n\nThe other renderer option provided out of the box is `SpriteFontRenderer`, which allows you to\nprovide a `SpriteFont` based off of a sprite sheet. TODO\n\n\n#### DebugTextRenderer\n\nThis renderer is intended to be used for Golden Tests. Rendering normal font-based text in Golden\nTests is unreliable due to differences in font definitions across platforms and different algorithms\nused for anti-aliasing. This renderer will render text as if each word was a solid rectangle, making\nit possible to test the layout, positioning and sizing of the elements without having to rely on\nfont-based rendering.\n\n\n## Inline Text Elements\n\nA `TextElement` is a \"pre-compiled\", formatted and laid-out piece of text with a specific styling\napplied, ready to be rendered at any given position.\n\nA `InlineTextElement` implements the `TextElement` interface and must implement their two methods,\none that teaches how to translate it around and another on how to draw it to the canvas:\n\n```dart\n  void translate(double dx, double dy);\n  void draw(Canvas canvas);\n```\n\nThese methods are intended to be overwritten by the implementations of `InlineTextElement`, and\nprobably will not be called directly by users; because a convenient `render` method is provided:\n\n```dart\n  void render(\n    Canvas canvas,\n    Vector2 position, {\n    Anchor anchor = Anchor.topLeft,\n  })\n```\n\nThat allows the element to be rendered at a specific position, using a given anchor.\n\nThe interface also mandates (and provides) a getter for the `LineMetrics` object associated with\nthat `InlineTextElement`, which allows you (and the `render` implementation) to access sizing\ninformation related to the element (width, height, ascend, etc).\n\n```dart\n  LineMetrics get metrics;\n```\n\n\n## Text Elements, Text Nodes, and Text Styles\n\nWhile normal renderers always work with a `InlineTextElement` directly, there is a bigger underlying\ninfrastructure that can be used to render more rich or formatter text.\n\nText Elements are a superset of Inline Text Elements that represent an arbitrary rendering block\nwithin a rich-text document. Essentially, they are concrete and \"physical\": they are objects that\nare ready to be rendered on a canvas.\n\nThis property distinguishes them from Text Nodes, which are structured pieces of text, and from Text\nStyles (called `FlameTextStyle` in code to make it easier to work alongside Flutter's `TextStyle`),\nwhich are descriptors for how arbitrary pieces of text ought to be rendered.\n\nSo, in the most general case, a user would use a `TextNode` to describe a desired piece of rich\ntext; define a `FlameTextStyle` to apply to it; and use that to generate a `TextElement`. Depending\non the type of rendering, the `TextElement` generated will be an `InlineTextElement`, which brings\nus back to the normal flow of the rendering pipeline. The unique property of the Inline-Text-type\nelement is that it exposes a LineMetrics that can be used for advanced rendering; while the other\nelements only expose a simpler `draw` method which is unaware of sizing and positioning.\n\nHowever, the other types of Text Elements, Text Nodes, and Text Styles must be used if the intent is\nto create an entire document (multiple blocks or paragraphs), enriched with formatted text. In order\nto render an arbitrary TextElement, you can alternatively use the `TextElementComponent` (see above).\n\nSee [examples of such usage](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/rich_text_example.dart).\n\n\n### Text Nodes and the Document Root\n\nA `DocumentRoot` is not a `TextNode` (inheritance-wise) in itself but represents a grouping of\n`BlockNodes` that layout a \"page\" or \"document\" of rich text laid out in multiple blocks or\nparagraphs. It represents the entire document and can receive a global Style.\n\nThe first step to define your rich-text document is to create a Node, which will likely be a\n`DocumentRoot`.\n\nIt will first contain the top-most list of Block Nodes that can define headers, paragraphs or\ncolumns.\n\nThen each of those blocks can contain other blocks or the Inline Text Nodes, either Plain Text Nodes\nor some rich-text with specific formatting.\n\nNote that the hierarchy defined by the node structure is also used for styling purposes as per\ndefined in the `FlameTextStyle` class.\n\nThe actual nodes all inherit from `TextNode` and are broken down by the following diagram:\n\n```{mermaid}\n%%{init: { 'theme': 'dark' } }%%\ngraph TD\n    %% Config %%\n    classDef default fill:#282828,stroke:#F6BE00;\n\n    %% Nodes %%\n    TextNode(\"\n        <big><strong>TextNode</strong></big>\n        Can be thought of as an HTML DOM node;\n        each subclass can be thought of as a specific tag.\n    \")\n    BlockNode(\"\n        <big><strong>BlockNode</strong></big>\n        #quot;div#quot;\n    \")\n    InlineTextNode(\"\n        <big><strong>InlineTextNode</strong></big>\n        #quot;span#quot;\n    \")\n    ColumnNode(\"\n        <big><strong>ColumnNode</strong></big>\n        column-arranged group of other Block Nodes\n    \")\n    TextBlockNode(\"\n        <big><strong>TextBlockNode</strong></big>\n        a #quot;div#quot; with an InlineTextNode as a direct child\n    \")\n    HeaderNode(\"\n        <big><strong>HeaderNode</strong></big>\n        #quot;h1#quot; / #quot;h2#quot; / etc\n    \")\n    ParagraphNode(\"\n        <big><strong>ParagraphNode</strong></big>\n        #quot;p#quot;\n    \")\n    GroupTextNode(\"\n        <big><strong>GroupTextNode</strong></big>\n        groups other TextNodes in a single line\n    \")\n    PlainTextNode(\"\n        <big><strong>PlainTextNode</strong></big>\n        just plain text, unformatted\n    \")\n    ItalicTextNode(\"\n        <big><strong>ItalicTextNode</strong></big>\n        #quot;i#quot; / #quot;em#quot;\n    \")\n    BoldTextNode(\"\n        <big><strong>BoldTextNode</strong></big>\n        #quot;b#quot; / #quot;strong#quot;\n    \")\n    TextNode ----> BlockNode\n    TextNode --------> InlineTextNode\n    BlockNode --> ColumnNode\n    BlockNode --> TextBlockNode\n    TextBlockNode --> HeaderNode\n    TextBlockNode --> ParagraphNode\n    InlineTextNode --> GroupTextNode\n    InlineTextNode --> PlainTextNode\n    InlineTextNode --> BoldTextNode\n    InlineTextNode --> ItalicTextNode\n```\n\n\n### (Flame) Text Styles\n\nText Styles can be applied to nodes to generate elements. They all inherit from `FlameTextStyle`\nabstract class (which is named as is to avoid confusion with Flutter's `TextStyle`).\n\nThey follow a tree-like structure, always having `DocumentStyle` as the root; this structure is\nleveraged to apply cascading style to the analogous Node structure. In fact, they are pretty similar\nto, and can be thought of as, CSS definitions.\n\nThe full inheritance chain can be seen on the following diagram:\n\n```{mermaid}\n%%{init: { 'theme': 'dark' } }%%\nclassDiagram\n    %% Nodes %%\n    class FlameTextStyle {\n        copyWith()\n        merge()\n    }\n\n    note for FlameTextStyle \"Root for all styles.\n    Not to be confused with Flutter's TextStyle.\"\n\n    class DocumentStyle {\n        <<for the entire Document Root>>\n        size\n        padding\n        background [BackgroundStyle]\n        specific styles [for blocks & inline]\n    }\n\n    class BlockStyle {\n        <<for Block Nodes>>\n        margin, padding\n        background [BackgroundStyle]\n        text [InlineTextStyle]\n    }\n\n    class BackgroundStyle {\n        <<for Block or Document>>\n        color\n        border\n    }\n\n    class InlineTextStyle {\n        <<for any nodes>>\n        font, color\n    }\n\n    FlameTextStyle <|-- DocumentStyle\n    FlameTextStyle <|-- BlockStyle\n    FlameTextStyle <|-- BackgroundStyle\n    FlameTextStyle <|-- InlineTextStyle\n```\n\n\n### Text Elements\n\nFinally, we have the elements, that represent a combination of a node (\"what\") with a style (\"how\"),\nand therefore represent a pre-compiled, laid-out piece of rich text to be rendered on the Canvas.\n\nInline Text Elements specifically can alternatively be thought of as a combination of a\n`TextRenderer` (simplified \"how\") and a string (single line of \"what\").\n\nThat is because an `InlineTextStyle` can be converted to a specific `TextRenderer` via the\n`asTextRenderer` method, which is then used to lay out each line of text into a unique\n`InlineTextElement`.\n\nWhen using the renderer directly, the entire layout process is skipped, and a single\n`TextPainterTextElement` or `SpriteFontTextElement` is returned.\n\nAs you can see, both definitions of an Element are, essentially, equivalent, all things considered.\nBut it still leaves us with two paths for rendering text. Which one to pick? How to solve this\nconundrum?\n\nWhen in doubt, the following guidelines can help you picking the best path for you:\n\n- for the simplest way to render text, use `TextPaint` (basic renderer implementation)\n  - you can use the FCS provided component `TextComponent` for that.\n- for rendering Sprite Fonts, you must use `SpriteFontRenderer` (a renderer implementation that\n  accepts a `SpriteFont`);\n- for rendering multiple lines of text, with automatic line breaks, you have two options:\n  - use the FCS `TextBoxComponent`, which uses any text renderer to draw each line of text as an\n    Element, and does its own layout and line breaking;\n  - use the Text Node & Style system to create your pre-laid-out Elements. Note: there is no current\n    FCS component for it.\n- finally, in order to have formatted (or rich) text, you must use Text Nodes & Styles.\n"
  },
  {
    "path": "doc/flame/router.md",
    "content": "\n```{flutter-app}\n:sources: ../flame/examples\n:page: router\n:show: widget code infobox\n\nThis example app shows the use of the `RouterComponent` to move across multiple\nscreens within the game. In addition, the \"pause\" button stops time and applies\nvisual effects to the content of the page below it.\n```\n\n\n# RouterComponent\n\nMost games consist of more than a single screen: there is a main menu, a settings page, the\ngameplay screen, pop-up dialogs, and so on. Managing the transitions between these screens can\nquickly become messy. The `RouterComponent` solves this by providing a stack-based navigation\nmodel, similar in spirit to Flutter's [Navigator][Flutter Navigator] class, except that it works\nwith Flame components instead of Flutter widgets.\n\nA typical game will usually consist of multiple pages: the splash screen, the starting menu page,\nthe settings page, credits, the main game page, several pop-ups, etc. The router will organize\nall these destinations and allow you to transition between them.\n\nInternally, the `RouterComponent` contains a stack of routes. When you request it to show a route,\nit will be placed on top of all other pages in the stack. Later you can `pop()` to remove the\ntopmost page from the stack. The pages of the router are addressed by their unique names.\n\nEach page in the router can be either transparent or opaque. If a page is opaque, then the pages\nbelow it in the stack are not rendered and do not receive pointer events (such as taps or drags).\nOn the contrary, if a page is transparent, then the page below it will be rendered and receive\nevents normally. Such transparent pages are useful for implementing modal dialogs, inventory or\ndialogue UIs, etc. If you want your route to be visually transparent but for the routes below it\nto not receive events, make sure to add a background component to your route that captures the\nevents by using one of the [event capturing mixins](inputs/inputs.md).\n\nUsage example:\n\n```dart\nclass MyGame extends FlameGame {\n  late final RouterComponent router;\n\n  @override\n  void onLoad() {\n    add(\n      router = RouterComponent(\n        routes: {\n          'home': Route(HomePage.new),\n          'level-selector': Route(LevelSelectorPage.new),\n          'settings': Route(SettingsPage.new, transparent: true),\n          'pause': PauseRoute(),\n          'confirm-dialog': OverlayRoute.existing(),\n        },\n        initialRoute: 'home',\n      ),\n    );\n  }\n}\n\nclass PauseRoute extends Route { ... }\n```\n\n```{note}\nUse `hide Route` if any of your imported packages export\nanother class called `Route`\n\ne.g.: `import 'package:flutter/material.dart' hide Route;`\n```\n\n\n[Flutter Navigator]: https://api.flutter.dev/flutter/widgets/Navigator-class.html\n\n\n## Route\n\nThe **Route** component holds information about the content of a particular page. `Route`s are\nmounted as children to the `RouterComponent`.\n\nThe main property of a `Route` is its `builder`, the function that creates the component with\nthe content of its page.\n\nIn addition, the routes can be either transparent or opaque (default). An opaque route prevents\nthe route below it from rendering or receiving pointer events, while a transparent route does\nnot. As a rule of thumb, declare the route opaque if it is full-screen, and transparent if it\nis supposed to cover only a part of the screen.\n\nBy default, routes maintain the state of the page component after being popped from the stack\nand the `builder` function is only called the first time a route is activated. Setting\n`maintainState` to `false` drops the page component after the route is popped from the route stack\nand the `builder` function is called each time the route is activated.\n\nThe current route can be replaced using `pushReplacementNamed` or `pushReplacement`.  Each method\nsimply executes `pop` on the current route and then `pushNamed` or `pushRoute`.\n\n\n## WorldRoute\n\nThe **WorldRoute** is a special route that allows setting active game worlds via the router.\nThis type of route can for example be used for swapping levels implemented as separate worlds in\nyour game.\n\nBy default, the `WorldRoute` will replace the current world with the new one and by default it will\nkeep the state of the world after being popped from the stack. If you want the world to be recreated\neach time the route is activated, set `maintainState` to `false`.\n\nIf you are not using the built-in `CameraComponent` you can pass in the camera that you want to use\nexplicitly in the constructor.\n\n```dart\nfinal router = RouterComponent(\n  routes: {\n    'level1': WorldRoute(MyWorld1.new),\n    'level2': WorldRoute(MyWorld2.new, maintainState: false),\n  },\n);\n\nclass MyWorld1 extends World {\n  @override\n  Future<void> onLoad() async {\n    add(BackgroundComponent());\n    add(PlayerComponent());\n  }\n}\n\nclass MyWorld2 extends World {\n   @override\n   Future<void> onLoad() async {\n      add(BackgroundComponent());\n      add(PlayerComponent());\n      add(EnemyComponent());\n   }\n}\n```\n\n\n## OverlayRoute\n\nThe **OverlayRoute** is a special route that allows adding game overlays via the router. These\nroutes are transparent by default.\n\nThere are two constructors for the `OverlayRoute`. The first constructor requires a builder function\nthat describes how the overlay's widget is to be built. The second constructor can be used when the\nbuilder function was already specified within the `GameWidget`:\n\n```dart\nfinal router = RouterComponent(\n  routes: {\n    'ok-dialog': OverlayRoute(\n      (context, game) {\n        return Center(\n          child: DecoratedContainer(...),\n        );\n      },\n    ),  // OverlayRoute\n    'confirm-dialog': OverlayRoute.existing(),\n  },\n);\n```\n\nOverlays that were defined within the `GameWidget` don't even need to be declared within the routes\nmap beforehand: the `RouterComponent.pushOverlay()` method can do it for you. Once an overlay route\nwas registered, it can be activated either via the regular `.pushNamed()` method, or via the\n`.pushOverlay()`. The two methods will do exactly the same, though you can use the second one to\nmake it more clear in your code that an overlay is being added instead of a regular route.\n\nThe current overlay can be replaced using `pushReplacementOverlay`.  This method executes\n`pushReplacementNamed` or `pushReplacement` based on the status of the overlay being pushed.\n\n\n## ValueRoute\n\n```{flutter-app}\n:sources: ../flame/examples\n:page: value_route\n:show: widget code infobox\n:width: 280\n```\n\nA **ValueRoute** is a route that will return a value when it is eventually popped from the stack.\nSuch routes can be used, for example, for dialog boxes that ask for some feedback from the user.\n\nIn order to use `ValueRoute`s, two steps are required:\n\n1. Create a route derived from the `ValueRoute<T>` class, where `T` is the type of the value that\n   your route will return. Inside that class override the `build()` method to construct the\n   component that will be displayed. The component should use the `completeWith(value)` method to\n   pop the route and return the specified value.\n\n   ```dart\n   class YesNoDialog extends ValueRoute<bool> {\n     YesNoDialog(this.text) : super(value: false);\n     final String text;\n\n     @override\n     Component build() {\n       return PositionComponent(\n         children: [\n           RectangleComponent(),\n           TextComponent(text: text),\n           Button(\n             text: 'Yes',\n             action: () => completeWith(true),\n           ),\n           Button(\n             text: 'No',\n             action: () => completeWith(false),\n           ),\n         ],\n       );\n     }\n   }\n   ```\n\n2. Display the route using `Router.pushAndWait()`, which returns a future that resolves with the\n   value returned from the route.\n\n   ```dart\n   Future<void> foo() async {\n     final result = await game.router.pushAndWait(YesNoDialog('Are you sure?'));\n     if (result) {\n       // ... the user is sure\n     } else {\n       // ... the user was not so sure\n     }\n   }\n   ```\n"
  },
  {
    "path": "doc/flame/structure.md",
    "content": "# Assets Directory Structure\n\nGames rely heavily on external assets like images for sprites, audio files for sound effects, and\ntile maps for levels. Organizing these files consistently ensures that Flame's built-in loaders\n(and Flutter's own [asset system](https://docs.flutter.dev/ui/assets/assets-and-images)) can find\nthem without extra configuration.\n\nFlame has a proposed structure for your project that includes the standard Flutter `assets`\ndirectory in addition to some children: `audio`, `images` and `tiles`.\n\nIf using the following example code:\n\n```dart\nclass MyGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    await FlameAudio.play('explosion.mp3');\n\n    // Load some images\n    await Flame.images.load('player.png');\n    await Flame.images.load('enemy.png');\n    \n    // Or load all images in your images folder\n    await Flame.images.loadAllImages();\n\n    final map1 = await TiledComponent.load('level.tmx', tileSize);\n  }\n}\n```\n\nThe following file structure is where Flame would expect to find the files:\n\n```text\n.\n└── assets\n    ├── audio\n    │   └── explosion.mp3\n    ├── images\n    │   ├── enemy.png\n    │   ├── player.png\n    │   └── spritesheet.png\n    └── tiles\n        ├── level.tmx\n        └── map.json\n```\n\nOptionally you can split your `audio` folder into two subfolders, one for `music` and one for `sfx`.\n\nDon't forget to add these files to your `pubspec.yaml` file:\n\n```yaml\nflutter:\n  assets:\n    - assets/audio/explosion.mp3\n    - assets/images/player.png\n    - assets/images/enemy.png\n    - assets/tiles/level.tmx\n```\n\nIf you want to change this structure, this is possible by using the `prefix` parameter and creating\nyour instances of `AssetsCache`, `Images`, and `AudioCache`, instead of using the\nglobal ones provided by Flame.\n\nAdditionally, `AssetsCache` and `Images` can receive a custom\n[`AssetBundle`](https://api.flutter.dev/flutter/services/AssetBundle-class.html).\nThis can be used to make Flame look for assets in a different location other than the `rootBundle`,\nlike the file system for example.\n"
  },
  {
    "path": "doc/index.md",
    "content": "```{include} README.md\n```\n\n```{toctree}\n:hidden:\n\nFlame             <flame/flame.md>\nBridge Packages   <bridge_packages/bridge_packages.md>\nOther Modules     <other_modules/other_modules.md>\nTutorials         <tutorials/tutorials.md>\nDevelopment       <development/development.md>\nResources         <resources/resources.md>\n```\n"
  },
  {
    "path": "doc/other_modules/jenny/jenny.md",
    "content": "<!-- cSpell:ignore Slughorn horcrux horcruxes Moste Potente -->\n\n# Jenny\n\nThe **jenny** library is a toolset for adding *dialogue* into a game. The dialogue may be quite\ncomplex, including user-controlled interactions, branching, dynamically-generated content, commands,\nmarkup, state controlled either from Jenny or from the game, custom functions and commands, etc.\nThe `jenny` library is an unofficial port of the [Yarn Spinner] library for Unity. The name of the\nlibrary comes from [spinning jenny], a kind of yarn-spinning machine.\n\nAdding dialogue into any game generally consists of two stages:\n\n1. Writing the text of the dialogue;\n2. Interactively displaying it within the game.\n\nWith `jenny`, these two tasks are completely separate, allowing the creation of game content and\ndevelopment of the game engine to be independent.\n\n[Yarn Spinner]: https://docs.yarnspinner.dev/\n[spinning jenny]: https://en.wikipedia.org/wiki/Spinning_jenny\n\n\n## Writing dialogue\n\nIn `jenny`, the dialogue is written in plain text and stored in `.yarn` files that are added\nto the game as assets. The `.yarn` file format is developed by the authors of [Yarn Spinner], and\nis specifically designed for writing dialogue.\n\nThe simplest form of the yarn dialogue looks like a play:\n\n```yarn\ntitle: Scene1_Gregory_and_Sampson\n---\nSampson: Gregory, on my word, we'll not carry coals.\nGregory: No, for then we should be colliers.\nSampson: I mean, an we be in choler, we'll draw.\nGregory: Ay, while you live, draw your neck out of collar.\nSampson: I strike quickly being moved.\nGregory: But thou art not quickly moved to strike.\n===\n```\n\nThis simple exchange, when rendered within a game, will be shown as a sequence of phrases spoken\nin turn by the two characters. The `DialogRunner` will allow you to control whether the dialogue\nproceeds automatically or requires \"clicking-through\" by the user.\n\nThe `.yarn` format supports many more advanced features too, allowing the dialogue to proceed\nnon-linearly, supporting variables and conditional execution, giving the player an ability to\nselect their response, etc. Most importantly, the format is so intuitive that it can be generally\nunderstood without having to learn it:\n\n```yarn\ntitle: Slughorn_encounter\n---\n<<if visited(\"Horcrux_question\")>>\n  Slughorn: Sorry, Tom, I don't have time right now.\n  <<stop>>\n<<endif>>\n\nSlughorn: Oh hello, Tom, is there anything I can help you with?\nTom: Good {time_of_day()}, Professor.\n-> I was curious about the 12 uses of the dragon blood.\n    Slughorn: Such an inquisitive mind! You can read about that in the \"Moste \\\n              Potente Potions\" in the Restricted Section of the library.\n    <<give restricted_library_pass>>\n    Tom: Thank you, Professor, this is very munificent of you.\n-> I wanted to ask... about Horcruxes <<if $knows_about_horcruxes>>\n    <<jump Horcrux_question>>\n-> I just wanted to say how much I always admire your lectures.\n    Slughorn: Thank you, Tom. I do enjoy flattery, even if it is well-deserved.\n===\n\ntitle: Horcrux_question\n---\nSlughorn: Where... did you hear that?\n-> Tom: It was mentioned in an old book in the library...\n    Slughorn: I see that you have read more books from the Restricted Section \\\n              than is wise.\n    Slughorn: I'm sorry, Tom, I should have seen you'd be tempted...\n    <<take restricted_library_pass>>\n    -> But Professor!..\n        Slughorn: This is for your good, Tom. Many of those books are dangerous!\n        Slughorn: Now off you go. And do your best to forget about what you \\\n                  asked...\n        <<stop>>\n-> Tom: I overheard it... And the word felt sharp and frigid, like it was the \\\n   embodiment of Dark Art <<if luck() >= 80>>\n    Slughorn: It is a very Dark Art indeed, it is not good for you to know \\\n              about it...\n    Tom: But if I don't know about this Dark Art, how can I defend myself \\\n         against it?\n    Slughorn: It is a Ritual, one of the darkest known to wizard-kind ...\n    ...\n    <<achievement \"The Darkest Secret\">>\n===\n```\n\nThis fragment demonstrates many of the features of the `.yarn` language, including:\n\n- ability to divide the text into smaller chunks called *nodes*;\n- control the flow of the dialog via commands such as `<<if>>` or `<<jump>>`;\n- different dialogue path depending on player's choices;\n- disable certain menu choices dynamically;\n- keep state information in variables;\n- user-defined functions (`time_of_day`, `luck`) and commands (`<<give>>`, `<<take>>`).\n\nFor more information, see the [Yarn Language](language/language.md) section.\n\n\n## Using the dialogue in a game\n\nBy itself, the `jenny` library does not integrate with any game engine. However, it provides a\nruntime that can be used to build such an integration. This runtime consists of the following\ncomponents:\n\n- [`YarnProject`](runtime/yarn_project.md) -- the central repository of information, which knows\n  about all your yarn scripts, variables, custom functions and commands, settings, etc.\n- [`DialogueRunner`](runtime/dialogue_runner.md) -- an executor that can run a specific dialogue\n  node. This executor will send the dialogue lines into one or more `DialogueView`s.\n- [`DialogueView`](runtime/dialogue_view.md) -- an abstract interface describing how the dialogue\n  will be presented to the end user. Implementing this interface is the primary way of integrating\n  `jenny` into a specific environment.\n\n\n```{toctree}\n:hidden:\n\nYarnSpinner language  <language/language.md>\nJenny API             <runtime/jenny_runtime.md>\n```\n"
  },
  {
    "path": "doc/other_modules/jenny/language/commands/character.md",
    "content": "# `<<character>>`\n\nThe **\\<\\<character\\>\\>** command declares a character with the given name, and one or more aliases\nthat can be used in the scripts.\n\nThe command has several purposes:\n\n- it protects you from accidentally misspelling a character's name in your script;\n- it allows a character to have *full name*, which doesn't have to be an ID;\n- it allows declaring multiple aliases for the same character, which can be used in different\n  nodes (an alias may even be in a different language than the full name);\n- you can associate additional data with each character, which will then be available at runtime.\n\nThe format of this command is the following:\n\n```yarn\n<<character \"FULL NAME\" alias1 alias2...>>\n```\n\nThe *full name* here is optional: if given, it will be considered *the* name of the character.\nHowever, if the name is omitted, then the first alias will be considered the true character's name.\nEach *alias* must be a valid ID, and at least one alias must be provided. For example:\n\n```yarn\n// A well-mannered seven-year-old girl, who nevertheless always gets into\n// all kinds of zany adventures.\n<<character Alice>>\n\n// A magical cat known for his ability to grin majestically, and partially\n// vanish. He is mad (by his own admission).\n<<character \"Cheshire Cat\" Cat Cheshire>>\n\n// A foul-tempered Queen, who is also a playing card. Described as\n// \"a blind fury\", her favorite saying is \"Off with their heads!\".\n// Not to be confused with Red Queen.\n<<character \"Queen of Hearts\" Queen QoH QH>>\n```\n\nAfter a character is declared, any of its aliases can be used in the script: they will all refer\nto the same `Character` object. At the same time, using a character without declaring it first is\nnot allowed (unless a special flag in `YarnProject` is set to allow this).\n\n```yarn\ntitle: Alice_and_the_Cat\n---\nAlice: But I don't want to go among mad people.\nCat:   Oh, you can't help that, we're all mad here. I'm mad. You're mad.\nAlice: How do you know I'm mad?\nCat:   You must be, or you wouldn't have come here.\nAlice: And how do you know that you're mad?\nCat:   To begin with, a dog's not mad. You grant that?\nAlice: I suppose so.\nCat:   Well then, you see a dog growls when it's angry, and wags its tail \\\n       when it's pleased.\nCat:   Now, [i]I[/i] growl when I'm pleased, and wag my tail when I'm angry. \\\n       Therefore, I'm mad.\nAlice: [i]I[/i] call it purring, not growling.\nCat:   Call it what you like.\n===\n```\n"
  },
  {
    "path": "doc/other_modules/jenny/language/commands/commands.md",
    "content": "# Commands\n\nThe **commands** are special instructions surrounded with double angle-brackets: `<<stop>>`. There\nare both *built-in* and *user-defined* commands.\n\nThe **built-in** commands are those that are supported by the YarnSpinner runtime itself. Typically\nthey would alter the execution of the dialogue, or perform a similar dialogue-related function. The\nfull list of such commands is given below.\n\nThe **user-defined** commands are those that you yourself create and then use within your yarn\nscripts. For a full description of these commands, see the document on [user-defined commands].\n\n\n## Built-in commands\n\n\n### Variables\n\n**[\\<\\<character\\>\\>](character.md)**\n: Declares a character (person).\n\n**[\\<\\<declare\\>\\>](declare.md)**\n: Declares a global variable.\n\n**[\\<\\<local\\>\\>](local.md)**\n: Declares a local variable.\n\n**[\\<\\<set\\>\\>](set.md)**\n: Updates the value of a variable (either local or global).\n\n\n### Control flow\n\n**[\\<\\<if\\>\\>](if.md)**\n: Conditionally executes certain statements. This is equivalent to the **if** keyword in most\n  programming languages.\n\n**[\\<\\<jump\\>\\>](jump.md)**\n: Switches execution to another node.\n\n**[\\<\\<stop\\>\\>](stop.md)**\n: Stops executing the current node.\n\n**[\\<\\<visit\\>\\>](visit.md)**\n: Temporarily jumps to another node, and then comes back.\n\n**[\\<\\<wait\\>\\>](wait.md)**\n: Pauses the dialogue for the specified amount of time.\n\n\n[user-defined commands]: user_defined_commands.md\n\n```{toctree}\n:hidden:\n\n<<character>>          <character.md>\n<<declare>>            <declare.md>\n<<if>>                 <if.md>\n<<jump>>               <jump.md>\n<<local>>              <local.md>\n<<set>>                <set.md>\n<<stop>>               <stop.md>\n<<visit>>              <visit.md>\n<<wait>>               <wait.md>\nUser-defined commands  <user_defined_commands.md>\n```\n"
  },
  {
    "path": "doc/other_modules/jenny/language/commands/declare.md",
    "content": "# `<<declare>>`\n\nThe **\\<\\<declare\\>\\>** command creates a new global variable and assigns an initial value to it.\nAfter this command is encountered, the declared variable will be available for use anywhere a\nvariable might be needed, including in inline expressions, other commands, and even other declare\nstatements.\n\nUnlike most other commands, the `<<declare>>` command is executed at compile time, i.e. when the\nyarn scripts are parsed. When the dialogue runs, it has no effect, since by that time the variable\nis already initialized and ready for use. For this reason, the `<<declare>>` commands must be\nplaced outside of nodes, at the root level of the script, making it clear that these commands do\nnot execute when a node runs.\n\nFor example:\n\n```yarn\n<<declare $monicker = \"boy\">>\n\n---------------\ntitle: Greeting\n---------------\nTeacher: Welcome to the class, {$monicker}!\n===\n```\n\nHere the `<<declare>>` command introduces a new variable called `$monicker`, of type `String`, and\nassigns it an initial value of `\"boy\"`. Later on, this variable is used inside the \"Greeting\" node.\nBy that time, the value of the variable can be anything: it could be changed in some other node, or\nby the game itself. The `<<declare>>` statement, however, is necessary to tell Jenny that this is\na valid variable name, and what type it has.\n\nFrom the project organization standpoint, the recommended approach is to put all the `<<declare>>`\nstatements into a separate file, and then make sure that this yarn file is parsed first. This will\nensure that all global variables are declared before they are used in subsequent nodes.\n\nIf your game supports save-games, then you would probably want to store the values of yarn global\nvariables too. In this case restoring the saved values should be done *after* all yarn scripts are\nparsed (otherwise the engine will think that a variable is declared twice).\n\n\n## Syntax\n\nThere are several forms of the `<<declare>>` statement. The most common one is the following:\n\n```yarn\n<<declare $VARIABLE = EXPRESSION>>\n```\n\nHere `$VARIABLE` is the name of the variable being declared (all variables in Yarn start with a `$`\nsign), and `EXPRESSION` is either a literal or a more complicated [expression] that will be\nevaluated at compile time in order to provide the initial value for the variable. The type of the\nvariable will be deduced from the type of the `EXPRESSION`.\n\nAnother possible syntax for the `<<declare>>` command is this:\n\n```yarn\n<<declare $VARIABLE as TYPE>>\n```\n\nwhere `TYPE` is one of `Bool`, `Number`, or `String`. This will create a variable of the given type,\nand initialize it with values `false`, `0`, or `\"\"` respectively.\n\nFinally, it is possible to combine these two syntaxes:\n\n```yarn\n<<declare $VARIABLE = EXPRESSION as TYPE>>\n```\n\nThis can be useful when the type of the `EXPRESSION` is not immediately obvious, and you want to\nmake the declaration more explicit. The compiler will check that the type of the `EXPRESSION` is\nthe same as `TYPE`, and will throw a compile-time error otherwise.\n\n\n## Examples\n\n```yarn\n<<declare $prefix = \"Mr.\">>\n<<declare $gold = 100>>\n<<declare $been_to_hell = false>>\n\n<<declare $name as String>>\n<<declare $distanceTraveled as Number>>\n\n<<declare $birthDay = randomRange(1, 365) as Number>>\n<<declare $vulgarity = GetObscenitySetting() as Bool>>\n```\n\n:::{note}\nIt is a good idea to accompany each `<<declare>>` with a doc-comment explaining the purpose of the\nvariable, similarly to how you would document public members of a class.\n:::\n\n\n[expression]: ../expressions/expressions.md\n"
  },
  {
    "path": "doc/other_modules/jenny/language/commands/if.md",
    "content": "# `<<if>>`\n\nThe **\\<\\<if\\>\\>** command evaluates its condition, and based on that decides which statements to\nexecute next. This is equivalent to `if` keyword in most programming languages. This command may\nhave multiple parts, which look as follows:\n\n```yarn\n<<if condition1>>\n  statements1...\n<<elseif condition2>>\n  statements2...\n<<else>>\n  statementsN...\n<<endif>>\n```\n\n- The conditions within each command must have boolean type.\n- There could be any number of `<<elseif>>` blocks.\n- The `<<elseif>>` blocks and `<<else>>` are optional.\n- The final `<<endif>>` is mandatory.\n- The statements within each block must be indented.\n\nAt runtime, the condition within the `if` block is evaluated first. If it turns out to be `true`,\nthen the dialogue proceeds with executing `statements1`, and no other conditions are evaluated nor\nother statement blocks executed. However, if `condition1` evaluated to `false`, then `condition2`\nis calculated. If it is true, then the dialogue runner will execute `statements2`, and if false it\nwill fall-through into the `else` block and execute `statementsN`. In the end, the dialogue will\nproceed to statements that occur after the final `<<endif>>`.\n\n\n## Example\n\nIn this dialogue a *Guard* will greet you differently depending on your reputation with the\ncitizens of the area. If your reputation falls below −100, you'll be attacked on sight.\n\n```yarn\ntitle: GuardGreeting\n---\n<<if $reputation >= 100>>\n  Guard: Hail to the savior of the people!\n<<elseif $reputation >= 30>>\n  Guard: Nice to meet you, sir!\n<<elseif $reputation >= 0>>\n  Guard: Hello\n<<elseif $reputation > -30>>\n  Guard: I'm keeping an eye on you...\n<<elseif $reputation > -100>>\n  Guard: You filthy scum!\n<<else>>\n  Guard: You'll pay for your crimes! #auto\n  <<attack>>\n<<endif>>\n===\n```\n"
  },
  {
    "path": "doc/other_modules/jenny/language/commands/jump.md",
    "content": "# `<<jump>>`\n\nThe **\\<\\<jump\\>\\>** command stops executing the current node, and then immediately starts running\nthe target node. This is similar to a `goto` in many programming languages. For example:\n\n```yarn\n<<jump FarewellScene>>\n```\n\nThe argument of this command is the id of the node to jump to. It can be given either as a plain\nnode ID, or as an expression in curly braces:\n\n```yarn\n<<jump {\"Ending_\" + $ending}>>\n```\n\nIf the expression evaluates at runtime to an unknown name, then a `NameError` exception will be\nthrown.\n\n\n## See Also\n\n- [\\<\\<visit\\>\\>](visit.md) command, which jumps into the destination node temporarily and then\n  returns to the same place in the dialogue as before.\n"
  },
  {
    "path": "doc/other_modules/jenny/language/commands/local.md",
    "content": "# `<<local>>`\n\nThe **\\<\\<local\\>\\>** command creates a new variable within the current node, and initializes it\nto some starting value. Thus, it is similar to [\\<\\<declare\\>\\>][declare], except that the variable\nit creates is visible within a single node only.\n\nThe syntax of the `<<local>>` command can be one of the following:\n\n```yarn\n<<local $VARIABLE = EXPRESSION>>\n<<local $VARIABLE = EXPRESSION as TYPE>>\n```\n\nThis would create a variable with the name `$VARIABLE` (all variables in YarnSpinner start with a\n`$` sign), and assign it the value of `EXPRESSION`. In the second form, it will ensure that the\ntype of the expression is equal to `TYPE`, otherwise a compile-time error will be thrown. Thus, the\nsecond form serves as the explicit annotation for the type of the variable created.\n\nThe following restrictions apply:\n\n- each local variable can be declared only once within a node;\n- the name of a local variable cannot coincide with the name of any global variable.\n\n\n## Examples\n\nIn this example the variable `$roll` will only be needed temporarily within this one node, so it\nwouldn't make sense to declare it as global.\n\n```yarn\ntitle: a_dice_roll\n---\n<<local $roll = dice(6)>>\n<<if $roll == 1>>\n  You've rolled 1, rotten luck...\n<<elseif $roll == 2>>\n  You've rolled 2, which is still below the average. Try harder!\n<<elseif $roll == 3>>\n  You've rolled 3.14159265 (well, almost).\n<<elseif $roll == 4>>\n  Your roll is an unlucky number. Please roll again\n<<else>>\n  You've rolled 10 (when rounded to the nearest ten). Good job!\n<<endif>>\n===\n```\n\n[declare]: declare.md\n"
  },
  {
    "path": "doc/other_modules/jenny/language/commands/set.md",
    "content": "# `<<set>>`\n\nThe **\\<\\<set\\>\\>** command is used to update the value of an existing variable. The variable\nmust be declared with [\\<\\<declare\\>\\>][declare] or [\\<\\<local\\>\\>][local] before it can be used\nin `<<set>>`.\n\nThe command `<<set>>` allows either regular assignment, or modifying assignment, like follows:\n\n```yarn\n// Regular assignment\n<<set $VARIABLE = EXPRESSION>>\n<<set $VARIABLE to EXPRESSION>>\n\n// Modifying assignments\n<<set $VARIABLE += EXPRESSION>>\n<<set $VARIABLE -= EXPRESSION>>\n<<set $VARIABLE *= EXPRESSION>>\n<<set $VARIABLE /= EXPRESSION>>\n<<set $VARIABLE %= EXPRESSION>>\n\n// These modifying assignments are equivalent to the following:\n<<set $VARIABLE = $VARIABLE + EXPRESSION>>\n<<set $VARIABLE = $VARIABLE - EXPRESSION>>\n<<set $VARIABLE = $VARIABLE * EXPRESSION>>\n<<set $VARIABLE = $VARIABLE / EXPRESSION>>\n<<set $VARIABLE = $VARIABLE % EXPRESSION>>\n```\n\nIn all cases, the `EXPRESSION` must have the same type as the `$VARIABLE`. If not, a compile-time\nerror will be thrown.\n\n\n## Examples\n\n```yarn\n<<declare $favorite_color as String>>\n\ntitle: ColorQuiz\n---\nWhat is your favorite color?\n-> White\n   <<set $favorite_color to \"White\">>\n-> Red\n   <<set $favorite_color to \"Red\">>\n-> Yellow\n   <<set $favorite_color = \"Yellow\">>\n-> Blue\n   Oh, Nice! Which shade of blue?\n   -> Azure\n   -> Cerulean\n   -> Lapis Lazuli\n   Umm, I don't know how to spell that. I'll just put you down as \"blue\".\n   <<set $favorite_color = \"Blue\">>\n-> Black\n   <<set $favorite_color = \"Black\">>\n   That's mine too!\n   <<set $affinity += 3>>\n-> Prefer not to tell\n   Aww... Maybe if I ask again really nicely?\n   <<jump ColorQuiz>>\n===\n```\n\n\n[declare]: declare.md\n[local]: local.md\n"
  },
  {
    "path": "doc/other_modules/jenny/language/commands/stop.md",
    "content": "# `<<stop>>`\n\nThe **\\<\\<stop\\>\\>** command: immediately stops evaluating the current node, as if you jumped to\nits end. This command takes no arguments.\n\nNormally, the effect of this command is that it stops the dialogue. However, if you're only\nvisiting the current node from a different one, then `<<stop>>` will only exit the current node,\nand the execution flow will return to the parent. Thus, the `<<stop>>` command is similar to\n`return;` in many programming languages.\n\n```yarn\n<<stop>>\n```\n"
  },
  {
    "path": "doc/other_modules/jenny/language/commands/user_defined_commands.md",
    "content": "# User-defined commands\n\nIn addition to the built-in commands, you can also declare your own **user-defined commands** for\nuse in your yarn scripts. Typically, these commands would perform some in-game action that can be\nviewed as a natural part of the dialogue. For example, you can create commands for such action as\n`<<wave>>`, `<<smile>>`, `<<frown>>`, `<<moveCamera>>`, `<<zoom>>`, `<<shakeCamera>>`,\n`<<fadeOut>>`, `<<walk>>`, `<<give>>`, `<<take>>`, `<<achievement>>`, `<<GainExperience>>`,\n`<<startQuest>>`, `<<finishQuest>>`, `<<openTrade>>`, `<<drawWeapon>>`, and so on.\n\nIn many cases, the commands will need to take arguments. The arguments of a user-defined command\nare processed according to the following rules:\n\n- First, all content after the command name and until the closing `>>` is parsed according to the\n  rules of regular line parsing, where interpolated expressions are allowed but markup and hashtags\n  are not.\n- At runtime, the content of that line is evaluated, meaning that we substitute the values of all\n  expressions.\n- The evaluated argument string is then broken into individual arguments at whitespace, and the\n  types of these arguments are checked against the signature of the backing function.\n- Then, the backing function is called with the parsed arguments.\n- Lastly, all dialogue views in the dialogue runner receive the `onCommand()` event.\n\nAs a concrete example, consider the following command:\n\n```yarn\n<<give Gold {round(100 * $multiplier)}>>\n```\n\nFirst note that, unlike builtin commands, the arguments of the command are treated as text, and any\nexpressions need to be placed in curly brackets.\n\nThen, at runtime the expression is evaluated, and (assuming `$multiplier` is 1.5) the command's\nargument string becomes `\"Gold 150\"`. The string is then broken at white spaces and each argument\nis parsed according to its type in the backing Dart function. For example, if the function's\nsignature is `void give(String item, int amount)`, then it will be invoked as `give(\"Gold\", 150)`.\nIf, on the other hand, the number or types of arguments do not match the expected signature, then\na `DialogueException` will be raised.\n"
  },
  {
    "path": "doc/other_modules/jenny/language/commands/visit.md",
    "content": "# `<<visit>>`\n\nThe **\\<\\<visit\\>\\>** command temporarily puts the current node on hold, executes the target node,\nand after it finishes, resumes execution of the previous node. This is similar to a function call\nin many programming languages.\n\nThe `<<visit>>` command can be useful for splitting a large dialogue into several smaller nodes,\nor for reusing some common dialogue lines in several nodes. For example:\n\n```yarn\ntitle: RoamingTrader1\n---\n<<if $roaming_trader_introduced>>\n  Hello again, {$player}!\n<<else>>\n  <<visit RoamingTraderIntro>>\n<<endif>>\n\n-> What do you think about the Calamity?  <<if $calamity_started>>\n   <<visit RoamingTrader_Calamity>>\n-> Have you seen a weird-looking girl running by? <<if $quest_little_girl>>\n   <<visit RoamingTrader_LittleGirl>>\n-> What do you have for trade?\n   <<OpenTrade>>\n\nPleasure doing business with you! #auto\n===\n```\n\nThe argument of this command is the id of the node to jump to. It can be given either as a plain\nnode ID, or as an expression in curly braces:\n\n```yarn\n<<visit {\"RewardChoice_\" + string($choice)}>>\n```\n\nIf the expression evaluates at runtime to an unknown name, then a `NameError` exception will be\nthrown.\n"
  },
  {
    "path": "doc/other_modules/jenny/language/commands/wait.md",
    "content": "# `<<wait>>`\n\nThe **\\<\\<wait\\>\\>** command forces the dialogue engine to wait for the specified duration\n(in seconds) before resuming the dialogue. The number of seconds can be 0, but cannot be negative.\nThis command takes a single argument, which must be a numeric expression. For example:\n\n```yarn\n// Wait for a quarter of a second\n<<wait 0.25>>\n\n// Wait for the amount of time given by the $delay variable\n<<wait $delay>>\n```\n"
  },
  {
    "path": "doc/other_modules/jenny/language/expressions/expressions.md",
    "content": "# Expressions\n\nThe **expressions** in YarnSpinner provide a way to dynamically change the flow or the content\nof the dialogue, based on [variables], combined with [operators] or [function] calls. They are\nused in several places:\n\n- to insert a dynamic text into a [line];\n- to create or update a [variable];\n- as part of a [command] such as `<<if>>` or `<<set>>`;\n- to compute the values of [markup] attributes.\n\nAn expression always evaluates synchronously, meaning that it cannot wait for user's input, nor\nperform an action over time, nor carry out any computationally intensive calculations in a\ndifferent thread. If such functionality is really desired, then it can be achieved via a\n[user-defined command] that waits for the calculation to succeed and then stores the result into\nsome global [variable], which can then be accessed from an expression.\n\n\n```{toctree}\n:hidden:\n\nVariables   <variables.md>\nOperators   <operators.md>\nFunctions   <functions/functions.md>\n```\n\n[command]: ../commands/commands.md\n[function]: functions/functions.md\n[line]: ../lines.md\n[markup]: ../markup.md\n[operators]: operators.md\n[user-defined command]: ../commands/user_defined_commands.md\n[variable]: variables.md\n[variables]: variables.md\n"
  },
  {
    "path": "doc/other_modules/jenny/language/expressions/functions/functions.md",
    "content": "# Functions\n\nA **function** in YarnSpinner is the same notion as in any other programming language, or in math:\nit takes a certain number of arguments, and then computes and returns the result. A function call\nis indicated by the name of the function, followed by its arguments in parentheses. The parentheses\nare required, even when there are no arguments:\n\n```yarn\n<<set $roll_2d6 = dice(6) + dice(6)>>\n<<set $random = random()>>\n```\n\nThere are around 20 built-in functions in Jenny, listed below; and it is also possible to add\nuser-defined functions as well.\n\n\n## Built-in functions\n\n- **Random functions**\n  - [`dice(n)`](random.md#dicen)\n  - [`random()`](random.md#random)\n  - [`random_range(a, b)`](random.md#random_rangea-b)\n\n- **Numeric functions**\n  - [`ceil(x)`](numeric.md#ceilx)\n  - [`dec(x)`](numeric.md#decx)\n  - [`decimal(x)`](numeric.md#decimalx)\n  - [`floor(x)`](numeric.md#floorx)\n  - [`inc(x)`](numeric.md#incx)\n  - [`int(x)`](numeric.md#intx)\n  - [`round(x)`](numeric.md#roundx)\n  - [`round_places(x, n)`](numeric.md#round_placesx-n)\n\n- **Type conversion functions**\n  - [`bool(x)`](type.md#boolx)\n  - [`number(x)`](type.md#numberx)\n  - [`string(x)`](type.md#stringx)\n\n- **Other functions**\n  - [`if(condition, then, else)`](misc.md#ifcondition-then-else)\n  - [`plural(x, ...)`](misc.md#pluralx-words)\n  - [`visit_count(node)`](misc.md#visit_countnode)\n  - [`visited(node)`](misc.md#visitednode)\n\n\n## User-defined functions\n\nIn addition to the built-in functions, you can also define any number of **user-defined functions**\nwhich can later be used in your yarn scripts. The syntax for these functions is exactly the same\nas for the built-in functions: it consists of a function name, followed by the arguments in\nparentheses.\n\nEach user-defined function has a fixed signature, declared at the time when the function is added\nto the `YarnProject`. A function must have a fixed number of arguments of specific types, and a\nfixed return type.\n\nAll user-defined functions must be added to the `YarnProject` before they can be used. A compile\nerror will be raised if the parser encounters an unknown function, or if the number or types of\narguments do not match.\n\nUser-defined functions can be used for a variety of purposes, such as:\n\n- implement functionality that is currently missing in Jenny;\n- interface with the game engine;\n- provide access to \"variables\" stored outside of Jenny;\n- etc.\n\n```yarn\ntitle: Blacksmith\n---\n// This example showcases several hypothetical user-defined functions:\n// - broken(slot): checks whether the item in the given slot is broken;\n// - name(slot): gives the name for an item in a slot, e.g. \"sword\" or \"bow\";\n// - money(): returns the current amount of money that the player has.\n// At the same time, functions `round()` and `plural()` are built-in.\n\n<<if broken(\"main_hand\")>>\n  <<local $repair_cost = round(value(\"main_hand\") / 5)>>\n\n  Blacksmith: Your {name(\"main_hand\")} seems to be completely broken!\n  Blacksmith: I can fix it for just {plural($repair_cost, \"% coin\")}\n  -> Ok, do it  <<if money() >= $repair_cost>>\n  -> I'll be fine...\n<<endif>>\n===\n```\n\n```{seealso}\n- [`FunctionStorage`](../../../runtime/function_storage.md) -- document\n  describing how to add user-defined functions to a `YarnProject`.\n```\n\n\n```{toctree}\n:hidden:\n\nRandom functions          <random.md>\nNumeric functions         <numeric.md>\nType conversion functions <type.md>\nMiscellaneous functions   <misc.md>\n```\n"
  },
  {
    "path": "doc/other_modules/jenny/language/expressions/functions/misc.md",
    "content": "# Miscellaneous functions\n\n\n## if(condition, then, else)\n\nThis function implements the ternary-if condition, it is equivalent to the `?:` operator in Dart.\n\nThe function evaluates its `condition` (which must be a boolean), and then returns either the value\nof `then` if the condition was `true`, or the value of `else` if the condition was `false`. The\ntypes of arguments `then` and `else` must be the same.\n\nNote: Only one of the `then`/`else` values will be evaluated, depending on the `condition`. This\nmay be important in cases when evaluating those expressions may produce a side-effect.\n\n```yarn\ntitle: Birth\n---\nDoctor: Congratulations, you have a { if($gender == \"m\", \"boy\", \"girl\") }!\n===\n```\n\n\n## plural(x, words...)\n\nReturns the correct plural form depending on the value of variable `x`.\n\nThis function is locale-dependent, and its implementation and signature changes depending on the\n`locale` property in the `YarnProject`. In all cases, the first argument `x` must be numeric,\nwhile all other arguments should be strings.\n\nThe purpose of this function is to form correct plural phrases, according to the rules of the\ncurrent language. For example, suppose you need to say `{$n} items`, where `$n` is a variable. If\nyou simply plug in the value of the variable like that, you'll end up getting phrases like\n\"23 items\", or \"1 items\" -- which is not what you want. So instead, the `plural()` function can be\nused, which will select the correct plural form of the word \"item\":\n\n```yarn\nI have {plural($n, \"% item\")}.\n```\n\nIn English locale (`en`), the function `plural()` takes either 1 or 2 `word`s after the numeral\n`$x`. The first word is the singular form, and the second is the plural. The second word can be\nomitted if the singular form is simple enough that its plural form can be obtained by adding either\n`-s` or `-es`. For example:\n\n```yarn\n// Here \"foot\" is an irregular noun, so its plural form must be specified\n// explicitly. At the same time, \"inch\" is regular, and the function\n// plural() will know to add \"es\" to make its plural form.\nThe distance is {plural($ft, \"% foot\", \"% feet\")} and {plural($in, \"% inch\")}.\n```\n\nIn locales other than English, the number of plural words can be anywhere from 1 to 3. Usually,\nthe first word is the singular form, while others are different plurals -- their meaning would\ndepend on a particular language. For example, in Ukrainian locale (`uk`) the function `plural()`\nrequires 3 words: the singular form, the \"few\" plural form, and the \"many\" plural form:\n\n<!--- cSpell:ignore мене монета монети монет -->\n```yarn\n// Assuming locale == 'uk'\nУ мене є {plural($coins, \"% монета\", \"% монети\", \"% монет\")}.\n\n// Produces phrases like this:\n//   У мене є 21 монета\n//   У мене є 23 монети\n//   У мене є 25 монет\n```\n\nNote that in all examples above the words contain the `%` sign. This is used as a placeholder where\nthe numeral itself should be placed. It is allowed for some (or all) of the `words` to not contain\nthe `%` sign.\n\n\n## visit_count(node)\n\nReturns the number of times that the `node` was visited.\n\nA node is considered \"visited\" if the dialogue enters and then exits that node. The node can be\nexited either through the normal dialogue flow, or via the [\\<\\<stop\\>\\>] command. However, if a\nruntime exception occurs while running the node, then the visit will not count.\n\nThe `node` argument must be a string, and it must contain a valid node name. If a node with the\ngiven name does not exist in the project, an exception will be thrown.\n\n```yarn\ntitle: LuckyWheel\n---\n<<if visit_count(\"LuckyWheel\") < 5>>\n  Clown: Would you like to spin a wheel and get fabulous prizes?\n  -> I sure do!\n     <<jump SpinLuckyWheel>>\n  -> I don't talk to strangers...\n     <<stop>>\n<<else>>\n  Clown: Sorry kid, we're all out of prizes for now.\n<<endif>>\n===\n```\n\n```{seealso}\n- [`visited(node)`](#visitednode)\n```\n\n\n## visited(node)\n\nReturns `true` if the node with the given title was visited, and `false` otherwise.\n\nFor a node to be considered \"visited\", the dialogue must enter and then exit the node at least\nonce. For example, within a node \"X\" the expression `visited(\"X\")` will return `false` during the\nfirst run of this node, and `true` upon all subsequent runs.\n\nThe `node` argument must be a string, and it must contain a valid node name. If a node with the\ngiven name does not exist in the project, an exception will be thrown.\n\n```yarn\ntitle: MerchantDialogue\n---\n<<if not visited(\"MerchantDialogue\")>>\n  // This part of the dialogue will run only during the first interaction\n  // with the merchant.\n  Merchant: Greetings! My name is Linn.\n  Merchant: I offer exquisite wares for the most fastidious customers!\n  Player: Hi. I'm Bob. I like stuff.\n<<endif>>\n...\n===\n```\n\n```{seealso}\n- [`visit_count(node)`](#visit_countnode)\n```\n\n\n[\\<\\<stop\\>\\>]: ../../commands/stop.md\n"
  },
  {
    "path": "doc/other_modules/jenny/language/expressions/functions/numeric.md",
    "content": "# Numeric functions\n\nThese functions are used to manipulate numeric values. Most of them take a single numeric argument\nand produce a numeric result.\n\n\n## `ceil(x)`\n\nReturns the value `x` rounded up towards positive infinity. In other words, this returns the\nsmallest integer value greater than or equal to `x`.\n\n```yarn\ntitle: ceil\n---\n{ ceil(0)     }  // 0\n{ ceil(0.3)   }  // 1\n{ ceil(5)     }  // 5\n{ ceil(5.001) }  // 6\n{ ceil(5.999) }  // 6\n{ ceil(-2.07) }  // -2\n===\n```\n\n```{seealso}\n- [`floor(x)`](#floorx)\n- [`int(x)`](#intx)\n```\n\n\n## `dec(x)`\n\nReturns the value `x` reduced towards the previous integer. Thus, if `x` is already an integer\nthis returns `x - 1`, but if `x` is not an integer then this returns `floor(x)`.\n\n```yarn\ntitle: dec\n---\n{ dec(0)     }  // -1\n{ dec(0.3)   }  // 0\n{ dec(5.0)   }  // 4\n{ dec(5.001) }  // 5\n{ dec(5.999) }  // 5\n{ dec(-2.07) }  // -3\n===\n```\n\n```{seealso}\n- [`inc(x)`](#incx)\n```\n\n\n## `decimal(x)`\n\nReturns a fractional part of `x`.\n\nIf `x` is positive, then the returned value will be between `0` (inclusive) and `1` (exclusive).\nIf `x` is negative, then the returned value will be between `0` and `-1`. In all cases it should\nhold that `x == int(x) + decimal(x)`.\n\n```yarn\ntitle: decimal\n---\n{ decimal(0)     }  // 0\n{ decimal(0.3)   }  // 0.3\n{ decimal(5.0)   }  // 0\n{ decimal(5.001) }  // 0.001\n{ decimal(5.999) }  // 0.999\n{ decimal(-2.07) }  // -0.07\n===\n```\n\n```{seealso}\n- [`int(x)`](#intx)\n```\n\n\n## `floor(x)`\n\nReturns the value `x` rounded down towards negative infinity. In other words, this returns the\nlargest integer value less than or equal to `x`.\n\n```yarn\ntitle: floor\n---\n{ floor(0)     }  // 0\n{ floor(0.3)   }  // 0\n{ floor(5)     }  // 5\n{ floor(5.001) }  // 5\n{ floor(5.999) }  // 5\n{ floor(-2.07) }  // -3\n===\n```\n\n```{seealso}\n- [`ceil(x)`](#ceilx)\n- [`int(x)`](#intx)\n```\n\n\n## `inc(x)`\n\nReturns the value `x` increased towards the next integer. Thus, if `x` is already an integer\nthis returns `x + 1`, but if `x` is not an integer then this returns `ceil(x)`.\n\n```yarn\ntitle: inc\n---\n{ inc(0)     }  // 1\n{ inc(0.3)   }  // 1\n{ inc(5.0)   }  // 6\n{ inc(5.001) }  // 6\n{ inc(5.999) }  // 6\n{ inc(-2.07) }  // -2\n===\n```\n\n```{seealso}\n- [`dec(x)`](#decx)\n```\n\n\n## `int(x)`\n\nTruncates the fractional part of `x`, rounding it towards zero, and returns just the integer part\nof the argument `x`.\n\n```yarn\ntitle: int\n---\n{ int(0)     }  // 0\n{ int(0.3)   }  // 0\n{ int(5.0)   }  // 5\n{ int(5.001) }  // 5\n{ int(5.999) }  // 5\n{ int(-2.07) }  // -2\n===\n```\n\n```{seealso}\n- [`decimal(x)`](#decimalx)\n- [`round(x)`](#roundx)\n```\n\n\n## `round(x)`\n\nRounds the value `x` towards a nearest integer.\n\nThe values that end with `.5` are rounded up if `x` is positive, and down if `x` is negative.\n\n```yarn\ntitle: round\n---\n{ round(0)     }  // 0\n{ round(0.3)   }  // 0\n{ round(5.0)   }  // 5\n{ round(5.001) }  // 5\n{ round(5.5)   }  // 6\n{ round(5.999) }  // 6\n{ round(-2.07) }  // -2\n{ round(-2.5) }   // -3\n===\n```\n\n```{seealso}\n- [`round_places(x, n)`](#round_placesx-n)\n```\n\n\n## `round_places(x, n)`\n\nRounds the value `x` to `n` decimal places.\n\nThe value `x` can be either positive, negative, or zero, but it must be an integer. Rounding to\n`0` decimal places is equivalent to the regular `round(x)` function. If `n` is positive, then the\nfunction will attempt to keep that many digits after the decimal point in `x`. If `n` is negative,\nthen `round_places()` will round `x` to nearest tens, hundreds, thousands, etc:\n\n```yarn\ntitle: round_places\n---\n{ round_places(0, 1)     }  // 0\n{ round_places(0.3, 1)   }  // 0.3\n{ round_places(5.001, 1) }  // 5.0\n{ round_places(5.001, 2) }  // 5.0\n{ round_places(5.001, 3) }  // 5.001\n{ round_places(5.5, 1)   }  // 5.5\n{ round_places(5.999, 1) }  // 6.0\n{ round_places(-2.07, 1) }  // -2.1\n{ round_places(13, -1)   }  // 10\n{ round_places(252, -2)  }  // 200\n===\n```\n\n```{seealso}\n- [`round(x)`](#roundx)\n```\n"
  },
  {
    "path": "doc/other_modules/jenny/language/expressions/functions/random.md",
    "content": "# Random functions\n\nThese functions produce random results each time they run.\n\nInternally, each function uses `YarnSpinner.random` random generator, which can be replaced with a\ncustom generator if you need reproducible draws for debug purposes, or to prevent the player from\ngetting different results upon reload.\n\n\n## `dice(n)`\n\nReturns a random integer between `1` and `n`, inclusive. For example, `dice(6)` will return a\nrandom integer from 1 to 6, as if throwing a regular six-sided die.\n\nThe argument `n` must be numeric, and greater or equal than 1. If `n` is a non-integer, then it\nwill be truncated to an integer value at runtime. Thus, `dice(3.5)` is equivalent to `dice(3)`.\n\n```yarn\n<<set $roll = dice(6)>>\n<<set $coin_flip = if(dice(2) == 1, \"H\", \"T\")>>\n```\n\n\n## `random()`\n\nReturns a random floating-point between `0` and `1`.\n\nThis function can be used to implement events with a prescribed probability. For example:\n\n```yarn\n<<if random() < 0.001>>\n  // This happens only with 0.1% probability\n  You found it! The Holy Grail!\n<<endif>>\n```\n\n\n## `random_range(a, b)`\n\nReturns a random integer between `a` and `b` inclusive.\n\nBoth arguments `a` and `b` must be numeric, and they will be truncated to integers upon evaluation.\nThe value of `a` must be less than or equal to `b`, or otherwise a runtime exception will be thrown.\n\nThe purpose of this function is similar to `dice()`, but it can be used in situations where a\ncustom range is desired.\n\n```yarn\n<<set $coin_flip = bool(random_range(0, 1))>>\n```\n"
  },
  {
    "path": "doc/other_modules/jenny/language/expressions/functions/type.md",
    "content": "# Type conversion functions\n\nThese functions convert values of one type into another type, if possible. All of these functions\ntake a single argument of arbitrary type, and return the result of the type corresponding to the\nname of the function.\n\n\n## `bool(x)`\n\nConverts its argument into a boolean value.\n\n- If `x` is already a boolean, then it returns the argument as-is.\n- If `x` is numeric, then the result is `false` when `x` is `0`, and `true` for all other values\n  of `x`.\n- If `x` is string, then the function will check whether that string can be found within\n  `YarnProject.trueValues` or `YarnProject.falseValues` sets. If yes, then it will return the\n  `true` / `false` value respectively. Otherwise, an error will be thrown.\n\n\n## `number(x)`\n\nConverts its argument `x` into a numeric value.\n\n- If `x` is boolean, then it returns `1` for `true` and `0` for `false`.\n- If `x` is numeric, then it is returned unmodified.\n- If `x` is string, then the function attempts to parse that string as a number. A runtime\n  exception will be raised if `x` does not have a valid format for a number. The following formats\n  are recognized:\n  - integer: `\"-3\"`, `\"214\"`\n  - decimal: `\"0.745\"`, `\"3.14159\"`, `\".1\"`, `\"-3.\"`\n  - scientific: `\"2e5\"`, `\"3.11e-05\"`\n  - hexadecimal: `\"0xDEAD\"`, `\"0x7F\"`\n\n\n## `string(x)`\n\nConverts its argument `x` into a string value.\n\n- If `x` is boolean, returns strings `\"true\"` or `\"false\"`.\n- If `x` is numeric, converts it into a string representation using the standard Dart's\n  `.toString()` method, which attempts to produce the shortest string that can represent\n  the number `x`. In particular,\n  - if `x` is integer-valued, returns its decimal representation without a decimal point;\n  - if `x` is a double in the range `1e-6` to `1e21`, returns its decimal representation\n    with a decimal point;\n  - for all other doubles, returns `x` written in the scientific (exponential) format.\n- If `x` is a string, then it is returned as-is.\n"
  },
  {
    "path": "doc/other_modules/jenny/language/expressions/operators.md",
    "content": "# Operators\n\nThe **operators** are special symbols that perform common mathematical operations. For example,\noperator `+` performs summation, and thus we can write `$x + $y` to denote the sum of variables\n`$x` and `$y`. There are over 20 different operators in YarnSpinner, which can be loosely grouped\ninto the following categories:\n\n\n## Operator types\n\n\n### Arithmetic\n\nThe **arithmetic** operators, have the same meaning as in regular math. These apply to numeric\narguments (with the exception of `+` which can also be used with strings):\n\n```{list-table}\n:align: left\n:class: first-col-align-center\n:header-rows: 1\n:widths: 1 2 9\n\n* - operator\n  - name\n  - notes\n* - `+`\n  - addition\n  -\n* - `-`\n  - subtraction\n  - Also, a unary minus\n* - `*`\n  - multiplication\n  -\n* - `/`\n  - division\n  - Division by `0` is not allowed, and will throw a runtime error if it occurs.\n* - `%`\n  - modulo\n  - This operator can apply to both integer and decimal numbers, and it returns\n    the remainder of integer division of two numbers. The right-hand side of\n    `%` cannot be zero or a negative number, otherwise a runtime error will be\n    thrown. The result of `x % y` is always a number in the range `[0; y)`,\n    regardless of the sign of `x`.\n* - `+`\n  - concatenation\n  - When applied to strings, the `+` operator simply glues them together. For\n    example, `\"Hello\" + \"World\"` produces string `\"HelloWorld\"`.\n```\n\n\n### Logical\n\nThe **logical** operators apply to boolean values. These operators can be written either in\nsymbolic or word form -- both forms are equivalent:\n\n```{list-table}\n:align: left\n:class: first-col-align-center\n:header-rows: 1\n:widths: 1 2 9\n\n* - operator\n  - name\n  - notes\n* - `!`, `not`\n  - logical NOT\n  - This is a unary operator that inverts its operand: `!true` is `false`,\n    and `!false` is `true`.\n* - `&&`, `and`\n  - logical AND\n  - Returns `true` if both of its arguments are `true`.\n* - `||`, `or`\n  - logical OR\n  - Returns `true` if at least one of its arguments is `true`.\n* - `^`, `xor`\n  - logical XOR\n  - Returns `true` if the arguments are different, and `false` if they are\n    the same.\n```\n\n\n### Assignment\n\nThe **assignment** operators modify the value of a variable. The left-hand side of such an operator\nis the variable that shall be modified, the right-hand side is the expression of the same type as\nthe variable on the left:\n\n```{list-table}\n:align: left\n:class: first-col-align-center\n:header-rows: 1\n:widths: 1 2 9\n\n* - operator\n  - name\n  - notes\n* - `=`, `to`\n  - assign\n  - `$var = X` stores the value of `X` into the variable `$var`\n* - `+=`\n  - increase\n  - `$var += X` is equivalent to `$var = $var + X`\n* - `-=`\n  - decrease\n  - `$var -= X` is equivalent to `$var = $var - X`\n* - `*=`\n  - multiply\n  - `$var *= X` is equivalent to `$var = $var * X`\n* - `/=`\n  - divide\n  - `$var /= X` is equivalent to `$var = $var / X`\n* - `%=`\n  - reduce modulo\n  - `$var %= X` is equivalent to `$var = $var % X`\n```\n\nUnlike all other operators, the assignment operators do not produce a value. This means they\ncannot be used inside a larger expression, for example the following is invalid: `3 + ($x += 7)`.\nInstead, the assignment operators are only usable at the top level of commands such as\n[\\<\\<set\\>\\>], [\\<\\<declare\\>\\>], and [\\<\\<local\\>\\>].\n\n\n### Relational\n\nThe **relational** operators compare various values. The first two operators in this list can be\napplied to operands of any types, as long as the types are the same. The remaining four operators\ncan only be used with numbers. Regardless of the types of operands, the result of every\nrelational operator is a boolean value, which can be either assigned to a variable, or used in a\nlarger expression:\n\n```{list-table}\n:align: left\n:class: first-col-align-center\n:header-rows: 1\n:widths: 1 3 8\n\n* - operator\n  - name\n  - notes\n* - `==`\n  - equality\n  -\n* - `!=`\n  - inequality\n  -\n* - `<`\n  - less than\n  -\n* - `<=`\n  - less than or equal\n  -\n* - `>`\n  - greater than\n  -\n* - `>=`\n  - greater than or equal\n  -\n```\n\nNote that operator chaining is not supported. Thus, for example, `$x == $y == $z` will first\ncompare variables `$x` and `$y`, then the result of that comparison, which is either `true` or\n`false`, will be compared with variable `$z`. Given that such expressions would be highly\nconfusing to a reader, we recommend against using them. If you need to compare that all three\nvalues `$x`, `$y` and `$z` are the same, then you should use the `&&` operator instead:\n`$x == $y && $x == $z`.\n\n\n## Precedence\n\nJust as in mathematics, the operators have precedence ordering among them, meaning that some\noperators will always evaluate before the others. For example, if you write `3 + 4 * 5`, then\nthe result will be `23` instead of `35` because multiplication has higher precedence than addition\nand thus evaluates first.\n\nThe precedence order is as follows, from highest to lowest:\n\n- `*`, `/`, `%`;\n- `-`, `+`;\n- `==`, `!=`, `<`, `<=`, `>=`, `>`;\n- `!`;\n- `&&`, `^`;\n- `||`;\n- `=`, `+=`, `-=`, `*=`, `/=`, `%=`.\n\nYou can use parentheses `()` in order to alter the order of evaluation. For example, `(3 + 4) * 5`\nis `35` instead of `23`.\n\n\n[\\<\\<declare\\>\\>]: ../commands/declare.md\n[\\<\\<local\\>\\>]: ../commands/local.md\n[\\<\\<set\\>\\>]: ../commands/set.md\n"
  },
  {
    "path": "doc/other_modules/jenny/language/expressions/variables.md",
    "content": "# Variables\n\nA **variable** is a place to store some piece of information -- it is the same notion as in any\nother programming language. Each variable has a **name**, **value**, **type**, and a **scope**.\n\n\n## Name\n\nThe **name** of a variable is how you refer to it in a `.yarn` script. The names of all variables\nstart with a `$` sign, followed by a letter or an underscore, and then by any number of letters,\ndigits, or underscores. Thus, the following are all valid variables names:\n\n```text\n$i\n$WARNING\n$_secret_\n$door10\n$climbed_over_wall_and_avoided_all_guard_patrols\n$DoorPassword\n```\n\nwhile the following are NOT valid names:\n\n```text\n$2000_years\n$[main]\n@today\nvictory\n```\n\n\n## Type\n\nEach variable has a certain **type** associated with it. The type of a variable is determined when\nthe variable is first declared, and it never changes afterwards.\n\nThere are three types of variables in YarnSpinner: `string`, `number`, and `bool`.\n\n- `bool` variables can store either `true` or `false` and nothing else;\n- `number` variables may contain either integer or decimal numbers, such as `0`, `42`, `2.5`;\n- `string` variables contain arbitrary text, for example `\"the most random number is 4\"`.\n\n```yarn\n// Creates a variable $money of type number, and gives it initial value of 100\n<<declare $money = 100>>\n\n// Creates variable $name of type string, the initial value will be \"\"\n<<declare $name as String>>\n```\n\n\n## Value\n\nEach variable stores a single **value**. This value can be replaced with another value at any time,\nbut the type of the new value must be the same.\n\nEach variable will have an initial value assigned to it when the variable is first created, and\nthen new values can be assigned with the [\\<\\<set\\>\\>][set] command.\n\n```yarn\n<<set $money += 10>>  // increases the value of $money by 10\n```\n\n\n## Scope\n\nThe **scope** of a variable is where exactly it can be accessed. In YarnSpinner, the variables can\nbe either global or local.\n\n- The **global** variables are introduced via the [\\<\\<declare\\>\\>][declare] command, and once\n  created can be accessed anywhere. The names of all global variables are unique.\n- The **local** variables are created with the [\\<\\<local\\>\\>][local] command, and can only be used\n  within the node where they were created. It is possible to have a local variable with the same\n  name in different nodes, and they will be considered different variables.\n\n```yarn\n<<declare $global_variable = 0>>\n\ntitle: MyNode\n---\n<<local $local_variable = 1>>\n===\n```\n\n\n[declare]: ../commands/declare.md\n[local]: ../commands/local.md\n[set]: ../commands/set.md\n"
  },
  {
    "path": "doc/other_modules/jenny/language/language.md",
    "content": "# YarnSpinner language\n\n**YarnSpinner** is the language in which `.yarn` files are written. You can check out the\n[official documentation] for the YarnSpinner language, however, here we will be describing the\n**Jenny** implementation, which may not contain all the original features, but may also contain\nsome that were not implemented in the YarnSpinner yet.\n\n\n## Yarn files\n\nAny Yarn project will contain one or more `.yarn` files. These are plain text files in UTF-8\nencoding. As such, they can be edited in any text editor or IDE.\n\nHaving multiple `.yarn` files helps you better organize your project, but Jenny doesn't impose any\nrequirements on the number of files or their relationship.\n\nEach `.yarn` file may contain **comments**, **tags**, **[commands]**, and **[nodes]**.\nFor example:\n\n```yarn\n// This is a comment\n// The line below, however, is a tag:\n# Chapter 1d\n\n<<declare $visited_graveyard = false>>\n<<declare $money = 25>>  // is this too much?\n\ntitle: Start\n---\n// Node content\n===\n```\n\n\n### Comments\n\nA comment starts with `//` and continues until the end of the line. All the text inside a comment\nwill be completely ignored by Jenny as if it wasn't there.\n\nThere are no multi-line comments in YarnSpinner.\n\n\n### Tags\n\nFile-level tags start with a `#` and continue until the end of the line. A tag can be used to\ninclude some per-file custom project metadata. These tags are not interpreted by Jenny in any way.\n\n\n### Commands\n\nThe commands are explained in more details [later][commands], but at this point it is\nworth pointing out that only a limited number of commands are allowed at the root level of a file\n(that is, outside of nodes). Currently, these commands are:\n\n- `<<declare>>`\n- `<<character>>`\n\nThe commands outside of nodes are compile-time instructions, that is they are executed during the\ncompilation of a YarnProject.\n\n\n### Nodes\n\nNodes represent the main bulk of content in a yarn file, and are explained in a dedicated\n[section][nodes]. There could be multiple nodes in a single file, placed one after another.\nNo special separator is needed between nodes: as soon as one node ends, the next one can begin.\n\n\n```{toctree}\n:hidden:\n\nNodes        <nodes.md>\nLines        <lines.md>\nOptions      <options.md>\nCommands     <commands/commands.md>\nExpressions  <expressions/expressions.md>\nMarkup       <markup.md>\n```\n\n[commands]: commands/commands.md\n[nodes]: nodes.md\n[official documentation]: https://docs.yarnspinner.dev/getting-started/writing-in-yarn\n"
  },
  {
    "path": "doc/other_modules/jenny/language/lines.md",
    "content": "# Lines\n\nA **line** is the most common element of the Yarn dialogue. It's just a single phrase that a\ncharacter in the game says. In a `.yarn` file, a **line** is represented by a single line of text\nin a [node body]. A line may contain the following elements:\n\n- A character ID;\n- Normal text;\n- Escaped text;\n- Interpolated expressions;\n- Markup;\n- Hashtags;\n- A comment at the end of the line;\n- (a line, however, cannot contain commands).\n\nA **line** is represented with the [DialogueLine] class in Jenny runtime.\n\n\n## Character ID\n\nIf a line starts with a single word followed by a `:`, then that word is presumed to be the name\nof the character who is speaking that line. In the following example there are two characters\ntalking to each other: Prosser and Ford, and the last line has no character ID.\n\n```yarn\ntitle: Bulldozer_Conversation\n---\nProsser: You want me to come and lie there...\nFord: Yes\nProsser: In front of the bulldozer?\nFord: Yes\nProsser: In the mud.\nFord: In, as you say, the mud.\n(low rumbling noise...)\n===\n```\n\nIt is worth emphasizing that a character ID must be a valid ID -- that is, it cannot contain\nspaces or other special characters. In the example below \"Harry Potter\" is not a valid character ID,\nwhile all other alternatives are ok.\n\n```yarn\ntitle: Hello\n---\nHarry Potter: Hello, Hermione!\nHarry_Potter: Hello, Hermione!\nHarryPotter: Hello, Hermione!\nHarry: Hello, Hermione!\n===\n```\n\nIf you want to have a line that starts with a `WORD + ':'`, but you don't want that word to be\ninterpreted as a character name, then the colon can be [escaped](#escaped-text):\n\n```yarn\ntitle: Warning\n---\nAttention\\: The cake is NOT a lie\n===\n```\n\n```{note}\nAll characters must be **declared** using the [\\<\\<character\\>\\>] command\nbefore they can be used in a script.\n```\n\n\n## Interpolated expressions\n\nYou can insert dynamic text into a line with the help of **interpolated expression**s. These\nexpressions are surrounded with curly braces `{}`, and everything inside the braces will be\nevaluated, and then the result of the evaluation will be inserted into the text.\n\n```yarn\ntitle: Greeting\n---\nTrader: Hello, {$player_name}! Would you like to see my wares?\nPlayer: I have only {plural($money, \"% coin\")}, do you have anything I can afford?\n===\n```\n\nThe expressions will be evaluated at runtime when the line is delivered, which means it can produce\ndifferent text during different runs of the line.\n\n```yarn\ntitle: Exam_Greeting\n---\n<<if $n_attempts == 0>>\n  Professor: Welcome to the exam!\n  <<jump Exam>>\n<<elseif $n_attempts < 5>>\n  Professor: You have tried {plural($n_attempts, \"% time\")} already, but I \\\n             can give you another try.\n  <<jump Exam>>\n<<else>>\n  Professor: You've failed 5 times in a row! How is this even possible?\n<<endif>> \n===\n```\n\nAfter evaluation, the text of the expression will be inserted into the line as-is, without any\nfurther processing. Which means that the text of the expression may contain special characters\n(such as `[`, `]`, `{`, `}`, `\\`, etc), and they don't need to be escaped. It also means that the\nexpression cannot contain markup, or produce a hashtag, etc.\n\nRead more about expressions in the [Expressions] section.\n\n\n## Markup\n\nThe **markup** is a mechanism for text annotation. It is somewhat similar to HTML tags, except that\nit uses square brackets `[]` instead of angular ones:\n\n```yarn\ntitle: Markup\n---\nWizard: No, no, no! [em]This is insanity![/em]\n===\n```\n\nThe markup tags do not alter the text of the line, they merely insert annotations in it. Thus, the\nline above will be delivered in game as \"No, no, no! This is insanity!\", however there will be\nadditional information attached to the line that shows that the last 17 characters were marked with\nthe `em` tag.\n\nMarkup tags can be nested, or be zero-width, they can also include parameters whose values can be\ndynamic. Read more about this in the [Markup] document.\n\n\n## Hashtags\n\nHashtags may appear at the end of the line, and take the following form: `#text`. That is, a hashtag\nis a `#` symbol followed by any text that doesn't contain whitespace.\n\nHashtags are used to add line-level metadata. There can be no line content after a hashtag (though\ncomments are allowed). A line can have multiple hashtags associated with it.\n\n<!-- cSpell:ignore HPMOR (Harry Potter and the Methods of Rationality) -->\n```yarn\ntitle: Hashtags\n---\nHarry: There is no justice in the laws of Nature, Headmaster, no term for \\\n       fairness in the equations of motion. #sad // HPMOR.39\nHarry: The universe is neither evil, nor good, it simply does not care.\nHarry: The stars don't care, or the Sun, or the sky.\nHarry: But they don't have to! We care! #elated #volume:+1\nHarry: There is light in the world, and it is us! #volume:+2\n===\n```\n\nIn most cases the Jenny engine does not interpret the tags, but merely stores them as part of the\nline information. It is up to the programmer to examine these tags at runtime.\n\n\n## Escaped text\n\nWhenever you have a line that needs to include a character that would normally be interpreted as\none of the special syntaxes mentioned above, then such a character can be **escaped** with a\nbackslash `\\`.\n\nThe following escape sequences are recognized: `\\\\`, `\\/`, `\\#`, `\\<`, `\\>`, `\\[`, `\\]`, `\\{`, `\\}`,\n`\\:`, `\\-`, `\\n`. In addition, there is also `\\⏎` (i.e. backslash followed immediately by a\nnewline).\n\n```yarn\ntitle: Escapes\n---\n\\// This is not a comment  // but this is\nThis is not a \\#hashtag\nThis is not a \\<<command>>\n\\{This line\\} does not contain an expression\nNot a \\[markup\\]\n===\n```\n\nThe `\\⏎` escape can be used to split a single long line into multiple physical lines, that would\nstill be treated by Jenny as if it was a single line. This escape sequence consumes both the\nnewline symbol and all the whitespace at the start of the next line:\n\n```yarn\ntitle: One_long_line\n---\nThis line is so long that it becomes uncomfortable to read in a text editor. \\\n    Therefore, we use the backslash-newline escape sequence to split it into \\\n    several physical lines. The indentation at the start of the continuation \\\n    lines is for convenience only, and will be removed from the resulting \\\n    text.\n===\n```\n\n\n[node body]: nodes.md#body\n[DialogueLine]: ../runtime/dialogue_line.md\n[Expressions]: expressions/expressions.md\n[Markup]: markup.md\n"
  },
  {
    "path": "doc/other_modules/jenny/language/markup.md",
    "content": "<!-- cSpell:ignore lorem ipsum dolor sit amet, consectetur adipiscing elit -->\n<!-- cSpell:ignore Malfoy -->\n\n# Markup\n\n**Markup** is a mechanism for annotating fragments of a dialogue line. They are somewhat similar to\nHTML tags, or you can imagine them as comments in a google document. Importantly, markup tags\nonly annotate the text, but do not alter its content or display in the game. It is up to the\ndeveloper to actually use the markup information in their game.\n\n\n## Syntax\n\nMarkup tags are denoted with the name of the tag, placed in square brackets: `[tag_name]`. The\ncorresponding closing tag would be `[/tag_name]`. Every markup tag must have a corresponding\nclosing tag:\n\n```yarn\nHello, [wavy]world[/wavy]!\n```\n\nMarkup tags may nest within each other, though they must nest properly, in the sense that one\nmarkup range must be fully inside or fully outside another:\n\n```yarn\nLorem [S]ipsum dolor [A]sit[/A] amet[/S], consectetur [B]adipiscing[/B] elit\n```\n\nThe special **close-all** markup tag `[/]` closes all currently opened markup ranges. It is also\nhandy in situations where the name of the markup tag is long and you don't want to repeat it:\n\n```yarn\nLorem ipsum dolor sit amet, [bold]consectetur adipiscing elit[/]\n```\n\nThe **self-closing** markup tags have the form `[tag_name/]`. These tags mark a single location\nwithin the text. In addition, if such tag is surrounded by spaces on both sides, then a single\nspace after the tag will be removed from the resulting text. If this is undesired, then simply\nadd an extra space after the markup tag:\n\n```yarn\nLorem ipsum dolor sit amet, [wave/] consectetur adipiscing elit.\n```\n\nMarkup tags also accept parameters, which are similar to HTML tag attributes. The names of these\nparameters can be arbitrary IDs, and the values are expressions that will be evaluated each time\nthe line is executed. Thus, the values of attributes can be dynamic:\n\n```yarn\nLorem ipsum [color name=$color]dolor sit amet[/color]\n```\n\nMarkup tags can surround dynamic text (interpolated expressions), which will cause the length of\nthe marked up span to be different every time the line is run. At the same time, markup cannot be\ngenerated dynamically -- in the sense that the interpolated expressions will always be inserted\nas-is, even if they contain some text in square brackets.\n\n```yarn\nHello, [b]{$player}[/b]!\n```\n\nLastly, it should be noted that if you want to have an actual text in square brackets within a\nline, then in order to prevent it from being parsed as markup you can escape the square brackets\nwith a backslash `\\`:\n\n```yarn\nHello, \\[world\\]!\n```\n\n```{seealso}\n- [MarkupAttribute](../runtime/markup_attribute.md): the runtime representation\n  of a markup attribute within a line.\n```\n\n\n## Examples\n\n\n### Mark a piece of text with a different style\n\nIn this example the word \"Voldemort\" is rendered with a special \"cursed\" markup, indicating that\nthe word itself is cursed (it is up to you how to actually render this in a game). Similarly, the\nword \"stupid\" in the second line has an emphasis, which may be rendered as italic text.\n\n```yarn\ntitle: Scene117_Harry_MrMalfoy\n---\nHarry: I'm not afraid of [cursed]Voldemort[/cursed]!\nMrMalfoy: You must be really brave... or really [i]stupid[/i]?\n===\n```\n\n\n### Provide additional information about a text fragment\n\nIn this example the word \"Llewellyn\" has a tooltip information associated with it. A game might\nrender this with a special style suggesting that the user may hover over that word to see a\ntooltip with a minimap for where to find this NPC.\n\n```yarn\ntitle: MonkDialogue\n---\nMonk: Visit [tooltip place=\"TS\" x=23 y=-74]Llewellyn[/] in Thunderstorm, \\\n      he will be able to help you.\n===\n```\n\n\n### Indicate where special non-text tokens may be inserted\n\nThe `[item/]` markup tag will be replaced by the item's name, which will also be interactive:\ntapping this name would bring up the item's description card.\n\n```yarn\ntitle: BlacksmithQuest\n---\n<<local $reward = if($chapter==1, \"A0325\", \"A1018\")>>\nSmith: Find me my lost ring, and I'll give you this [item id=$reward/].\n===\n```\n"
  },
  {
    "path": "doc/other_modules/jenny/language/nodes.md",
    "content": "# Nodes\n\nA **node** is a small section of text, that represents a single conversation or interaction with\nan NPC. Each node has a **title**, which can be used to *run* that node in a [DialogueRunner], or to\n[jump] to that node from another node.\n\nYou can think of a node as if it was a function in a regular programming language. Running a node\nis equivalent to calling a function, and it is not possible to start execution in the middle of a\nnode/function. When a function becomes too large, we will usually want to split it into multiple\nsmaller ones -- the same is true for nodes, when a node becomes too long it is a good idea to split\nit into several smaller nodes.\n\nEach node consists of a **header** and a **body**. The header is separated from the body with 3\n(or more) dashes, and the body is terminated with 3 \"=\" signs:\n\n```yarn\n// NODE HEADER\n---\n// NODE BODY\n===\n```\n\nIn addition, you can use 3 (or more) dashes to separate the header from the previous content, which\nmeans the following is also a valid node:\n\n```yarn\n---------------\n// NODE HEADER\n---------------\n// NODE BODY\n===\n```\n\nA **node** is represented with a [Node] class in Jenny runtime.\n\n[Node]: ../runtime/node.md\n\n\n## Header\n\nThe header of a node consists of one or more lines of the form `TAG: CONTENT`. One of these lines\nmust contain the node's **title**, which is the name of the node:\n\n```yarn\ntitle: NodeName\n```\n\nThe title of a node must be a valid ID (that is, starts with a letter, followed by any number of\nletters, digits, or underscores). All nodes within a single project must have unique titles.\n\nBesides the title, you can add any number of extra tags into the node's header. Jenny will store\nthese tags with the node's metadata, but will not interpret them in any other way. You will then\nbe able to access these tags programmatically\n\n```yarn\ntitle: Alert\ncolorID: 0\nmodal: true\n---\nWARNING\\: Entering Radioactive Zone!\n===\n```\n\n\n## Body\n\nThe body of a node is where the dialogue itself is located. The body is just a sequence of\nstatements, where each statement is either a [Line], an [Option], or a [Command]. For example:\n\n```yarn\ntitle: Gloomy_Morning\ncamera_zoom: 2\n---\nYou  : Good morning!\nGuard: You call this good? 'Tis as crappy as could be\nYou  : Why, what happened?\nGuard: Don't you see the fog? Chills me through to the bones\nYou  : Sorry to hear that... \nYou  : So, can I pass?\nGuard: Can I get some exercise cutting you into pieces? Maybe that'll warm me up!\nYou  : Ok, I think I'll be going. Hope you feel better soon!\n===\n```\n\n[DialogueRunner]: ../runtime/dialogue_runner.md\n[jump]: commands/jump.md\n[Line]: lines.md\n[Option]: options.md\n[Command]: commands/commands.md\n"
  },
  {
    "path": "doc/other_modules/jenny/language/options.md",
    "content": "# Options\n\n**Options** are special lines that display a menu of choices for the player, and the player must\nselect one of them in order to continue. The options are indicated with an arrow `->` at the start\nof the line:\n\n```yarn\ntitle: Adventure\n---\nYou arrive at the edge of the forest. The road dives in, but there is another \\\none going around the edge.\n-> Go straight ahead, on the beaten path (x)\n-> Take the road along the forest's edge\n-> Turn back\n===\n```\n\nAn option is typically followed by an indented list of statements (which may, again, be lines,\noptions, or commands). These statements indicate how the dialogue should proceed if the player\nchooses that particular option. After the control flow finishes running through the block\ncorresponding to the selected option, the dialogue resumes after the option set.\n\nOther than the arrow indicator, an option follows the same syntax as the [line]. Thus, it can have\na character name, the main text, interpolated expressions, markup, and hashtags. One additional\nfeature that an option can have is the **conditional**. A conditional is a short-form `<<if>>`\ncommand after the text of an option (but before the hashtags):\n\n```yarn\ntitle: Bridge\n---\nGuard: 50 coins and you can cross the bridge.\n-> Alright, take the money  <<if $gold >= 50>>\n   <<take gold 50>>\n   <<grant bridge_pass>>\n-> I have so much money, here, take a 100  <<if $gold >= 10000>>\n   <<take gold 100>>\n   <<grant bridge_pass>>\n   Guard: Wow, so generous!\n   Guard: But I wouldn't recommend going around telling everyone that you \\\n          have \"so much money\"\n-> That's too expensive!\n   Guard: Is it? My condolences\n-> How about I [s]kick your butt[/s] instead?\n   <<if $power < 1000>>\n      <<fight>>\n   <<else>>\n      You make a very reasonable point, sir, my apologies.\n      <<grant bridge_pass>>\n   <<endif>>\n===\n```\n\nWhen the conditional evaluates to `true`, the option is delivered to the game normally. If the\nconditional turns out to be false, the option is still delivered, but is marked as `unavailable`.\nIt is up to the game whether to display such option as greyed out, or crossed, or not show it at\nall; however, such option cannot be selected.\n\nAs you have noticed, options always come in groups: after all, the player must select among several\npossible choices. Thus, any sequence of options that are adjacent to each other in the dialogue,\nwill always be delivered as a single bundle to the frontend. This is called the **choice set**.\n\n[line]: lines.md\n"
  },
  {
    "path": "doc/other_modules/jenny/runtime/character.md",
    "content": "# Character\n\n```{dartdoc}\n:package: jenny\n:symbol: Character\n:file: src/character.dart\n\n[<<character>>]: ../language/commands/character.md\n```\n\n\n## See Also\n\n- [CharacterStorage]: the container where all Character objects within a YarnProject are cached.\n\n\n[CharacterStorage]: character_storage.md\n"
  },
  {
    "path": "doc/other_modules/jenny/runtime/character_storage.md",
    "content": "# CharacterStorage\n\n```{dartdoc}\n:package: jenny\n:symbol: CharacterStorage\n:file: src/character_storage.dart\n```\n\n\n## Accessing character storage\n\nCharacter storage is accessed via the [YarnProject].\n\n```dart\nfinal characters = yarnProject.characters;\n```\n\n\n## Removing characters\n\nThere may be situations where characters need to be removed from storage. For example, in a game\nwith many scenes, characters could be removed after a scene and new characters loaded for the next\nscene.\n\nRemove all characters with `clear`.\n\n```dart\nyarnProject.characters.clear();\n```\n\nUse `remove` to remove a single character. Pass in the name of the character or any of its\naliases. The character and all its aliases will be removed.\n\n```dart\nyarnProject.characters.remove('Jenny');\n```\n\n[YarnProject]: yarn_project.md\n"
  },
  {
    "path": "doc/other_modules/jenny/runtime/command_storage.md",
    "content": "# CommandStorage\n\nThe **CommandStorage** is a part of [YarnProject] responsible for storing all [user-defined\ncommands]. You can access it as the `YarnProject.commands` property.\n\nThe command storage can be used to register any number of custom commands, making them available to\nuse in yarn scripts. Such commands must be registered before parsing the yarn scripts, or the\ncompiler will throw an error that the command is not recognized.\n\nIn order to register a function as a yarn command, the function must satisfy several requirements:\n\n- The function's return value must be `void` or `Future<void>`. If the function returns a future,\n  then that future will be awaited before proceeding to the next step of the dialogue. This makes it\n  possible to create commands that take a certain time to unfold in the game, for example\n  `<<walk>>`, `<<moveCamera>>`, or `<<prompt>>`.\n- The function's arguments must be of types that are known to Yarn: `String`, `num`, `int`,\n  `double`, or `bool`. All arguments must be positional, non-nullable and can't have any defaults.\n- In order to register the function, use methods `addCommand0()` ... `addCommand5()`, according to\n  the number of function's arguments.\n- If the function's signature has 1 or more booleans at the end, then those arguments will be\n  considered optional and will default to `false`.\n\n\n## Methods\n\n**hasCommand**(`String name`) → `bool`\n: Returns the status of whether the command `name` has been added to the storage.\n\n**addCommand0**(`String name`, `FutureOr<void> Function() fn`)\n: Registers a no-argument function `fn` as the command `name`.\n\n**addCommand1**(`String name`, `FutureOr<void> Function(T1) fn`)\n: Registers a single-argument function `fn` as the command `name`.\n\n**addCommand2**(`String name`, `FutureOr<void> Function(T1, T2) fn`)\n: Registers a two-argument function `fn` as the command `name`.\n\n**addCommand3**(`String name`, `FutureOr<void> Function(T1, T2, T3) fn`)\n: Registers a three-argument function `fn` as the command `name`.\n\n**addCommand4**(`String name`, `FutureOr<void> Function(T1, T2, T3, T4) fn`)\n: Registers a four-argument function `fn` as the command `name`.\n\n**addCommand5**(`String name`, `FutureOr<void> Function(T1, T2, T3, T4, T5) fn`)\n: Registers a five-argument function `fn` as the command `name`.\n\n**addOrphanedCommand**(`name`)\n: Registers a command `name` which is not backed by any Dart function. Such command will still be\n  delivered to [DialogueView]s via the `onCommand()` callback, but its arguments will not be parsed.\n\n**clear**\n: Removes all user-defined commands\n\n**remove**(`String name`)\n: Removes the user-defined command with the specified `name`.\n\n\n## Properties\n\n**length** → `int`\n: The number of user-defined commands registered so far.\n\n**isEmpty** → `bool`\n: Returns `true` if no user-defined commands were registered.\n\n**isNotEmpty** → `bool`\n: Returns `true` if any commands have been registered\n\n\n## Examples\n\n\n### `<<StartQuest>>`\n\nSuppose we want to have a yarn command `<<StartQuest>>`, which would initiate a quest. The command\nwould take the quest name and quest ID as arguments. Technically, just the ID should be enough --\nbut then it would be really difficult to read the yarn script and understand what quest is being\ninitiated. So, instead we'll pass both the ID and the name, and then check at runtime that the ID\nof the quest matches its name.\n\nA typical invocation of this command might look like this (note that the name of the quest is in\nquotes, otherwise it would be parsed as four different arguments `\"Get\"`, `\"rid\"`, `\"of\"`, and\n`\"bandits\"`):\n\n```yarn\n<<StartQuest Q037 \"Get rid of bandits\">>\n```\n\nIn order to implement this command, we create a Dart function `startQuest()` with two string\narguments. The function will do a brief animated \"Started quest X\" message, but we don't want the\ngame dialogue to wait for that message, so we'll make the function return `void`, not a future.\nFinally, we register the command with `commands.addCommand2()`.\n\n```dart\nclass MyGame {\n  late YarnProject yarnProject;\n\n  void startQuest(String questId, String questName) {\n    assert(quests.containsKey(questId));\n    assert(quests[questId]!.name == questName);\n    // ...\n  }\n  @override\n  void onLoad() {\n    yarnProject = YarnProject()\n      ..commands.addCommand2('StartQuest', startQuest);\n  }\n}\n```\n\nNote that the name of the Dart function is different from the name of the command -- you can choose\nwhatever names suit your programming style best.\n\n\n### `<<prompt>>`\n\nThe `<<prompt>>` function will open a modal dialogue and ask the user to enter their response. This\ncommand will be waiting for the user's input, so it must return a future. Also, we want to return\nthe result of the prompt into the dialogue -- but, unfortunately, the commands are not expressions,\nand are not supposed to return values. So instead we will write the result into a global variable\n`$prompt`, and then the dialogue can access that variable in order to read the result of the prompt.\n\n```dart\nclass MyGame {\n  final YarnProject yarnProject = YarnProject();\n\n  Future<void> prompt(String message) async {\n    // This will wait until the modal dialog is popped from the router stack\n    final name = await router.pushAndWait(KeyboardDialog(message));\n    yarnProject.variables.setVariable(r'$prompt', name);\n  }\n\n  @override\n  void onLoad() {\n    yarnProject\n      ..variables.setVariable(r'$prompt', '')\n      ..commands.addCommand1('prompt', prompt);\n  }\n}\n```\n\nThen in a yarn script this command can be used like this:\n\n```yarn\n<<declare $name as String>>\n\ntitle: Greeting\n---\nGuide: Hello, my name is Jenny, and you?\n<<prompt \"Enter your name:\">>\n<<set $player = $prompt>>  // Store the name for later\nGuide: Nice to meet you, {$player}\n===\n```\n\n\n### `<<give>>`\n\nSuppose that we want to make a command that will give the player a certain item, or a number of\nitems. This command would take 3 arguments: the person who gives the items, the name of the item,\nand the quantity. For example:\n\n```yarn\n<<give {$quest_reward} TraderJoe>>\n```\n\nNote that the quest reward variable will contain both the reward item and its amount, for example\nit could be `\"100 gold\"`, `\"5 potion_of_healing\"`, or `'1 \"Sword of Darkness\"'`. When such\nvariable is substituted into the command at runtime, the command becomes equivalent to\n\n```yarn\n<<give 100 gold TraderJoe>>\n<<give 5 potion_of_healing TraderJoe>>\n<<give 1 \"Sword of Darkness\" TraderJoe>>\n```\n\nwhich will then be parsed as a regular 3-argument command corresponding to the following Dart\nfunction:\n\n```dart\n/// Takes [amount] of [item]s from [source] and gives them to the player.\nvoid give(int amount, String item, String source) {\n  // ...\n}\n```\n\n\n## See also\n\n- The description of [user-defined commands] in the YarnSpinner language.\n- The [UserDefinedCommand] class, which is used to inform a [DialogueView] that a custom command\n  is being executed.\n\n\n[DialogueView]: dialogue_view.md\n[UserDefinedCommand]: user_defined_command.md\n[YarnProject]: yarn_project.md\n[user-defined commands]: ../language/commands/user_defined_commands.md\n"
  },
  {
    "path": "doc/other_modules/jenny/runtime/dialogue_choice.md",
    "content": "# DialogueChoice\n\nThe **DialogueChoice** class represents multiple [Option] lines in the `.yarn` script, which will be\npresented to the user so that they can make a choice for how the dialogue should proceed. The\n`DialogueChoice` objects will be delivered to your `DialogueView` with the method\n`onChoiceStart()`.\n\n\n## Properties\n\n**options** `List<DialogueOption>`\n: The list of [DialogueOption]s comprising this choice set.\n\n\n[Option]: ../language/options.md\n[DialogueOption]: dialogue_option.md\n"
  },
  {
    "path": "doc/other_modules/jenny/runtime/dialogue_line.md",
    "content": "# DialogueLine\n\n```{dartdoc}\n:package: jenny\n:symbol: DialogueLine\n:file: src/structure/dialogue_line.dart\n\n[Line]: ../language/lines.md\n```\n"
  },
  {
    "path": "doc/other_modules/jenny/runtime/dialogue_option.md",
    "content": "# DialogueOption\n\nThe **DialogueOption** class represents a single [Option] line in the `.yarn` script. Multiple\noptions will be grouped into [DialogueChoice] objects.\n\n\n## Properties\n\n**text** `String`\n: The computed text of the option, after evaluating the inline expressions, stripping the markup,\n  and processing the escape sequences.\n\n**tags** `List<String>`\n: The list of hashtags for this option. If there are no hashtags, the list will be empty. Each entry\n  in the list will be a simple string starting with `#`.\n\n**attributes** `List<MarkupAttribute>`\n: The list of markup spans associated with the option. Each [MarkupAttribute] corresponds to a\n  single span within the **text**, delineated with markup tags.\n\n**isAvailable** `bool`\n: The result of evaluating the *conditional* of this option. If the option has no conditional, this\n  will return `true`.\n\n**isDisabled** `bool`\n: Same as `!isAvailable`.\n\n\n[Option]: ../language/options.md\n[DialogueChoice]: dialogue_choice.md\n"
  },
  {
    "path": "doc/other_modules/jenny/runtime/dialogue_runner.md",
    "content": "# DialogueRunner\n\n```{dartdoc}\n:file: src/dialogue_runner.dart\n:symbol: DialogueRunner\n:package: jenny\n```\n\n\n## Execution model\n\nThe `DialogueRunner` uses futures as a main mechanism for controlling the timing of the dialogue\nprogression. For each event, the dialogue runner will invoke the corresponding callback on all its\n[DialogueView]s, and each of those callbacks may return a future. The dialogue runner then awaits\non all of these futures (in parallel), before proceeding to the next event.\n\nFor a simple `.yarn` script like this\n\n```yarn\ntitle: main\n---\nHello\n-> Hi\n-> Go away\n   <<jump Away>>\n===\n\ntitle: Away\n---\n<<OhNo>>\n===\n```\n\nthe sequence of emitted events will be as follows (assuming the second option is selected):\n\n- `onDialogueStart()`\n- `onNodeStart(Node(\"main\"))`\n- `onLineStart(Line(\"Hello\"))`\n- `onLineFinish(Line(\"Hello\"))`\n- `onChoiceStart(Choice([\"Hi\", \"Go away\"]))`\n- `onChoiceFinish(Option(\"Go away\"))`\n- `onNodeFinish(Node(\"main\"))`\n- `onNodeStart(Node(\"Away\"))`\n- `onCommand(Command(\"OhNo\"))`\n- `onNodeFinish(Node(\"Away\"))`\n- `onDialogueFinish()`\n\n:::{note}\nKeep in mind that if a `DialogueError` is thrown while running the dialogue, then the dialogue will\nterminate immediately and none of the `*Finish` callbacks will run.\n:::\n\n\n[DialogueView]: dialogue_view.md\n"
  },
  {
    "path": "doc/other_modules/jenny/runtime/dialogue_view.md",
    "content": "# DialogueView\n\n```{dartdoc}\n:file: src/dialogue_view.dart\n:symbol: DialogueView\n:package: jenny\n\n[Completer]: https://api.dart.dev/stable/2.18.6/dart-async/Completer-class.html\n[FutureOr]: https://api.dart.dev/stable/2.18.6/dart-async/FutureOr-class.html\n[user-defined command]: user_defined_command.md\n[<<jump>>]: ../language/commands/jump.md\n[<<stop>>]: ../language/commands/stop.md\n[<<visit>>]: ../language/commands/visit.md\n```\n"
  },
  {
    "path": "doc/other_modules/jenny/runtime/function_storage.md",
    "content": "# FunctionStorage\n\nThe **FunctionStorage** is a part of [YarnProject] responsible for storing all [user-defined\nfunctions]. You can access it as the `YarnProject.functions` property.\n\nThe function storage can be used to register any number of custom functions, making them\navailable to use in yarn scripts. Such functions must be registered before parsing the yarn\nscripts, or the compiler will throw an error that the function name is not recognized.\n\nA Dart function can be registered as a user-defined function in Jenny, if it satisfies the\nfollowing requirements:\n\n- its return type is one of `int`, `double`, `num`, `bool`, or `String`;\n- all its arguments are positional, i.e. there are no named arguments;\n- all its arguments have types `int`, `int?`, `double`, `double?`, `num`, `num?`, `bool`, `bool?`,\n  `String`, or `String?`;\n- the nullable arguments, if any, must come after the non-nullable ones. These arguments become\n  optional in Yarn scripts, and if not provided they will be passed as `null` values;\n- the first argument in a function can also be `YarnProject`. If such argument is present, then\n  it will be passed automatically. For example, if you have a function `fn(YarnProject, int)`,\n  then it can be invoked from the yarn script simply as `fn(1)`.\n\nA Dart function can then be added using one of the methods `addFunction0`, ..., `addFunction4`,\ndepending on how many arguments the function has (this is a limitation of Dart's template language).\nWhen registering a function, you give it a `name`, under this function will be known in Yarn\nscripts. This name can be the same, or different from the real function's name. For example, you\nmay have a function `hasVisitedTheWizard()` in your game, but you'd register it under the name\n`has_visited_the_wizard()` in your YarnProject.\n\nKeep in mind that the name of the user-defined function must be:\n\n- unique;\n- a valid ID;\n- cannot be the same as any built-in function.\n\n\n## Methods\n\n**hasFunction**(`String name`) → `bool`\n: Returns the status of whether the function `name` has been already registered.\n\n**addFunction0**(`String name`, `T0 Function() fn`)\n: Registers a no-argument function `fn` as the user-defined function `name`.\n\n**addFunction1**(`String name`, `T0 Function(T1) fn1`)\n: Registers a single-argument function `fn1` under the name `name`.\n\n**addFunction2**(`String name`, `T0 Function(T1, T2) fn2`)\n: Registers a two-argument function `fn2` with the given `name`.\n\n**addFunction3**(`String name`, `T0 Function(T1, T2, T3) fn3`)\n: Registers a three-argument function `fn3` with the name `name`.\n\n**addFunction4**(`String name`, `T0 Function(T1, T2, T3, T4) fn4`)\n: Registers a four-argument function `fn4` as `name`.\n\n**clear**\n: Removes all user-defined functions\n\n**remove**(`String name`)\n: Removes the user-defined function with the specified `name`.\n\n\n## Properties\n\n**length** → `int`\n: The number of user-defined functions registered so far.\n\n**isEmpty** → `bool`\n: Returns `true` if no user-defined functions were registered.\n\n**isNotEmpty** → `bool`\n: Has any functions been registered at all?\n\n\n[user-defined functions]: ../language/expressions/functions/functions.md#user-defined-functions\n"
  },
  {
    "path": "doc/other_modules/jenny/runtime/jenny_runtime.md",
    "content": "# Jenny Runtime\n\n```{toctree}\n:hidden:\n\nCharacter          <character.md>\nCharacterStorage   <character_storage.md>\nCommandStorage     <command_storage.md>\nDialogueChoice     <dialogue_choice.md>\nDialogueLine       <dialogue_line.md>\nDialogueOption     <dialogue_option.md>\nDialogueRunner     <dialogue_runner.md>\nDialogueView       <dialogue_view.md>\nFunctionStorage    <function_storage.md>\nMarkupAttribute    <markup_attribute.md>\nNode               <node.md>\nUserDefinedCommand <user_defined_command.md>\nVariableStorage    <variable_storage.md>\nYarnProject        <yarn_project.md>\n```\n"
  },
  {
    "path": "doc/other_modules/jenny/runtime/markup_attribute.md",
    "content": "# MarkupAttribute\n\nA **MarkupAttribute** is a descriptor of a subrange of text in a [line], demarcated with markup\ntags. For example, in a `.yarn` line below there are two ranges of text surrounded by markup tags,\nand therefore there will be two `MarkupAttribute`s associated with this line:\n\n```yarn\n[b]Jenny[/b] is a library based on \\\n    [link url=\"docs.yarnspinner.dev\"]YarnSpinner[/link] for Unity.\n```\n\nThese `MarkupAttribute`s can be found in the `.attributes` property of a [DialogueLine][line].\n\n\n## Properties\n\n**name** `String`\n: The name of the markup tag. In the example above, the name of the first attribute is `\"b\"`, and\n  the second is `\"link\"`.\n\n**start**, **end** `int`\n: The location of the marked-up span within the final text of the line. The first index is\n  inclusive, while the second is exclusive. The `start` may be equal to `end` for a zero-width\n  markup attribute.\n\n**length** `int`\n: The length of marked-up text. This is always equal to `end - start`.\n\n**parameters** `Map<String, dynamic>`\n: The set of parameters associated with this markup attribute. In the example above, the first\n  markup attribute has no parameters, so this map will be empty. The second markup attribute has a\n  single parameter, so this map will be equal to `{\"url\": \"docs.yarnspinner.dev\"}`.\n\n  The type of each parameter will be either `String`, `num`, or `bool`, depending on the type of\n  expression give in the `.yarn` script. The expressions for parameter values can be dynamic, that\n  is they can be evaluated at runtime. In the example below, the parameter `color` will be equal to\n  the value of the variable `$color`, which may change each time the line is run.\n\n  ```yarn\n  My [i]favorite[/i] color is [bb color=$color]{$color}[/bb].\n  ```\n\n[line]: dialogue_line.md\n"
  },
  {
    "path": "doc/other_modules/jenny/runtime/node.md",
    "content": "# Node\n\nThe **Node** class represents a single [node] within the `.yarn` script. The objects of this class\nwill be delivered to your [DialogueView]s with the methods `onNodeStart()`, `onNodeFinish()`.\n\n\n## Properties\n\n**title** `String`\n: The title (name) of the node.\n\n**tags** `Map<String, String>`\n: Additional tags specified in the header of the node. The map will be empty if there were no tags\n  besides the required `title` tag.\n\n**iterator** `Iterator<DialogueEntry>`\n: The content of the node, which is a sequence of `DialogueLine`s, `DialogueChoice`s, or\n  `Command`s.\n\n[node]: ../language/nodes.md\n[DialogueView]: dialogue_view.md\n"
  },
  {
    "path": "doc/other_modules/jenny/runtime/user_defined_command.md",
    "content": "# UserDefinedCommand\n\nThe **UserDefinedCommand** class represents a single invocation of a custom (non-built-in) command\nwithin a yarn script. Objects of this type will be delivered to a [DialogueView] in its\n`.onCommand()` method.\n\n\n## Properties\n\n**name** `String`\n: The name of the command, without the angle brackets. For example, if the command is `<<smile>>`\n  in the yarn script, then its name will be `\"smile\"`.\n\n**argumentString** `String`\n: Command arguments, as a single string. For example, if the command is `<<move Hippo {$delta}>>`,\n  and the value of variable `$delta` is `3.17`, then the argument string will be `\"Hippo 3.17\"`.\n\n  The `argumentString` is re-evaluated every time the command is executed, however, it is an error\n  to access this property before the command was executed by the dialogue runner.\n\n**arguments** `List<dynamic>?`\n: Command arguments, as a list of parsed values. This property will be null if the command was\n  declared without a signature (i.e. as an \"orphaned command\"). However, if the command was linked\n  as an external function, then the number and types of arguments in the list will correspond to\n  the arguments of that function.\n\n  In the same example as above, the `arguments` will be `['Hippo', 3.17]`, assuming the linked Dart\n  function is `move(String target, double distance)`.\n\n\n## See also\n\n- The description of [User-defined Commands] in the YarnSpinner language.\n- The guide on how to register a new custom command in the [CommandStorage] document.\n\n\n[CommandStorage]: command_storage.md\n[DialogueView]: dialogue_view.md\n[User-defined Commands]: ../language/commands/user_defined_commands.md\n"
  },
  {
    "path": "doc/other_modules/jenny/runtime/variable_storage.md",
    "content": "# VariableStorage\n\n```{dartdoc}\n:package: jenny\n:symbol: VariableStorage\n:file: src/variable_storage.dart\n```\n\n\n## Accessing variable storage\n\nVariable storage is accessed via the [YarnProject].\n\n```dart\nfinal variables = yarnProject.variables;\n```\n\n\n## Removing variables\n\nIn most cases variables should be retained for the life of the [YarnProject]. However there may be\nsituations where variables need to be removed from storage. For example, in a game with many\nscenes, variables specific to that scene could be removed if they are no longer required.\n\nRemove all variables with `clear`. By default this will retain node visit counts, which are also\nstored as variables. Node visit counts are used by Yarn for logic such as 'do this if the node has\nalready been visited', so it's best to leave these alone. However, to remove them as well set\n`clearNodeVisits` to `true`.\n\n```dart\n/// Clear all variables except node visit counts.\nyarnProject.variables.clear();\n\n/// Clear all variables including node visit counts.\nyarnProject.variables.clear(clearNodeVisits: true);\n```\n\nUse `remove` to remove a single variable.\n\n```dart\nyarnProject.variables.remove('money');\n```\n\n[YarnProject]: yarn_project.md\n"
  },
  {
    "path": "doc/other_modules/jenny/runtime/yarn_project.md",
    "content": "# Yarn Project\n\nA **YarnProject** is the central hub for all yarn scripts and the accompanying information.\nGenerally, there would be a single `YarnProject` in a game, though it is also possible to make\nseveral yarn projects if their content is completely independent.\n\nThe standard sequence of initializing a `YarnProject` is the following:\n\n- link user-defined functions;\n- link user-defined commands;\n- set the locale (if different from `en`);\n- parse a `.yarn` script containing declarations of global variables and characters;\n- parse all other `.yarn` scripts;\n- restore the variables from a save-game storage.\n\nFor example:\n\n```dart\nfinal yarn = YarnProject()\n  ..functions.addFunction0('money', player.getMoney)\n  ..commands.addCommand1('achievement', player.earnAchievement)\n  ..parse(readFile('project.yarn'))\n  ..parse(readFile('chapter1.yarn'))\n  ..parse(readFile('chapter2.yarn'));\n```\n\n\n## Properties\n\n**locale** `String`\n: The language used in this `YarnProject` (the default is `'en'`). Selecting a different language\n  changes the builtin `plural()` function.\n\n**random** `Random`\n: The random number generator. This can be replaced with a different generator, if, for example,\n  you need to control the seed.\n\n**nodes** `Map<String, Node>`\n: All [Node]s loaded into the project, keyed by their titles.\n\n**variables** `VariableStorage`\n: The container for all global variables used in this yarn project. There could be several reasons\n  to access this storage:\n\n  <!-- markdownlint-disable MD006 MD007 -->\n  - to change the value of a yarn variable from the game. This enables you to pass the information\n    from the game into the dialogue. For example, your dialogue may have variable `$gold`, which\n    you may want to update whenever the player's amount of money changes within the game.\n  - to store the values of all yarn variables during the save game, and to restore them when\n    loading the game.\n  <!-- markdownlint-enable MD006 MD007 -->\n\n**functions** `FunctionStorage`\n: The [container][FunctionStorage] for all user-defined functions linked into the project. The main\n  reason to access this property is to register new custom function to be available at runtime.\n\n  Note that all custom functions must be added to the `YarnProject` before they can be used in a\n  dialogue script -- otherwise a compile error will occur when encountering an unknown function.\n\n**commands** `CommandStorage`\n: The [container][CommandStorage] for all user-defined commands linked into the project. The main\n  reason to access this container is to register new custom commands.\n\n  All custom commands must be added before they can be used in the dialogue script.\n\n**characters** `CharacterStorage`\n: The [container][CharacterStorage] for all [Character] objects declared in your yarn scripts.\n\n**strictCharacterNames** `bool`\n: If `true` (default), the validity of character names will be strictly enforced. That is, all\n  characters must be declared before they can be used, using the [\\<\\<character\\>\\>] commands. If\n  this property is set to false, then new [Character] objects will be created automatically as\n  they are encountered in scripts.\n\n**trueValues**, **falseValues** `Set<String>`\n: The strings that can be recognized as `true`/`false` values respectively.\n\n**variables** `VariableStorage`\n: The [container][VariableStorage] for all variables declared and manipulated in your yarn scripts.\n  This is also used for maintaining the visit counts for nodes that the user has visited. To\n  implement a 'save game' feature it is possible to save the variables from\n  `VariableStorage.variables` and later restore them again.\n\n\n## Methods\n\n**parse**(`String text`)\n: Parses and compiles the `text` of a yarn script. After this command, the nodes contained within\n  the script will be runnable.\n\n  This method can be executed multiple times, and each time the new nodes will be added to the\n  existing ones.\n\n\n[\\<\\<character\\>\\>]: ../language/commands/character.md\n[Character]: character.md\n[CharacterStorage]: character_storage.md\n[CommandStorage]: command_storage.md\n[FunctionStorage]: function_storage.md\n[Node]: node.md\n[VariableStorage]: variable_storage.md\n"
  },
  {
    "path": "doc/other_modules/other_modules.md",
    "content": "# Other Modules\n\n:::{package} jenny\n\nThis module lets you add interactive dialogue into your game. The module itself handles Yarn scripts\nand the dialogue runtime; use bridge package `flame_jenny` in order to add it into a Flame game.\n:::\n\n:::{package} oxygen\n\nOxygen is a lightweight Entity Component System framework written in Dart, with a focus on\nperformance and ease of use. This package replaces the Flame Component System with the Oxygen\nEntity Component System.\n:::\n\n\n```{toctree}\n:hidden:\n\njenny    <jenny/jenny.md>\noxygen   <oxygen/oxygen.md>\n```\n"
  },
  {
    "path": "doc/other_modules/oxygen/components.md",
    "content": "# Components\n\nComponents in Oxygen are different than the ones from FCS mainly because instead of containing logic\nthey only contain data. This data is then used in systems which in turn define the logic. To\naccommodate people who are switching from FCS to Oxygen we implemented a few components to help you\nget started. Some of these components are based on the multiple functionalities that the\n`PositionComponent` from FCS has. Others are just easy wrappers around certain Flame API\nfunctionality, they are often accompanied by predefined systems that you can use.\n\nComponents can be registered to the world using the `world.registerComponent` method on\n`OxygenGame`.\n\n\n## PositionComponent\n\nThe `PositionComponent` as its name implies is a component that describes the position of an\nentity. And it is registered to the world by default.\n\nCreating a positioned entity using OxygenGame:\n\n```dart\ngame.createEntity(\n  position: Vector2(100, 100),\n  size: // ...\n);\n```\n\nCreating a positioned entity using the World:\n\n```dart\nworld.createEntity()\n  ..add<PositionComponent, Vector2>(Vector2(100, 100));\n```\n\n\n## SizeComponent\n\nThe `SizeComponent` as its name implies is a component that describes the size of an entity.\nAnd it is registered to the world by default.\n\nCreating a sized entity using OxygenGame:\n\n```dart\ngame.createEntity(\n  position: // ...\n  size: Vector2(50, 50),\n);\n```\n\nCreating a sized entity using the World:\n\n```dart\nworld.createEntity()\n  ..add<SizeComponent, Vector2>(Vector2(50, 50));\n```\n\n\n## AnchorComponent\n\nThe `AnchorComponent` as its name implies is a component that describes the anchor position of an\nentity. And it is registered to the world by default.\n\nThis component is especially useful when you are using the `BaseSystem`. But can also\nbe used for your anchoring logic.\n\nCreating an anchored entity using OxygenGame:\n\n```dart\ngame.createEntity(\n  position: // ...\n  size: // ...\n  anchor: Anchor.center,\n);\n```\n\nCreating an anchored entity using the World:\n\n```dart\nworld.createEntity()\n  ..add<AnchorComponent, Anchor>(Anchor.center);\n```\n\n\n### AngleComponent\n\nThe `AngleComponent` as its name implies is a component that describes the angle of an entity and\nit is registered to the world by default. The angle is in radians.\n\nThis component is especially useful when you are using the `BaseSystem`. But can also\nbe used for your angle logic.\n\nCreating an angled entity using OxygenGame:\n\n```dart\ngame.createEntity(\n  position: // ...\n  size: // ...\n  angle: 1.570796,\n);\n```\n\nCreating an angled entity using the World:\n\n```dart\nworld.createEntity()\n  ..add<AngleComponent, double>(1.570796);\n```\n\n\n## FlipComponent\n\nThe `FlipComponent` can be used to flip your rendering on either the X or Y axis. It is registered\nto the world by default.\n\nThis component is especially useful when you are using the `BaseSystem`. But can also\nbe used for your flipping logic.\n\nCreating an entity that is flipped on its X-axis using OxygenGame:\n\n```dart\ngame.createEntity(\n  position: // ...\n  size: // ...\n  flipX: true\n);\n```\n\nCreating an entity that is flipped on its X-axis using the World:\n\n```dart\nworld.createEntity()\n  ..add<FlipComponent, FlipInit>(FlipInit(flipX: true));\n```\n\n\n## SpriteComponent\n\nThe `SpriteComponent` as its name implies is a component that describes the sprite of an entity and\nit is registered to the world by default.\n\nThis allows you to assign a Sprite to an Entity.\n\nCreating an entity with a sprite using OxygenGame:\n\n```dart\ngame.createEntity(\n  position: // ...\n  size: // ...\n)..add<SpriteComponent, SpriteInit>(\n  SpriteInit(await game.loadSprite('pizza.png')),\n);\n```\n\nCreating an entity with a sprite using World:\n\n```dart\nworld.createEntity()\n  ..add<SpriteComponent, SpriteInit>(\n    SpriteInit(await game.loadSprite('pizza.png')),\n  );\n```\n\n\n## TextComponent\n\nThe `TextComponent` as its name implies is a component that adds a text component to an entity.\nAnd it is registered to the world by default.\n\nThis allows you to add text to your entity, combined with the `PositionComponent` you can use it\nas a text entity.\n\nCreating an entity with text using OxygenGame:\n\n```dart\ngame.createEntity(\n  position: // ...\n  size: // ...\n)..add<TextComponent, TextInit>(\n  TextInit(\n    'Your text',\n    config: const TextPaintConfig(),\n  ),\n);\n```\n\nCreating an entity with text using World:\n\n```dart\nworld.createEntity()\n  ..add<TextComponent, TextInit>(\n    TextInit(\n      'Your text',\n      config: const TextPaintConfig(),\n    ),\n  );\n```\n\n\n## ParticleComponent\n\nThe `ParticleComponent` wraps a `Particle` from Flame. Combined with the `ParticleSystem` you can\neasily add particles to your game without having to worry about how to render a particle or when/how\nto update one.\n\nCreating an entity with a particle using OxygenGame:\n\n```dart\ngame.createEntity(\n  position: // ...\n  size: // ...\n)..add<ParticleComponent, Particle>(\n  // Your Particle.\n);\n```\n\nCreating an entity with a particle using World:\n\n```dart\nworld.createEntity()\n  ..add<ParticleComponent, Particle>(\n    // Your Particle.\n  );\n```\n"
  },
  {
    "path": "doc/other_modules/oxygen/oxygen.md",
    "content": "# Oxygen\n\nWe (the Flame organization) built an ECS (Entity Component System) named Oxygen.\n\nIf you want to use Oxygen specifically for Flame as a replacement for the\nFCS(Flame Component System) you should use our bridge library\n[flame_oxygen](https://github.com/flame-engine/flame/tree/main/packages/flame_oxygen) and if you\njust want to use it in a Dart project you can use the\n[oxygen](https://github.com/flame-engine/oxygen) library directly.\n\nIf you are not familiar with Oxygen yet we recommend you read up on its\n[documentation](https://github.com/flame-engine/oxygen/tree/main/doc).\n\nTo use it in your game you just need to add `flame_oxygen` to your `pubspec.yaml`, as can be seen\nin the\n[Oxygen example](https://github.com/flame-engine/flame/tree/main/packages/flame_oxygen/example)\nand in the `pub.dev` [installation instructions](https://pub.dev/packages/flame_oxygen).\n\n\n## OxygenGame (Game extension)\n\nIf you are going to use Oxygen in your project it can be a good idea to use the Oxygen specific\nextension of the `Game` class.\n\nIt is called `OxygenGame` and it will give you full access to the Oxygen framework while also\nhaving full access to the Flame game loop.\n\nInstead of using `onLoad`, as you are used to with Flame, `OxygenGame` comes with the `init`\nmethod. This method is called in the `onLoad` but before the world initialization, allowing you\nto register components and systems and do anything else that you normally would do in `onLoad`.\n\nA simple `OxygenGame` implementation example can be seen in the\n[example folder](https://github.com/flame-engine/flame/tree/main/packages/flame_oxygen/example).\n\nThe `OxygenGame` also comes with it's own `createEntity` method that automatically adds certain\ndefault components on the entity. This is especially helpful when you are using the\n[BaseSystem](#basesystem) as your base.\n\n\n## Systems\n\nSystems define the logic of your game. In FCS you normally would add your logic inside a component\nwith Oxygen we use systems for that. Oxygen itself is completely platform agnostic, meaning it has\nno render loop. It only knows `execute`, which is a method equal to the `update` method in Flame.\n\nOn each `execute` Oxygen automatically calls all the systems that were registered in order. But in\nFlame we can have different logic for different loops (render/update). So in `flame_oxygen` we\nintroduced the `RenderSystem` and `UpdateSystem` mixin. These mixins allow you to add the `render`\nmethod and the `update` method respectively to your custom system. For more information see the\n[RenderSystem](#mixin-rendersystem) and [UpdateSystem](#mixin-updatesystem) section.\n\nIf you are coming from FCS you might expect certain default functionality that you normally got\nfrom the `PositionComponent`. As mentioned before components do not contain any kind of logic, but\nto give you the same default functionality we also created a class called `BaseSystem`. This system\nacts almost identical to the prerender logic from the `PositionComponent` in FCS. You only have\nto subclass it to your own system. For more information see the\n[BaseSystem](#basesystem) section.\n\nSystems can be registered to the world using the `world.registerSystem` method on\n[OxygenGame](#oxygengame-game-extension).\n\n\n### mixin GameRef\n\nThe `GameRef` mixin allows a system to become aware of the `OxygenGame` instance its attached to.\nThis allows easy access to the methods on the game class.\n\n```dart\nclass YourSystem extends System with GameRef<YourGame> {\n  @override\n  void init() {\n    // Access to game using the .game property\n  }\n\n  // ...\n}\n```\n\n\n### mixin RenderSystem\n\nThe `RenderSystem` mixin allows a system to be registered for the render loop.\nBy adding a `render` method to the system you get full access to the canvas as\nyou normally would in Flame.\n\n```dart\nclass SimpleRenderSystem extends System with RenderSystem {\n  Query? _query;\n\n  @override\n  void init() {\n    _query = createQuery([/* Your filters */]);\n  }\n\n  void render(Canvas canvas) {\n    for (final entity in _query?.entities ?? <Entity>[]) {\n      // Render entity based on components\n    }\n  }\n}\n```\n\n\n### mixin UpdateSystem\n\nThe `MixinSystem` mixin allows a system to be registered for the update loop.\nBy adding a `update` method to the system you get full access to the delta time as you\nnormally would in Flame.\n\n```dart\nclass SimpleUpdateSystem extends System with UpdateSystem {\n  Query? _query;\n\n  @override\n  void init() {\n    _query = createQuery([/* Your filters */]);\n  }\n\n  void update(double dt) {\n    for (final entity in _query?.entities ?? <Entity>[]) {\n      // Update components values\n    }\n  }\n}\n```\n\n\n### BaseSystem\n\nThe `BaseSystem` is an abstract class whose logic can be compared to the `PositionComponent`\nfrom FCS. The `BaseSystem` automatically filters all entities that have the `PositionComponent`\nand `SizeComponent` from `flame_oxygen`. On top of that you can add your own filters by defining\na getter called `filters`. These filters are then used to filter down the entities you are\ninterested in.\n\nThe `BaseSystem` is also fully aware of the game instance. You can access the game instance by using\nthe `game` property. This also gives you access to the `createEntity` helper method on `OxygenGame`.\n\nOn each render loop the `BaseSystem` will prepare your canvas the same way the `PositionComponent`\nfrom FCS would (translating, rotating and setting the anchor. After that it will call the\n`renderEntity` method so you can add your own render logic for that entity on a prepared canvas.\n\nThe following components will be checked by `BaseSystem` for the preparation of the canvas:\n\n- `PositionComponent` (required)\n- `SizeComponent` (required)\n- `AnchorComponent` (optional, defaults to `Anchor.topLeft`)\n- `AngleComponent` (optional, defaults to `0`)\n\n```dart\nclass SimpleBaseSystem extends BaseSystem {\n  @override\n  List<Filter<Component>> get filters => [];\n\n  @override\n  void renderEntity(Canvas canvas, Entity entity) {\n    // The canvas is translated, rotated and fully prepared for rendering.\n  }\n}\n```\n\n\n### ParticleSystem\n\nThe `ParticleSystem` is a simple system that brings the Particle API from Flame to Oxygen. This\nallows you to use the [ParticleComponent](components.md#particlecomponent) without having to worry\nabout how it will render or when to update it. As most of that logic is already contained in the\nParticle itself.\n\nSimply register the `ParticleSystem` and the `ParticleComponent` to your world like so:\n\n```dart\nworld.registerSystem(ParticleSystem());\n\nworld.registerComponent<ParticleComponent, Particle>(() => ParticleComponent);\n```\n\nYou can now create a new entity with a `ParticleComponent`. For more info about that see the\n`ParticleComponent` section.\n\n```{toctree}\n:hidden:\n\nComponents     <components.md>\n```\n"
  },
  {
    "path": "doc/resources/resources.md",
    "content": "# Resources\n\n- [Flame API](https://pub.dev/documentation/flame/--VERSION--/)\n- [Flame Examples](https://examples.flame-engine.org/#/)\n\n```{toctree}\n:hidden:\n\nFlame API        <https://pub.dev/documentation/flame/--VERSION--/>\nFlame Examples   <https://examples.flame-engine.org/#/>\n```\n"
  },
  {
    "path": "doc/tutorials/bare_flame_game.md",
    "content": "# Bare Flame game\n\nThis tutorial assumes that you have basic familiarity with using the command line, and the following\nprograms on your computer (all of them are free):\n\n- [Flutter], version 3.13.0 or above.\n- [Android Studio], or any other IDE for example [Visual Studio Code].\n- [git] (optional), to save your project on GitHub.\n\n\n## 1. Check flutter installation\n\nFirst, let's verify that your Flutter SDK was installed correctly and is accessible from the command\nline:\n\n```shell\n$ flutter doctor\nDoctor summary (to see all details, run flutter doctor -v):\n[✓] Flutter (Channel stable, 3.13.7, on macOS 13.6 22G120 darwin-arm64, locale en)\n[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0)\n[✓] Xcode - develop for iOS and macOS (Xcode 15.0)\n[✓] Chrome - develop for the web\n[✓] Android Studio (version 2021.2)\n[✓] IntelliJ IDEA Community Edition (version 2022.2.2)\n[✓] VS Code (version 1.83.0)\n[✓] Connected device (2 available)\n[✓] Network resources\n\n• No issues found!\n```\n\nYour output will be slightly different, but the important thing is to verify that no errors are\nreported and that your Flutter version is at least **3.13.0**.\n\n\n## 2. Create the Project Directory\n\nNow you need to come up with a name for your project. The name can only use lowercase Latin letters,\ndigits, and underscores. It must also be a valid Dart identifier (thus, for example, it cannot be a\nkeyword). In this tutorial, we're going to call the project **syzygy**, which is a totally real\nnon-made-up word.\n\nCreate the directory for your new project:\n\n```shell\nmkdir -p ~/projects/syzygy\ncd ~/projects/syzygy\n```\n\n\n## 3. Initialize empty Flutter project\n\nTo turn this barren directory into an actual Flutter project, run the following command:\n\n```shell\nflutter create .\n```\n\n(I have omitted the output for brevity, but there will be lots of output).\n\nYou can verify that the project files were created successfully:\n\n```shell\n$ ls\nREADME.md               android/   lib/           pubspec.yaml   test/\nanalysis_options.yaml   ios/       pubspec.lock   syzygy.iml     web/\n```\n\n\n## 4. Open the project in Android Studio\n\nLaunch Android Studio, then in the project selection window choose `[Open]` and navigate to your\nproject directory. With any luck, the project will now look like this:\n\n![Project in Android Studio](../images/tutorials/android-studio-screenshot-1.webp)\n\nIf you see only the `main.dart` file but not the side panel, then click the vertical `[Project]`\nbutton at the left edge of the window.\n\nBefore we proceed, let's fix the view in the left panel. Locate the button in the top left corner\nthat says `[Android]` in the screenshot. In this dropdown select the first option \"Project\". Your\nproject window should now look like this:\n\n![Project in Android Studio](../images/tutorials/android-studio-screenshot-2.webp)\n\nThe important part is that you should be able to see all files in your project directory.\n\n\n## 5. Clean up the project files\n\nThe default project created by Flutter is not very useful for making a Flame game, so we should get\nrid of it.\n\nFirst, open the file `pubspec.yaml` and replace it with the following code (adjusting the `name` and\n`description` to match your project):\n\n```yaml\nname: syzygy\ndescription: Syzygy Flame game\nversion: 0.0.0\npublish_to: none\n\nenvironment:\n  sdk: ^3.0.0\n  flutter: ^3.13.0\n\ndependencies:\n  flutter:\n    sdk: flutter\n  flame: ^--VERSION--\n```\n\nAfter that, press the `[Pub get]` button at the top of the window, or you could run command `flutter\npub get` from the terminal. This will \"apply\" the changes in `pubspec` file to your project, in\nparticular, it will download the Flame library which we have declared as a dependency. In the\nfuture, you should run `flutter pub get` whenever you make changes to this file.\n\nNow, open the file `lib/main.dart` and replace its content with the following:\n\n```dart\nimport 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart';\n\nvoid main() {\n  final game = FlameGame();\n  runApp(GameWidget(game: game));\n}\n```\n\nLastly, remove the file `test/widget_test.dart` completely.\n\n\n## 6. Run the project\n\nLet's verify that everything is working as intended, and the project can run.\n\nIn the menu bar at the top of the window find a dropdown that says `<no device selected>`. In that\ndropdown choose `<Chrome (web)>` instead.\n\nAfter that open the `main.dart` file and press the green arrow next to the `void main()` function in\nline 4. Select `[Run main.dart]` from the menu.\n\nThis should open a new Chrome window (which may take 10-30 seconds) and run your project in that\nwindow. For now, it will simply show a black screen, which is expected because we created the game\nin its simplest blank configuration.\n\n\n## 7. Sync to GitHub\n\nThe last step is to upload your project to GitHub. This is not required but strongly recommended as\nit will serve as a backup for your code. This step assumes that you already have a GitHub account.\n\nLog into your GitHub account, select `[Your repositories]` from your profile dropdown, and press the\ngreen `[New]` button. In the form, enter the repository name the same as your project name; select\ntype \"private\"; and opt out of adding initial files like `README`, `license`, and `.gitignore`.\n\nNow go to your project's directory in the terminal and execute the following commands (make sure to\nreplace the URL with the link to the repository that you just created):\n\n```shell\ngit init\ngit add --all\ngit commit -m 'Initial commit'\ngit remote add origin https://github.com/your-github-username/syzygy.git\ngit branch -M main\ngit push -u origin main\n```\n\nAt this point, if you go to your repository page on GitHub, you shall see that all your project\nfiles are there.\n\n\n## 8. Done\n\nThat's it! By this point you have\n\n- Created an initial blank state Flame project;\n- Set up the Android Studio IDE for that project;\n- Created a GitHub repository for the project.\n\nHappy coding!\n\n\n[Flutter]: https://docs.flutter.dev/get-started/install\n[git]: https://git-scm.com/downloads\n[Android Studio]: https://developer.android.com/studio\n[Visual Studio Code]: https://code.visualstudio.com/download\n"
  },
  {
    "path": "doc/tutorials/basic_shader/basic_shader.md",
    "content": "# Basic shader tutorial\n\nThis tutorial will give you a brief understanding of how to create and use basic shaders on\n`SpriteComponent`s with `PostProcess` and `PostProcessComponent` using Dart/Flutter and the Flame\nengine.\n\nThis tutorial assumes that you have a working Flame project set up. If you don't, please follow\nthe [](bare_flame_game.md) tutorial first.\n\nThe tutorial consists of 4 steps. We will create a simple outline shader for sprites which have a\ntransparent background layer.\n\n```{note}\nThis tutorial is intended to work on images with transparent\nbackground, like `.png` files.\n```\n\n*Created by Kornél (Hoodead) Lapu.*\n\n\n```{toctree}\n:hidden:\n\n1. Sprite Component         <step1.md>\n2. Outline Post Process     <step2.md>\n3. Shader                   <step3.md>\n4. User Input               <step4.md>\n5. Takeaways                <takeaways.md>\n```\n"
  },
  {
    "path": "doc/tutorials/basic_shader/step1.md",
    "content": "# 1. Sprite Component\n\n\n## Architecture and Responsibilities\n\nLet's create the component where we render our sprite and apply the shader. We will split this\ninto two classes:\n\n- a `SpriteComponent` subclass that loads the image and handles input events\n- a `PostProcessComponent` subclass that wraps the sprite and applies the shader\n\nThis separation means that shader changes only require editing the wrapper class, while sprite\nchanges like adding input event mixins or additional children only require editing the sprite\nclass.\n\n\n## Image resource\n\nFor this tutorial we need an image with a transparent background to apply the outline shader to.\nCreate an `assets/images/` directory in your project and add your `.png` image there.\n\nDon't forget to register the assets folder in `pubspec.yaml`:\n\n```yaml\nflutter:\n  assets:\n    - assets/images/\n```\n\n\n## Sprite\n\nCreate a new file named `sword_component.dart` (replace \"sword\" with your own image name):\n\n```dart\nimport 'package:flame/components.dart';\n\nclass SwordSprite extends SpriteComponent {\n  @override\n  Future<void> onLoad() async {\n    sprite = await Sprite.load('sword.png');\n    size = sprite!.srcSize;\n  }\n}\n```\n\n\n## Wrapper\n\nNext, add the wrapper class that applies the post process. In the same file, create:\n\n```dart\nimport 'package:flame/components.dart';\nimport 'package:flame/post_process.dart';\n\nimport 'package:basic_shader_tutorial/outline_postprocess.dart';\n\nclass OutlinedSwordSprite extends PostProcessComponent {\n  OutlinedSwordSprite({super.position, super.anchor})\n    : super(\n        children: [SwordSprite()],\n        postProcess: OutlinePostProcess(anchor: anchor ?? Anchor.topLeft),\n      );\n}\n```\n\n\n## Result\n\nThe final `sword_component.dart` file looks like this:\n\n```dart\nimport 'package:flame/components.dart';\nimport 'package:flame/post_process.dart';\n\nimport 'package:basic_shader_tutorial/outline_postprocess.dart';\n\nclass OutlinedSwordSprite extends PostProcessComponent {\n  OutlinedSwordSprite({super.position, super.anchor})\n    : super(\n        children: [SwordSprite()],\n        postProcess: OutlinePostProcess(anchor: anchor ?? Anchor.topLeft),\n      );\n}\n\nclass SwordSprite extends SpriteComponent {\n  @override\n  Future<void> onLoad() async {\n    sprite = await Sprite.load('sword.png');\n    size = sprite!.srcSize;\n  }\n}\n```\n\nThis won't compile yet because `OutlinePostProcess` doesn't exist. Let's create it in the next\nstep!\n"
  },
  {
    "path": "doc/tutorials/basic_shader/step2.md",
    "content": "# 2. Outline Post Process\n\n\n## Responsibility\n\nThe `PostProcess` class manages the fragment (pixel) shader. It is responsible for loading the\nshader program, creating GPU resources, and keeping uniform variables up to date each frame. You\ncan also expose runtime settings through uniforms, for example to enable or disable effects.\n\n\n## Post process\n\nCreate a new file named `outline_postprocess.dart`. This class loads the shader program in\n`onLoad()` and passes uniform values to the GPU each frame in `postProcess()`:\n\n```dart\nimport 'dart:ui';\n\nimport 'package:flutter/material.dart';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/post_process.dart';\n\nextension on Color {\n  Vector4 toVector4() {\n    return Vector4(r, g, b, a);\n  }\n}\n\nclass OutlinePostProcess extends PostProcess {\n  final double outlineSize;\n  Color outlineColor;\n  final Anchor anchor;\n\n  OutlinePostProcess({\n    this.outlineSize = 7.0,\n    this.outlineColor = Colors.purpleAccent,\n    this.anchor = Anchor.topLeft,\n  });\n\n  late final FragmentProgram _fragmentProgram;\n  late final FragmentShader _fragmentShader =\n      _fragmentProgram.fragmentShader();\n  late final Paint _myPaint = Paint()..shader = _fragmentShader;\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    _fragmentProgram =\n        await FragmentProgram.fromAsset('assets/shaders/outline.frag');\n  }\n\n  @override\n  void postProcess(Vector2 size, Canvas canvas) {\n    final preRenderedSubtree = rasterizeSubtree();\n\n    _fragmentShader.setFloatUniforms((value) {\n      value\n        ..setVector(size)\n        ..setFloat(outlineSize)\n        ..setVector(outlineColor.toVector4());\n    });\n\n    _fragmentShader.setImageSampler(0, preRenderedSubtree);\n\n    canvas\n      ..save()\n      ..translate(-size.x * anchor.x, -size.y * anchor.y)\n      ..drawRect(Offset.zero & size.toSize(), _myPaint)\n      ..restore();\n  }\n}\n```\n\nWith this file in place, the syntax error from the previous step will go away.\n\nSince the `PostProcessComponent` is the parent of the `SpriteComponent`, the post process renders\nfirst and the sprite is drawn on top. The `rasterizeSubtree()` call captures all children into an\nimage that the shader can sample from.\n\n\n## Usage\n\nNow we need to wire everything together. Open `main.dart` and add both a plain sprite and an\noutlined sprite to the world so we can compare them side by side:\n\n```dart\nimport 'package:flutter/material.dart';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\n\nimport 'package:basic_shader_tutorial/sword_component.dart';\n\nvoid main() {\n  runApp(\n    GameWidget(game: MyGame()),\n  );\n}\n\nclass MyGame extends FlameGame {\n  MyGame() : super(world: MyWorld());\n\n  @override\n  Color backgroundColor() => Colors.green;\n}\n\nclass MyWorld extends World {\n  @override\n  Future<void> onLoad() async {\n    add(\n      SwordSprite()\n        ..position = Vector2(-200, 0)\n        ..anchor = Anchor.center,\n    );\n\n    add(\n      OutlinedSwordSprite(\n        position: Vector2(200, 0),\n        anchor: Anchor.center,\n      ),\n    );\n  }\n}\n```\n\nHere we use a custom `FlameGame` subclass to override the background color. Adjust the positions\nand color to suit your own images.\n\nRun the application. You should see only one sprite, the outlined one is missing. The console\nwill show why:\n`[...] Unhandled Exception: Exception: Asset 'assets/shaders/outline.frag' not found [...]`\n\nWe haven't created the shader file yet. Let's do that in the next step.\n"
  },
  {
    "path": "doc/tutorials/basic_shader/step3.md",
    "content": "# 3. Shader\n\n\n## Considerations\n\nIn this section we will create the fragment (pixel) shader program that runs on the GPU.\n\nKeep in mind that shader code requires different thinking from regular Dart code, since the\nfragment shader runs once per pixel, every frame.\n\n```{note}\nBe mindful of branching and looping in shaders, as operations\nscale linearly with pixel count and loop iterations per frame.\n```\n\n```{note}\nShader optimization is out of scope for this tutorial. As a\nquick example, comparing squared distances instead of using\n`sqrt` would be more efficient.\n```\n\n\n## Shader code\n\nCreate a new directory at `assets/shaders/` and a file named `outline.frag`:\n\n```glsl\n#version 460 core\n\nprecision mediump float;\n\n#include <flutter/runtime_effect.glsl>\n\nuniform vec2 uSize;\nuniform float uOutlineWidth;\nuniform vec4 uOutlineColor;\nuniform sampler2D uTexture;\n\nconst int MAX_SAMPLE_DISTANCE = 8;\n\nout vec4 fragColor;\n\nvoid main() {\n  vec2 uv = FlutterFragCoord().xy / uSize;\n  vec4 texColor = texture(uTexture, uv);\n\n  // If the current pixel is not transparent, render the original color\n  if (texColor.a > 0.0) {\n    fragColor = texColor;\n    return;\n  }\n\n  // Check surrounding pixels for outline\n  vec2 texelSize = 1.0 / uSize;\n  bool foundOpaqueNearby = false;\n\n  // Sample in the bounding square pattern around the current pixel\n  // You must use static const loop counts in GLSL\n  for (int x = -MAX_SAMPLE_DISTANCE; x <= MAX_SAMPLE_DISTANCE; x++) {\n    for (int y = -MAX_SAMPLE_DISTANCE; y <= MAX_SAMPLE_DISTANCE; y++) {\n      if (x == 0 && y == 0) continue;\n\n      // Check real distance instead of manhattan distance\n      float distance = sqrt(float( x*x + y*y ));\n      if (distance > uOutlineWidth) continue;\n\n      // Sample the shifted pixel from the current pixel (uv)\n      vec2 offset = vec2(float(x), float(y)) * texelSize;\n      vec4 sampleColor = texture(uTexture, uv + offset);\n\n      if (sampleColor.a > 0.0) {\n        // We found solid color in the iteration --> sprite is nearby\n        foundOpaqueNearby = true;\n        break;\n      }\n    }\n    // Break out from outer loop too\n    if (foundOpaqueNearby) break;\n  }\n\n  if (foundOpaqueNearby) {\n    fragColor = uOutlineColor;\n  } else {\n    fragColor = vec4(0.0, 0.0, 0.0, 0.0);\n  }\n}\n```\n\nFor each transparent pixel, the shader checks whether any nearby pixel is opaque. If so, it\ncolors the pixel with the outline color (passed in as a uniform). Otherwise, it stays fully\ntransparent. This is why transparent `.png` images are required.\n\n```{note}\nGLSL loop bounds must be compile-time constants, so the\n`uOutlineWidth` uniform cannot be used directly. Make sure\n`MAX_SAMPLE_DISTANCE` is at least as large as the outline\nwidth you set in Dart.\n```\n\n\n## Shader resource\n\nRegister the shader in `pubspec.yaml` so Flutter bundles it at build time:\n\n```yaml\nflutter:\n  assets:\n    - assets/images/\n  shaders:\n    - assets/shaders/outline.frag\n```\n\nRun the application. You should now see two sprites: one plain and one with a colored outline.\n\n![Image of the reference and the shader](../../images/tutorials/basic_shader/final_result.png)\n\nThe basic shader is working. It's time to experiment!\n"
  },
  {
    "path": "doc/tutorials/basic_shader/step4.md",
    "content": "# 4. User Input\n\nIn this step we add mouse hover support so the outline color changes when the cursor enters the\nsprite.\n\n\n## Event handling\n\nOpen `sword_component.dart` and add the `HoverCallbacks` mixin to `OutlinedSwordSprite`:\n\n```dart\nimport 'package:flame/events.dart';\n\nclass OutlinedSwordSprite extends PostProcessComponent\n    with HoverCallbacks {\n  // ...\n}\n```\n\nThen add a field to store the original color, and override the hover callbacks to swap it:\n\n```dart\nColor? _originalPostProcessColor;\n\n@override\nvoid onHoverEnter() {\n  super.onHoverEnter();\n\n  final outlinePostProcess = postProcess as OutlinePostProcess;\n  _originalPostProcessColor = outlinePostProcess.outlineColor;\n  outlinePostProcess.outlineColor = Colors.blue;\n}\n\n@override\nvoid onHoverExit() {\n  final outlinePostProcess = postProcess as OutlinePostProcess;\n  outlinePostProcess.outlineColor =\n      _originalPostProcessColor ?? Colors.purpleAccent;\n\n  super.onHoverExit();\n}\n```\n\n\n## Full solution\n\nThe final `sword_component.dart` with hover support:\n\n```dart\nimport 'package:flutter/material.dart';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/post_process.dart';\n\nimport 'package:basic_shader_tutorial/outline_postprocess.dart';\n\nclass OutlinedSwordSprite extends PostProcessComponent\n    with HoverCallbacks {\n  OutlinedSwordSprite({super.position, super.anchor})\n    : super(\n        children: [SwordSprite()],\n        postProcess: OutlinePostProcess(anchor: anchor ?? Anchor.topLeft),\n      );\n\n  @override\n  void onChildrenChanged(\n    Component component,\n    ChildrenChangeType changeType,\n  ) {\n    _recalculateBoundingSize();\n    super.onChildrenChanged(component, changeType);\n  }\n\n  void _recalculateBoundingSize() {\n    final boundingBox = Vector2.zero();\n\n    final rectChildren = children.query<PositionComponent>();\n    if (rectChildren.isNotEmpty) {\n      final boundingRect = rectChildren\n          .map((child) => child.toRect())\n          .reduce((a, b) => a.expandToInclude(b));\n\n      boundingBox.setValues(boundingRect.width, boundingRect.height);\n    }\n\n    size = boundingBox;\n  }\n\n  Color? _originalPostProcessColor;\n\n  @override\n  void onHoverEnter() {\n    super.onHoverEnter();\n\n    final outlinePostProcess = postProcess as OutlinePostProcess;\n    _originalPostProcessColor = outlinePostProcess.outlineColor;\n    outlinePostProcess.outlineColor = Colors.blue;\n  }\n\n  @override\n  void onHoverExit() {\n    final outlinePostProcess = postProcess as OutlinePostProcess;\n    outlinePostProcess.outlineColor =\n        _originalPostProcessColor ?? Colors.purpleAccent;\n\n    super.onHoverExit();\n  }\n}\n\nclass SwordSprite extends SpriteComponent {\n  @override\n  Future<void> onLoad() async {\n    sprite = await Sprite.load('sword.png');\n    size = sprite!.srcSize;\n  }\n}\n```\n\nWhen you hover over the sprite, the outline turns blue. When the cursor leaves, it reverts to the\noriginal color.\n\n![GIF of mouse hover](../../images/tutorials/basic_shader/hover_demo.webp)\n"
  },
  {
    "path": "doc/tutorials/basic_shader/takeaways.md",
    "content": "# Takeaways\n\n\n## Conclusion\n\nThere are three layers involved when using shaders in Flame:\n\n- **Component layer** (`SpriteComponent` and `PostProcessComponent`): connects shaders to Flame\n  components, holds game logic and handles user input.\n- **Post process layer** (`PostProcess`): bridges components and shaders, manages runtime settings\n  and updates uniforms each frame.\n- **GLSL shader** (`.frag` file): the GPU program that determines the final pixel colors.\n\n\n## Closure\n\nWe hope this tutorial helped you understand the basics of using shaders in Flame. Feel free to\ntweak the code to suit your needs, happy coding!\n\nIf you find any errors or have suggestions, please let us know through GitHub or Discord.\n"
  },
  {
    "path": "doc/tutorials/klondike/app/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    always_use_package_imports: false\n    prefer_relative_imports: true\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/main.dart",
    "content": "import 'package:flutter/widgets.dart';\nimport 'package:web/web.dart' as web;\n\nimport 'step2/main.dart' as step2;\nimport 'step3/main.dart' as step3;\nimport 'step4/main.dart' as step4;\nimport 'step5/main.dart' as step5;\n\nvoid main() {\n  var page = web.window.location.search;\n  if (page.startsWith('?')) {\n    page = page.substring(1);\n  }\n  return switch (page) {\n    'step2' => step2.main(),\n    'step3' => step3.main(),\n    'step4' => step4.main(),\n    'step5' => step5.main(),\n    _ => runApp(\n      Directionality(\n        textDirection: TextDirection.ltr,\n        child: Text('Error=> unknown page name \"$page\"'),\n      ),\n    ),\n  };\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step2/components/foundation.dart",
    "content": "import 'package:flame/components.dart';\n\nclass Foundation extends PositionComponent {\n  @override\n  bool get debugMode => true;\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step2/components/pile.dart",
    "content": "import 'package:flame/components.dart';\n\nclass Pile extends PositionComponent {\n  @override\n  bool get debugMode => true;\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step2/components/stock.dart",
    "content": "import 'package:flame/components.dart';\n\nclass Stock extends PositionComponent {\n  @override\n  bool get debugMode => true;\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step2/components/waste.dart",
    "content": "import 'package:flame/components.dart';\n\nclass Waste extends PositionComponent {\n  @override\n  bool get debugMode => true;\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step2/klondike_game.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/game.dart';\n\nimport 'components/foundation.dart';\nimport 'components/pile.dart';\nimport 'components/stock.dart';\nimport 'components/waste.dart';\n\nclass KlondikeGame extends FlameGame {\n  static const double cardGap = 175.0;\n  static const double cardWidth = 1000.0;\n  static const double cardHeight = 1400.0;\n  static const double cardRadius = 100.0;\n  static final Vector2 cardSize = Vector2(cardWidth, cardHeight);\n\n  @override\n  Future<void> onLoad() async {\n    await Flame.images.load('klondike-sprites.png');\n\n    final stock = Stock()\n      ..size = cardSize\n      ..position = Vector2(cardGap, cardGap);\n    final waste = Waste()\n      ..size = cardSize\n      ..position = Vector2(cardWidth + 2 * cardGap, cardGap);\n    final foundations = List.generate(\n      4,\n      (i) => Foundation()\n        ..size = cardSize\n        ..position = Vector2(\n          (i + 3) * (cardWidth + cardGap) + cardGap,\n          cardGap,\n        ),\n    );\n    final piles = List.generate(\n      7,\n      (i) => Pile()\n        ..size = cardSize\n        ..position = Vector2(\n          cardGap + i * (cardWidth + cardGap),\n          cardHeight + 2 * cardGap,\n        ),\n    );\n\n    world.add(stock);\n    world.add(waste);\n    world.addAll(foundations);\n    world.addAll(piles);\n\n    camera.viewfinder.visibleGameSize = Vector2(\n      cardWidth * 7 + cardGap * 8,\n      4 * cardHeight + 3 * cardGap,\n    );\n    camera.viewfinder.position = Vector2(cardWidth * 3.5 + cardGap * 4, 0);\n    camera.viewfinder.anchor = Anchor.topCenter;\n  }\n}\n\nSprite klondikeSprite(double x, double y, double width, double height) {\n  return Sprite(\n    Flame.images.fromCache('klondike-sprites.png'),\n    srcPosition: Vector2(x, y),\n    srcSize: Vector2(width, height),\n  );\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step2/main.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart';\n\nimport 'klondike_game.dart';\n\nvoid main() {\n  final game = KlondikeGame();\n  runApp(GameWidget(game: game));\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step3/components/card.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport '../klondike_game.dart';\nimport '../rank.dart';\nimport '../suit.dart';\n\nclass Card extends PositionComponent {\n  Card(int intRank, int intSuit)\n    : rank = Rank.fromInt(intRank),\n      suit = Suit.fromInt(intSuit),\n      _faceUp = false,\n      super(size: KlondikeGame.cardSize);\n\n  final Rank rank;\n  final Suit suit;\n  bool _faceUp;\n\n  bool get isFaceUp => _faceUp;\n  void flip() => _faceUp = !_faceUp;\n\n  @override\n  String toString() => rank.label + suit.label; // e.g. \"Q♠\" or \"10♦\"\n\n  @override\n  void render(Canvas canvas) {\n    if (_faceUp) {\n      _renderFront(canvas);\n    } else {\n      _renderBack(canvas);\n    }\n  }\n\n  static final Paint backBackgroundPaint = Paint()\n    ..color = const Color(0xff380c02);\n  static final Paint backBorderPaint1 = Paint()\n    ..color = const Color(0xffdbaf58)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10;\n  static final Paint backBorderPaint2 = Paint()\n    ..color = const Color(0x5CEF971B)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 35;\n  static final RRect cardRRect = RRect.fromRectAndRadius(\n    KlondikeGame.cardSize.toRect(),\n    const Radius.circular(KlondikeGame.cardRadius),\n  );\n  static final RRect backRRectInner = cardRRect.deflate(40);\n  static final Sprite flameSprite = klondikeSprite(1367, 6, 357, 501);\n\n  void _renderBack(Canvas canvas) {\n    canvas.drawRRect(cardRRect, backBackgroundPaint);\n    canvas.drawRRect(cardRRect, backBorderPaint1);\n    canvas.drawRRect(backRRectInner, backBorderPaint2);\n    flameSprite.render(canvas, position: size / 2, anchor: Anchor.center);\n  }\n\n  static final Paint frontBackgroundPaint = Paint()\n    ..color = const Color(0xff000000);\n  static final Paint redBorderPaint = Paint()\n    ..color = const Color(0xffece8a3)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10;\n  static final Paint blackBorderPaint = Paint()\n    ..color = const Color(0xff7ab2e8)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10;\n  static final blueFilter = Paint()\n    ..colorFilter = const ColorFilter.mode(\n      Color(0x880d8bff),\n      BlendMode.srcATop,\n    );\n  static final Sprite redJack = klondikeSprite(81, 565, 562, 488);\n  static final Sprite redQueen = klondikeSprite(717, 541, 486, 515);\n  static final Sprite redKing = klondikeSprite(1305, 532, 407, 549);\n  static final Sprite blackJack = klondikeSprite(81, 565, 562, 488)\n    ..paint = blueFilter;\n  static final Sprite blackQueen = klondikeSprite(717, 541, 486, 515)\n    ..paint = blueFilter;\n  static final Sprite blackKing = klondikeSprite(1305, 532, 407, 549)\n    ..paint = blueFilter;\n\n  void _renderFront(Canvas canvas) {\n    canvas.drawRRect(cardRRect, frontBackgroundPaint);\n    canvas.drawRRect(\n      cardRRect,\n      suit.isRed ? redBorderPaint : blackBorderPaint,\n    );\n\n    final rankSprite = suit.isBlack ? rank.blackSprite : rank.redSprite;\n    final suitSprite = suit.sprite;\n    _drawSprite(canvas, rankSprite, 0.1, 0.08);\n    _drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5);\n    _drawSprite(canvas, rankSprite, 0.1, 0.08, rotate: true);\n    _drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5, rotate: true);\n    switch (rank.value) {\n      case 1:\n        _drawSprite(canvas, suitSprite, 0.5, 0.5, scale: 2.5);\n      case 2:\n        _drawSprite(canvas, suitSprite, 0.5, 0.25);\n        _drawSprite(canvas, suitSprite, 0.5, 0.25, rotate: true);\n      case 3:\n        _drawSprite(canvas, suitSprite, 0.5, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.5);\n        _drawSprite(canvas, suitSprite, 0.5, 0.2, rotate: true);\n      case 4:\n        _drawSprite(canvas, suitSprite, 0.3, 0.25);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25);\n        _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);\n      case 5:\n        _drawSprite(canvas, suitSprite, 0.3, 0.25);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25);\n        _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.5, 0.5);\n      case 6:\n        _drawSprite(canvas, suitSprite, 0.3, 0.25);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25);\n        _drawSprite(canvas, suitSprite, 0.3, 0.5);\n        _drawSprite(canvas, suitSprite, 0.7, 0.5);\n        _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);\n      case 7:\n        _drawSprite(canvas, suitSprite, 0.3, 0.2);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.35);\n        _drawSprite(canvas, suitSprite, 0.3, 0.5);\n        _drawSprite(canvas, suitSprite, 0.7, 0.5);\n        _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);\n      case 8:\n        _drawSprite(canvas, suitSprite, 0.3, 0.2);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.35);\n        _drawSprite(canvas, suitSprite, 0.3, 0.5);\n        _drawSprite(canvas, suitSprite, 0.7, 0.5);\n        _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.5, 0.35, rotate: true);\n      case 9:\n        _drawSprite(canvas, suitSprite, 0.3, 0.2);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.3);\n        _drawSprite(canvas, suitSprite, 0.3, 0.4);\n        _drawSprite(canvas, suitSprite, 0.7, 0.4);\n        _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);\n      case 10:\n        _drawSprite(canvas, suitSprite, 0.3, 0.2);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.3);\n        _drawSprite(canvas, suitSprite, 0.3, 0.4);\n        _drawSprite(canvas, suitSprite, 0.7, 0.4);\n        _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.5, 0.3, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);\n      case 11:\n        _drawSprite(canvas, suit.isRed ? redJack : blackJack, 0.5, 0.5);\n      case 12:\n        _drawSprite(canvas, suit.isRed ? redQueen : blackQueen, 0.5, 0.5);\n      case 13:\n        _drawSprite(canvas, suit.isRed ? redKing : blackKing, 0.5, 0.5);\n    }\n  }\n\n  void _drawSprite(\n    Canvas canvas,\n    Sprite sprite,\n    double relativeX,\n    double relativeY, {\n    double scale = 1,\n    bool rotate = false,\n  }) {\n    if (rotate) {\n      canvas.save();\n      canvas.translate(size.x / 2, size.y / 2);\n      canvas.rotate(pi);\n      canvas.translate(-size.x / 2, -size.y / 2);\n    }\n    sprite.render(\n      canvas,\n      position: Vector2(relativeX * size.x, relativeY * size.y),\n      anchor: Anchor.center,\n      size: sprite.srcSize.scaled(scale),\n    );\n    if (rotate) {\n      canvas.restore();\n    }\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step3/components/foundation.dart",
    "content": "import 'package:flame/components.dart';\n\nclass Foundation extends PositionComponent {\n  @override\n  bool get debugMode => true;\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step3/components/pile.dart",
    "content": "import 'package:flame/components.dart';\n\nclass Pile extends PositionComponent {\n  @override\n  bool get debugMode => true;\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step3/components/stock.dart",
    "content": "import 'package:flame/components.dart';\n\nclass Stock extends PositionComponent {\n  @override\n  bool get debugMode => true;\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step3/components/waste.dart",
    "content": "import 'package:flame/components.dart';\n\nclass Waste extends PositionComponent {\n  @override\n  bool get debugMode => true;\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step3/klondike_game.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/game.dart';\n\nimport 'components/card.dart';\nimport 'components/foundation.dart';\nimport 'components/pile.dart';\nimport 'components/stock.dart';\nimport 'components/waste.dart';\n\nclass KlondikeGame extends FlameGame {\n  static const double cardGap = 175.0;\n  static const double cardWidth = 1000.0;\n  static const double cardHeight = 1400.0;\n  static const double cardRadius = 100.0;\n  static final Vector2 cardSize = Vector2(cardWidth, cardHeight);\n\n  @override\n  Future<void> onLoad() async {\n    await Flame.images.load('klondike-sprites.png');\n\n    final stock = Stock()\n      ..size = cardSize\n      ..position = Vector2(cardGap, cardGap);\n    final waste = Waste()\n      ..size = cardSize\n      ..position = Vector2(cardWidth + 2 * cardGap, cardGap);\n    final foundations = List.generate(\n      4,\n      (i) => Foundation()\n        ..size = cardSize\n        ..position = Vector2(\n          (i + 3) * (cardWidth + cardGap) + cardGap,\n          cardGap,\n        ),\n    );\n    final piles = List.generate(\n      7,\n      (i) => Pile()\n        ..size = cardSize\n        ..position = Vector2(\n          cardGap + i * (cardWidth + cardGap),\n          cardHeight + 2 * cardGap,\n        ),\n    );\n\n    world.add(stock);\n    world.add(waste);\n    world.addAll(foundations);\n    world.addAll(piles);\n\n    camera.viewfinder.visibleGameSize = Vector2(\n      cardWidth * 7 + cardGap * 8,\n      4 * cardHeight + 3 * cardGap,\n    );\n    camera.viewfinder.position = Vector2(cardWidth * 3.5 + cardGap * 4, 0);\n    camera.viewfinder.anchor = Anchor.topCenter;\n\n    final random = Random();\n    for (var i = 0; i < 7; i++) {\n      for (var j = 0; j < 4; j++) {\n        final card = Card(random.nextInt(13) + 1, random.nextInt(4))\n          ..position = Vector2(100 + i * 1150, 100 + j * 1500)\n          ..addToParent(world);\n        // flip the card face-up with 90% probability\n        if (random.nextDouble() < 0.9) {\n          card.flip();\n        }\n      }\n    }\n  }\n}\n\nSprite klondikeSprite(double x, double y, double width, double height) {\n  return Sprite(\n    Flame.images.fromCache('klondike-sprites.png'),\n    srcPosition: Vector2(x, y),\n    srcSize: Vector2(width, height),\n  );\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step3/main.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart';\n\nimport 'klondike_game.dart';\n\nvoid main() {\n  final game = KlondikeGame();\n  runApp(GameWidget(game: game));\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step3/rank.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flutter/foundation.dart';\nimport 'klondike_game.dart';\n\n@immutable\nclass Rank {\n  factory Rank.fromInt(int value) {\n    assert(\n      value >= 1 && value <= 13,\n      'value is outside of the bounds of what a rank can be',\n    );\n    return _singletons[value - 1];\n  }\n\n  Rank._(\n    this.value,\n    this.label,\n    double x1,\n    double y1,\n    double x2,\n    double y2,\n    double w,\n    double h,\n  ) : redSprite = klondikeSprite(x1, y1, w, h),\n      blackSprite = klondikeSprite(x2, y2, w, h);\n\n  final int value;\n  final String label;\n  final Sprite redSprite;\n  final Sprite blackSprite;\n\n  static final List<Rank> _singletons = [\n    Rank._(1, 'A', 335, 164, 789, 161, 120, 129),\n    Rank._(2, '2', 20, 19, 15, 322, 83, 125),\n    Rank._(3, '3', 122, 19, 117, 322, 80, 127),\n    Rank._(4, '4', 213, 12, 208, 315, 93, 132),\n    Rank._(5, '5', 314, 21, 309, 324, 85, 125),\n    Rank._(6, '6', 419, 17, 414, 320, 84, 129),\n    Rank._(7, '7', 509, 21, 505, 324, 92, 128),\n    Rank._(8, '8', 612, 19, 607, 322, 78, 127),\n    Rank._(9, '9', 709, 19, 704, 322, 84, 130),\n    Rank._(10, '10', 810, 20, 805, 322, 137, 127),\n    Rank._(11, 'J', 15, 170, 469, 167, 56, 126),\n    Rank._(12, 'Q', 92, 168, 547, 165, 132, 128),\n    Rank._(13, 'K', 243, 170, 696, 167, 92, 123),\n  ];\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step3/suit.dart",
    "content": "import 'package:flame/sprite.dart';\nimport 'package:flutter/foundation.dart';\nimport 'klondike_game.dart';\n\n@immutable\nclass Suit {\n  factory Suit.fromInt(int index) {\n    assert(\n      index >= 0 && index <= 3,\n      'index is outside of the bounds of what a suit can be',\n    );\n    return _singletons[index];\n  }\n\n  Suit._(this.value, this.label, double x, double y, double w, double h)\n    : sprite = klondikeSprite(x, y, w, h);\n\n  final int value;\n  final String label;\n  final Sprite sprite;\n\n  static final List<Suit> _singletons = [\n    Suit._(0, '♥', 1176, 17, 172, 183),\n    Suit._(1, '♦', 973, 14, 177, 182),\n    Suit._(2, '♣', 974, 226, 184, 172),\n    Suit._(3, '♠', 1178, 220, 176, 182),\n  ];\n\n  /// Hearts and Diamonds are red, while Clubs and Spades are black.\n  bool get isRed => value <= 1;\n  bool get isBlack => value >= 2;\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step4/components/card.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport '../klondike_game.dart';\nimport '../pile.dart';\nimport '../rank.dart';\nimport '../suit.dart';\nimport 'tableau_pile.dart';\n\nclass Card extends PositionComponent with DragCallbacks {\n  Card(int intRank, int intSuit)\n    : rank = Rank.fromInt(intRank),\n      suit = Suit.fromInt(intSuit),\n      super(size: KlondikeGame.cardSize);\n\n  final Rank rank;\n  final Suit suit;\n  Pile? pile;\n  bool _faceUp = false;\n  bool _isDragging = false;\n  final List<Card> attachedCards = [];\n\n  bool get isFaceUp => _faceUp;\n  bool get isFaceDown => !_faceUp;\n  void flip() => _faceUp = !_faceUp;\n\n  @override\n  String toString() => rank.label + suit.label; // e.g. \"Q♠\" or \"10♦\"\n\n  //#region Rendering\n\n  @override\n  void render(Canvas canvas) {\n    if (_faceUp) {\n      _renderFront(canvas);\n    } else {\n      _renderBack(canvas);\n    }\n  }\n\n  static final Paint backBackgroundPaint = Paint()\n    ..color = const Color(0xff380c02);\n  static final Paint backBorderPaint1 = Paint()\n    ..color = const Color(0xffdbaf58)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10;\n  static final Paint backBorderPaint2 = Paint()\n    ..color = const Color(0x5CEF971B)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 35;\n  static final RRect cardRRect = RRect.fromRectAndRadius(\n    KlondikeGame.cardSize.toRect(),\n    const Radius.circular(KlondikeGame.cardRadius),\n  );\n  static final RRect backRRectInner = cardRRect.deflate(40);\n  static final Sprite flameSprite = klondikeSprite(1367, 6, 357, 501);\n\n  void _renderBack(Canvas canvas) {\n    canvas.drawRRect(cardRRect, backBackgroundPaint);\n    canvas.drawRRect(cardRRect, backBorderPaint1);\n    canvas.drawRRect(backRRectInner, backBorderPaint2);\n    flameSprite.render(canvas, position: size / 2, anchor: Anchor.center);\n  }\n\n  static final Paint frontBackgroundPaint = Paint()\n    ..color = const Color(0xff000000);\n  static final Paint redBorderPaint = Paint()\n    ..color = const Color(0xffece8a3)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10;\n  static final Paint blackBorderPaint = Paint()\n    ..color = const Color(0xff7ab2e8)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10;\n  static final blueFilter = Paint()\n    ..colorFilter = const ColorFilter.mode(\n      Color(0x880d8bff),\n      BlendMode.srcATop,\n    );\n  static final Sprite redJack = klondikeSprite(81, 565, 562, 488);\n  static final Sprite redQueen = klondikeSprite(717, 541, 486, 515);\n  static final Sprite redKing = klondikeSprite(1305, 532, 407, 549);\n  static final Sprite blackJack = klondikeSprite(81, 565, 562, 488)\n    ..paint = blueFilter;\n  static final Sprite blackQueen = klondikeSprite(717, 541, 486, 515)\n    ..paint = blueFilter;\n  static final Sprite blackKing = klondikeSprite(1305, 532, 407, 549)\n    ..paint = blueFilter;\n\n  void _renderFront(Canvas canvas) {\n    canvas.drawRRect(cardRRect, frontBackgroundPaint);\n    canvas.drawRRect(\n      cardRRect,\n      suit.isRed ? redBorderPaint : blackBorderPaint,\n    );\n\n    final rankSprite = suit.isBlack ? rank.blackSprite : rank.redSprite;\n    final suitSprite = suit.sprite;\n    _drawSprite(canvas, rankSprite, 0.1, 0.08);\n    _drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5);\n    _drawSprite(canvas, rankSprite, 0.1, 0.08, rotate: true);\n    _drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5, rotate: true);\n    switch (rank.value) {\n      case 1:\n        _drawSprite(canvas, suitSprite, 0.5, 0.5, scale: 2.5);\n      case 2:\n        _drawSprite(canvas, suitSprite, 0.5, 0.25);\n        _drawSprite(canvas, suitSprite, 0.5, 0.25, rotate: true);\n      case 3:\n        _drawSprite(canvas, suitSprite, 0.5, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.5);\n        _drawSprite(canvas, suitSprite, 0.5, 0.2, rotate: true);\n      case 4:\n        _drawSprite(canvas, suitSprite, 0.3, 0.25);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25);\n        _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);\n      case 5:\n        _drawSprite(canvas, suitSprite, 0.3, 0.25);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25);\n        _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.5, 0.5);\n      case 6:\n        _drawSprite(canvas, suitSprite, 0.3, 0.25);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25);\n        _drawSprite(canvas, suitSprite, 0.3, 0.5);\n        _drawSprite(canvas, suitSprite, 0.7, 0.5);\n        _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);\n      case 7:\n        _drawSprite(canvas, suitSprite, 0.3, 0.2);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.35);\n        _drawSprite(canvas, suitSprite, 0.3, 0.5);\n        _drawSprite(canvas, suitSprite, 0.7, 0.5);\n        _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);\n      case 8:\n        _drawSprite(canvas, suitSprite, 0.3, 0.2);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.35);\n        _drawSprite(canvas, suitSprite, 0.3, 0.5);\n        _drawSprite(canvas, suitSprite, 0.7, 0.5);\n        _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.5, 0.35, rotate: true);\n      case 9:\n        _drawSprite(canvas, suitSprite, 0.3, 0.2);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.3);\n        _drawSprite(canvas, suitSprite, 0.3, 0.4);\n        _drawSprite(canvas, suitSprite, 0.7, 0.4);\n        _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);\n      case 10:\n        _drawSprite(canvas, suitSprite, 0.3, 0.2);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.3);\n        _drawSprite(canvas, suitSprite, 0.3, 0.4);\n        _drawSprite(canvas, suitSprite, 0.7, 0.4);\n        _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.5, 0.3, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);\n      case 11:\n        _drawSprite(canvas, suit.isRed ? redJack : blackJack, 0.5, 0.5);\n      case 12:\n        _drawSprite(canvas, suit.isRed ? redQueen : blackQueen, 0.5, 0.5);\n      case 13:\n        _drawSprite(canvas, suit.isRed ? redKing : blackKing, 0.5, 0.5);\n    }\n  }\n\n  void _drawSprite(\n    Canvas canvas,\n    Sprite sprite,\n    double relativeX,\n    double relativeY, {\n    double scale = 1,\n    bool rotate = false,\n  }) {\n    if (rotate) {\n      canvas.save();\n      canvas.translate(size.x / 2, size.y / 2);\n      canvas.rotate(pi);\n      canvas.translate(-size.x / 2, -size.y / 2);\n    }\n    sprite.render(\n      canvas,\n      position: Vector2(relativeX * size.x, relativeY * size.y),\n      anchor: Anchor.center,\n      size: sprite.srcSize.scaled(scale),\n    );\n    if (rotate) {\n      canvas.restore();\n    }\n  }\n\n  //#endregion\n\n  //#region Dragging\n\n  @override\n  void onDragStart(DragStartEvent event) {\n    super.onDragStart(event);\n    if (pile?.canMoveCard(this) ?? false) {\n      _isDragging = true;\n      priority = 100;\n      if (pile is TableauPile) {\n        attachedCards.clear();\n        final extraCards = (pile! as TableauPile).cardsOnTop(this);\n        for (final card in extraCards) {\n          card.priority = attachedCards.length + 101;\n          attachedCards.add(card);\n        }\n      }\n    }\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    if (!_isDragging) {\n      return;\n    }\n    final delta = event.localDelta;\n    position.add(delta);\n    attachedCards.forEach((card) => card.position.add(delta));\n  }\n\n  @override\n  void onDragEnd(DragEndEvent event) {\n    super.onDragEnd(event);\n    if (!_isDragging) {\n      return;\n    }\n    _isDragging = false;\n    final dropPiles = parent!\n        .componentsAtPoint(position + size / 2)\n        .whereType<Pile>()\n        .toList();\n    if (dropPiles.isNotEmpty) {\n      if (dropPiles.first.canAcceptCard(this)) {\n        pile!.removeCard(this);\n        dropPiles.first.acquireCard(this);\n        if (attachedCards.isNotEmpty) {\n          attachedCards.forEach((card) => dropPiles.first.acquireCard(card));\n          attachedCards.clear();\n        }\n        return;\n      }\n    }\n    pile!.returnCard(this);\n    if (attachedCards.isNotEmpty) {\n      attachedCards.forEach((card) => pile!.returnCard(card));\n      attachedCards.clear();\n    }\n  }\n\n  //#endregion\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step4/components/foundation_pile.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\n\nimport '../klondike_game.dart';\nimport '../pile.dart';\nimport '../suit.dart';\nimport 'card.dart';\n\nclass FoundationPile extends PositionComponent implements Pile {\n  FoundationPile(int intSuit, {super.position})\n    : suit = Suit.fromInt(intSuit),\n      super(size: KlondikeGame.cardSize);\n\n  final Suit suit;\n  final List<Card> _cards = [];\n\n  //#region Pile API\n\n  @override\n  bool canMoveCard(Card card) {\n    return _cards.isNotEmpty && card == _cards.last;\n  }\n\n  @override\n  bool canAcceptCard(Card card) {\n    final topCardRank = _cards.isEmpty ? 0 : _cards.last.rank.value;\n    return card.suit == suit &&\n        card.rank.value == topCardRank + 1 &&\n        card.attachedCards.isEmpty;\n  }\n\n  @override\n  void removeCard(Card card) {\n    assert(canMoveCard(card));\n    _cards.removeLast();\n  }\n\n  @override\n  void returnCard(Card card) {\n    card.position = position;\n    card.priority = _cards.indexOf(card);\n  }\n\n  @override\n  void acquireCard(Card card) {\n    assert(card.isFaceUp);\n    card.position = position;\n    card.priority = _cards.length;\n    card.pile = this;\n    _cards.add(card);\n  }\n\n  //#endregion\n\n  //#region Rendering\n\n  final _borderPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10\n    ..color = const Color(0x50ffffff);\n  late final _suitPaint = Paint()\n    ..color = suit.isRed ? const Color(0x3a000000) : const Color(0x64000000)\n    ..blendMode = BlendMode.luminosity;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(KlondikeGame.cardRRect, _borderPaint);\n    suit.sprite.render(\n      canvas,\n      position: size / 2,\n      anchor: Anchor.center,\n      size: Vector2.all(KlondikeGame.cardWidth * 0.6),\n      overridePaint: _suitPaint,\n    );\n  }\n\n  //#endregion\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step4/components/stock_pile.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\n\nimport '../klondike_game.dart';\nimport '../pile.dart';\nimport 'card.dart';\nimport 'waste_pile.dart';\n\nclass StockPile extends PositionComponent with TapCallbacks implements Pile {\n  StockPile({super.position}) : super(size: KlondikeGame.cardSize);\n\n  /// Which cards are currently placed onto this pile. The first card in the\n  /// list is at the bottom, the last card is on top.\n  final List<Card> _cards = [];\n\n  //#region Pile API\n\n  @override\n  bool canMoveCard(Card card) => false;\n\n  @override\n  bool canAcceptCard(Card card) => false;\n\n  @override\n  void removeCard(Card card) => throw StateError('cannot remove cards');\n\n  @override\n  void returnCard(Card card) => throw StateError('cannot remove cards');\n\n  @override\n  void acquireCard(Card card) {\n    assert(card.isFaceDown);\n    card.pile = this;\n    card.position = position;\n    card.priority = _cards.length;\n    _cards.add(card);\n  }\n\n  //#endregion\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    final wastePile = parent!.firstChild<WastePile>()!;\n    if (_cards.isEmpty) {\n      wastePile.removeAllCards().reversed.forEach((card) {\n        card.flip();\n        acquireCard(card);\n      });\n    } else {\n      for (var i = 0; i < 3; i++) {\n        if (_cards.isNotEmpty) {\n          final card = _cards.removeLast();\n          card.flip();\n          wastePile.acquireCard(card);\n        }\n      }\n    }\n  }\n\n  //#region Rendering\n\n  final _borderPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10\n    ..color = const Color(0xFF3F5B5D);\n  final _circlePaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 100\n    ..color = const Color(0x883F5B5D);\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(KlondikeGame.cardRRect, _borderPaint);\n    canvas.drawCircle(\n      Offset(width / 2, height / 2),\n      KlondikeGame.cardWidth * 0.3,\n      _circlePaint,\n    );\n  }\n\n  //#endregion\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step4/components/tableau_pile.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\n\nimport '../klondike_game.dart';\nimport '../pile.dart';\nimport 'card.dart';\n\nclass TableauPile extends PositionComponent implements Pile {\n  TableauPile({super.position}) : super(size: KlondikeGame.cardSize);\n\n  /// Which cards are currently placed onto this pile.\n  final List<Card> _cards = [];\n  final Vector2 _fanOffset1 = Vector2(0, KlondikeGame.cardHeight * 0.05);\n  final Vector2 _fanOffset2 = Vector2(0, KlondikeGame.cardHeight * 0.2);\n\n  //#region Pile API\n\n  @override\n  bool canMoveCard(Card card) => card.isFaceUp;\n\n  @override\n  bool canAcceptCard(Card card) {\n    if (_cards.isEmpty) {\n      return card.rank.value == 13;\n    } else {\n      final topCard = _cards.last;\n      return card.suit.isRed == !topCard.suit.isRed &&\n          card.rank.value == topCard.rank.value - 1;\n    }\n  }\n\n  @override\n  void removeCard(Card card) {\n    assert(_cards.contains(card) && card.isFaceUp);\n    final index = _cards.indexOf(card);\n    _cards.removeRange(index, _cards.length);\n    if (_cards.isNotEmpty && _cards.last.isFaceDown) {\n      flipTopCard();\n    }\n    layOutCards();\n  }\n\n  @override\n  void returnCard(Card card) {\n    card.priority = _cards.indexOf(card);\n    layOutCards();\n  }\n\n  @override\n  void acquireCard(Card card) {\n    card.pile = this;\n    card.priority = _cards.length;\n    _cards.add(card);\n    layOutCards();\n  }\n\n  //#endregion\n\n  void flipTopCard() {\n    assert(_cards.last.isFaceDown);\n    _cards.last.flip();\n  }\n\n  void layOutCards() {\n    if (_cards.isEmpty) {\n      return;\n    }\n    _cards[0].position.setFrom(position);\n    for (var i = 1; i < _cards.length; i++) {\n      _cards[i].position\n        ..setFrom(_cards[i - 1].position)\n        ..add(_cards[i - 1].isFaceDown ? _fanOffset1 : _fanOffset2);\n    }\n    height = KlondikeGame.cardHeight * 1.5 + _cards.last.y - _cards.first.y;\n  }\n\n  List<Card> cardsOnTop(Card card) {\n    assert(card.isFaceUp && _cards.contains(card));\n    final index = _cards.indexOf(card);\n    return _cards.getRange(index + 1, _cards.length).toList();\n  }\n\n  //#region Rendering\n\n  final _borderPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10\n    ..color = const Color(0x50ffffff);\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(KlondikeGame.cardRRect, _borderPaint);\n  }\n\n  //#endregion\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step4/components/waste_pile.dart",
    "content": "import 'package:flame/components.dart';\n\nimport '../klondike_game.dart';\nimport '../pile.dart';\nimport 'card.dart';\n\nclass WastePile extends PositionComponent implements Pile {\n  WastePile({super.position}) : super(size: KlondikeGame.cardSize);\n\n  final List<Card> _cards = [];\n  final Vector2 _fanOffset = Vector2(KlondikeGame.cardWidth * 0.2, 0);\n\n  //#region Pile API\n\n  @override\n  bool canMoveCard(Card card) => _cards.isNotEmpty && card == _cards.last;\n\n  @override\n  bool canAcceptCard(Card card) => false;\n\n  @override\n  void removeCard(Card card) {\n    assert(canMoveCard(card));\n    _cards.removeLast();\n    _fanOutTopCards();\n  }\n\n  @override\n  void returnCard(Card card) {\n    card.priority = _cards.indexOf(card);\n    _fanOutTopCards();\n  }\n\n  @override\n  void acquireCard(Card card) {\n    assert(card.isFaceUp);\n    card.pile = this;\n    card.position = position;\n    card.priority = _cards.length;\n    _cards.add(card);\n    _fanOutTopCards();\n  }\n\n  //#endregion\n\n  List<Card> removeAllCards() {\n    final cards = _cards.toList();\n    _cards.clear();\n    return cards;\n  }\n\n  void _fanOutTopCards() {\n    final n = _cards.length;\n    for (var i = 0; i < n; i++) {\n      _cards[i].position = position;\n    }\n    if (n == 2) {\n      _cards[1].position.add(_fanOffset);\n    } else if (n >= 3) {\n      _cards[n - 2].position.add(_fanOffset);\n      _cards[n - 1].position.addScaled(_fanOffset, 2);\n    }\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step4/klondike_game.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/game.dart';\n\nimport 'components/card.dart';\nimport 'components/foundation_pile.dart';\nimport 'components/stock_pile.dart';\nimport 'components/tableau_pile.dart';\nimport 'components/waste_pile.dart';\n\nclass KlondikeGame extends FlameGame {\n  static const double cardGap = 175.0;\n  static const double cardWidth = 1000.0;\n  static const double cardHeight = 1400.0;\n  static const double cardRadius = 100.0;\n  static final Vector2 cardSize = Vector2(cardWidth, cardHeight);\n  static final cardRRect = RRect.fromRectAndRadius(\n    const Rect.fromLTWH(0, 0, cardWidth, cardHeight),\n    const Radius.circular(cardRadius),\n  );\n\n  @override\n  Future<void> onLoad() async {\n    await Flame.images.load('klondike-sprites.png');\n\n    final stock = StockPile(position: Vector2(cardGap, cardGap));\n    final waste = WastePile(\n      position: Vector2(cardWidth + 2 * cardGap, cardGap),\n    );\n    final foundations = List.generate(\n      4,\n      (i) => FoundationPile(\n        i,\n        position: Vector2((i + 3) * (cardWidth + cardGap) + cardGap, cardGap),\n      ),\n    );\n    final piles = List.generate(\n      7,\n      (i) => TableauPile(\n        position: Vector2(\n          cardGap + i * (cardWidth + cardGap),\n          cardHeight + 2 * cardGap,\n        ),\n      ),\n    );\n\n    world.add(stock);\n    world.add(waste);\n    world.addAll(foundations);\n    world.addAll(piles);\n\n    camera.viewfinder.visibleGameSize = Vector2(\n      cardWidth * 7 + cardGap * 8,\n      4 * cardHeight + 3 * cardGap,\n    );\n    camera.viewfinder.position = Vector2(cardWidth * 3.5 + cardGap * 4, 0);\n    camera.viewfinder.anchor = Anchor.topCenter;\n\n    final cards = [\n      for (var rank = 1; rank <= 13; rank++)\n        for (var suit = 0; suit < 4; suit++) Card(rank, suit),\n    ];\n    cards.shuffle();\n    world.addAll(cards);\n\n    var cardToDeal = cards.length - 1;\n    for (var i = 0; i < 7; i++) {\n      for (var j = i; j < 7; j++) {\n        piles[j].acquireCard(cards[cardToDeal--]);\n      }\n      piles[i].flipTopCard();\n    }\n    for (var n = 0; n <= cardToDeal; n++) {\n      stock.acquireCard(cards[n]);\n    }\n  }\n}\n\nSprite klondikeSprite(double x, double y, double width, double height) {\n  return Sprite(\n    Flame.images.fromCache('klondike-sprites.png'),\n    srcPosition: Vector2(x, y),\n    srcSize: Vector2(width, height),\n  );\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step4/main.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart';\n\nimport 'klondike_game.dart';\n\nvoid main() {\n  final game = KlondikeGame();\n  runApp(GameWidget(game: game));\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step4/pile.dart",
    "content": "import 'components/card.dart';\n\nabstract class Pile {\n  /// Returns true if the [card] can be taken away from this pile and moved\n  /// somewhere else.\n  bool canMoveCard(Card card);\n\n  /// Returns true if the [card] can be placed on top of this pile. The [card]\n  /// may have other cards \"attached\" to it.\n  bool canAcceptCard(Card card);\n\n  /// Removes [card] from this pile; this method will only be called for a card\n  /// that both belong to this pile, and for which [canMoveCard] returns true.\n  void removeCard(Card card);\n\n  /// Places a single [card] on top of this pile. This method will only be\n  /// called for a card for which [canAcceptCard] returns true.\n  void acquireCard(Card card);\n\n  /// Returns the [card] (which already belongs to this pile) in its proper\n  /// place.\n  void returnCard(Card card);\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step4/rank.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flutter/foundation.dart';\nimport 'klondike_game.dart';\n\n@immutable\nclass Rank {\n  factory Rank.fromInt(int value) {\n    assert(\n      value >= 1 && value <= 13,\n      'value is outside of the bounds of what a rank can be',\n    );\n    return _singletons[value - 1];\n  }\n\n  Rank._(\n    this.value,\n    this.label,\n    double x1,\n    double y1,\n    double x2,\n    double y2,\n    double w,\n    double h,\n  ) : redSprite = klondikeSprite(x1, y1, w, h),\n      blackSprite = klondikeSprite(x2, y2, w, h);\n\n  final int value;\n  final String label;\n  final Sprite redSprite;\n  final Sprite blackSprite;\n\n  static final List<Rank> _singletons = [\n    Rank._(1, 'A', 335, 164, 789, 161, 120, 129),\n    Rank._(2, '2', 20, 19, 15, 322, 83, 125),\n    Rank._(3, '3', 122, 19, 117, 322, 80, 127),\n    Rank._(4, '4', 213, 12, 208, 315, 93, 132),\n    Rank._(5, '5', 314, 21, 309, 324, 85, 125),\n    Rank._(6, '6', 419, 17, 414, 320, 84, 129),\n    Rank._(7, '7', 509, 21, 505, 324, 92, 128),\n    Rank._(8, '8', 612, 19, 607, 322, 78, 127),\n    Rank._(9, '9', 709, 19, 704, 322, 84, 130),\n    Rank._(10, '10', 810, 20, 805, 322, 137, 127),\n    Rank._(11, 'J', 15, 170, 469, 167, 56, 126),\n    Rank._(12, 'Q', 92, 168, 547, 165, 132, 128),\n    Rank._(13, 'K', 243, 170, 696, 167, 92, 123),\n  ];\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step4/suit.dart",
    "content": "import 'package:flame/sprite.dart';\nimport 'package:flutter/foundation.dart';\nimport 'klondike_game.dart';\n\n@immutable\nclass Suit {\n  factory Suit.fromInt(int index) {\n    assert(\n      index >= 0 && index <= 3,\n      'index is outside of the bounds of what a suit can be',\n    );\n    return _singletons[index];\n  }\n\n  Suit._(this.value, this.label, double x, double y, double w, double h)\n    : sprite = klondikeSprite(x, y, w, h);\n\n  final int value;\n  final String label;\n  final Sprite sprite;\n\n  static final List<Suit> _singletons = [\n    Suit._(0, '♥', 1176, 17, 172, 183),\n    Suit._(1, '♦', 973, 14, 177, 182),\n    Suit._(2, '♣', 974, 226, 184, 172),\n    Suit._(3, '♠', 1178, 220, 176, 182),\n  ];\n\n  /// Hearts and Diamonds are red, while Clubs and Spades are black.\n  bool get isRed => value <= 1;\n  bool get isBlack => value >= 2;\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step5/components/card.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flutter/animation.dart';\n\nimport '../klondike_game.dart';\nimport '../klondike_world.dart';\nimport '../pile.dart';\nimport '../rank.dart';\nimport '../suit.dart';\nimport 'foundation_pile.dart';\nimport 'stock_pile.dart';\nimport 'tableau_pile.dart';\n\nclass Card extends PositionComponent\n    with DragCallbacks, TapCallbacks, HasWorldReference<KlondikeWorld> {\n  Card(int intRank, int intSuit, {this.isBaseCard = false})\n    : rank = Rank.fromInt(intRank),\n      suit = Suit.fromInt(intSuit),\n      super(\n        size: KlondikeGame.cardSize,\n      );\n\n  final Rank rank;\n  final Suit suit;\n  Pile? pile;\n\n  // A Base Card is rendered in outline only and is NOT playable. It can be\n  // added to the base of a Pile (e.g. the Stock Pile) to allow it to handle\n  // taps and short drags (on an empty Pile) with the same behavior and\n  // tolerances as for regular cards (see KlondikeGame.dragTolerance) and using\n  // the same event-handling code, but with different handleTapUp() methods.\n  final bool isBaseCard;\n\n  bool _faceUp = false;\n  bool _isAnimatedFlip = false;\n  bool _isFaceUpView = false;\n  bool _isDragging = false;\n  Vector2 _whereCardStarted = Vector2(0, 0);\n\n  final List<Card> attachedCards = [];\n\n  bool get isFaceUp => _faceUp;\n  bool get isFaceDown => !_faceUp;\n  void flip() {\n    if (_isAnimatedFlip) {\n      // Let the animation determine the FaceUp/FaceDown state.\n      _faceUp = _isFaceUpView;\n    } else {\n      // No animation: flip and render the card immediately.\n      _faceUp = !_faceUp;\n      _isFaceUpView = _faceUp;\n    }\n  }\n\n  @override\n  String toString() => rank.label + suit.label; // e.g. \"Q♠\" or \"10♦\"\n\n  //#region Rendering\n\n  @override\n  void render(Canvas canvas) {\n    if (isBaseCard) {\n      _renderBaseCard(canvas);\n      return;\n    }\n    if (_isFaceUpView) {\n      _renderFront(canvas);\n    } else {\n      _renderBack(canvas);\n    }\n  }\n\n  static final Paint backBackgroundPaint = Paint()\n    ..color = const Color(0xff380c02);\n  static final Paint backBorderPaint1 = Paint()\n    ..color = const Color(0xffdbaf58)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10;\n  static final Paint backBorderPaint2 = Paint()\n    ..color = const Color(0x5CEF971B)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 35;\n  static final RRect cardRRect = RRect.fromRectAndRadius(\n    KlondikeGame.cardSize.toRect(),\n    const Radius.circular(KlondikeGame.cardRadius),\n  );\n  static final RRect backRRectInner = cardRRect.deflate(40);\n  static final Sprite flameSprite = klondikeSprite(1367, 6, 357, 501);\n\n  void _renderBack(Canvas canvas) {\n    canvas.drawRRect(cardRRect, backBackgroundPaint);\n    canvas.drawRRect(cardRRect, backBorderPaint1);\n    canvas.drawRRect(backRRectInner, backBorderPaint2);\n    flameSprite.render(canvas, position: size / 2, anchor: Anchor.center);\n  }\n\n  void _renderBaseCard(Canvas canvas) {\n    canvas.drawRRect(cardRRect, backBorderPaint1);\n  }\n\n  static final Paint frontBackgroundPaint = Paint()\n    ..color = const Color(0xff000000);\n  static final Paint redBorderPaint = Paint()\n    ..color = const Color(0xffece8a3)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10;\n  static final Paint blackBorderPaint = Paint()\n    ..color = const Color(0xff7ab2e8)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10;\n  static final blueFilter = Paint()\n    ..colorFilter = const ColorFilter.mode(\n      Color(0x880d8bff),\n      BlendMode.srcATop,\n    );\n  static final Sprite redJack = klondikeSprite(81, 565, 562, 488);\n  static final Sprite redQueen = klondikeSprite(717, 541, 486, 515);\n  static final Sprite redKing = klondikeSprite(1305, 532, 407, 549);\n  static final Sprite blackJack = klondikeSprite(81, 565, 562, 488)\n    ..paint = blueFilter;\n  static final Sprite blackQueen = klondikeSprite(717, 541, 486, 515)\n    ..paint = blueFilter;\n  static final Sprite blackKing = klondikeSprite(1305, 532, 407, 549)\n    ..paint = blueFilter;\n\n  void _renderFront(Canvas canvas) {\n    canvas.drawRRect(cardRRect, frontBackgroundPaint);\n    canvas.drawRRect(\n      cardRRect,\n      suit.isRed ? redBorderPaint : blackBorderPaint,\n    );\n\n    final rankSprite = suit.isBlack ? rank.blackSprite : rank.redSprite;\n    final suitSprite = suit.sprite;\n    _drawSprite(canvas, rankSprite, 0.1, 0.08);\n    _drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5);\n    _drawSprite(canvas, rankSprite, 0.1, 0.08, rotate: true);\n    _drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5, rotate: true);\n    switch (rank.value) {\n      case 1:\n        _drawSprite(canvas, suitSprite, 0.5, 0.5, scale: 2.5);\n      case 2:\n        _drawSprite(canvas, suitSprite, 0.5, 0.25);\n        _drawSprite(canvas, suitSprite, 0.5, 0.25, rotate: true);\n      case 3:\n        _drawSprite(canvas, suitSprite, 0.5, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.5);\n        _drawSprite(canvas, suitSprite, 0.5, 0.2, rotate: true);\n      case 4:\n        _drawSprite(canvas, suitSprite, 0.3, 0.25);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25);\n        _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);\n      case 5:\n        _drawSprite(canvas, suitSprite, 0.3, 0.25);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25);\n        _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.5, 0.5);\n      case 6:\n        _drawSprite(canvas, suitSprite, 0.3, 0.25);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25);\n        _drawSprite(canvas, suitSprite, 0.3, 0.5);\n        _drawSprite(canvas, suitSprite, 0.7, 0.5);\n        _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);\n      case 7:\n        _drawSprite(canvas, suitSprite, 0.3, 0.2);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.35);\n        _drawSprite(canvas, suitSprite, 0.3, 0.5);\n        _drawSprite(canvas, suitSprite, 0.7, 0.5);\n        _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);\n      case 8:\n        _drawSprite(canvas, suitSprite, 0.3, 0.2);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.35);\n        _drawSprite(canvas, suitSprite, 0.3, 0.5);\n        _drawSprite(canvas, suitSprite, 0.7, 0.5);\n        _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.5, 0.35, rotate: true);\n      case 9:\n        _drawSprite(canvas, suitSprite, 0.3, 0.2);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.3);\n        _drawSprite(canvas, suitSprite, 0.3, 0.4);\n        _drawSprite(canvas, suitSprite, 0.7, 0.4);\n        _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);\n      case 10:\n        _drawSprite(canvas, suitSprite, 0.3, 0.2);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.3);\n        _drawSprite(canvas, suitSprite, 0.3, 0.4);\n        _drawSprite(canvas, suitSprite, 0.7, 0.4);\n        _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.5, 0.3, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);\n      case 11:\n        _drawSprite(canvas, suit.isRed ? redJack : blackJack, 0.5, 0.5);\n      case 12:\n        _drawSprite(canvas, suit.isRed ? redQueen : blackQueen, 0.5, 0.5);\n      case 13:\n        _drawSprite(canvas, suit.isRed ? redKing : blackKing, 0.5, 0.5);\n    }\n  }\n\n  void _drawSprite(\n    Canvas canvas,\n    Sprite sprite,\n    double relativeX,\n    double relativeY, {\n    double scale = 1,\n    bool rotate = false,\n  }) {\n    if (rotate) {\n      canvas.save();\n      canvas.translate(size.x / 2, size.y / 2);\n      canvas.rotate(pi);\n      canvas.translate(-size.x / 2, -size.y / 2);\n    }\n    sprite.render(\n      canvas,\n      position: Vector2(relativeX * size.x, relativeY * size.y),\n      anchor: Anchor.center,\n      size: sprite.srcSize.scaled(scale),\n    );\n    if (rotate) {\n      canvas.restore();\n    }\n  }\n\n  //#endregion\n\n  //#region Card-Dragging\n\n  @override\n  void onTapCancel(TapCancelEvent event) {\n    if (pile is StockPile) {\n      _isDragging = false;\n      handleTapUp();\n    }\n  }\n\n  @override\n  void onDragStart(DragStartEvent event) {\n    super.onDragStart(event);\n    if (pile is StockPile) {\n      _isDragging = false;\n      return;\n    }\n    // Clone the position, else _whereCardStarted changes as the position does.\n    _whereCardStarted = position.clone();\n    attachedCards.clear();\n    if (pile?.canMoveCard(this, MoveMethod.drag) ?? false) {\n      _isDragging = true;\n      priority = 100;\n      if (pile is TableauPile) {\n        final extraCards = (pile! as TableauPile).cardsOnTop(this);\n        for (final card in extraCards) {\n          card.priority = attachedCards.length + 101;\n          attachedCards.add(card);\n        }\n      }\n    }\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    if (!_isDragging) {\n      return;\n    }\n    final delta = event.localDelta;\n    position.add(delta);\n    attachedCards.forEach((card) => card.position.add(delta));\n  }\n\n  @override\n  void onDragEnd(DragEndEvent event) {\n    super.onDragEnd(event);\n    if (!_isDragging) {\n      return;\n    }\n    _isDragging = false;\n\n    // If short drag, return card to Pile and treat it as having been tapped.\n    final shortDrag =\n        (position - _whereCardStarted).length < KlondikeGame.dragTolerance;\n    if (shortDrag && attachedCards.isEmpty) {\n      doMove(\n        _whereCardStarted,\n        onComplete: () {\n          pile!.returnCard(this);\n          // Card moves to its Foundation Pile next, if valid, or it stays put.\n          handleTapUp();\n        },\n      );\n      return;\n    }\n\n    // Find out what is under the center-point of this card when it is dropped.\n    final dropPiles = parent!\n        .componentsAtPoint(position + size / 2)\n        .whereType<Pile>()\n        .toList();\n    if (dropPiles.isNotEmpty) {\n      if (dropPiles.first.canAcceptCard(this)) {\n        // Found a Pile: move card(s) the rest of the way onto it.\n        pile!.removeCard(this, MoveMethod.drag);\n        if (dropPiles.first is TableauPile) {\n          // Get TableauPile to handle positions, priorities and moves of cards.\n          (dropPiles.first as TableauPile).dropCards(this, attachedCards);\n          attachedCards.clear();\n        } else {\n          // Drop a single card onto a FoundationPile.\n          final dropPosition = (dropPiles.first as FoundationPile).position;\n          doMove(\n            dropPosition,\n            onComplete: () {\n              dropPiles.first.acquireCard(this);\n            },\n          );\n        }\n        return;\n      }\n    }\n\n    // Invalid drop (middle of nowhere, invalid pile or invalid card for pile).\n    doMove(\n      _whereCardStarted,\n      onComplete: () {\n        pile!.returnCard(this);\n      },\n    );\n    if (attachedCards.isNotEmpty) {\n      attachedCards.forEach((card) {\n        final offset = card.position - position;\n        card.doMove(\n          _whereCardStarted + offset,\n          onComplete: () {\n            pile!.returnCard(card);\n          },\n        );\n      });\n      attachedCards.clear();\n    }\n  }\n\n  //#endregion\n\n  //#region Card-Tapping\n\n  // Tap a face-up card to make it auto-move and go out (if acceptable), but\n  // if it is face-down and on the Stock Pile, pass the event to that pile.\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    handleTapUp();\n  }\n\n  void handleTapUp() {\n    // Can be called by onTapUp or after a very short (failed) drag-and-drop.\n    // We need to be more user-friendly towards taps that include a short drag.\n    if (pile?.canMoveCard(this, MoveMethod.tap) ?? false) {\n      final suitIndex = suit.value;\n      if (world.foundations[suitIndex].canAcceptCard(this)) {\n        pile!.removeCard(this, MoveMethod.tap);\n        doMove(\n          world.foundations[suitIndex].position,\n          onComplete: () {\n            world.foundations[suitIndex].acquireCard(this);\n          },\n        );\n      }\n    } else if (pile is StockPile) {\n      world.stock.handleTapUp(this);\n    }\n  }\n\n  //#endRegion\n\n  //#region Effects\n\n  void doMove(\n    Vector2 to, {\n    double speed = 10.0,\n    double start = 0.0,\n    int startPriority = 100,\n    Curve curve = Curves.easeOutQuad,\n    VoidCallback? onComplete,\n  }) {\n    assert(speed > 0.0, 'Speed must be > 0 widths per second');\n    final dt = (to - position).length / (speed * size.x);\n    assert(dt > 0, 'Distance to move must be > 0');\n    add(\n      CardMoveEffect(\n        to,\n        EffectController(duration: dt, startDelay: start, curve: curve),\n        transitPriority: startPriority,\n        onComplete: () {\n          onComplete?.call();\n        },\n      ),\n    );\n  }\n\n  void doMoveAndFlip(\n    Vector2 to, {\n    double speed = 10.0,\n    double start = 0.0,\n    Curve curve = Curves.easeOutQuad,\n    VoidCallback? whenDone,\n  }) {\n    assert(speed > 0.0, 'Speed must be > 0 widths per second');\n    final dt = (to - position).length / (speed * size.x);\n    assert(dt > 0, 'Distance to move must be > 0');\n    priority = 100;\n    add(\n      MoveToEffect(\n        to,\n        EffectController(duration: dt, startDelay: start, curve: curve),\n        onComplete: () {\n          turnFaceUp(\n            onComplete: whenDone,\n          );\n        },\n      ),\n    );\n  }\n\n  void turnFaceUp({\n    double time = 0.3,\n    double start = 0.0,\n    VoidCallback? onComplete,\n  }) {\n    assert(!_isFaceUpView, 'Card must be face-down before turning face-up.');\n    assert(time > 0.0, 'Time to turn card over must be > 0');\n    assert(start >= 0.0, 'Start tim must be >= 0');\n    _isAnimatedFlip = true;\n    anchor = Anchor.topCenter;\n    position += Vector2(width / 2, 0);\n    priority = 100;\n    add(\n      ScaleEffect.to(\n        Vector2(scale.x / 100, scale.y),\n        EffectController(\n          startDelay: start,\n          curve: Curves.easeOutSine,\n          duration: time / 2,\n          onMax: () {\n            _isFaceUpView = true;\n          },\n          reverseDuration: time / 2,\n          onMin: () {\n            _isAnimatedFlip = false;\n            _faceUp = true;\n            anchor = Anchor.topLeft;\n            position -= Vector2(width / 2, 0);\n          },\n        ),\n        onComplete: () {\n          onComplete?.call();\n        },\n      ),\n    );\n  }\n\n  //#endregion\n}\n\nclass CardMoveEffect extends MoveToEffect {\n  CardMoveEffect(\n    super.destination,\n    super.controller, {\n    super.onComplete,\n    this.transitPriority = 100,\n  });\n\n  final int transitPriority;\n\n  @override\n  void onStart() {\n    super.onStart(); // Flame connects MoveToEffect to EffectController.\n    parent?.priority = transitPriority;\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step5/components/flat_button.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/text.dart';\nimport 'package:flutter/material.dart';\n\nclass FlatButton extends ButtonComponent {\n  FlatButton(\n    String text, {\n    super.size,\n    super.onReleased,\n    super.position,\n  }) : super(\n         button: ButtonBackground(const Color(0xffece8a3)),\n         buttonDown: ButtonBackground(Colors.red),\n         children: [\n           TextComponent(\n             text: text,\n             textRenderer: TextPaint(\n               style: TextStyle(\n                 fontSize: 0.5 * size!.y,\n                 fontWeight: FontWeight.bold,\n                 color: const Color(0xffdbaf58),\n               ),\n             ),\n             position: size / 2.0,\n             anchor: Anchor.center,\n           ),\n         ],\n         anchor: Anchor.center,\n       );\n}\n\nclass ButtonBackground extends PositionComponent with HasAncestor<FlatButton> {\n  final _paint = Paint()..style = PaintingStyle.stroke;\n\n  late double cornerRadius;\n\n  ButtonBackground(Color color) {\n    _paint.color = color;\n  }\n\n  @override\n  void onMount() {\n    super.onMount();\n    size = ancestor.size;\n    cornerRadius = 0.3 * size.y;\n    _paint.strokeWidth = 0.05 * size.y;\n  }\n\n  late final _background = RRect.fromRectAndRadius(\n    size.toRect(),\n    Radius.circular(cornerRadius),\n  );\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(_background, _paint);\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step5/components/foundation_pile.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\n\nimport '../klondike_game.dart';\nimport '../pile.dart';\nimport '../suit.dart';\nimport 'card.dart';\n\nclass FoundationPile extends PositionComponent implements Pile {\n  FoundationPile(int intSuit, this.checkWin, {super.position})\n    : suit = Suit.fromInt(intSuit),\n      super(size: KlondikeGame.cardSize);\n\n  final VoidCallback checkWin;\n\n  final Suit suit;\n  final List<Card> _cards = [];\n\n  //#region Pile API\n\n  bool get isFull => _cards.length == 13;\n\n  @override\n  bool canMoveCard(Card card, MoveMethod method) =>\n      _cards.isNotEmpty && card == _cards.last && method != MoveMethod.tap;\n\n  @override\n  bool canAcceptCard(Card card) {\n    final topCardRank = _cards.isEmpty ? 0 : _cards.last.rank.value;\n    return card.suit == suit &&\n        card.rank.value == topCardRank + 1 &&\n        card.attachedCards.isEmpty;\n  }\n\n  @override\n  void removeCard(Card card, MoveMethod method) {\n    assert(canMoveCard(card, method));\n    _cards.removeLast();\n  }\n\n  @override\n  void returnCard(Card card) {\n    card.position = position;\n    card.priority = _cards.indexOf(card);\n  }\n\n  @override\n  void acquireCard(Card card) {\n    assert(card.isFaceUp);\n    card.position = position;\n    card.priority = _cards.length;\n    card.pile = this;\n    _cards.add(card);\n    if (isFull) {\n      checkWin(); // Get KlondikeWorld to check all FoundationPiles.\n    }\n  }\n\n  //#endregion\n\n  //#region Rendering\n\n  final _borderPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10\n    ..color = const Color(0x50ffffff);\n  late final _suitPaint = Paint()\n    ..color = suit.isRed ? const Color(0x3a000000) : const Color(0x64000000)\n    ..blendMode = BlendMode.luminosity;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(KlondikeGame.cardRRect, _borderPaint);\n    suit.sprite.render(\n      canvas,\n      position: size / 2,\n      anchor: Anchor.center,\n      size: Vector2.all(KlondikeGame.cardWidth * 0.6),\n      overridePaint: _suitPaint,\n    );\n  }\n\n  //#endregion\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step5/components/stock_pile.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\n\nimport '../klondike_game.dart';\nimport '../pile.dart';\nimport 'card.dart';\nimport 'waste_pile.dart';\n\nclass StockPile extends PositionComponent\n    with HasGameReference<KlondikeGame>\n    implements Pile {\n  StockPile({super.position}) : super(size: KlondikeGame.cardSize);\n\n  /// Which cards are currently placed onto this pile. The first card in the\n  /// list is at the bottom, the last card is on top.\n  final List<Card> _cards = [];\n\n  //#region Pile API\n\n  @override\n  bool canMoveCard(Card card, MoveMethod method) => false;\n  // Can be moved by onTapUp callback (see below).\n\n  @override\n  bool canAcceptCard(Card card) => false;\n\n  @override\n  void removeCard(Card card, MoveMethod method) =>\n      throw StateError('cannot remove cards');\n\n  @override\n  // Card cannot be removed but could have been dragged out of place.\n  void returnCard(Card card) => card.priority = _cards.indexOf(card);\n\n  @override\n  void acquireCard(Card card) {\n    assert(card.isFaceDown);\n    card.pile = this;\n    card.position = position;\n    card.priority = _cards.length;\n    _cards.add(card);\n  }\n\n  //#endregion\n\n  void handleTapUp(Card card) {\n    final wastePile = parent!.firstChild<WastePile>()!;\n    if (_cards.isEmpty) {\n      assert(card.isBaseCard, 'Stock Pile is empty, but no Base Card present');\n      card.position = position; // Force Base Card (back) into correct position.\n      wastePile.removeAllCards().reversed.forEach((card) {\n        card.flip();\n        acquireCard(card);\n      });\n    } else {\n      for (var i = 0; i < game.klondikeDraw; i++) {\n        if (_cards.isNotEmpty) {\n          final card = _cards.removeLast();\n          card.doMoveAndFlip(\n            wastePile.position,\n            whenDone: () {\n              wastePile.acquireCard(card);\n            },\n          );\n        }\n      }\n    }\n  }\n\n  //#region Rendering\n\n  final _borderPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10\n    ..color = const Color(0xFF3F5B5D);\n  final _circlePaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 100\n    ..color = const Color(0x883F5B5D);\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(KlondikeGame.cardRRect, _borderPaint);\n    canvas.drawCircle(\n      Offset(width / 2, height / 2),\n      KlondikeGame.cardWidth * 0.3,\n      _circlePaint,\n    );\n  }\n\n  //#endregion\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step5/components/tableau_pile.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\n\nimport '../klondike_game.dart';\nimport '../pile.dart';\nimport 'card.dart';\n\nclass TableauPile extends PositionComponent implements Pile {\n  TableauPile({super.position}) : super(size: KlondikeGame.cardSize);\n\n  /// Which cards are currently placed onto this pile.\n  final List<Card> _cards = [];\n  final Vector2 _fanOffset1 = Vector2(0, KlondikeGame.cardHeight * 0.05);\n  final Vector2 _fanOffset2 = Vector2(0, KlondikeGame.cardHeight * 0.2);\n\n  //#region Pile API\n\n  @override\n  bool canMoveCard(Card card, MoveMethod method) =>\n      card.isFaceUp && (method == MoveMethod.drag || card == _cards.last);\n  // Drag can move multiple cards: tap can move last card only (to Foundation).\n\n  @override\n  bool canAcceptCard(Card card) {\n    if (_cards.isEmpty) {\n      return card.rank.value == 13;\n    } else {\n      final topCard = _cards.last;\n      return card.suit.isRed == !topCard.suit.isRed &&\n          card.rank.value == topCard.rank.value - 1;\n    }\n  }\n\n  @override\n  void removeCard(Card card, MoveMethod method) {\n    assert(_cards.contains(card) && card.isFaceUp);\n    final index = _cards.indexOf(card);\n    _cards.removeRange(index, _cards.length);\n    if (_cards.isNotEmpty && _cards.last.isFaceDown) {\n      flipTopCard();\n      return;\n    }\n    layOutCards();\n  }\n\n  @override\n  void returnCard(Card card) {\n    card.priority = _cards.indexOf(card);\n    layOutCards();\n  }\n\n  @override\n  void acquireCard(Card card) {\n    card.pile = this;\n    card.priority = _cards.length;\n    _cards.add(card);\n    layOutCards();\n  }\n\n  //#endregion\n\n  void dropCards(Card firstCard, [List<Card> attachedCards = const []]) {\n    final cardList = [firstCard];\n    cardList.addAll(attachedCards);\n    Vector2 nextPosition = _cards.isEmpty ? position : _cards.last.position;\n    var nCardsToMove = cardList.length;\n    for (final card in cardList) {\n      card.pile = this;\n      card.priority = _cards.length;\n      if (_cards.isNotEmpty) {\n        nextPosition =\n            nextPosition + (card.isFaceDown ? _fanOffset1 : _fanOffset2);\n      }\n      _cards.add(card);\n      card.doMove(\n        nextPosition,\n        startPriority: card.priority,\n        onComplete: () {\n          nCardsToMove--;\n          if (nCardsToMove == 0) {\n            calculateHitArea(); // Expand the hit-area.\n          }\n        },\n      );\n    }\n  }\n\n  void flipTopCard({double start = 0.1}) {\n    assert(_cards.last.isFaceDown);\n    _cards.last.turnFaceUp(\n      start: start,\n      onComplete: layOutCards,\n    );\n  }\n\n  void layOutCards() {\n    if (_cards.isEmpty) {\n      calculateHitArea(); // Shrink hit-area when all cards have been removed.\n      return;\n    }\n    _cards[0].position.setFrom(position);\n    _cards[0].priority = 0;\n    for (var i = 1; i < _cards.length; i++) {\n      _cards[i].priority = i;\n      _cards[i].position\n        ..setFrom(_cards[i - 1].position)\n        ..add(_cards[i - 1].isFaceDown ? _fanOffset1 : _fanOffset2);\n    }\n    calculateHitArea(); // Adjust hit-area to more cards or fewer cards.\n  }\n\n  void calculateHitArea() {\n    height =\n        KlondikeGame.cardHeight * 1.5 +\n        (_cards.length < 2 ? 0.0 : _cards.last.y - _cards.first.y);\n  }\n\n  List<Card> cardsOnTop(Card card) {\n    assert(card.isFaceUp && _cards.contains(card));\n    final index = _cards.indexOf(card);\n    return _cards.getRange(index + 1, _cards.length).toList();\n  }\n\n  //#region Rendering\n\n  final _borderPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10\n    ..color = const Color(0x50ffffff);\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(KlondikeGame.cardRRect, _borderPaint);\n  }\n\n  //#endregion\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step5/components/waste_pile.dart",
    "content": "import 'package:flame/components.dart';\n\nimport '../klondike_game.dart';\nimport '../pile.dart';\nimport 'card.dart';\n\nclass WastePile extends PositionComponent\n    with HasGameReference<KlondikeGame>\n    implements Pile {\n  WastePile({super.position}) : super(size: KlondikeGame.cardSize);\n\n  final List<Card> _cards = [];\n  final Vector2 _fanOffset = Vector2(KlondikeGame.cardWidth * 0.2, 0);\n\n  //#region Pile API\n\n  @override\n  bool canMoveCard(Card card, MoveMethod method) =>\n      _cards.isNotEmpty && card == _cards.last; // Tap and drag are both OK.\n\n  @override\n  bool canAcceptCard(Card card) => false;\n\n  @override\n  void removeCard(Card card, MoveMethod method) {\n    assert(canMoveCard(card, method));\n    _cards.removeLast();\n    _fanOutTopCards();\n  }\n\n  @override\n  void returnCard(Card card) {\n    card.priority = _cards.indexOf(card);\n    _fanOutTopCards();\n  }\n\n  @override\n  void acquireCard(Card card) {\n    assert(card.isFaceUp);\n    card.pile = this;\n    card.position = position;\n    card.priority = _cards.length;\n    _cards.add(card);\n    _fanOutTopCards();\n  }\n\n  //#endregion\n\n  List<Card> removeAllCards() {\n    final cards = _cards.toList();\n    _cards.clear();\n    return cards;\n  }\n\n  void _fanOutTopCards() {\n    if (game.klondikeDraw == 1) {\n      // No fan-out in Klondike Draw 1.\n      return;\n    }\n    final n = _cards.length;\n    for (var i = 0; i < n; i++) {\n      _cards[i].position = position;\n    }\n    if (n == 2) {\n      _cards[1].position.add(_fanOffset);\n    } else if (n >= 3) {\n      _cards[n - 2].position.add(_fanOffset);\n      _cards[n - 1].position.addScaled(_fanOffset, 2);\n    }\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step5/klondike_game.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/game.dart';\n\nimport 'klondike_world.dart';\n\nenum Action { newDeal, sameDeal, changeDraw, haveFun }\n\nclass KlondikeGame extends FlameGame<KlondikeWorld> {\n  static const double cardGap = 175.0;\n  static const double topGap = 500.0;\n  static const double cardWidth = 1000.0;\n  static const double cardHeight = 1400.0;\n  static const double cardRadius = 100.0;\n  static const double cardSpaceWidth = cardWidth + cardGap;\n  static const double cardSpaceHeight = cardHeight + cardGap;\n  static final Vector2 cardSize = Vector2(cardWidth, cardHeight);\n  static final cardRRect = RRect.fromRectAndRadius(\n    const Rect.fromLTWH(0, 0, cardWidth, cardHeight),\n    const Radius.circular(cardRadius),\n  );\n\n  /// Constant used to decide when a short drag is treated as a TapUp event.\n  static const double dragTolerance = cardWidth / 5;\n\n  /// Constant used when creating Random seed.\n  static const int maxInt = 0xFFFFFFFE; // = (2 to the power 32) - 1\n\n  // This KlondikeGame constructor also initiates the first KlondikeWorld.\n  KlondikeGame() : super(world: KlondikeWorld());\n\n  // These three values persist between games and are starting conditions\n  // for the next game to be played in KlondikeWorld. The actual seed is\n  // computed in KlondikeWorld but is held here in case the player chooses\n  // to replay a game by selecting Action.sameDeal.\n  int klondikeDraw = 1;\n  int seed = 1;\n  Action action = Action.newDeal;\n}\n\nSprite klondikeSprite(double x, double y, double width, double height) {\n  return Sprite(\n    Flame.images.fromCache('klondike-sprites.png'),\n    srcPosition: Vector2(x, y),\n    srcSize: Vector2(width, height),\n  );\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step5/klondike_world.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/flame.dart';\n\nimport 'components/card.dart';\nimport 'components/flat_button.dart';\nimport 'components/foundation_pile.dart';\nimport 'components/stock_pile.dart';\nimport 'components/tableau_pile.dart';\nimport 'components/waste_pile.dart';\n\nimport 'klondike_game.dart';\n\nclass KlondikeWorld extends World with HasGameReference<KlondikeGame> {\n  final cardGap = KlondikeGame.cardGap;\n  final topGap = KlondikeGame.topGap;\n  final cardSpaceWidth = KlondikeGame.cardSpaceWidth;\n  final cardSpaceHeight = KlondikeGame.cardSpaceHeight;\n\n  final stock = StockPile(position: Vector2(0.0, 0.0));\n  final waste = WastePile(position: Vector2(0.0, 0.0));\n  final List<FoundationPile> foundations = [];\n  final List<TableauPile> tableauPiles = [];\n  final List<Card> cards = [];\n  late Vector2 playAreaSize;\n\n  @override\n  Future<void> onLoad() async {\n    await Flame.images.load('klondike-sprites.png');\n\n    stock.position = Vector2(cardGap, topGap);\n    waste.position = Vector2(cardSpaceWidth + cardGap, topGap);\n\n    for (var i = 0; i < 4; i++) {\n      foundations.add(\n        FoundationPile(\n          i,\n          checkWin,\n          position: Vector2((i + 3) * cardSpaceWidth + cardGap, topGap),\n        ),\n      );\n    }\n    for (var i = 0; i < 7; i++) {\n      tableauPiles.add(\n        TableauPile(\n          position: Vector2(\n            i * cardSpaceWidth + cardGap,\n            cardSpaceHeight + topGap,\n          ),\n        ),\n      );\n    }\n\n    // Add a Base Card to the Stock Pile, above the pile and below other cards.\n    final baseCard = Card(1, 0, isBaseCard: true);\n    baseCard.position = stock.position;\n    baseCard.priority = -1;\n    baseCard.pile = stock;\n    stock.priority = -2;\n\n    for (var rank = 1; rank <= 13; rank++) {\n      for (var suit = 0; suit < 4; suit++) {\n        final card = Card(rank, suit);\n        card.position = stock.position;\n        cards.add(card);\n      }\n    }\n\n    add(stock);\n    add(waste);\n    addAll(foundations);\n    addAll(tableauPiles);\n    addAll(cards);\n    add(baseCard);\n\n    playAreaSize = Vector2(\n      7 * cardSpaceWidth + cardGap,\n      4 * cardSpaceHeight + topGap,\n    );\n    final gameMidX = playAreaSize.x / 2;\n\n    addButton('New deal', gameMidX, Action.newDeal);\n    addButton('Same deal', gameMidX + cardSpaceWidth, Action.sameDeal);\n    addButton('Draw 1 or 3', gameMidX + 2 * cardSpaceWidth, Action.changeDraw);\n    addButton('Have fun', gameMidX + 3 * cardSpaceWidth, Action.haveFun);\n\n    final camera = game.camera;\n    camera.viewfinder.visibleGameSize = playAreaSize;\n    camera.viewfinder.position = Vector2(gameMidX, 0);\n    camera.viewfinder.anchor = Anchor.topCenter;\n\n    deal();\n  }\n\n  void addButton(String label, double buttonX, Action action) {\n    final button = FlatButton(\n      label,\n      size: Vector2(KlondikeGame.cardWidth, 0.6 * topGap),\n      position: Vector2(buttonX, topGap / 2),\n      onReleased: () {\n        if (action == Action.haveFun) {\n          // Shortcut to the \"win\" sequence, for Tutorial purposes only.\n          letsCelebrate();\n        } else {\n          // Restart with a new deal or the same deal as before.\n          game.action = action;\n          game.world = KlondikeWorld();\n        }\n      },\n    );\n    add(button);\n  }\n\n  void deal() {\n    assert(cards.length == 52, 'There are ${cards.length} cards: should be 52');\n\n    if (game.action != Action.sameDeal) {\n      // New deal: change the Random Number Generator's seed.\n      game.seed = Random().nextInt(KlondikeGame.maxInt);\n      if (game.action == Action.changeDraw) {\n        game.klondikeDraw = (game.klondikeDraw == 3) ? 1 : 3;\n      }\n    }\n    // For the \"Same deal\" option, re-use the previous seed, else use a new one.\n    cards.shuffle(Random(game.seed));\n\n    // Each card dealt must be seen to come from the top of the deck!\n    var dealPriority = 1;\n    for (final card in cards) {\n      card.priority = dealPriority++;\n    }\n\n    // Change priority as cards take off: so later cards fly above earlier ones.\n    var cardToDeal = cards.length - 1;\n    var nMovingCards = 0;\n    for (var i = 0; i < 7; i++) {\n      for (var j = i; j < 7; j++) {\n        final card = cards[cardToDeal--];\n        card.doMove(\n          tableauPiles[j].position,\n          speed: 15.0,\n          start: nMovingCards * 0.15,\n          startPriority: 100 + nMovingCards,\n          onComplete: () {\n            tableauPiles[j].acquireCard(card);\n            nMovingCards--;\n            if (nMovingCards == 0) {\n              var delayFactor = 0;\n              for (final tableauPile in tableauPiles) {\n                delayFactor++;\n                tableauPile.flipTopCard(start: delayFactor * 0.15);\n              }\n            }\n          },\n        );\n        nMovingCards++;\n      }\n    }\n    for (var n = 0; n <= cardToDeal; n++) {\n      stock.acquireCard(cards[n]);\n    }\n  }\n\n  void checkWin() {\n    // Callback from a Foundation Pile when it is full (Ace to King).\n    var nComplete = 0;\n    for (final f in foundations) {\n      if (f.isFull) {\n        nComplete++;\n      }\n    }\n    if (nComplete == foundations.length) {\n      letsCelebrate();\n    }\n  }\n\n  void letsCelebrate({int phase = 1}) {\n    // Deal won: bring all cards to the middle of the screen (phase 1)\n    // then scatter them to points just outside the screen (phase 2).\n    //\n    // First get the device's screen-size in game co-ordinates, then get the\n    // top-left of the off-screen area that will accept the scattered cards.\n    // Note: The play area is anchored at TopCenter, so topLeft.y is fixed.\n\n    final cameraZoom = game.camera.viewfinder.zoom;\n    final zoomedScreen = game.size / cameraZoom;\n    final screenCenter = (playAreaSize - KlondikeGame.cardSize) / 2;\n    final topLeft = Vector2(\n      (playAreaSize.x - zoomedScreen.x) / 2 - KlondikeGame.cardWidth,\n      -KlondikeGame.cardHeight,\n    );\n    final nCards = cards.length;\n    final offscreenHeight = zoomedScreen.y + KlondikeGame.cardSize.y;\n    final offscreenWidth = zoomedScreen.x + KlondikeGame.cardSize.x;\n    final spacing = 2.0 * (offscreenHeight + offscreenWidth) / nCards;\n\n    // Starting points, directions and lengths of offscreen rect's sides.\n    final corner = [\n      Vector2(0.0, 0.0),\n      Vector2(0.0, offscreenHeight),\n      Vector2(offscreenWidth, offscreenHeight),\n      Vector2(offscreenWidth, 0.0),\n    ];\n    final direction = [\n      Vector2(0.0, 1.0),\n      Vector2(1.0, 0.0),\n      Vector2(0.0, -1.0),\n      Vector2(-1.0, 0.0),\n    ];\n    final length = [\n      offscreenHeight,\n      offscreenWidth,\n      offscreenHeight,\n      offscreenWidth,\n    ];\n\n    var side = 0;\n    var cardsToMove = nCards;\n    var offScreenPosition = corner[side] + topLeft;\n    var space = length[side];\n    var cardNum = 0;\n\n    while (cardNum < nCards) {\n      final cardIndex = phase == 1 ? cardNum : nCards - cardNum - 1;\n      final card = cards[cardIndex];\n      card.priority = cardIndex + 1;\n      if (card.isFaceDown) {\n        card.flip();\n      }\n      // Start cards a short time apart to give a riffle effect.\n      final delay = phase == 1 ? cardNum * 0.02 : 0.5 + cardNum * 0.04;\n      final destination = (phase == 1) ? screenCenter : offScreenPosition;\n      card.doMove(\n        destination,\n        speed: (phase == 1) ? 15.0 : 5.0,\n        start: delay,\n        onComplete: () {\n          cardsToMove--;\n          if (cardsToMove == 0) {\n            if (phase == 1) {\n              letsCelebrate(phase: 2);\n            } else {\n              // Restart with a new deal after winning or pressing \"Have fun\".\n              game.action = Action.newDeal;\n              game.world = KlondikeWorld();\n            }\n          }\n        },\n      );\n      cardNum++;\n      if (phase == 1) {\n        continue;\n      }\n\n      // Phase 2: next card goes to same side with full spacing, if possible.\n      offScreenPosition = offScreenPosition + direction[side] * spacing;\n      space = space - spacing;\n      if ((space < 0.0) && (side < 3)) {\n        // Out of space: change to the next side and use excess spacing there.\n        side++;\n        offScreenPosition = corner[side] + topLeft - direction[side] * space;\n        space = length[side] + space;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step5/main.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart';\n\nimport 'klondike_game.dart';\n\nvoid main() {\n  final game = KlondikeGame();\n  runApp(GameWidget(game: game));\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step5/pile.dart",
    "content": "import 'components/card.dart';\n\nenum MoveMethod { drag, tap }\n\nabstract class Pile {\n  /// Returns true if the [card] can be taken away from this pile and moved\n  /// somewhere else. A tapping move may need additional validation.\n  bool canMoveCard(Card card, MoveMethod method);\n\n  /// Returns true if the [card] can be placed on top of this pile. The [card]\n  /// may have other cards \"attached\" to it.\n  bool canAcceptCard(Card card);\n\n  /// Removes [card] from this pile; this method will only be called for a card\n  /// that both belong to this pile, and for which [canMoveCard] returns true.\n  void removeCard(Card card, MoveMethod method);\n\n  /// Places a single [card] on top of this pile. This method will only be\n  /// called for a card for which [canAcceptCard] returns true.\n  void acquireCard(Card card);\n\n  /// Returns a [card], which already belongs to this pile, to its proper place.\n  void returnCard(Card card);\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step5/rank.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flutter/foundation.dart';\nimport 'klondike_game.dart';\n\n@immutable\nclass Rank {\n  factory Rank.fromInt(int value) {\n    assert(\n      value >= 1 && value <= 13,\n      'value is outside of the bounds of what a rank can be',\n    );\n    return _singletons[value - 1];\n  }\n\n  Rank._(\n    this.value,\n    this.label,\n    double x1,\n    double y1,\n    double x2,\n    double y2,\n    double w,\n    double h,\n  ) : redSprite = klondikeSprite(x1, y1, w, h),\n      blackSprite = klondikeSprite(x2, y2, w, h);\n\n  final int value;\n  final String label;\n  final Sprite redSprite;\n  final Sprite blackSprite;\n\n  static final List<Rank> _singletons = [\n    Rank._(1, 'A', 335, 164, 789, 161, 120, 129),\n    Rank._(2, '2', 20, 19, 15, 322, 83, 125),\n    Rank._(3, '3', 122, 19, 117, 322, 80, 127),\n    Rank._(4, '4', 213, 12, 208, 315, 93, 132),\n    Rank._(5, '5', 314, 21, 309, 324, 85, 125),\n    Rank._(6, '6', 419, 17, 414, 320, 84, 129),\n    Rank._(7, '7', 509, 21, 505, 324, 92, 128),\n    Rank._(8, '8', 612, 19, 607, 322, 78, 127),\n    Rank._(9, '9', 709, 19, 704, 322, 84, 130),\n    Rank._(10, '10', 810, 20, 805, 322, 137, 127),\n    Rank._(11, 'J', 15, 170, 469, 167, 56, 126),\n    Rank._(12, 'Q', 92, 168, 547, 165, 132, 128),\n    Rank._(13, 'K', 243, 170, 696, 167, 92, 123),\n  ];\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/lib/step5/suit.dart",
    "content": "import 'package:flame/sprite.dart';\nimport 'package:flutter/foundation.dart';\nimport 'klondike_game.dart';\n\n@immutable\nclass Suit {\n  factory Suit.fromInt(int index) {\n    assert(\n      index >= 0 && index <= 3,\n      'index is outside of the bounds of what a suit can be',\n    );\n    return _singletons[index];\n  }\n\n  Suit._(this.value, this.label, double x, double y, double w, double h)\n    : sprite = klondikeSprite(x, y, w, h);\n\n  final int value;\n  final String label;\n  final Sprite sprite;\n\n  static final List<Suit> _singletons = [\n    Suit._(0, '♥', 1176, 17, 172, 183),\n    Suit._(1, '♦', 973, 14, 177, 182),\n    Suit._(2, '♣', 974, 226, 184, 172),\n    Suit._(3, '♠', 1178, 220, 176, 182),\n  ];\n\n  /// Hearts and Diamonds are red, while Clubs and Spades are black.\n  bool get isRed => value <= 1;\n  bool get isBlack => value >= 2;\n}\n"
  },
  {
    "path": "doc/tutorials/klondike/app/pubspec.yaml",
    "content": "name: klondike\nresolution: workspace\ndescription: Klondike tutorial\nversion: 1.0.0\npublish_to: none\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  web: ^1.1.0\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n\nflutter:\n  assets:\n    - assets/images/\n"
  },
  {
    "path": "doc/tutorials/klondike/app/web/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"UTF-8\">\n  <meta content=\"IE=Edge\" http-equiv=\"X-UA-Compatible\">\n  <meta name=\"description\" content=\"Klondike tutorial\">\n  <base href=\"$FLUTTER_BASE_HREF\">\n\n  <!-- iOS meta tags & icons -->\n  <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n  <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\">\n  <meta name=\"apple-mobile-web-app-title\" content=\"Klondike\">\n\n  <title>Klondike</title>\n</head>\n<body>\n  <script>\n    if ('serviceWorker' in navigator) {\n      window.addEventListener('load', function () {\n        navigator.serviceWorker.register('flutter_service_worker.js');\n      });\n    }\n  </script>\n  <script src=\"main.dart.js\" type=\"application/javascript\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "doc/tutorials/klondike/klondike.md",
    "content": "# Klondike game tutorial\n\n[Klondike] is a popular solitaire card game. In this tutorial we will follow the step-by-step\nprocess for coding this game using the Flame engine.\n\nThis tutorial assumes that you have at least some familiarity with common programming concepts, and\nwith the [Dart] programming language.\n\n\n[Dart]: https://dart.dev/overview\n[Klondike]: https://en.wikipedia.org/wiki/Klondike_(solitaire)\n\n```{toctree}\n:hidden:\n\n1. Preparation          <step1.md>\n2. Scaffolding          <step2.md>\n3. Cards                <step3.md>\n4. Gameplay             <step4.md>\n5. Additional features  <step5.md>\n```\n"
  },
  {
    "path": "doc/tutorials/klondike/step1.md",
    "content": "# 1. Preparation\n\nBefore you begin any kind of game project, you need to give it a **name**. For\nthis tutorial the name will be simply `klondike`.\n\nHaving this name in mind, please head over to the [tutorial](../bare_flame_game.md)\nand complete the necessary set up steps. When you come back, you should\nalready have the `main.dart` file with the following content:\n\n```dart\nimport 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart';\n\nvoid main() {\n  final game = FlameGame();\n  runApp(GameWidget(game: game));\n}\n```\n\n\n## Planning\n\nThe start of any project usually feels overwhelming. Where even to begin?\nI always find it useful to create a rough sketch of what I am about to code,\nso that it can serve as a reference point. My sketch for the Klondike game is\nshown below:\n\n![Sketch of the klondike card game](../../images/tutorials/klondike-sketch.webp)\n\nHere you can see both the general layout of the game, as well as names of\nvarious objects. These names are the [standard terminology] for solitaire games.\nWhich is really lucky, because normally figuring out good names for various\nclasses is quite a challenging task.\n\nLooking at this sketch, we can already imagine the high-level structure of the\ngame. Obviously, there will be a `Card` class, but also the `Stock` class, the\n`Waste` class, a `Tableau` containing seven `Pile`s, and 4 `Foundation`s. There\nmay also be a `Deck`. All of these components will be tied together via the\n`KlondikeGame` derived from the `FlameGame`.\n\n\n## Assets\n\nAnother important aspect in any game development is the game's assets. These\nincludes images, sprites, animations, sounds, textures, data files, and so on.\nIn such a simple game as Klondike we won't need lots of fancy graphics, but\nstill some sprites will be needed in order to draw the cards.\n\nIn order to prepare the graphic assets, I first took a physical playing card and\nmeasured it to be 63mm × 88mm, which is the ratio of approximately `10:14`.\nThus, I decided that my in-game cards should be rendered at 1000×1400 pixels,\nand I should draw all my images with this scale in mind.\n\nNote that the exact pixel dimensions are somewhat irrelevant here, since the\nimages will in the end be scaled up or down, according to the device's actual\nresolution. Here I'm using probably a bigger resolution than necessary for\nphones, but it would also work nicely for larger devices like an iPad.\n\nAnd now, without further ado, here's my graphic asset for the Klondike game\n(I'm not an artist, so don't judge too harshly):\n\n![Klondike sprites](app/assets/images/klondike-sprites.png)\n\nRight-click the image, choose \"Save as...\", and store it in the `assets/images`\nfolder of the project. At this point our project's structure looks like this\n(there are other files too, of course, but these are the important ones):\n\n```text\nklondike/\n ├─assets/\n │  └─images/\n │     └─klondike-sprites.png\n ├─lib/\n │  └─main.dart\n └─pubspec.yaml\n```\n\nBy the way, this kind of file is called the **sprite sheet**: it's just a\ncollection of multiple independent images in a single file. We are using a\nsprite sheet here for the simple reason that loading a single large image is\nfaster than many small images. In addition, rendering sprites that were\nextracted from a single source image can be faster too, since Flutter will\noptimize multiple such drawing commands into a single `drawAtlas` command.\n\nHere are the contents of my sprite sheet:\n\n- Numerals 2, 3, 4, ..., K, A. In theory, we could have rendered these in the\n    game as text strings, but then we would need to also include a font as an\n    asset -- seems simpler to just have them as images instead.\n- Suit marks: ♥, ♦, ♣, ♠. Again, we could have used Unicode characters for\n    these, but images are much easier to position precisely.\n  - In case you're wondering why these are yellow/blue instead of red/black\n        -- turns out, black symbols don't look very nice on a dark background,\n        so I had to adjust the color scheme.\n- Flame logo, for use on the backs of the cards.\n- Pictures of a Jack, a Queen, and a King. Normally there would be four times\n    more of these, with a different character for each suite, but I got too\n    tired drawing these.\n\nAlso, you need to tell Flutter about this image (just having it inside the\n`assets` folder is not enough). In order to do this, let's add the following\nlines into the `pubspec.yaml` file:\n\n```yaml\nflutter:\n  assets:\n    - assets/images/\n```\n\nAlright, enough with preparing -- onward to coding!\n\n\n[standard terminology]: https://en.wikipedia.org/wiki/Solitaire_terminology\n"
  },
  {
    "path": "doc/tutorials/klondike/step2.md",
    "content": "# 2. Scaffolding\n\nIn this section we will use broad strokes to outline the main elements of the\ngame. This includes the main game class, and the general layout.\n\n\n## KlondikeGame\n\nIn Flame universe, the **FlameGame** class is the cornerstone of most games.\nThis class runs the game loop, dispatches events, owns all the components that\ncomprise the game (the component tree), and usually also serves as the central\nrepository for the game's state.\n\nSo, create a new file called `klondike_game.dart` inside the `lib/` folder, and\ndeclare the `KlondikeGame` class inside:\n\n```dart\nimport 'package:flame/game.dart';\nimport 'package:flame/flame.dart';\n\nclass KlondikeGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    await Flame.images.load('klondike-sprites.png');\n  }\n}\n```\n\nFor now we only declared the `onLoad` method, which is a special handler that\nis called when the game instance is attached to the Flutter widget tree for the\nfirst time. You can think of it as a delayed asynchronous constructor.\nCurrently, the only thing that `onLoad` does is that it loads the sprites image\ninto the game; but we will be adding more soon. Any image or other resource that\nyou want to use in the game needs to be loaded first, which is a relatively slow\nI/O operation, hence the need for `await` keyword.\n\nI am loading the image into the global `Flame.images` cache here. An alternative\napproach is to load it into the `Game.images` cache instead, but then it would\nhave been more difficult to access that image from other classes.\n\nAlso note that I am `await`ing the image to finish loading before initializing\nanything else in the game. This is for convenience: it means that by the time\nall other components are initialized, they can assume the sprite sheet is already\nloaded. We can even add a helper function to extract a sprite from the common\nsprite sheet:\n\n```dart\nSprite klondikeSprite(double x, double y, double width, double height) {\n  return Sprite(\n    Flame.images.fromCache('klondike-sprites.png'),\n    srcPosition: Vector2(x, y),\n    srcSize: Vector2(width, height),\n  );\n}\n```\n\nThis helper function won't be needed in this chapter, but will be used\nextensively in the next.\n\nLet's incorporate this class into the project so that it isn't orphaned. Open\nthe `main.dart` find the line which says `final game = FlameGame();` and replace\nthe `FlameGame` with `KlondikeGame`. You will need to import the class too.\nAfter all is done, the file should look like this:\n\n```dart\nimport 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart';\nimport 'klondike_game.dart';\n\nvoid main() {\n  final game = KlondikeGame();\n  runApp(GameWidget(game: game));\n}\n```\n\n\n## Other classes\n\nSo far we have the main `KlondikeGame` class, and now we need to create objects\nthat we will add to the game. In Flame these objects are called *components*,\nand when added to the game they form a \"game component tree\". All entities that\nexist in the game must be components.\n\nAs we already mentioned in the previous chapter, our game mainly consists of\n`Card` components. However, since drawing the cards will take some effort, we\nwill defer implementation of that class to the next chapter.\n\nFor now, let's create the container classes, as shown on the sketch. These are:\n`Stock`, `Waste`, `Pile` and `Foundation`. Inside the `lib/` folder create a\nsub-directory `components`, and then the file `lib/components/stock.dart`. In that\nfile write\n\n```dart\nimport 'package:flame/components.dart';\n\nclass Stock extends PositionComponent {\n  @override\n  bool get debugMode => true;\n}\n```\n\nHere we declare the `Stock` class as a `PositionComponent` (which is a component\nthat has a position and size). We also turn on the debug mode for this class so\nthat we can see it on the screen even though we don't have any rendering logic\nyet.\n\nLikewise, create three more classes `Foundation`, `Pile` and `Waste`, each in\nits corresponding file. For now all four classes will have exactly the same\nlogic inside, we'll be adding more functionality into those classes in\nsubsequent chapters.\n\nAt this moment the directory structure of your game should look like this:\n\n```text\nklondike/\n ├─assets/\n │  └─images/\n │     └─klondike-sprites.png\n ├─lib/\n │  ├─components/\n │  │  ├─foundation.dart\n │  │  ├─pile.dart\n │  │  ├─stock.dart\n │  │  └─waste.dart\n │  ├─klondike_game.dart\n │  └─main.dart\n ├─analysis_options.yaml\n └─pubspec.yaml\n```\n\n\n## Game structure\n\nOnce we have some basic components, they need to be added to the game. It is\ntime to make a decision about the high-level structure of the game.\n\nThere exist multiple approaches here, which differ in their complexity,\nextendability, and overall philosophy. The approach that we will be taking in\nthis tutorial is based on using the [World] component, together with a [Camera].\n\nThe idea behind this approach is the following: imagine that your game **world**\nexists independently from the device, that it exists already in our heads, and\non the sketch, even though we haven't done any coding yet. This world will have\na certain size, and each element in the world will have certain coordinates. It\nis up to us to decide what will be the size of the world, and what is the unit\nof measurement for that size. The important part is that the world exists\nindependently from the device, and its dimensions likewise do not depend on the\npixel resolution of the screen.\n\nAll elements that are part of the world will be added to the `World` component,\nand the `World` component will be then added to the game.\n\nThe second part of the overall structure is a **camera** (`CameraComponent`).\nThe purpose of the camera is to be able to look at the world, to make sure that\nit renders at the right size on the screen of the user's device.\n\nThus, the overall structure of the component tree will look approximately like\nthis:\n\n```text\nKlondikeGame\n ├─ World\n │   ├─ Stock\n │   ├─ Waste\n │   ├─ Foundation (×4)\n │   └─ Pile (×7)\n └─ CameraComponent\n```\n\nFor this game I've been drawing my image assets having in mind the dimension of\na single card at 1000×1400 pixels. So, this will serve as the reference size for\ndetermining the overall layout. Another important measurement that affects the\nlayout is the inter-card distance. It seems like it should be somewhere between\n150 to 200 units (relative to the card width), so we will declare it as a\nvariable `cardGap` that can be adjusted later if needed. For simplicity, both\nthe vertical and horizontal inter-card distance will be the same, and the\nminimum padding between the cards and the edges of the screen will also be equal\nto `cardGap`.\n\nAlright, let's put all this together and implement our `KlondikeGame` class.\n\nFirst, we declare several global constants which describe the dimensions of a\ncard and the distance between cards. We declare them as constants because we are\nnot planning to change these values during the game:\n\n```dart\n  static const double cardWidth = 1000.0;\n  static const double cardHeight = 1400.0;\n  static const double cardGap = 175.0;\n  static const double cardRadius = 100.0;\n  static final Vector2 cardSize = Vector2(cardWidth, cardHeight);\n```\n\nNext, we will create a `Stock` component, the `Waste`, four `Foundation`s and\nseven `Pile`s, setting their sizes and positions in the world. The positions\nare calculated using simple arithmetics. This should all happen inside the\n`onLoad` method, after loading the sprite sheet:\n\n```dart\n    final stock = Stock()\n      ..size = cardSize\n      ..position = Vector2(cardGap, cardGap);\n    final waste = Waste()\n      ..size = cardSize\n      ..position = Vector2(cardWidth + 2 * cardGap, cardGap);\n    final foundations = List.generate(\n      4,\n      (i) => Foundation()\n        ..size = cardSize\n        ..position =\n            Vector2((i + 3) * (cardWidth + cardGap) + cardGap, cardGap),\n    );\n    final piles = List.generate(\n      7,\n      (i) => Pile()\n        ..size = cardSize\n        ..position = Vector2(\n          cardGap + i * (cardWidth + cardGap),\n          cardHeight + 2 * cardGap,\n        ),\n    );\n```\n\nSince Flame version 1.9.0, `FlameGame` sets up default `world` and `camera`\nobjects. `KlondikeGame` is an extension of `FlameGame`, so we can add to that\n`world` all the components that we just created.\n\n```dart\n    world.add(stock);\n    world.add(waste);\n    world.addAll(foundations);\n    world.addAll(piles);\n```\n\n```{note}\nYou may be wondering when you need to `await` the result of `add()`, and when\nyou don't. The short answer is: usually you don't need to wait, but if you want\nto, then it won't hurt either.\n\nIf you check the documentation for `.add()` method, you'll see that the returned\nfuture only waits until the component is finished loading, not until it is\nactually mounted to the game. As such, you only have to wait for the future from\n`.add()` if your logic requires that the component is fully loaded before it can\nproceed. This is not very common.\n\nIf you don't `await` the future from `.add()`, then the component will be added\nto the game anyways, and in the same amount of time.\n```\n\nLastly, we use FlameGame's `camera` object to look at the `world`. Internally,\nthe camera consists of two parts: a **viewport** and a **viewfinder**. The\ndefault viewport is `MaxViewport`, which takes up the entire available screen\nsize -- this is exactly what we need for our game, so no need to change\nanything. The viewfinder, on the other hand, needs to be set up to take the\ndimensions of the underlying world into account.\n\nWe want the entire card layout to be visible on the screen without the need to\nscroll. In order to accomplish this, we specify that we want the entire world\nsize (which is `7*cardWidth + 8*cardGap` by `4*cardHeight + 3*cardGap`) to be\nable to fit into the screen. The `.visibleGameSize` setting ensures that no\nmatter the size of the device, the zoom level will be adjusted such that the\nspecified chunk of the game world will be visible.\n\nThe game size calculation is obtained like this: there are 7 cards in the\ntableau and 6 gaps between them, add 2 more \"gaps\" to account for padding, and\nyou get the width of `7*cardWidth + 8*cardGap`. Vertically, there are two rows\nof cards, but in the bottom row we need some extra space to be able to display\na tall pile -- by my rough estimate, thrice the height of a card is sufficient\nfor this -- which gives the total height of the game world as\n`4*cardHeight + 3*cardGap`.\n\nNext, we specify which part of the world will be in the \"center\" of the\nviewport. In this case I specify that the \"center\" of the viewport should\nbe at the top center of the screen, and the corresponding point within\nthe game world is at coordinates `[(7*cardWidth + 8*cardGap)/2, 0]`.\n\nThe reason for such choice for the viewfinder's position and anchor is\nbecause of how we want it to respond if the game size becomes too wide or\ntoo tall: in case of too wide we want it to be centered on the screen,\nbut if the screen is too tall, we want the content to be aligned at the\ntop.\n\n```dart\n    camera.viewfinder.visibleGameSize =\n           Vector2(cardWidth * 7 + cardGap * 8, 4 * cardHeight + 3 * cardGap);\n    camera.viewfinder.position = Vector2(cardWidth * 3.5 + cardGap * 4, 0);\n    camera.viewfinder.anchor = Anchor.topCenter;\n```\n\nIf you run the game now, you should see the placeholders for where the various\ncomponents will be. If you are running the game in the browser, try resizing the\nwindow and see how the game responds to this.\n\n```{flutter-app}\n:sources: ../tutorials/klondike/app\n:page: step2\n:show: popup code\n```\n\nAnd this is it with this step -- we've created the basic game structure upon\nwhich everything else will be built. In the next step, we'll learn how to render\nthe card objects, which are the most important visual objects in this game.\n\n[World]: ../../flame/camera#world\n[Camera]: ../../flame/camera#cameracomponent\n"
  },
  {
    "path": "doc/tutorials/klondike/step3.md",
    "content": "# 3. Cards\n\nIn this chapter we will begin implementing the most visible component in the\ngame -- the **Card** component, which corresponds to a single real-life card.\nThere will be 52 `Card` objects in the game.\n\nEach card has a **rank** (from 1 to 13, where 1 is an Ace, and 13 is a King)\nand a **suit** (from 0 to 3: hearts ♥, diamonds ♦, clubs ♣, and spades ♠).\nAlso, each card will have a boolean flag **faceUp**, which controls whether\nthe card is currently facing up or down. This property is important both for\nrendering, and for certain aspects of the gameplay logic.\n\nThe rank and the suit are simple properties of a card, they aren't components,\nso we need to make a decision on how to represent them. There are several\npossibilities: either as a simple `int`, or as an `enum`, or as objects. The\nchoice will depend on what operations we need to perform with them. For the\nrank, we will need to be able to tell whether one rank is one higher/lower than\nanother rank. Also, we need to produce the text label and a sprite corresponding\nto the given rank. For suits, we need to know whether two suits are of different\ncolors, and also produce a text label and a sprite. Given these requirements,\nI decided to represent both `Rank` and `Suit` as classes.\n\n\n## Suit\n\nCreate file `suit.dart` and declare an `@immutable class Suit` there, with no\nparent. The `@immutable` annotation here is just a hint for us that the objects\nof this class should not be modified after creation.\n\nNext, we define the factory constructor for the class: `Suit.fromInt(i)`. We\nuse a factory constructor here in order to enforce the singleton pattern for\nthe class: instead of creating a new object every time, we are returning one\nof the pre-built objects that we store in the `_singletons` list:\n\n```dart\n  factory Suit.fromInt(int index) {\n    assert(index >= 0 && index <= 3);\n    return _singletons[index];\n  }\n```\n\nAfter that, there is a private constructor `Suit._()`. This constructor\ninitializes the main properties of each `Suit` object: the numeric value, the\nstring label, and the sprite object which we will later use to draw the suit\nsymbol on the canvas. The sprite object is initialized using the\n`klondikeSprite()` function that we created in the previous chapter:\n\n```dart\n  Suit._(this.value, this.label, double x, double y, double w, double h)\n      : sprite = klondikeSprite(x, y, w, h);\n\n  final int value;\n  final String label;\n  final Sprite sprite;\n```\n\nThen comes the static list of all `Suit` objects in the game. Note that we\ndefine it as static variable so it is evaluated lazily (as if it was marked\nwith the `late` keyword) meaning that it will be only initialized the first\ntime it is needed. This is important: as we can see above, the constructor\ntries to retrieve an image from the global cache, so it can only be invoked\nafter the image is loaded into the cache.\n\n```dart\n  static final List<Suit> _singletons = [\n    Suit._(0, '♥', 1176, 17, 172, 183),\n    Suit._(1, '♦', 973, 14, 177, 182),\n    Suit._(2, '♣', 974, 226, 184, 172),\n    Suit._(3, '♠', 1178, 220, 176, 182),\n  ];\n```\n\nThe last four numbers in the constructor are the coordinates of the sprite\nimage within the sprite sheet `klondike-sprites.png`. If you're wondering how I\nobtained these numbers, the answer is that I used a free online service\n[spritecow.com] -- it's a handy tool for locating sprites within a sprite sheet.\n\nLastly, I have simple getters to determine the \"color\" of a suit. This will be\nneeded later when we need to enforce the rule that cards can only be placed\ninto columns by alternating colors.\n\n```dart\n  /// Hearts and Diamonds are red, while Clubs and Spades are black.\n  bool get isRed => value <= 1;\n  bool get isBlack => value >= 2;\n```\n\n\n## Rank\n\nThe `Rank` class is very similar to `Suit`. The main difference is that `Rank`\ncontains two sprites instead of one, separately for ranks of \"red\" and \"black\"\ncolors. The full code for the `Rank` class is as follows:\n\n```dart\nimport 'package:flame/components.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flutter/foundation.dart';\n\n@immutable\nclass Rank {\n  factory Rank.fromInt(int value) {\n    assert(value >= 1 && value <= 13);\n    return _singletons[value - 1];\n  }\n\n  Rank._(\n    this.value,\n    this.label,\n    double x1,\n    double y1,\n    double x2,\n    double y2,\n    double w,\n    double h,\n  )   : redSprite = klondikeSprite(x1, y1, w, h),\n        blackSprite = klondikeSprite(x2, y2, w, h);\n\n  final int value;\n  final String label;\n  final Sprite redSprite;\n  final Sprite blackSprite;\n\n  static final List<Rank> _singletons = [\n    Rank._(1, 'A', 335, 164, 789, 161, 120, 129),\n    Rank._(2, '2', 20, 19, 15, 322, 83, 125),\n    Rank._(3, '3', 122, 19, 117, 322, 80, 127),\n    Rank._(4, '4', 213, 12, 208, 315, 93, 132),\n    Rank._(5, '5', 314, 21, 309, 324, 85, 125),\n    Rank._(6, '6', 419, 17, 414, 320, 84, 129),\n    Rank._(7, '7', 509, 21, 505, 324, 92, 128),\n    Rank._(8, '8', 612, 19, 607, 322, 78, 127),\n    Rank._(9, '9', 709, 19, 704, 322, 84, 130),\n    Rank._(10, '10', 810, 20, 805, 322, 137, 127),\n    Rank._(11, 'J', 15, 170, 469, 167, 56, 126),\n    Rank._(12, 'Q', 92, 168, 547, 165, 132, 128),\n    Rank._(13, 'K', 243, 170, 696, 167, 92, 123),\n  ];\n}\n```\n\n\n## Card component\n\nNow that we have the `Rank` and the `Suit` classes, we can finally start\nimplementing the **Card** component. Create file `components/card.dart` and\ndeclare the `Card` class extending from the `PositionComponent`:\n\n```dart\nclass Card extends PositionComponent {}\n```\n\nThe constructor of the class will take integer rank and suit, and make the\ncard initially facing down. Also, we initialize the size of the component to\nbe equal to the `cardSize` constant defined in the `KlondikeGame` class:\n\n```dart\n  Card(int intRank, int intSuit)\n      : rank = Rank.fromInt(intRank),\n        suit = Suit.fromInt(intSuit),\n        _faceUp = false,\n        super(size: KlondikeGame.cardSize);\n\n  final Rank rank;\n  final Suit suit;\n  bool _faceUp;\n```\n\nThe `_faceUp` property is private (indicated by the underscore) and non-final,\nmeaning that it can change during the lifetime of a card. We should create some\npublic accessors and mutators for this variable:\n\n```dart\n  bool get isFaceUp => _faceUp;\n  bool get isFaceDown => !_faceUp;\n  void flip() => _faceUp = !_faceUp;\n```\n\nLastly, let's add a simple `toString()` implementation, which may turn out to\nbe useful when we need to debug the game:\n\n```dart\n  @override\n  String toString() => rank.label + suit.label; // e.g. \"Q♠\" or \"10♦\"\n```\n\nBefore we proceed with implementing the rendering, we need to add some cards\ninto the game. Head over to the `KlondikeGame` class and add the following at\nthe bottom of the `onLoad` method:\n\n```dart\n    final random = Random();\n    for (var i = 0; i < 7; i++) {\n      for (var j = 0; j < 4; j++) {\n        final card = Card(random.nextInt(13) + 1, random.nextInt(4))\n          ..position = Vector2(100 + i * 1150, 100 + j * 1500)\n          ..addToParent(world);\n        if (random.nextDouble() < 0.9) { // flip face up with 90% probability\n          card.flip();\n        }\n      }\n    }\n```\n\nThis snippet is a temporary code -- we will remove it in the next chapter --\nbut for now it lays down 28 random cards on the table, most of them facing up.\n\n\n### Rendering\n\nIn order to be able to see a card, we need to implement its `render()` method.\nSince the card has two distinct states -- face up or down -- we will\nimplement rendering for these two states separately. Add the following methods\ninto the `Card` class:\n\n```dart\n  @override\n  void render(Canvas canvas) {\n    if (_faceUp) {\n      _renderFront(canvas);\n    } else {\n      _renderBack(canvas);\n    }\n  }\n\n  void _renderFront(Canvas canvas) {}\n  void _renderBack(Canvas canvas) {}\n```\n\n\n### renderBack()\n\nSince rendering the back of a card is simpler, we will do it first.\n\nThe `render()` method of a `PositionComponent` operates in a local coordinate\nsystem, which means we don't need to worry about where the card is located on\nthe screen. This local coordinate system has the origin at the top-left corner\nof the component, and extends to the right by `width` and down by `height`\npixels.\n\nThere is a lot of artistic freedom in how to draw the back of a card, but my\nimplementation contains a solid background, a border, a flame logo in the\nmiddle, and another decorative border:\n\n```dart\n  void _renderBack(Canvas canvas) {\n    canvas.drawRRect(cardRRect, backBackgroundPaint);\n    canvas.drawRRect(cardRRect, backBorderPaint1);\n    canvas.drawRRect(backRRectInner, backBorderPaint2);\n    flameSprite.render(canvas, position: size / 2, anchor: Anchor.center);\n  }\n```\n\nThe most interesting part here is the rendering of a sprite: we want to\nrender it in the middle (`size/2`), and we use `Anchor.center` to tell the\nengine that we want the *center* of the sprite to be at that point.\n\nVarious properties used in the `_renderBack()` method are defined as follows:\n\n```dart\n  static final Paint backBackgroundPaint = Paint()\n    ..color = const Color(0xff380c02);\n  static final Paint backBorderPaint1 = Paint()\n    ..color = const Color(0xffdbaf58)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10;\n  static final Paint backBorderPaint2 = Paint()\n    ..color = const Color(0x5CEF971B)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 35;\n  static final RRect cardRRect = RRect.fromRectAndRadius(\n    KlondikeGame.cardSize.toRect(),\n    const Radius.circular(KlondikeGame.cardRadius),\n  );\n  static final RRect backRRectInner = cardRRect.deflate(40);\n  static final Sprite flameSprite = klondikeSprite(1367, 6, 357, 501);\n```\n\nI declared these properties as static because they will all be the same across\nall 52 card objects, so we might as well save some resources by having them\ninitialized only once.\n\n\n### renderFront()\n\nWhen rendering the face of a card, we will follow the standard card design: the\nrank and the suit in two opposite corners, plus the number of pips equal to the\nrank value. The court cards (jack, queen, king) will have special images in the\ncenter.\n\nAs before, we begin by declaring some constants that will be used for rendering.\nThe background of a card will be black, whereas the border will be different\ndepending on whether the card is of a \"red\" suit or \"black\":\n\n```dart\n  static final Paint frontBackgroundPaint = Paint()\n    ..color = const Color(0xff000000);\n  static final Paint redBorderPaint = Paint()\n    ..color = const Color(0xffece8a3)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10;\n  static final Paint blackBorderPaint = Paint()\n    ..color = const Color(0xff7ab2e8)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10;\n```\n\nNext, we also need the images for the court cards:\n\n```dart\n  static final Sprite redJack = klondikeSprite(81, 565, 562, 488);\n  static final Sprite redQueen = klondikeSprite(717, 541, 486, 515);\n  static final Sprite redKing = klondikeSprite(1305, 532, 407, 549);\n```\n\nNote that I'm calling these sprites `redJack`, `redQueen`, and `redKing`. This\nis because, after some trial, I found that the images that I have don't look\nvery well on black-suit cards. So what I decided to do is to take these images\nand *tint* them with a blueish hue. Tinting of a sprite can be achieved by\nusing a paint with `colorFilter` set to the specified color and the `srcATop`\nblending mode:\n\n```dart\n  static final blueFilter = Paint()\n    ..colorFilter = const ColorFilter.mode(\n      Color(0x880d8bff),\n      BlendMode.srcATop,\n    );\n  static final Sprite blackJack = klondikeSprite(81, 565, 562, 488)\n    ..paint = blueFilter;\n  static final Sprite blackQueen = klondikeSprite(717, 541, 486, 515)\n    ..paint = blueFilter;\n  static final Sprite blackKing = klondikeSprite(1305, 532, 407, 549)\n    ..paint = blueFilter;\n```\n\nNow we can start coding the render method itself. First, draw the background\nand the card border:\n\n```dart\n  void _renderFront(Canvas canvas) {\n    canvas.drawRRect(cardRRect, frontBackgroundPaint);\n    canvas.drawRRect(\n      cardRRect,\n      suit.isRed ? redBorderPaint : blackBorderPaint,\n    );\n  }\n```\n\nIn order to draw the rest of the card, I need one more helper method. This\nmethod will draw the provided sprite on the canvas at the specified place (the\nlocation is relative to the dimensions of the card). The sprite can be\noptionally scaled. In addition, if flag `rotate=true` is passed, the sprite\nwill be drawn as if it was rotated 180º around the center of the card:\n\n```dart\n  void _drawSprite(\n    Canvas canvas,\n    Sprite sprite,\n    double relativeX,\n    double relativeY, {\n    double scale = 1,\n    bool rotate = false,\n  }) {\n    if (rotate) {\n      canvas.save();\n      canvas.translate(size.x / 2, size.y / 2);\n      canvas.rotate(pi);\n      canvas.translate(-size.x / 2, -size.y / 2);\n    }\n    sprite.render(\n      canvas,\n      position: Vector2(relativeX * size.x, relativeY * size.y),\n      anchor: Anchor.center,\n      size: sprite.srcSize.scaled(scale),\n    );\n    if (rotate) {\n      canvas.restore();\n    }\n  }\n```\n\nLet's draw the rank and the suit symbols in the corners of the card. Add the\nfollowing to the `_renderFront()` method:\n\n```dart\n    final rankSprite = suit.isBlack ? rank.blackSprite : rank.redSprite;\n    final suitSprite = suit.sprite;\n    _drawSprite(canvas, rankSprite, 0.1, 0.08);\n    _drawSprite(canvas, rankSprite, 0.1, 0.08, rotate: true);\n    _drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5);\n    _drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5, rotate: true);\n```\n\nThe middle of the card is rendered in the same manner: we will create a big\nswitch statement on the card's rank, and draw pips accordingly. The code\nbelow may seem long, but it is actually quite repetitive and consists only\nof drawing various sprites in different places on the card's face:\n\n```dart\n    switch (rank.value) {\n      case 1:\n        _drawSprite(canvas, suitSprite, 0.5, 0.5, scale: 2.5);\n      case 2:\n        _drawSprite(canvas, suitSprite, 0.5, 0.25);\n        _drawSprite(canvas, suitSprite, 0.5, 0.25, rotate: true);\n      case 3:\n        _drawSprite(canvas, suitSprite, 0.5, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.5);\n        _drawSprite(canvas, suitSprite, 0.5, 0.2, rotate: true);\n      case 4:\n        _drawSprite(canvas, suitSprite, 0.3, 0.25);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25);\n        _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);\n      case 5:\n        _drawSprite(canvas, suitSprite, 0.3, 0.25);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25);\n        _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.5, 0.5);\n      case 6:\n        _drawSprite(canvas, suitSprite, 0.3, 0.25);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25);\n        _drawSprite(canvas, suitSprite, 0.3, 0.5);\n        _drawSprite(canvas, suitSprite, 0.7, 0.5);\n        _drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);\n      case 7:\n        _drawSprite(canvas, suitSprite, 0.3, 0.2);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.35);\n        _drawSprite(canvas, suitSprite, 0.3, 0.5);\n        _drawSprite(canvas, suitSprite, 0.7, 0.5);\n        _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);\n      case 8:\n        _drawSprite(canvas, suitSprite, 0.3, 0.2);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.35);\n        _drawSprite(canvas, suitSprite, 0.3, 0.5);\n        _drawSprite(canvas, suitSprite, 0.7, 0.5);\n        _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.5, 0.35, rotate: true);\n      case 9:\n        _drawSprite(canvas, suitSprite, 0.3, 0.2);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.3);\n        _drawSprite(canvas, suitSprite, 0.3, 0.4);\n        _drawSprite(canvas, suitSprite, 0.7, 0.4);\n        _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);\n      case 10:\n        _drawSprite(canvas, suitSprite, 0.3, 0.2);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2);\n        _drawSprite(canvas, suitSprite, 0.5, 0.3);\n        _drawSprite(canvas, suitSprite, 0.3, 0.4);\n        _drawSprite(canvas, suitSprite, 0.7, 0.4);\n        _drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.5, 0.3, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);\n        _drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);\n      case 11:\n        _drawSprite(canvas, suit.isRed? redJack : blackJack, 0.5, 0.5);\n      case 12:\n        _drawSprite(canvas, suit.isRed? redQueen : blackQueen, 0.5, 0.5);\n      case 13:\n        _drawSprite(canvas, suit.isRed? redKing : blackKing, 0.5, 0.5);\n    }\n```\n\nAnd this is it with the rendering of the `Card` component. If you run the code\nnow, you would see four rows of cards neatly spread on the table. Refreshing\nthe page will lay down a new set of cards. Remember that we have laid these\ncards in this way only temporarily, in order to be able to check that rendering\nworks properly.\n\nIn the next chapter we will discuss how to implement interactions with the\ncards, that is, how to make them draggable and tappable.\n\n```{flutter-app}\n:sources: ../tutorials/klondike/app\n:page: step3\n:show: popup code\n```\n\n[spritecow.com]: http://www.spritecow.com/\n"
  },
  {
    "path": "doc/tutorials/klondike/step4.md",
    "content": "# 4. Gameplay\n\nIn this chapter we will be implementing the core of Klondike's gameplay: how the cards move between\nthe stock and the waste, the piles and the foundations.\n\nBefore we begin though, let's clean up all those cards that we left scattered across the table in\nthe previous chapter. Open the `KlondikeGame` class and erase the loop at the bottom of `onLoad()`\nthat was adding 28 cards onto the table.\n\n\n## The piles\n\nAnother small refactoring that we need to do is to rename our components: `Stock` ⇒ `StockPile`,\n`Waste` ⇒ `WastePile`, `Foundation` ⇒ `FoundationPile`, and `Pile` ⇒ `TableauPile`. This is\nbecause these components have some common features in how they handle interactions with the cards,\nand it would be convenient to have all of them implement a common API. We will call the interface\nthat they will all be implementing the `Pile` class.\n\n```{note}\nRefactors and changes in architecture happen during development all the time:\nit's almost impossible to get the structure right on the first try. Do not be\nanxious about changing code that you have written in the past: it is a good\nhabit to have.\n```\n\nAfter such a rename, we can begin implementing each of these components.\n\n\n### Stock pile\n\nThe **stock** is a place in the top-left corner of the playing field which holds the cards that are\nnot currently in play. We will need to build the following functionality for this component:\n\n1. Ability to hold cards that are not currently in play, face down;\n2. Tapping the stock should reveal top 3 cards and move them to the **waste** pile;\n3. When the cards run out, there should be a visual indicating that this is the stock pile;\n4. When the cards run out, tapping the empty stock should move all the cards from the waste pile\n    into the stock, turning them face down.\n\nThe first question that needs to be decided here is this: who is going to own the `Card` components?\nPreviously we have been adding them directly to the game field, but now wouldn't it be better to\nsay that the cards belong to the `Stock` component, or to the waste, or piles, or foundations? While\nthis approach is tempting, I believe it would make our life more complicated as we need to move a\ncard from one place to another.\n\nSo, I decided to stick with my first approach: the `Card` components are owned directly by the\n`KlondikeGame` itself, whereas the `StockPile` and other piles are merely aware of which cards are\ncurrently placed there.\n\nHaving this in mind, let's start implementing the `StockPile` component:\n\n```dart\nclass StockPile extends PositionComponent {\n  StockPile({super.position}) : super(size: KlondikeGame.cardSize);\n\n  /// Which cards are currently placed onto this pile. The first card in the\n  /// list is at the bottom, the last card is on top.\n  final List<Card> _cards = [];\n\n  void acquireCard(Card card) {\n    assert(!card.isFaceUp);\n    card.position = position;\n    card.priority = _cards.length;\n    _cards.add(card);\n  }\n}\n```\n\nHere the `acquireCard()` method stores the provided card into the internal list `_cards`; it also\nmoves that card to the `StockPile`'s position and adjusts the cards priority so that they are\ndisplayed in the right order. However, this method does not mount the card as a child of the\n`StockPile` component -- it remains belonging to the top-level game.\n\nSpeaking of the game class, let's open the `KlondikeGame` and add the following lines to create a\nfull deck of 52 cards and put them onto the stock pile (this should be added at the end of the\n`onLoad` method):\n\n```dart\nfinal cards = [\n  for (var rank = 1; rank <= 13; rank++)\n    for (var suit = 0; suit < 4; suit++)\n      Card(rank, suit)\n];\nworld.addAll(cards);\ncards.forEach(stock.acquireCard);\n```\n\nThis concludes the first step of our short plan at the beginning of this section. For the second\nstep, though, we need to have a waste pile -- so let's make a quick detour and implement the\n`WastePile` class.\n\n\n### Waste pile\n\nThe **waste** is a pile next to the stock. During the course of the game we will be taking the cards\nfrom the top of the stock pile and putting them into the waste. The functionality of this class is\nquite simple: it holds a certain number of cards face up, fanning out the top 3.\n\nLet's start implementing the `WastePile` class same way as we did with the `StockPile` class, only\nnow the cards are expected to be face up:\n\n```dart\nclass WastePile extends PositionComponent {\n  WastePile({super.position}) : super(size: KlondikeGame.cardSize);\n\n  final List<Card> _cards = [];\n\n  void acquireCard(Card card) {\n    assert(card.isFaceUp);\n    card.position = position;\n    card.priority = _cards.length;\n    _cards.add(card);\n  }\n}\n```\n\nSo far, this puts all cards into a single neat pile, whereas we wanted a fan-out of top three. So,\nlet's add a dedicated method `_fanOutTopCards()` for this, which we will call at the end of each\n`acquireCard()`:\n\n```dart\n  void _fanOutTopCards() {\n    final n = _cards.length;\n    for (var i = 0; i < n; i++) {\n      _cards[i].position = position;\n    }\n    if (n == 2) {\n      _cards[1].position.add(_fanOffset);\n    } else if (n >= 3) {\n      _cards[n - 2].position.add(_fanOffset);\n      _cards[n - 1].position.addScaled(_fanOffset, 2);\n    }\n  }\n```\n\nThe `_fanOffset` variable here helps determine the shift between cards in the fan, which I decided\nto be about 20% of the card's width:\n\n```dart\n  final Vector2 _fanOffset = Vector2(KlondikeGame.cardWidth * 0.2, 0);\n```\n\nNow that the waste pile is ready, let's get back to the `StockPile`.\n\n\n### Stock pile -- tap to deal cards\n\nThe second item on our todo list is the first interactive functionality in the game: tap the stock\npile to deal 3 cards onto the waste.\n\nAdding tap functionality to the components in Flame is quite simple: we just add the mixin\n`TapCallbacks` to the component that we want to be tappable:\n\n```dart\nclass StockPile extends PositionComponent with TapCallbacks { ... }\n```\n\nOh, and we also need to say what we want to happen when the tap occurs. Here we want the top 3 cards\nto be turned face up and moved to the waste pile. So, add the following method to the `StockPile`\nclass:\n\n```dart\n  @override\n  void onTapUp(TapUpEvent event) {\n  final wastePile = parent!.firstChild<WastePile>()!;\n    for (var i = 0; i < 3; i++) {\n      if (_cards.isNotEmpty) {\n        final card = _cards.removeLast();\n        card.flip();\n        wastePile.acquireCard(card);\n      }\n    }\n  }\n```\n\nYou have probably noticed that the cards move from one pile to another immediately, which looks very\nunnatural. However, this is how it is going to be for now -- we will defer making the game more\nsmooth till the next chapter of the tutorial.\n\nAlso, the cards are organized in a well-defined order right now, starting from Kings and ending with\nAces. This doesn't make a very exciting gameplay though, so add line\n\n```dart\n    cards.shuffle();\n```\n\nin the `KlondikeGame` class right after the list of cards is created.\n\n\n:::{seealso}\nFor more information about tap functionality, see [](../../flame/inputs/tap_events.md).\n:::\n\n\n### Stock pile -- visual representation\n\nCurrently, when the stock pile has no cards, it simply shows an empty space -- there is no visual\ncue that this is where the stock is. Such cue is needed, though, because we want the user to be\nable to click the stock pile when it is empty in order to move all the cards from the waste back to\nthe stock so that they can be dealt again.\n\nIn our case, the empty stock pile will have a card-like border, and a circle in the middle:\n\n```dart\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(KlondikeGame.cardRRect, _borderPaint);\n    canvas.drawCircle(\n      Offset(width / 2, height / 2),\n      KlondikeGame.cardWidth * 0.3,\n      _circlePaint,\n    );\n  }\n```\n\nwhere the paints are defined as\n\n```dart\n  final _borderPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10\n    ..color = const Color(0xFF3F5B5D);\n  final _circlePaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 100\n    ..color = const Color(0x883F5B5D);\n```\n\nand the `cardRRect` in the `KlondikeGame` class as\n\n```dart\n  static final cardRRect = RRect.fromRectAndRadius(\n    const Rect.fromLTWH(0, 0, cardWidth, cardHeight),\n    const Radius.circular(cardRadius),\n  );\n```\n\nNow when you click through the stock pile till the end, you should be able to see the placeholder\nfor the stock cards.\n\n\n### Stock pile -- refill from the waste\n\nThe last piece of functionality to add, is to move the cards back from the waste pile into the stock\npile when the user taps on an empty stock. To implement this, we will modify the `onTapUp()` method\nlike so:\n\n```dart\n  @override\n  void onTapUp(TapUpEvent event) {\n    final wastePile = parent!.firstChild<WastePile>()!;\n    if (_cards.isEmpty) {\n      wastePile.removeAllCards().reversed.forEach((card) {\n        card.flip();\n        acquireCard(card);\n      });\n    } else {\n      for (var i = 0; i < 3; i++) {\n        if (_cards.isNotEmpty) {\n          final card = _cards.removeLast();\n          card.flip();\n          wastePile.acquireCard(card);\n        }\n      }\n    }\n  }\n```\n\nIf you're curious why we needed to reverse the list of cards removed from the waste pile, then it is\nbecause we want to simulate the entire waste pile being turned over at once, and not each card being\nflipped one by one in their places. You can check that this is working as intended by verifying that\non each subsequent run through the stock pile, the cards are dealt in the same order as they were\ndealt in the first run.\n\nThe method `WastePile.removeAllCards()` still needs to be implemented though:\n\n```dart\n  List<Card> removeAllCards() {\n    final cards = _cards.toList();\n    _cards.clear();\n    return cards;\n  }\n```\n\nThis pretty much concludes the `StockPile` functionality, and we already implemented the `WastePile`\n-- so the only two components remaining are the `FoundationPile` and the `TableauPile`. We'll start\nwith the first one because it looks simpler.\n\n\n### Foundation piles\n\nThe **foundation** piles are the four piles in the top right corner of the game. This is where we\nwill be building the ordered runs of cards from Ace to King. The functionality of this class is\nsimilar to the `StockPile` and the `WastePile`: it has to be able to hold cards face up, and there\nhas to be some visual to show where the foundation is when there are no cards there.\n\nFirst, let's implement the card-holding logic:\n\n```dart\nclass FoundationPile extends PositionComponent {\n  FoundationPile({super.position}) : super(size: KlondikeGame.cardSize);\n\n  final List<Card> _cards = [];\n\n  void acquireCard(Card card) {\n    assert(card.isFaceUp);\n    card.position = position;\n    card.priority = _cards.length;\n    _cards.add(card);\n  }\n}\n```\n\nFor visual representation of a foundation, I've decided to make a large icon of that foundation's\nsuit, in grey color. Which means we'd need to update the definition of the class to include the\nsuit information:\n\n```dart\nclass FoundationPile extends PositionComponent {\n  FoundationPile(int intSuit, {super.position})\n      : suit = Suit.fromInt(intSuit),\n        super(size: KlondikeGame.cardSize);\n\n  final Suit suit;\n  ...\n}\n```\n\nThe code in the `KlondikeGame` class that generates the foundations will have to be adjusted\naccordingly in order to pass the suit index to each foundation.\n\nNow, the rendering code for the foundation pile will look like this:\n\n```dart\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(KlondikeGame.cardRRect, _borderPaint);\n    suit.sprite.render(\n      canvas,\n      position: size / 2,\n      anchor: Anchor.center,\n      size: Vector2.all(KlondikeGame.cardWidth * 0.6),\n      overridePaint: _suitPaint,\n    );\n  }\n```\n\nHere we need to have two paint objects, one for the border and one for the suits:\n\n```dart\n  final _borderPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10\n    ..color = const Color(0x50ffffff);\n  late final _suitPaint = Paint()\n    ..color = suit.isRed? const Color(0x3a000000) : const Color(0x64000000)\n    ..blendMode = BlendMode.luminosity;\n```\n\nThe suit paint uses `BlendMode.luminosity` in order to convert the regular yellow/blue colors of\nthe suit sprites into grayscale. The \"color\" of the paint is different depending whether the suit\nis red or black because the original luminosity of those sprites is different. Therefore, I had to\npick two different colors in order to make them look the same in grayscale.\n\n\n### Tableau Piles\n\nThe last piece of the game to be implemented is the `TableauPile` component. There are seven of\nthese piles in total, and they are where the majority of the game play is happening.\n\nThe `TableauPile` also needs a visual representation, in order to indicate that it's a place where\na King can be placed when it is empty. I believe it could be just an empty frame, and that should\nbe sufficient:\n\n```dart\nclass TableauPile extends PositionComponent {\n  TableauPile({super.position}) : super(size: KlondikeGame.cardSize);\n\n  final _borderPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 10\n    ..color = const Color(0x50ffffff);\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(KlondikeGame.cardRRect, _borderPaint);\n  }\n}\n```\n\nOh, and the class will need to be able hold the cards too, obviously. Here, some of the cards will\nbe face down, while others will be face up. Also we will need a small amount of vertical fanning,\nsimilar to how we did it for the `WastePile` component:\n\n```dart\n  /// Which cards are currently placed onto this pile.\n  final List<Card> _cards = [];\n  final Vector2 _fanOffset = Vector2(0, KlondikeGame.cardHeight * 0.05);\n\n  void acquireCard(Card card) {\n    if (_cards.isEmpty) {\n      card.position = position;\n    } else {\n      card.position = _cards.last.position + _fanOffset;\n    }\n    card.priority = _cards.length;\n    _cards.add(card);\n  }\n```\n\nAll that remains now is to head over to the `KlondikeGame` and make sure that the cards are dealt\ninto the `TableauPile`s at the beginning of the game. Modify the code at the end of the `onLoad()`\nmethod so that it looks like this:\n\n```dart\n  @override\n  Future<void> onLoad() async {\n    ...\n\n    final cards = [\n      for (var rank = 1; rank <= 13; rank++)\n        for (var suit = 0; suit < 4; suit++)\n          Card(rank, suit)\n    ];\n    cards.shuffle();\n    world.addAll(cards);\n\n    int cardToDeal = cards.length - 1;\n    for (var i = 0; i < 7; i++) {\n      for (var j = i; j < 7; j++) {\n        piles[j].acquireCard(cards[cardToDeal--]);\n      }\n      piles[i].flipTopCard();\n    }\n    for(int n = 0; n <= cardToDeal; n++) {\n      stock.acquireCard(cards[n]);\n    }\n  }\n```\n\nNote how we deal the cards from the deck and place them into `TableauPile`s one by one, and only\nafter that we put the remaining cards into the stock.\n\nRecall that we decided earlier that all the cards would be owned by the `KlondikeGame` itself. So\nthey are put into a generated List structure called `cards`, shuffled and added to the `world`. This\nList should always have 52 cards in it, so a descending index `cardToDeal` is used to deal 28 cards\none by one from the top of the deck into piles that acquire references to the cards in the deck. An\nascending index is used to deal the remaining 24 cards into the stock in correct shuffled order. At\nthe end of the deal there are still 52 `Card` objects in the `cards` list. In the card piles we\nused `removeList()` to retrieve a card from a pile, but not here because it would remove cards\nfrom `KlondikeGame`'s ownership.\n\nThe `flipTopCard` method in the `TableauPile` class is as trivial as it sounds:\n\n```dart\n  void flipTopCard() {\n    assert(_cards.last.isFaceDown);\n    _cards.last.flip();\n  }\n```\n\nIf you run the game at this point, it would be nicely set up and look as if it was ready to play.\nExcept that we can't move the cards yet, which is kinda a deal-breaker here. So without further ado,\npresenting you the next section:\n\n\n## Moving the cards\n\nMoving the cards is a somewhat more complicated topic than what we have had so far. We will split\nit into several smaller steps:\n\n1. Simple movement: grab a card and move it around.\n2. Ensure that the user can only move the cards that they are allowed to.\n3. Check that the cards are dropped at proper destinations.\n4. Drag a run of cards.\n\n\n### 1. Simple movement\n\nSo, we want to be able to drag the cards on the screen. This is even simpler than making the\n`StockPile` tappable: just head over into the `Card` class and add the `DragCallbacks` mixin:\n\n```dart\nclass Card extends PositionComponent with DragCallbacks {\n}\n```\n\nThe next step is to implement the actual drag event callbacks: `onDragStart`, `onDragUpdate`, and\n`onDragEnd`.\n\nWhen the drag gesture is initiated, the first thing that we need to do is to raise the priority of\nthe card, so that it is rendered above all others. Without this, the card would be occasionally\n\"sliding beneath\" other cards, which would look most unnatural:\n\n```dart\n  @override\n  void onDragStart(DragStartEvent event) {\n    priority = 100;\n  }\n```\n\nDuring the drag, the `onDragUpdate` event will be called continuously. Using this callback we will\nbe updating the position of the card so that it follows the movement of the finger (or the mouse).\nThe `event` object passed to this callback contains the most recent coordinate of the point of\ntouch, and also the `localDelta` property -- which is the displacement vector since the previous\ncall of `onDragUpdate`, considering the camera zoom.\n\n```dart\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    position += event.localDelta;\n  }\n```\n\nSo far this allows you to grab any card and drag it anywhere around the table. What we want,\nhowever, is to be able to restrict where the card is allowed or not allowed to go. This is where\nthe core of the logic of the game begins.\n\n\n### 2. Move only allowed cards\n\nThe first restriction that we impose is that the user should only be able to drag the cards that we\nallow, which include: (1) the top card of a waste pile, (2) the top card of a foundation pile, and\n(3) any face-up card in a tableau pile.\n\nThus, in order to determine whether a card can be moved or not, we need to know which pile it\ncurrently belongs to. There could be several ways that we go about it, but seemingly the most\nstraightforward is to let every card keep a reference to the pile in which it currently resides.\n\nSo, let's start by defining the abstract interface `Pile` that all our existing piles will be\nimplementing:\n\n```dart\nabstract class Pile {\n  bool canMoveCard(Card card);\n}\n```\n\nWe will expand this class further later, but for now let's make sure that each of the classes\n`StockPile`, `WastePile`, `FoundationPile`, and `TableauPile` are marked as implementing this\ninterface:\n\n```dart\nclass StockPile extends PositionComponent with TapCallbacks implements Pile {\n  ...\n  @override\n  bool canMoveCard(Card card) => false;\n}\n\nclass WastePile extends PositionComponent implements Pile {\n  ...\n  @override\n  bool canMoveCard(Card card) => _cards.isNotEmpty && card == _cards.last;\n}\n\nclass FoundationPile extends PositionComponent implements Pile {\n  ...\n  @override\n  bool canMoveCard(Card card) => _cards.isNotEmpty && card == _cards.last;\n}\n\nclass TableauPile extends PositionComponent implements Pile {\n  ...\n  @override\n  bool canMoveCard(Card card) => _cards.isNotEmpty && card == _cards.last;\n}\n```\n\nWe also wanted to let every `Card` know which pile it is currently in. For this, add the field\n`Pile? pile` into the `Card` class, and make sure to set it in each pile's `acquireCard()` method,\nlike so:\n\n```dart\n  void acquireCard(Card card) {\n    ...\n    card.pile = this;\n  }\n```\n\nNow we can put this new functionality to use: go into the `Card.onDragStart()` method and modify\nit so that it would check whether the card is allowed to be moved before starting the drag:\n\n```dart\n  void onDragStart(DragStartEvent event) {\n    if (pile?.canMoveCard(this) ?? false) {\n      super.onDragStart(event);\n      priority = 100;\n    }\n  }\n```\n\nWe have also added a call to `super.onDragStart()` which sets an `_isDragged` variable to `true`\nin the `DragCallbacks` mixin, we need to check this flag via the public `isDragged` getter in\nthe `onDragUpdate()` method and use `super.onDragEnd()` in `onDragEnd()` so the flag is set back\nto `false`:\n\n```dart\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    if (!isDragged) {\n      return;\n    }\n    position += event.localDelta;\n  }\n\n  @override\n  void onDragEnd(DragEndEvent event) {\n    super.onDragEnd(event);\n  }\n```\n\nNow only the proper cards can be dragged, but they still drop at random positions on the table,\nso let's work on that.\n\n\n### 3. Dropping the cards at proper locations\n\nAt this point what we want to do is to figure out where the dragged card is being dropped. More\nspecifically, we want to know into which *pile* it is being dropped. This can be achieved by using\nthe `componentsAtPoint()` API, which allows you to query which components are located at a given\nposition on the screen.\n\nThus, my first attempt at revising the `onDragEnd` callback looks like this:\n\n```dart\n  @override\n  void onDragEnd(DragEndEvent event) {\n    if (!isDragged) {\n      return;\n    }\n    super.onDragEnd(event);\n    final dropPiles = parent!\n        .componentsAtPoint(position + size / 2)\n        .whereType<Pile>()\n        .toList();\n    if (dropPiles.isNotEmpty) {\n      // if (card is allowed to be dropped into this pile) {\n      //   remove the card from the current pile\n      //   add the card into the new pile\n      // }\n    }\n    // return the card to where it was originally\n  }\n```\n\nThis still contains several placeholders for the functionality that still needs to be implemented,\nso let's get to it.\n\nFirst piece of the puzzle is the \"is card allowed to be dropped here?\" check. To implement this,\nfirst head over into the `Pile` class and add the `canAcceptCard()` abstract method:\n\n```dart\nabstract class Pile {\n  ...\n  bool canAcceptCard(Card card);\n}\n```\n\nObviously this now needs to be implemented for every `Pile` subclass, so let's get to it:\n\n```dart\nclass FoundationPile ... implements Pile {\n  ...\n  @override\n  bool canAcceptCard(Card card) {\n    final topCardRank = _cards.isEmpty? 0 : _cards.last.rank.value;\n    return card.suit == suit && card.rank.value == topCardRank + 1;\n  }\n}\n\nclass TableauPile ... implements Pile {\n  ...\n  @override\n  bool canAcceptCard(Card card) {\n    if (_cards.isEmpty) {\n      return card.rank.value == 13;\n    } else {\n      final topCard = _cards.last;\n      return card.suit.isRed == !topCard.suit.isRed &&\n          card.rank.value == topCard.rank.value - 1;\n    }\n  }\n}\n```\n\n(for the `StockPile` and the `WastePile` the method should just return false, since no cards should\nbe dropped there).\n\nAlright, next part is the \"remove the card from its current pile\". Once again, let's head over to\nthe `Pile` class and add the `removeCard()` abstract method:\n\n```dart\nabstract class Pile {\n  ...\n  void removeCard(Card card);\n}\n```\n\nThen we need to re-visit all four pile subclasses and implement this method:\n\n```dart\nclass StockPile ... implements Pile {\n  ...\n  @override\n  void removeCard(Card card) => throw StateError('cannot remove cards from here');\n}\n\nclass WastePile ... implements Pile {\n  ...\n  @override\n  void removeCard(Card card) {\n    assert(canMoveCard(card));\n    _cards.removeLast();\n    _fanOutTopCards();\n  }\n}\n\nclass FoundationPile ... implements Pile {\n  ...\n  @override\n  void removeCard(Card card) {\n    assert(canMoveCard(card));\n    _cards.removeLast();\n  }\n}\n\nclass TableauPile ... implements Pile {\n  ...\n  @override\n  void removeCard(Card card) {\n    assert(_cards.contains(card) && card.isFaceUp);\n    final index = _cards.indexOf(card);\n    _cards.removeRange(index, _cards.length);\n    if (_cards.isNotEmpty && _cards.last.isFaceDown) {\n      flipTopCard();\n    }\n  }\n}\n```\n\nThe next action in our pseudo-code is to \"add the card to the new pile\". But this one we have\nalready implemented: it's the `acquireCard()` method. So all we need is to declare it in the `Pile`\ninterface:\n\n```dart\nabstract class Pile {\n  ...\n  void acquireCard(Card card);\n}\n```\n\nThe last piece that's missing is \"return the card to where it was\". You can probably guess how we\nare going to go about this one: add the `returnCard()` method into the `Pile` interface, and then\nimplement this method in all four pile subclasses:\n\n```dart\nclass StockPile ... implements Pile {\n  ...\n  @override\n  void returnCard(Card card) => throw StateError('cannot remove cards from here');\n}\n\nclass WastePile ... implements Pile {\n  ...\n  @override\n  void returnCard(Card card) {\n    card.priority = _cards.indexOf(card);\n    _fanOutTopCards();\n  }\n}\n\nclass FoundationPile ... implements Pile {\n  ...\n  @override\n  void returnCard(Card card) {\n    card.position = position;\n    card.priority = _cards.indexOf(card);\n  }\n}\n\nclass TableauPile ... implements Pile {\n  ...\n  @override\n  void returnCard(Card card) {\n    final index = _cards.indexOf(card);\n    card.position =\n        index == 0 ? position : _cards[index - 1].position + _fanOffset;\n    card.priority = index;\n  }\n}\n```\n\nNow, putting this all together, the `Card`'s `onDragEnd` method will look like this:\n\n```dart\n  @override\n  void onDragEnd(DragEndEvent event) {\n    if (!isDragged) {\n      return;\n    }\n    super.onDragEnd(event);\n    final dropPiles = parent!\n        .componentsAtPoint(position + size / 2)\n        .whereType<Pile>()\n        .toList();\n    if (dropPiles.isNotEmpty) {\n      if (dropPiles.first.canAcceptCard(this)) {\n        pile!.removeCard(this);\n        dropPiles.first.acquireCard(this);\n        return;\n      }\n    }\n    pile!.returnCard(this);\n  }\n```\n\nOk, that was quite a lot of work -- but if you run the game now, you'd be able to move the cards\nproperly from one pile to another, and they will never go where they are not supposed to go. The\nonly thing that remains is to be able to move multiple cards at once between tableau piles. So take\na short break, and then on to the next section!\n\n\n### 4. Moving a run of cards\n\nIn this section we will be implementing the necessary changes to allow us to move small stacks of\ncards between the tableau piles. Before we begin, though, we need to make a small fix first.\n\nYou have probably noticed when running the game in the previous section that the cards in the\ntableau piles clamp too closely together. That is, they are at the correct distance when they face\ndown, but they should be at a larger distance when they face up, which is not currently the case.\nThis makes it really difficult to see which cards are available for dragging.\n\nSo, let's head over into the `TableauPile` class and create a new method `layOutCards()`, whose job\nwould be to ensure that all cards currently in the pile have the right positions:\n\n```dart\n  final Vector2 _fanOffset1 = Vector2(0, KlondikeGame.cardHeight * 0.05);\n  final Vector2 _fanOffset2 = Vector2(0, KlondikeGame.cardHeight * 0.20);\n\n  void layOutCards() {\n    if (_cards.isEmpty) {\n      return;\n    }\n    _cards[0].position.setFrom(position);\n    for (var i = 1; i < _cards.length; i++) {\n      _cards[i].position\n        ..setFrom(_cards[i - 1].position)\n        ..add(_cards[i - 1].isFaceDown ? _fanOffset1 : _fanOffset2);\n    }\n  }\n```\n\nMake sure to call this method at the end of `removeCard()`, `returnCard()`, and `acquireCard()` --\nreplacing any current logic that handles card positioning.\n\nAnother problem that you may have noticed is that for taller card stacks it becomes hard to place a\ncard there. This is because our logic for determining in which pile the card is being dropped checks\nwhether the center of the card is inside any of the `TableauPile` components -- but those components\nhave only the size of a single card! To fix this inconsistency, all we need is to declare that the\nheight of the tableau pile is at least as tall as all the cards in it, or even higher. Add this line\nat the end of the `layOutCards()` method:\n\n```dart\n    height = KlondikeGame.cardHeight * 1.5 + _cards.last.y - _cards.first.y;\n```\n\nThe factor `1.5` here adds a little bit extra space at the bottom of each pile. The card to be\ndropped should be overlapping the hitbox by a little over half its width and height. If you are\napproaching from below, it would be just overlapping the nearest card (i.e. the one that is fully\nvisible). You can temporarily turn the debug mode on to see the hitboxes.\n\n![Illustration of Tableau Pile Hitboxes](../../images/tutorials/klondike-tableau-hitboxes.png)\n\nOk, let's get to our main topic: how to move a stack of cards at once.\n\nFirst thing that we're going to add is the list of `attachedCards` for every card. This list will\nbe non-empty only when the card is being dragged while having other cards on top. Add the following\ndeclaration to the `Card` class:\n\n```dart\n  final List<Card> attachedCards = [];\n```\n\nNow, in order to create this list in `onDragStart`, we need to query the `TableauPile` for the list\nof cards that are on top of the given card. Let's add such a method into the `TableauPile` class:\n\n```dart\n  List<Card> cardsOnTop(Card card) {\n    assert(card.isFaceUp && _cards.contains(card));\n    final index = _cards.indexOf(card);\n    return _cards.getRange(index + 1, _cards.length).toList();\n  }\n```\n\nWhile we are in the `TableauPile` class, let's also update the `canMoveCard()` method to allow\ndragging cards that are not necessarily on top:\n\n```dart\n  @override\n  bool canMoveCard(Card card) => card.isFaceUp;\n```\n\nHeading back into the `Card` class, we can use this method in order to populate the list of\n`attachedCards` when the card starts to move:\n\n```dart\n  @override\n  void onDragStart(DragStartEvent event) {\n    if (pile?.canMoveCard(this) ?? false) {\n      super.onDragStart();\n      priority = 100;\n      if (pile is TableauPile) {\n        attachedCards.clear();\n        final extraCards = (pile! as TableauPile).cardsOnTop(this);\n        for (final card in extraCards) {\n          card.priority = attachedCards.length + 101;\n          attachedCards.add(card);\n        }\n      }\n    }\n  }\n```\n\nNow all we need to do is to make sure that the attached cards are also moved with the main card in\nthe `onDragUpdate` method:\n\n```dart\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    if (!isDragged) {\n      return;\n    }\n    final delta = event.localDelta;\n    position.add(delta);\n    attachedCards.forEach((card) => card.position.add(delta));\n  }\n```\n\nThis does the trick, almost. All that remains is to fix any loose ends. For example, we don't want\nto let the user drop a stack of cards onto a foundation pile, so let's head over into the\n`FoundationPile` class and modify the `canAcceptCard()` method accordingly:\n\n```dart\n  @override\n  bool canAcceptCard(Card card) {\n    final topCardRank = _cards.isEmpty ? 0 : _cards.last.rank.value;\n    return card.suit == suit &&\n        card.rank.value == topCardRank + 1 &&\n        card.attachedCards.isEmpty;\n  }\n```\n\nSecondly, we need to properly take care of the stack of card as it is being dropped into a tableau\npile. So, go back into the `Card` class and update its `onDragEnd()` method to also move the\nattached cards into the pile, and the same when it comes to returning the cards into the old pile:\n\n```dart\n  @override\n  void onDragEnd(DragEndEvent event) {\n    if (!isDragged) {\n      return;\n    }\n    super.onDragEnd(event);\n    final dropPiles = parent!\n        .componentsAtPoint(position + size / 2)\n        .whereType<Pile>()\n        .toList();\n    if (dropPiles.isNotEmpty) {\n      if (dropPiles.first.canAcceptCard(this)) {\n        pile!.removeCard(this);\n        dropPiles.first.acquireCard(this);\n        if (attachedCards.isNotEmpty) {\n          attachedCards.forEach((card) => dropPiles.first.acquireCard(card));\n          attachedCards.clear();\n        }\n        return;\n      }\n    }\n    pile!.returnCard(this);\n    if (attachedCards.isNotEmpty) {\n      attachedCards.forEach((card) => pile!.returnCard(card));\n      attachedCards.clear();\n    }\n  }\n```\n\nWell, this is it! The game is now fully playable. Press the button below to see what the resulting\ncode looks like, or to play it live. In the next section we will discuss how to make it more\nanimated with the help of effects.\n\n```{flutter-app}\n:sources: ../tutorials/klondike/app\n:page: step4\n:show: popup code\n```\n"
  },
  {
    "path": "doc/tutorials/klondike/step5.md",
    "content": "# 5. Animations, restarting, buttons and a New World\n\nIn this chapter we will be showing various ways to make the Klondike game more fun and easier to\nplay. The topics to be covered are:\n\n- Klondike Draw 1 and Draw 3\n- Animating and automating moves\n- Detecting and celebrating a win\n- Ending and restarting the game\n- How to implement your own FlameGame world\n- Simple action-buttons\n- Anchors and co-ordinates\n- Random number generation and seeding\n- Effects and EffectControllers\n\n\n## The Klondike draw\n\nThe Klondike patience game (or solitaire game in the USA) has two main variants: Draw 3 and Draw 1.\nCurrently the Klondike Flame Game is Draw 3, which is a lot more difficult than Draw 1, because\nalthough you can see 3 cards, you can only move one of them and that move changes the \"phase\" of\nother cards. So different cards are going to become available, not easy.\n\nIn Klondike Draw 1 just one card at a time is drawn from the Stock and shown, so every card in it is\navailable, and you can go through the Stock as many times as you like, just as in Klondike Draw 3.\n\nSo how do we implement Klondike Draw 1? Clearly only the Stock and Waste piles are involved, so\nmaybe we should have KlondikeGame provide a value 1 or 3 to each of them. They both have code for\nconstructors, so we could just add an extra parameter to that code, but in Flame there is another\nway, which works even if your component has a default constructor (no code for it) or your game has\nmany game-wide values. Let us call our value `klondikeDraw`. In your class declaration add the\n`HasGameReference<MyGame>` mixin, then write `game.klondikeDraw` wherever you need the value 1 or 3.\nFor class StockPile we will have:\n\n```dart\nclass StockPile extends PositionComponent\n    with TapCallbacks, HasGameReference<KlondikeGame>\n    implements Pile {\n```\n\nand\n\n```dart\n  @override\n  void onTapUp(TapUpEvent event) {\n    final wastePile = parent!.firstChild<WastePile>()!;\n    if (_cards.isEmpty) {\n      wastePile.removeAllCards().reversed.forEach((card) {\n        card.flip();\n        acquireCard(card);\n      });\n    } else {\n      for (var i = 0; i < game.klondikeDraw; i++) {\n        if (_cards.isNotEmpty) {\n          final card = _cards.removeLast();\n          card.flip();\n          wastePile.acquireCard(card);\n        }\n      }\n    }\n  }\n```\n\nFor class WastePile we will have:\n\n```dart\nclass WastePile extends PositionComponent\n    with HasGameReference<KlondikeGame>\n    implements Pile {\n```\n\nand\n\n```dart\n  void _fanOutTopCards() {\n    if (game.klondikeDraw == 1) {   // No fan-out in Klondike Draw 1.\n      return;\n    }\n    final n = _cards.length;\n    for (var i = 0; i < n; i++) {\n      _cards[i].position = position;\n    }\n    if (n == 2) {\n      _cards[1].position.add(_fanOffset);\n    } else if (n >= 3) {\n      _cards[n - 2].position.add(_fanOffset);\n      _cards[n - 1].position.addScaled(_fanOffset, 2);\n    }\n  }\n```\n\nThat makes the Stock and Waste piles play either Klondike Draw 1 or Klondike Draw 3, but how do you\ntell them which variant to play? For now, we will add a place-holder to the KlondikeGame class.\nWe just comment out whichever one we do not want and then rebuild.\n\n```dart\n  // final int klondikeDraw = 3;\n  final int klondikeDraw = 1;\n```\n\nThis is fine as a temporary measure, when we have not yet decided how to handle some aspect of\nour design, but ultimately we will have to provide some kind of **input** for the player to choose\nwhich flavor of Klondike to play, such as a menu screen, a settings screen or a button. Flame can\nincorporate Flutter widgets into a game and the next Tutorial (Ember) shows how to add a menu\nwidget, as its final step.\n\n\n## Making cards move\n\nIn Flame, if we need a component to do something, we use an `Effect` - a special component that can\nattach to another component, such as a card, and modify its properties. That includes any kind of\nmotion (or change of `position`). We also need an `EffectController`, which provides timing for an\neffect: when to start, how long to go for and what `Curve` to follow. The latter is not a curve in\nspace. It is a time-curve that specifies accelerations and decelerations during the time of the\neffect, such as start moving a card quickly and then slow down as it approaches its destination.\n\nTo move a card, we will add a `doMove()` method to the `Card` class. It will require a `to` location\nto go to. Optional parameters are `speed:` (default 10.0), `start:` (default zero),\n`curve:` (default `Curves.easeOutQuad`) and `onComplete:` (default `null`, i.e. no callback when\nthe move finishes). Speed is in card widths per second. Usually we will provide a callback, because\na bit of gameplay must be done **after** the animated move. The default `curve:` parameter gives us\na fast-in/slow-out move, much as a human player would do. So the following code is added to the\nend of the `Card` class:\n\n```dart\n  void doMove(\n    Vector2 to, {\n    double speed = 10.0,\n    double start = 0.0,\n    Curve curve = Curves.easeOutQuad,\n    VoidCallback? onComplete,\n  }) {\n    assert(speed > 0.0, 'Speed must be > 0 widths per second');\n    final dt = (to - position).length / (speed * size.x);\n    assert(dt > 0.0, 'Distance to move must be > 0');\n    priority = 100;\n    add(\n      MoveToEffect(\n        to,\n        EffectController(duration: dt, startDelay: start, curve: curve),\n        onComplete: () {\n          onComplete?.call();\n        },\n      ),\n    );\n  }\n```\n\nTo make this code compile we need to import `'package:flame/effects.dart'` and\n`'package:flutter/animation.dart'` at the top of the `components/card.dart` file. That done, we can\nstart using the new method to return the card(s) gracefully to where they came from, after being\ndropped in an invalid position. First, we need a private data item to store a card's position when\na drag-and-drop started. So let us insert new lines in two places as shown below:\n\n```dart\n  bool _isDragging = false;\n  Vector2 _whereCardStarted = Vector2(0, 0);\n\n  final List<Card> attachedCards = [];\n```\n\n```dart\n      _isDragging = true;\n      priority = 100;\n      // Copy each co-ord, else _whereCardStarted changes as the position does.\n      _whereCardStarted = Vector2(position.x, position.y);\n      if (pile is TableauPile) {\n```\n\nIt would be a mistake to write `_whereCardStarted = position;` here. In Dart, that would just\ncopy a reference: so `_whereCardStarted` would point to the same data as `position` while the\ndrag occurred and the card's `position` data changed. We can get around this by copying the card's\n**current** X and Y co-ordinates into a **new** `Vector2` object.\n\nTo animate cards being returned to their original piles after an invalid drag-and-drop, we replace\nfive lines at the end of the `onDragEnd()` method with:\n\n```dart\n    // Invalid drop (middle of nowhere, invalid pile or invalid card for pile).\n    doMove(\n      _whereCardStarted,\n      onComplete: () {\n        pile!.returnCard(this);\n      },\n    );\n    if (attachedCards.isNotEmpty) {\n      attachedCards.forEach((card) {\n        final offset = card.position - position;\n        card.doMove(\n          _whereCardStarted + offset,\n          onComplete: () {\n            pile!.returnCard(card);\n          },\n        );\n      });\n      attachedCards.clear();\n    }\n```\n\nIn each case, we use the default speed of 10 card-widths per second.\nNotice how the `onComplete:` parameters are used to return each card to the pile where it started.\nIt will then be added back to that pile's list of contents. Notice also that the list of attached\ncards (if any) is cleared immediately, as the animated cards start to move. This does not matter,\nbecause each moving card has a `MoveToEffect` and an `EffectController` added to it and these\ncontain all the data needed to get the right card to the right place at the right time. Thus\nno important information is lost by clearing the attached cards early. Also, by default, the\n`MoveToEffect` and `EffectController` in each moving card automatically get detached and deleted\nby Flame when the show is over.\n\nSome other automatic and animated moves we can try are dealing the cards, flipping cards from Stock\nto Waste pile, turning cards over automatically on the tableau piles, and settling cards into place\nafter a valid drag-and-drop. We will have a look at animating a flip first.\n\n\n## Animating a card-flip\n\nFlutter and Flame do not yet support 3-D effects (as at October 2023), but we can emulate them.\nTo make a card look as if it is turning over, we will shrink the width of the back-view, switch\nto the front view and expand back to full width. The code uses quite a few features of Effects\nand EffectControllers:\n\n```dart\n  void turnFaceUp({\n    double time = 0.3,\n    double start = 0.0,\n    VoidCallback? onComplete,\n  }) {\n    assert(!_isFaceUpView, 'Card must be face-down before turning face-up.');\n    assert(time > 0.0, 'Time to turn card over must be > 0');\n    _isAnimatedFlip = true;\n    anchor = Anchor.topCenter;\n    position += Vector2(width / 2, 0);\n    priority = 100;\n    add(\n      ScaleEffect.to(\n        Vector2(scale.x / 100, scale.y),\n        EffectController(\n          startDelay: start,\n          curve: Curves.easeOutSine,\n          duration: time / 2,\n          onMax: () {\n            _isFaceUpView = true;\n          },\n          reverseDuration: time / 2,\n          onMin: () {\n            _isAnimatedFlip = false;\n            _faceUp = true;\n            anchor = Anchor.topLeft;\n            position -= Vector2(width / 2, 0);\n          },\n        ),\n        onComplete: () {\n          onComplete?.call();\n        },\n      ),\n    );\n  }\n```\n\nSo how does all this work? We have a default time of 0.3 seconds for the flip to occur, a start time\nand an optional callback on completion, as before. Now we add a ScaleEffect to the card,\nwhich shrinks it almost to zero width, but leaves the height unchanged. However, that must take\nonly half the time, then we must switch from the face-down to the face-up view of the card and\nexpand it back out, also in half the time.\n\nThis is where we use some of the fancier parameters of the `EffectController` class. The\n`duration:` is set to `time / 2` and we use an `onMax:` callback, with inline code to change the\nview to face-up. That callback will happen after `time / 2`, when the `Effect` (whatever it is)\nhas reached its maximum (i.e. in this case, the view of the card has shrunk to a thin vertical\nline). After the switch to face-up view, the EffectController will take the Effect into reverse\nfor `reverseDuration: time / 2`. Everything is reversed: the view of the card expands and the\n`curve:` of time is applied in reverse order. In total, the timing follows a sine curve from\n0 to pi, giving a smooth animation in which the width of the card-view is always the projection\ninto 2-D of its 3-D position. Wow! That's a lot of work for a little EffectController!\n\nWe are not there yet! If you were to run just the `add()` part of the code, you would see some\nugly things happening. Yeah, yeah, been there, done that... when I was preparing this code!\nFirst off, the card shrinks to a line at its left. That is because all cards in this game have\nan `Anchor` at `topLeft`, which is the point used to set the card's `position`. We would like\nthe card to flip around its vertical center-line. Easy, just set `anchor = Anchor.topCenter`\nfirst: that makes the card flip realistically, but it jumps by half a card-width to the left\nbefore flipping.\n\nLong story short, see the lines between `assert(` and `add(` and their reversal in the `onMin:`\ncallback, which occurs when the Effect is finished, but before the final `onComplete:` callback.\nAt the beginning, the card's rendering `priority` is set to 100, so that it will ride above all\nother cards in the neighborhood. That value cannot always be saved and restored because we may\nnot know what the card's priority should be in whatever `Pile` is receiving it. So we have made\nsure that the receiver is always called in the `onComplete:` option, using a method that will\nadjust the positions and priorities of the cards in the pile.\n\nLast but not least, in the preceding code, notice the use of the variable `_isAnimatedFlip`.\nThis is a `bool` variable defined and initialized near the start of class `Card` in file\n`components/card.dart`, along with another new `bool` called `_isFaceUpView`. Initially these\nare set `false`, along with the existing `bool _faceUp = false` variable. What is the significance\nof these variables? It is **huge**. A few lines further down, we see:\n\n```dart\n  @override\n  void render(Canvas canvas) {\n    if (_isFaceUpView) {\n      _renderFront(canvas);\n    } else {\n      _renderBack(canvas);\n    }\n  }\n```\n\nThis is the code that makes every card visible on the screen, in either face-up or face-down state.\nAt the end of Klondike Tutorial Step 4, the `if` statement was `if (_faceUp) {`. This was OK\nbecause all moves of cards were instantaneous (leaving aside drags and drops): any change in the\ncard's face-up or face-down state could be rendered at the Flame Engine's next `tick` or soon after.\nThis posed no problem when we started to animate card moves, provided there were no flips involved.\nHowever, when we tapped a non-empty Stock Pile, the executed code was:\n\n```dart\n  final card = _cards.removeLast();\n  card.flip;\n  wastePile.acquireCard(card);\n```\n\nAnd the first thing `wastePile.acquireCard(` does is `assert(card.isFaceUp);`, which fails if an\nanimated flip is keeping the card face-down while the first half of the flip is occurring.\n\n\n## Model and View\n\nClearly the card cannot be in two states at once: it is not Schrödinger's cat! We can resolve the\ndilemma by using two definitions of \"face-up\": a Model type and a View type. The View version is\nused in rendering and animation (i.e. what appears on the screen) and the Model version in the logic\nof the game, the gameplay and its error-checks. That way, we do not have to revise all the logic\nof the Piles in this game in order to animate some of it. A more complex game might benefit from\nseparating the Model and the View during the design and early coding stages, even into\nseparate classes. In this game we are using just a little separation of Model and View. The\n`_isAnimatedFlip` variable is `true` while there is an animated flip going on, otherwise `false`,\nand the `Card` class's `flip()` function is expanded to:\n\n```dart\n  void flip() {\n    if (_isAnimatedFlip) {\n      // Let the animation determine the FaceUp/FaceDown state.\n      _faceUp = _isFaceUpView;\n    } else {\n      // No animation: flip and render the card immediately.\n      _faceUp = !_faceUp;\n      _isFaceUpView = _faceUp;\n    }\n  }\n```\n\nIn the Klondike Tutorial game we are still having to trigger a Model update in the `onComplete:`\ncallback of the flip animation. It might be nice, for impatient or rapid-fingered players, to\ntransfer a card from Stock Pile to Waste Pile instantaneously, in the Model, leaving the animation\nin the View to catch up later, with no `onComplete:` callback. That way, you could flip through\nthe Stock Pile very rapidly, by tapping fast. However, that is beyond the scope of this Tutorial.\n\n\n## Ending and restarting the game\n\nAs it stands, there is no easy way to finish the Klondike Tutorial game and start another, even if\nyou have won. We can only close the app and start it again. And there is no \"reward\" for winning.\n\nThere are various ways to tackle this, depending on the simplicity or complexity of your game and\non how long the `onLoad()` method is likely to take. They can range from writing your own\nGameWidget, to doing a few simple re-initializations in your Game class (i.e. KlondikeGame in this\ncase).\n\nIn the GameWidget case you would supply the Game with a VoidCallback function parameter named\n`reset` or `restart`. When the callback is invoked, it would use the Flutter conventions of a\n`StatefulWidget` (e.g. `setState(() {});)` to force the widget to be rebuilt and replaced, thus\nreleasing all references to the current Game instance, its state and all of its memory. There could\nalso be Flutter code to run a menu or other startup screen.\n\nRe-initialization should be undertaken only if the operations involved are few and simple. Otherwise\ncoding errors could lead to subtle problems, memory leaks and crashes in your game. It might be the\neasiest way to go in Klondike (as it is in the Ember Tutorial). Basically, we must clear all the\ncard references out of all the `Pile`s and then re-shuffle (or not) and re-deal, possibly changing\nfrom Klondike Draw 3 to Klondike Draw 1 or vice-versa.\n\nWell, that was not as easy as it looked! Re-initializing the `Pile`s and each `Card` was easy\nenough, but the difficult bit came next... Whether the player wins or restarts without winning, we\nhave 52 cards spread around various piles on the screen, some face-up and maybe some face-down. We\nwould like to animate the deal later, so it would be nice to collect the cards into a neat face-down\npile at top left, in the Stock Pile area: not the actual Stock Pile yet, because that gets created\nduring the deal.\n\nWriting a simple little loop to set each `Card` face-down and use its `doMove` method to make it\nmove independently to the top left fails. It causes one of those \"subtle problems\" referred to\nearlier. The cards all travel at the same speed but arrive at different times. The deal then\nproduces messy Tableau Piles with several cards out of position. Also the animated move of all\nthe cards to the Stock Pile area was a bit ugly.\n\nThe problem of the messy Tableau Piles was fixable, but at this point the reviewer of the code\nand documentation proposed a completely new approach which avoids re-initializing anything and\ncreates all the Components from scratch, which is the preferred Flutter/Flame way of doing things.\n\n\n## A New World\n\n\n### Start and restart actions\n\nWe wish to provide the following actions in the Klondike game:\n\n- A first start,\n- Any number of restarts with a new deal,\n- Any number of restarts with the same deal as before,\n- A switch between Klondike Draw 1 and Draw 3 and restart with a new deal, and\n- Have fun before restarting with a new deal (we'll keep that as a surprise for later).\n\nThe proposal is to have a new KlondikeWorld class, which replaces the default `world` provided by\nFlameGame. The new world contains (almost) everything we need to play the game and is created or\nre-created during each of the above actions.\n\n\n### A stripped-down KlondikeGame class\n\nHere is the new code for the KlondikeGame class (what is left of it).\n\n```dart\nenum Action { newDeal, sameDeal, changeDraw, haveFun }\n\nclass KlondikeGame extends FlameGame<KlondikeWorld> {\n  static const double cardGap = 175.0;\n  static const double topGap = 500.0;\n  static const double cardWidth = 1000.0;\n  static const double cardHeight = 1400.0;\n  static const double cardRadius = 100.0;\n  static const double cardSpaceWidth = cardWidth + cardGap;\n  static const double cardSpaceHeight = cardHeight + cardGap;\n  static final Vector2 cardSize = Vector2(cardWidth, cardHeight);\n  static final cardRRect = RRect.fromRectAndRadius(\n    const Rect.fromLTWH(0, 0, cardWidth, cardHeight),\n    const Radius.circular(cardRadius),\n  );\n\n  // Constant used when creating Random seed.\n  static const int maxInt = 0xFFFFFFFE; // = (2 to the power 32) - 1\n\n  // This KlondikeGame constructor also initiates the first KlondikeWorld.\n  KlondikeGame() : super(world: KlondikeWorld());\n\n  // These three values persist between games and are starting conditions\n  // for the next game to be played in KlondikeWorld. The actual seed is\n  // computed in KlondikeWorld but is held here in case the player chooses\n  // to replay a game by selecting Action.sameDeal.\n  int klondikeDraw = 1;\n  int seed = 1;\n  Action action = Action.newDeal;\n}\n```\n\nHuh! What happened to the `onLoad()` method? And what's this `seed` thing? And how does\nKlondikeWorld get into the act? Well, everything that used to be in the `onLoad()` method is now\nin the `onLoad()` method of KlondikeWorld, which is an extension of the `World` class and is a type\nof `Component`, so it can have an `onLoad()` method, as can any `Component` type. The content of\nthe method is much the same as before, except that `world.add(` becomes just `add(`. It also brings\nin some `addButton()` references, but more on these later.\n\n\n### Using a Random Number Generator seed\n\nThe `seed` is a common games-programming technique in any programming environment. Usually it allows\nyou to start a Random Number Generator from a known point (called the seed) and give your game\nreproducible behavior when you are in the development and testing stage. Here it is used to\nprovide exactly the same deal of the Klondike cards when the player requests `Same deal`.\n\n\n### Introducing the new KlondikeWorld class\n\nThe `class KlondikeGame` declaration specifies that this extension of the FlameGame class must\nhave a world of type KlondikeWorld (i.e. `FlameGame<KlondikeWorld>`). Didn't know we could do\nthat for a game, did we? So how does the first instance of KlondikeWorld get created? It's all in\nthe KlondikeGame constructor code:\n\n```dart\n  KlondikeGame() : super(world: KlondikeWorld());\n```\n\nThe constructor itself is a default constructor, but the colon `:` begins a constructor\ninitialization sequence which creates our world for the first time.\n\n\n### Buttons\n\nWe are going to use some buttons to activate the various ways of restarting the Klondike Game. First\nwe extend Flame's `ButtonComponent` to create class `FlatButton`, adapted from a Flat Button which\nused to be in Flame's Examples pages. `ButtonComponent` uses two `PositionComponent`s, one for when\nthe button is in its normal state (up) and one for when it is pressed. The two components are\n`mounted` and `rendered` alternately as the user presses the button and releases it. To press the\nbutton, tap and hold it down.\n\nIn our button, the two components are the button's outlines - the `buttonDown:` one makes\nthe outline of the button turn red when it is pressed, as a warning, because the four button-actions\nall end the current game and start another. That is also why they are positioned at the top of the\ncanvas, above all the cards, where you are less likely to press them accidentally. If you do press\none and have second thoughts, keep pressing and slide away, then the button will have no effect.\n\nThe four buttons trigger the restart actions described above and are labelled `New deal`,\n`Same deal`, `Draw 1 ⇌ 3` and `Have fun`. Flame also has a `SpriteButtonComponent`, based on two\nalternating `Sprite`s, a `HudButtonComponent` and an `AdvancedButtonComponent`. For further types\nof buttons and controllers, it would be best to use a Flutter overlay, menu or settings widget and\nhave access to Flutter's widgets for radio buttons, dropdown lists, sliders, etc. For the purposes\nof this Tutorial our FlatButton will do fine.\n\nWe use the `addButton()` method, during our world's `onLoad()`, to set up our four buttons and\nadd them to our `world`.\n\n```dart\n    playAreaSize =\n        Vector2(7 * cardSpaceWidth + cardGap, 4 * cardSpaceHeight + topGap);\n    final gameMidX = playAreaSize.x / 2;\n\n    addButton('New deal', gameMidX, Action.newDeal);\n    addButton('Same deal', gameMidX + cardSpaceWidth, Action.sameDeal);\n    addButton('Draw 1 or 3', gameMidX + 2 * cardSpaceWidth, Action.changeDraw);\n    addButton('Have fun', gameMidX + 3 * cardSpaceWidth, Action.haveFun);\n```\n\nThat places them above our four Foundation piles and centrally aligned with them. The first\nFoundation pile happens to be aligned around the top-center of the screen, so the first button\nis centred above it.\n\n\n### Anchors and co-ordinates\n\nThe expressions here and in the `addButton()` method may seem odd because the cards and piles all\nhave `Anchor.topLeft` but the buttons have `Anchor.center`. The `position` co-ordinates of a `Card`\nare where its top-left corner goes, but the `position` co-ordinates of a `FlatButton` are where its\n*center* goes and the various parts of a `FlatButton` are arranged (internally) around its center.\nThese examples can give us some insight into how co-ordinate systems work in Flame.\n\n\n### The `deal()` method\n\nThe last thing the KlondikeWorld's `onLoad()` method does is call the `deal()` method to shuffle\nand deal the cards. This method is now in the KlondikeWorld class and so are the `checkWin()` and\n`letsCelebrate()` methods, but more about those later. The deal process is the same as before but\nnow includes some animation:\n\n```dart\n  void deal() {\n    assert(cards.length == 52, 'There are ${cards.length} cards: should be 52');\n\n    if (game.action != Action.sameDeal) {\n      // New deal: change the Random Number Generator's seed.\n      game.seed = Random().nextInt(KlondikeGame.maxInt);\n      if (game.action == Action.changeDraw) {\n        game.klondikeDraw = (game.klondikeDraw == 3) ? 1 : 3;\n      }\n    }\n    // For the \"Same deal\" option, re-use the previous seed, else use a new one.\n    cards.shuffle(Random(game.seed));\n\n    var cardToDeal = cards.length - 1;\n    var nMovingCards = 0;\n    for (var i = 0; i < 7; i++) {\n      for (var j = i; j < 7; j++) {\n        final card = cards[cardToDeal--];\n        card.doMove(\n          tableauPiles[j].position,\n          start: nMovingCards * 0.15,\n          onComplete: () {\n            tableauPiles[j].acquireCard(card);\n            nMovingCards--;\n            if (nMovingCards == 0) {\n              var delayFactor = 0;\n              for (final tableauPile in tableauPiles) {\n                delayFactor++;\n                tableauPile.flipTopCard(start: delayFactor * 0.15);\n              }\n            }\n          },\n        );\n        nMovingCards++;\n      }\n    }\n    for (var n = 0; n <= cardToDeal; n++) {\n      stock.acquireCard(cards[n]);\n    }\n  }\n```\n\nFirst we implement the `Action` value for this game. In the very first game, the KlondikeGame class\nsets defaults of `Action.newDeal` and `klondikeDraw = 1`, but after that the player can select an\naction by pressing and releasing a button and KlondikeWorld saves it in KlondikeGame, or the player\nwins the game, in which case `Action.newDeal` is selected and saved automatically. The action\nusually generates and saves a new seed, but that is skipped if we have `Action.sameDeal`. Then we\nshuffle the cards, using whatever `seed` applies.\n\nThe deal logic is the same as we used in Klondike Tutorial Step 4 and the animation is fairly easy.\nWe just use `card.doMove(` for each card, with a changing destination and an increasing `start:`\nvalue, counting each moving card as it departs. For a few milliseconds after the loops terminate\n`nMovingCards` will be at a maximum of 28 (i.e. 1 + 2 + 3 + 4 + 5 + 6 + 7) and the remaining 24\ncards will go into a properly constructed Stock Pile.\n\nThen cards will be arriving over the next second or so and a problem arises. The cards do not\nnecessarily arrive in the order they are sent from the Stock Pile area. If we start turning over\nthe last cards in the columns too soon, we might turn over the wrong card and mess up the deal. The\nfollowing printout of the deal shows how arrivals can get out of order. The `j` variable is the\nTableau Pile number and `i` is the card's position in the pile. The King of Hearts for Pile 6 is\narriving before the Queen of Clubs that is the last card in Pile 5. And there are two more cards\nto go in Pile 6.\n\n```console\nflutter: Move done, i 3, j 6, 6♠ 5 moving cards.\nflutter: Move done, i 4, j 5, 9♥ 4 moving cards.\nflutter: Move done, i 4, j 6, K♥ 3 moving cards.\nflutter: Move done, i 5, j 5, Q♣ 2 moving cards.\nflutter: Move done, i 5, j 6, 2♠ 1 moving cards.\nflutter: Move done, i 6, j 6, 10♠ 0 moving cards.\nflutter: Pile 0 [Q♦]\nflutter: Pile 1 [J♣, Q♥]\nflutter: Pile 2 [5♥, 5♦, J♦]\nflutter: Pile 3 [A♠, Q♠, A♥, 5♠]\nflutter: Pile 4 [8♦, 10♣, 7♥, 3♥, 4♥]\nflutter: Pile 5 [4♠, 8♣, 5♣, 2♥, 9♥, Q♣]\nflutter: Pile 6 [4♣, 3♦, K♦, 6♠, K♥, 2♠, 10♠]\n```\n\nSo we count off the cards in the `onComplete()` callback code as they arrive. Only when all 28 cards\nhave arrived do we start turning over the last card of each Tableau Pile. When the deal has been\ncompleted our KlondikeWorld is also complete and ready for play.\n\n\n## More animations of moves\n\nThe `Card` class's `doMove()` and `turnFaceUp()` methods have been combined into a doMoveAndFlip()\nmethod, which is used to draw cards from the Stock Pile. The dropping of a card or cards onto a pile\nafter drag-and-drop also uses `doMove()` to settle the drop more gracefully. Finally, there is a\nshortcut to auto-move a card onto its Foundation Pile if it is ready to go out. This adds\n`TapCallbacks` to the `Card` class and an `onTapUp()` callback as follows:\n\n```dart\n  onTapUp(TapUpEvent event) {\n    if (isFaceUp) {\n      final suitIndex = suit.value;\n      if (game.foundations[suitIndex].canAcceptCard(this)) {\n        pile!.removeCard(this);\n        doMove(\n          game.foundations[suitIndex].position,\n          onComplete: () {\n            game.foundations[suitIndex].acquireCard(this);\n          },\n        );\n      }\n    } else if (pile is StockPile) {\n      game.stock.onTapUp(event);\n    }\n  }\n```\n\nIf a card is ready to go out, just tap on it and it will move automatically to the correct\nFoundation Pile for its suit. This saves a load of dragging-and-dropping when you are close to\nwinning the game! There is nothing new in the above code, except that if you tap the top card of\nthe Stock Pile, the `Card` object receives the tap first and forwards it on to the `stock` object.\n\n\n## A graphics glitch\n\nIf you moved multiple cards from one Tableau Pile to another, the internal code of the `TableauPile`\nclass would formerly (in Tutorial Step 4) move the cards into place abruptly, as soon as the\ndrag-and-drop ended. In the new code (Step 5), drags and drops use essentially the same code as\nbefore, so it is tempting to get that code to do a multi-card move as a series of animated moves\neach completing with an `acquireCard` call. But this caused some ugly graphics glitches. It\nappears they were due to `acquireCard` also calling the `layoutCards()` method of `TableauPile` and\ninstantly re-arranging all the cards in the pile, every time a card was acquired. The problem has\nbeen solved (with some difficulty as it turned out), by adding a `dropCards` method to\n`TableauPile`, which mimics some of the existing actions while dovetailing some card animations\nin as well.\n\nThe lesson to be learned is that it is worth giving some attention to animation and time-dependent\nconcerns at Game Design time. When was that? Back in Klondike Tutorial Step 1 Preparation and\nStep 2 Scaffolding.\n\n\n## Winning the game\n\nYou win the game when all cards in all suits, Ace to King, have been moved to the Foundation Piles,\n13 cards in each pile. The game now has code to recognize that event: an `isFull` test added to\nthe `FoundationPile`'s `acquireCard()` method, a callback to `KlondikeWorld` and a test as\nto whether all four Foundations are full. Here is the code:\n\n```dart\nclass FoundationPile extends PositionComponent implements Pile {\n  FoundationPile(int intSuit, this.checkWin, {super.position})\n      : suit = Suit.fromInt(intSuit),\n        super(size: KlondikeGame.cardSize);\n\n  final VoidCallback checkWin;\n\n  final Suit suit;\n  final List<Card> _cards = [];\n\n  //#region Pile API\n\n  bool get isFull => _cards.length == 13;\n```\n\n```dart\n  void acquireCard(Card card) {\n    assert(card.isFaceUp);\n    card.position = position;\n    card.priority = _cards.length;\n    card.pile = this;\n    _cards.add(card);\n    if (isFull) {\n      checkWin(); // Get KlondikeWorld to check all FoundationPiles.\n    }\n  }\n```\n\n```dart\n  void checkWin()\n  {\n    var nComplete = 0;\n    for (final f in foundations) {\n      if (f.isFull) {\n        nComplete++;\n      }\n    }\n    if (nComplete == foundations.length) {\n      letsCelebrate();\n    }\n  }\n```\n\nIt is often possible to calculate whether you can win from a given position of the cards in a\nKlondike game, or could have won but missed a vital move. It is frequently possible to calculate\nwhether the initial deal is winnable: a percentage of Klondike deals are not. But all that is far\nbeyond the scope of this Tutorial, so for now it is up to the player to work out whether to keep\nplaying and try to win, or give up and press one of the buttons.\n\n\n## Ending a game and re-starting it\n\nA game ends either after the player wins or they press and release one of the buttons. At that\npoint the KlondikeGame class must hold all the data needed to start a new game, namely an `Action`\nvalue, a `klondikeDraw` value (1 or 3) and a `seed` from the previous game. Each button has an\n`onReleased:` callback provided by the `addButton()` method in KlondikeWorld, with code as follows:\n\n```dart\n      onReleased: () {\n        if (action == Action.haveFun) {\n          // Shortcut to the \"win\" sequence, for Tutorial purposes only.\n          letsCelebrate();\n        } else {\n          // Restart with a new deal or the same deal as before.\n          game.action = action;\n          game.world = KlondikeWorld();\n        }\n      },\n```\n\nThe `letsCelebrate()` method is normally invoked only when the player wins. The functions of the\nother three buttons are to set the `Action` value in KlondikeGame and to set `world` in `FlameGame`\nto refer to a new KlondikeWorld, thus replacing the current one and leaving the former\nKlondikeWorld's storage to be disposed of by Garbage Collect. `FlameGame` will continue on to\ntrigger KlondikeWorld's `onLoad()` method.\n\nThe `letsCelebrate()` method ends with similar code, but forces a new deal:\n\n```dart\n              game.action = Action.newDeal;\n              game.world = KlondikeWorld();\n```\n\n\n## The `Have fun` button\n\nWhen you win the Klondike Game, the `letsCelebrate()` method puts on a little display. To save you\nhaving to play and win a whole game before you see it (**and** to test the method), we have\nprovided the `Have fun` button. Of course a real game could not have such a button...\n\nWell, this is it! The game is now more playable.\n\nWe could do more, but this game **is** a Tutorial above all else. Press the buttons below to see\nwhat the final code looks like, or to play it live.\n\nBut it is also time to have a look at the Ember Tutorial!\n\n```{flutter-app}\n:sources: ../tutorials/klondike/app\n:page: step5\n:show: popup code\n```\n"
  },
  {
    "path": "doc/tutorials/platformer/app/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    always_use_package_imports: false\n    prefer_relative_imports: true\n    "
  },
  {
    "path": "doc/tutorials/platformer/app/lib/actors/ember.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flutter/services.dart';\n\nimport '../ember_quest.dart';\nimport '../objects/ground_block.dart';\nimport '../objects/platform_block.dart';\nimport '../objects/star.dart';\nimport 'water_enemy.dart';\n\nclass EmberPlayer extends SpriteAnimationComponent\n    with KeyboardHandler, CollisionCallbacks, HasGameReference<EmberQuestGame> {\n  EmberPlayer({\n    required super.position,\n  }) : super(size: Vector2.all(64), anchor: Anchor.center);\n\n  final Vector2 velocity = Vector2.zero();\n  final Vector2 fromAbove = Vector2(0, -1);\n  final double gravity = 15;\n  final double jumpSpeed = 600;\n  final double moveSpeed = 200;\n  final double terminalVelocity = 150;\n  int horizontalDirection = 0;\n\n  bool hasJumped = false;\n  bool isOnGround = false;\n  bool hitByEnemy = false;\n\n  @override\n  Future<void> onLoad() async {\n    animation = SpriteAnimation.fromFrameData(\n      game.images.fromCache('ember.png'),\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        textureSize: Vector2.all(16),\n        stepTime: 0.12,\n      ),\n    );\n\n    add(\n      CircleHitbox(),\n    );\n  }\n\n  @override\n  bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {\n    horizontalDirection = 0;\n    horizontalDirection +=\n        (keysPressed.contains(LogicalKeyboardKey.keyA) ||\n            keysPressed.contains(LogicalKeyboardKey.arrowLeft))\n        ? -1\n        : 0;\n    horizontalDirection +=\n        (keysPressed.contains(LogicalKeyboardKey.keyD) ||\n            keysPressed.contains(LogicalKeyboardKey.arrowRight))\n        ? 1\n        : 0;\n\n    hasJumped = keysPressed.contains(LogicalKeyboardKey.space);\n    return true;\n  }\n\n  @override\n  void update(double dt) {\n    velocity.x = horizontalDirection * moveSpeed;\n    game.objectSpeed = 0;\n    // Prevent ember from going backwards at screen edge.\n    if (position.x - 36 <= 0 && horizontalDirection < 0) {\n      velocity.x = 0;\n    }\n    // Prevent ember from going beyond half screen.\n    if (position.x + 64 >= game.size.x / 2 && horizontalDirection > 0) {\n      velocity.x = 0;\n      game.objectSpeed = -moveSpeed;\n    }\n\n    // Apply basic gravity.\n    velocity.y += gravity;\n\n    // Determine if ember has jumped.\n    if (hasJumped) {\n      if (isOnGround) {\n        velocity.y = -jumpSpeed;\n        isOnGround = false;\n      }\n      hasJumped = false;\n    }\n\n    // Prevent ember from jumping to crazy fast.\n    velocity.y = velocity.y.clamp(-jumpSpeed, terminalVelocity);\n\n    // Adjust ember position.\n    position += velocity * dt;\n\n    // If ember fell in pit, then game over.\n    if (position.y > game.size.y + size.y) {\n      game.health = 0;\n    }\n\n    if (game.health <= 0) {\n      removeFromParent();\n    }\n\n    // Flip ember if needed.\n    if (horizontalDirection < 0 && scale.x > 0) {\n      flipHorizontally();\n    } else if (horizontalDirection > 0 && scale.x < 0) {\n      flipHorizontally();\n    }\n    super.update(dt);\n  }\n\n  @override\n  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {\n    if (other is GroundBlock || other is PlatformBlock) {\n      if (intersectionPoints.length == 2) {\n        // Calculate the collision normal and separation distance.\n        final mid =\n            (intersectionPoints.elementAt(0) +\n                intersectionPoints.elementAt(1)) /\n            2;\n\n        final collisionNormal = absoluteCenter - mid;\n        final separationDistance = (size.x / 2) - collisionNormal.length;\n        collisionNormal.normalize();\n\n        // If collision normal is almost upwards,\n        // ember must be on ground.\n        if (fromAbove.dot(collisionNormal) > 0.9) {\n          isOnGround = true;\n        }\n\n        // Resolve collision by moving ember along\n        // collision normal by separation distance.\n        position += collisionNormal.scaled(separationDistance);\n      }\n    }\n\n    if (other is Star) {\n      other.removeFromParent();\n      game.starsCollected++;\n    }\n\n    if (other is WaterEnemy) {\n      hit();\n    }\n    super.onCollision(intersectionPoints, other);\n  }\n\n  // This method runs an opacity effect on ember\n  // to make it blink.\n  void hit() {\n    if (!hitByEnemy) {\n      game.health--;\n      hitByEnemy = true;\n    }\n    add(\n      OpacityEffect.fadeOut(\n          EffectController(\n            alternate: true,\n            duration: 0.1,\n            repeatCount: 5,\n          ),\n        )\n        ..onComplete = () {\n          hitByEnemy = false;\n        },\n    );\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/platformer/app/lib/actors/water_enemy.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\n\nimport '../ember_quest.dart';\n\nclass WaterEnemy extends SpriteAnimationComponent\n    with HasGameReference<EmberQuestGame> {\n  final Vector2 gridPosition;\n  double xOffset;\n\n  final Vector2 velocity = Vector2.zero();\n\n  WaterEnemy({\n    required this.gridPosition,\n    required this.xOffset,\n  }) : super(size: Vector2.all(64), anchor: Anchor.bottomLeft);\n\n  @override\n  Future<void> onLoad() async {\n    animation = SpriteAnimation.fromFrameData(\n      game.images.fromCache('water_enemy.png'),\n      SpriteAnimationData.sequenced(\n        amount: 2,\n        textureSize: Vector2.all(16),\n        stepTime: 0.7,\n      ),\n    );\n    position = Vector2(\n      (gridPosition.x * size.x) + xOffset,\n      game.size.y - (gridPosition.y * size.y),\n    );\n    add(RectangleHitbox(collisionType: CollisionType.passive));\n    add(\n      MoveEffect.by(\n        Vector2(-2 * size.x, 0),\n        EffectController(\n          duration: 3,\n          alternate: true,\n          infinite: true,\n        ),\n      ),\n    );\n  }\n\n  @override\n  void update(double dt) {\n    velocity.x = game.objectSpeed;\n    position += velocity * dt;\n    if (position.x < -size.x || game.health <= 0) {\n      removeFromParent();\n    }\n    super.update(dt);\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/platformer/app/lib/ember_quest.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nimport 'actors/ember.dart';\nimport 'actors/water_enemy.dart';\nimport 'managers/segment_manager.dart';\nimport 'objects/ground_block.dart';\nimport 'objects/platform_block.dart';\nimport 'objects/star.dart';\nimport 'overlays/hud.dart';\n\nclass EmberQuestGame extends FlameGame\n    with HasCollisionDetection, HasKeyboardHandlerComponents {\n  EmberQuestGame();\n\n  late EmberPlayer _ember;\n  late double lastBlockXPosition = 0.0;\n  late UniqueKey lastBlockKey;\n\n  int starsCollected = 0;\n  int health = 3;\n  double cloudSpeed = 0.0;\n  double objectSpeed = 0.0;\n\n  @override\n  Future<void> onLoad() async {\n    //debugMode = true; // Uncomment to see the bounding boxes\n    await images.loadAll([\n      'block.png',\n      'ember.png',\n      'ground.png',\n      'heart_half.png',\n      'heart.png',\n      'star.png',\n      'water_enemy.png',\n    ]);\n    camera.viewfinder.anchor = Anchor.topLeft;\n\n    initializeGame(loadHud: true);\n  }\n\n  @override\n  void update(double dt) {\n    if (health <= 0) {\n      overlays.add('GameOver');\n    }\n    super.update(dt);\n  }\n\n  @override\n  Color backgroundColor() {\n    return const Color.fromARGB(255, 173, 223, 247);\n  }\n\n  void loadGameSegments(int segmentIndex, double xPositionOffset) {\n    for (final block in segments[segmentIndex]) {\n      final component = switch (block.blockType) {\n        const (GroundBlock) => GroundBlock(\n          gridPosition: block.gridPosition,\n          xOffset: xPositionOffset,\n        ),\n        const (PlatformBlock) => PlatformBlock(\n          gridPosition: block.gridPosition,\n          xOffset: xPositionOffset,\n        ),\n        const (Star) => Star(\n          gridPosition: block.gridPosition,\n          xOffset: xPositionOffset,\n        ),\n        const (WaterEnemy) => WaterEnemy(\n          gridPosition: block.gridPosition,\n          xOffset: xPositionOffset,\n        ),\n        _ => throw UnimplementedError(),\n      };\n      world.add(component);\n    }\n  }\n\n  void initializeGame({required bool loadHud}) {\n    // Assume that size.x < 3200\n    final segmentsToLoad = (size.x / 640).ceil();\n    segmentsToLoad.clamp(0, segments.length);\n\n    for (var i = 0; i <= segmentsToLoad; i++) {\n      loadGameSegments(i, (640 * i).toDouble());\n    }\n\n    _ember = EmberPlayer(\n      position: Vector2(128, canvasSize.y - 128),\n    );\n    world.add(_ember);\n    if (loadHud) {\n      camera.viewport.add(Hud());\n    }\n  }\n\n  void reset() {\n    starsCollected = 0;\n    health = 3;\n    initializeGame(loadHud: false);\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/platformer/app/lib/main.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nimport 'ember_quest.dart';\nimport 'overlays/game_over.dart';\nimport 'overlays/main_menu.dart';\n\nvoid main() {\n  runApp(\n    GameWidget<EmberQuestGame>.controlled(\n      gameFactory: EmberQuestGame.new,\n      overlayBuilderMap: {\n        'MainMenu': (_, game) => MainMenu(game: game),\n        'GameOver': (_, game) => GameOver(game: game),\n      },\n      initialActiveOverlays: const ['MainMenu'],\n    ),\n  );\n}\n"
  },
  {
    "path": "doc/tutorials/platformer/app/lib/managers/segment_manager.dart",
    "content": "import 'package:flame/components.dart';\n\nimport '../actors/water_enemy.dart';\nimport '../objects/ground_block.dart';\nimport '../objects/platform_block.dart';\nimport '../objects/star.dart';\n\nclass Block {\n  // gridPosition position is always segment based X,Y.\n  // 0,0 is the bottom left corner.\n  // 10,10 is the upper right corner.\n  final Vector2 gridPosition;\n  final Type blockType;\n  Block(this.gridPosition, this.blockType);\n}\n\nfinal segments = [\n  segment0,\n  segment1,\n  segment2,\n  segment3,\n  segment4,\n];\n\nfinal segment0 = [\n  Block(Vector2(0, 0), GroundBlock),\n  Block(Vector2(1, 0), GroundBlock),\n  Block(Vector2(2, 0), GroundBlock),\n  Block(Vector2(3, 0), GroundBlock),\n  Block(Vector2(4, 0), GroundBlock),\n  Block(Vector2(5, 0), GroundBlock),\n  Block(Vector2(5, 1), WaterEnemy),\n  Block(Vector2(5, 3), PlatformBlock),\n  Block(Vector2(6, 0), GroundBlock),\n  Block(Vector2(6, 3), PlatformBlock),\n  Block(Vector2(7, 0), GroundBlock),\n  Block(Vector2(7, 3), PlatformBlock),\n  Block(Vector2(8, 0), GroundBlock),\n  Block(Vector2(8, 3), PlatformBlock),\n  Block(Vector2(9, 0), GroundBlock),\n];\n\nfinal segment1 = [\n  Block(Vector2(0, 0), GroundBlock),\n  Block(Vector2(1, 0), GroundBlock),\n  Block(Vector2(1, 1), PlatformBlock),\n  Block(Vector2(1, 2), PlatformBlock),\n  Block(Vector2(1, 3), PlatformBlock),\n  Block(Vector2(2, 6), PlatformBlock),\n  Block(Vector2(3, 6), PlatformBlock),\n  Block(Vector2(6, 5), PlatformBlock),\n  Block(Vector2(7, 5), PlatformBlock),\n  Block(Vector2(7, 7), Star),\n  Block(Vector2(8, 0), GroundBlock),\n  Block(Vector2(8, 1), PlatformBlock),\n  Block(Vector2(8, 5), PlatformBlock),\n  Block(Vector2(8, 6), WaterEnemy),\n  Block(Vector2(9, 0), GroundBlock),\n];\n\nfinal segment2 = [\n  Block(Vector2(0, 0), GroundBlock),\n  Block(Vector2(1, 0), GroundBlock),\n  Block(Vector2(2, 0), GroundBlock),\n  Block(Vector2(3, 0), GroundBlock),\n  Block(Vector2(3, 3), PlatformBlock),\n  Block(Vector2(4, 0), GroundBlock),\n  Block(Vector2(4, 3), PlatformBlock),\n  Block(Vector2(5, 0), GroundBlock),\n  Block(Vector2(5, 3), PlatformBlock),\n  Block(Vector2(5, 4), WaterEnemy),\n  Block(Vector2(6, 0), GroundBlock),\n  Block(Vector2(6, 3), PlatformBlock),\n  Block(Vector2(6, 4), PlatformBlock),\n  Block(Vector2(6, 5), PlatformBlock),\n  Block(Vector2(6, 7), Star),\n  Block(Vector2(7, 0), GroundBlock),\n  Block(Vector2(8, 0), GroundBlock),\n  Block(Vector2(9, 0), GroundBlock),\n];\n\nfinal segment3 = [\n  Block(Vector2(0, 0), GroundBlock),\n  Block(Vector2(1, 0), GroundBlock),\n  Block(Vector2(1, 1), WaterEnemy),\n  Block(Vector2(2, 0), GroundBlock),\n  Block(Vector2(2, 1), PlatformBlock),\n  Block(Vector2(2, 2), PlatformBlock),\n  Block(Vector2(4, 4), PlatformBlock),\n  Block(Vector2(6, 6), PlatformBlock),\n  Block(Vector2(7, 0), GroundBlock),\n  Block(Vector2(7, 1), PlatformBlock),\n  Block(Vector2(8, 0), GroundBlock),\n  Block(Vector2(8, 8), Star),\n  Block(Vector2(9, 0), GroundBlock),\n];\n\nfinal segment4 = [\n  Block(Vector2(0, 0), GroundBlock),\n  Block(Vector2(1, 0), GroundBlock),\n  Block(Vector2(2, 0), GroundBlock),\n  Block(Vector2(2, 3), PlatformBlock),\n  Block(Vector2(3, 0), GroundBlock),\n  Block(Vector2(3, 1), WaterEnemy),\n  Block(Vector2(3, 3), PlatformBlock),\n  Block(Vector2(4, 0), GroundBlock),\n  Block(Vector2(5, 0), GroundBlock),\n  Block(Vector2(5, 5), PlatformBlock),\n  Block(Vector2(6, 0), GroundBlock),\n  Block(Vector2(6, 5), PlatformBlock),\n  Block(Vector2(6, 7), Star),\n  Block(Vector2(7, 0), GroundBlock),\n  Block(Vector2(8, 0), GroundBlock),\n  Block(Vector2(8, 3), PlatformBlock),\n  Block(Vector2(9, 0), GroundBlock),\n  Block(Vector2(9, 1), WaterEnemy),\n  Block(Vector2(9, 3), PlatformBlock),\n];\n"
  },
  {
    "path": "doc/tutorials/platformer/app/lib/objects/ground_block.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flutter/material.dart';\n\nimport '../ember_quest.dart';\nimport '../managers/segment_manager.dart';\n\nclass GroundBlock extends SpriteComponent\n    with HasGameReference<EmberQuestGame> {\n  final Vector2 gridPosition;\n  double xOffset;\n\n  final UniqueKey _blockKey = UniqueKey();\n  final Vector2 velocity = Vector2.zero();\n\n  GroundBlock({\n    required this.gridPosition,\n    required this.xOffset,\n  }) : super(size: Vector2.all(64), anchor: Anchor.bottomLeft);\n\n  @override\n  Future<void> onLoad() async {\n    final groundImage = game.images.fromCache('ground.png');\n    sprite = Sprite(groundImage);\n    position = Vector2(\n      (gridPosition.x * size.x) + xOffset,\n      game.size.y - (gridPosition.y * size.y),\n    );\n    add(RectangleHitbox(collisionType: CollisionType.passive));\n    if (gridPosition.x == 9 && position.x > game.lastBlockXPosition) {\n      game.lastBlockKey = _blockKey;\n      game.lastBlockXPosition = position.x + size.x;\n    }\n  }\n\n  @override\n  void update(double dt) {\n    velocity.x = game.objectSpeed;\n    position += velocity * dt;\n\n    if (position.x < -size.x) {\n      removeFromParent();\n      if (gridPosition.x == 0) {\n        game.loadGameSegments(\n          Random().nextInt(segments.length),\n          game.lastBlockXPosition,\n        );\n      }\n    }\n    if (gridPosition.x == 9) {\n      if (game.lastBlockKey == _blockKey) {\n        game.lastBlockXPosition = position.x + size.x - 10;\n      }\n    }\n    if (game.health <= 0) {\n      removeFromParent();\n    }\n\n    super.update(dt);\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/platformer/app/lib/objects/platform_block.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\n\nimport '../ember_quest.dart';\n\nclass PlatformBlock extends SpriteComponent\n    with HasGameReference<EmberQuestGame> {\n  final Vector2 gridPosition;\n  double xOffset;\n\n  final Vector2 velocity = Vector2.zero();\n\n  PlatformBlock({\n    required this.gridPosition,\n    required this.xOffset,\n  }) : super(size: Vector2.all(64), anchor: Anchor.bottomLeft);\n\n  @override\n  Future<void> onLoad() async {\n    final platformImage = game.images.fromCache('block.png');\n    sprite = Sprite(platformImage);\n    position = Vector2(\n      (gridPosition.x * size.x) + xOffset,\n      game.size.y - (gridPosition.y * size.y),\n    );\n    add(RectangleHitbox(collisionType: CollisionType.passive));\n  }\n\n  @override\n  void update(double dt) {\n    velocity.x = game.objectSpeed;\n    position += velocity * dt;\n    if (position.x < -size.x || game.health <= 0) {\n      removeFromParent();\n    }\n    super.update(dt);\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/platformer/app/lib/objects/star.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flutter/material.dart';\n\nimport '../ember_quest.dart';\n\nclass Star extends SpriteComponent with HasGameReference<EmberQuestGame> {\n  final Vector2 gridPosition;\n  double xOffset;\n\n  final Vector2 velocity = Vector2.zero();\n\n  Star({\n    required this.gridPosition,\n    required this.xOffset,\n  }) : super(size: Vector2.all(64), anchor: Anchor.center);\n\n  @override\n  Future<void> onLoad() async {\n    final starImage = game.images.fromCache('star.png');\n    sprite = Sprite(starImage);\n    position = Vector2(\n      (gridPosition.x * size.x) + xOffset + (size.x / 2),\n      game.size.y - (gridPosition.y * size.y) - (size.y / 2),\n    );\n    add(RectangleHitbox(collisionType: CollisionType.passive));\n    add(\n      SizeEffect.by(\n        Vector2.all(-24),\n        EffectController(\n          duration: 0.75,\n          reverseDuration: 0.5,\n          infinite: true,\n          curve: Curves.easeOut,\n        ),\n      ),\n    );\n  }\n\n  @override\n  void update(double dt) {\n    velocity.x = game.objectSpeed;\n    position += velocity * dt;\n    if (position.x < -size.x || game.health <= 0) {\n      removeFromParent();\n    }\n    super.update(dt);\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/platformer/app/lib/overlays/game_over.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport '../ember_quest.dart';\n\nclass GameOver extends StatelessWidget {\n  // Reference to parent game.\n  final EmberQuestGame game;\n  const GameOver({required this.game, super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    const blackTextColor = Color.fromRGBO(0, 0, 0, 1.0);\n    const whiteTextColor = Color.fromRGBO(255, 255, 255, 1.0);\n\n    return Material(\n      color: Colors.transparent,\n      child: Center(\n        child: Container(\n          padding: const EdgeInsets.all(10.0),\n          height: 200,\n          width: 300,\n          decoration: const BoxDecoration(\n            color: blackTextColor,\n            borderRadius: BorderRadius.all(\n              Radius.circular(20),\n            ),\n          ),\n          child: Column(\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              const Text(\n                'Game Over',\n                style: TextStyle(\n                  color: whiteTextColor,\n                  fontSize: 24,\n                ),\n              ),\n              const SizedBox(height: 40),\n              SizedBox(\n                width: 200,\n                height: 75,\n                child: ElevatedButton(\n                  onPressed: () {\n                    game.reset();\n                    game.overlays.remove('GameOver');\n                  },\n                  style: ElevatedButton.styleFrom(\n                    backgroundColor: whiteTextColor,\n                  ),\n                  child: const Text(\n                    'Play Again',\n                    style: TextStyle(\n                      fontSize: 28.0,\n                      color: blackTextColor,\n                    ),\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/platformer/app/lib/overlays/heart.dart",
    "content": "import 'package:flame/components.dart';\n\nimport '../ember_quest.dart';\n\nenum HeartState {\n  available,\n  unavailable,\n}\n\nclass HeartHealthComponent extends SpriteGroupComponent<HeartState>\n    with HasGameReference<EmberQuestGame> {\n  final int heartNumber;\n\n  HeartHealthComponent({\n    required this.heartNumber,\n    required super.position,\n    required super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.priority,\n  });\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    final availableSprite = await game.loadSprite(\n      'heart.png',\n      srcSize: Vector2.all(32),\n    );\n\n    final unavailableSprite = await game.loadSprite(\n      'heart_half.png',\n      srcSize: Vector2.all(32),\n    );\n\n    sprites = {\n      HeartState.available: availableSprite,\n      HeartState.unavailable: unavailableSprite,\n    };\n\n    current = HeartState.available;\n  }\n\n  @override\n  void update(double dt) {\n    if (game.health < heartNumber) {\n      current = HeartState.unavailable;\n    } else {\n      current = HeartState.available;\n    }\n    super.update(dt);\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/platformer/app/lib/overlays/hud.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flutter/material.dart';\n\nimport '../ember_quest.dart';\nimport 'heart.dart';\n\nclass Hud extends PositionComponent with HasGameReference<EmberQuestGame> {\n  Hud({\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority = 5,\n  });\n\n  late TextComponent _scoreTextComponent;\n\n  @override\n  Future<void>? onLoad() async {\n    _scoreTextComponent = TextComponent(\n      text: '${game.starsCollected}',\n      textRenderer: TextPaint(\n        style: const TextStyle(\n          fontSize: 32,\n          color: Color.fromRGBO(10, 10, 10, 1),\n        ),\n      ),\n      anchor: Anchor.center,\n      position: Vector2(game.size.x - 60, 20),\n    );\n    add(_scoreTextComponent);\n\n    final starSprite = await game.loadSprite('star.png');\n    add(\n      SpriteComponent(\n        sprite: starSprite,\n        position: Vector2(game.size.x - 100, 20),\n        size: Vector2.all(32),\n        anchor: Anchor.center,\n      ),\n    );\n\n    for (var i = 1; i <= game.health; i++) {\n      final positionX = 40 * i;\n      await add(\n        HeartHealthComponent(\n          heartNumber: i,\n          position: Vector2(positionX.toDouble(), 20),\n          size: Vector2.all(32),\n        ),\n      );\n    }\n\n    return super.onLoad();\n  }\n\n  @override\n  void update(double dt) {\n    _scoreTextComponent.text = '${game.starsCollected}';\n    super.update(dt);\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/platformer/app/lib/overlays/main_menu.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport '../ember_quest.dart';\n\nclass MainMenu extends StatelessWidget {\n  // Reference to parent game.\n  final EmberQuestGame game;\n\n  const MainMenu({required this.game, super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    const blackTextColor = Color.fromRGBO(0, 0, 0, 1.0);\n    const whiteTextColor = Color.fromRGBO(255, 255, 255, 1.0);\n\n    return Material(\n      color: Colors.transparent,\n      child: Center(\n        child: Container(\n          padding: const EdgeInsets.all(10.0),\n          height: 300,\n          width: 300,\n          decoration: const BoxDecoration(\n            color: blackTextColor,\n            borderRadius: BorderRadius.all(\n              Radius.circular(20),\n            ),\n          ),\n          child: Column(\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              const Text(\n                'Ember Quest',\n                style: TextStyle(\n                  color: whiteTextColor,\n                  fontSize: 24,\n                ),\n              ),\n              const SizedBox(height: 40),\n              SizedBox(\n                width: 200,\n                height: 75,\n                child: ElevatedButton(\n                  onPressed: () {\n                    game.overlays.remove('MainMenu');\n                  },\n                  style: ElevatedButton.styleFrom(\n                    backgroundColor: whiteTextColor,\n                  ),\n                  child: const Text(\n                    'Play',\n                    style: TextStyle(\n                      fontSize: 40.0,\n                      color: blackTextColor,\n                    ),\n                  ),\n                ),\n              ),\n              const SizedBox(height: 20),\n              const Text(\n                '''Use WASD or Arrow Keys for movement.\nSpace bar to jump.\nCollect as many stars as you can and avoid enemies!''',\n                textAlign: TextAlign.center,\n                style: TextStyle(\n                  color: whiteTextColor,\n                  fontSize: 14,\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/platformer/app/pubspec.yaml",
    "content": "name: ember_quest\nresolution: workspace\ndescription: A tutorial on how to create a basic side scrolling platformer using the Flame game engine for Flutter.\npublish_to: 'none' # Remove this line if you wish to publish to pub.dev\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n\nflutter:\n  uses-material-design: true\n  assets:\n    - assets/images/\n"
  },
  {
    "path": "doc/tutorials/platformer/app/web/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <!--\n    If you are serving your web app in a path other than the root, change the\n    href value below to reflect the base path you are serving from.\n\n    The path provided below has to start and end with a slash \"/\" in order for\n    it to work correctly.\n\n    For more details:\n    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base\n\n    This is a placeholder for base href that will be replaced by the value of\n    the `--base-href` argument provided to `flutter build`.\n  -->\n  <base href=\"$FLUTTER_BASE_HREF\">\n\n  <meta charset=\"UTF-8\">\n  <meta content=\"IE=Edge\" http-equiv=\"X-UA-Compatible\">\n  <meta name=\"description\" content=\"A new Flutter project.\">\n\n  <!-- iOS meta tags & icons -->\n  <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n  <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\">\n  <meta name=\"apple-mobile-web-app-title\" content=\"app\">\n  <link rel=\"apple-touch-icon\" href=\"icons/Icon-192.png\">\n\n  <!-- Favicon -->\n  <link rel=\"icon\" type=\"image/png\" href=\"favicon.png\"/>\n\n  <title>app</title>\n  <link rel=\"manifest\" href=\"manifest.json\">\n\n  <script>\n    // The value below is injected by flutter build, do not touch.\n    var serviceWorkerVersion = null;\n  </script>\n  <!-- This script adds the flutter initialization JS code -->\n  <script src=\"flutter.js\" defer></script>\n</head>\n<body>\n  <script>\n    window.addEventListener('load', function(ev) {\n      // Download main.dart.js\n      _flutter.loader.loadEntrypoint({\n        serviceWorker: {\n          serviceWorkerVersion: serviceWorkerVersion,\n        }\n      }).then(function(engineInitializer) {\n        return engineInitializer.initializeEngine();\n      }).then(function(appRunner) {\n        return appRunner.runApp();\n      });\n    });\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "doc/tutorials/platformer/platformer.md",
    "content": "# Ember Quest Game Tutorial\n\nIn this tutorial, we will follow a step-by-step process for coding a game using the Flame\nengine.\n\nThis tutorial assumes that you have at least some familiarity with common programming concepts, and\nwith the [Dart] programming language.\n\n\n[Dart]: https://dart.dev/overview\n\n```{toctree}\n:hidden:\n\n1. Preparation  <step_1.md>\n2. Start Coding  <step_2.md>\n3. Building the World  <step_3.md>\n4. Adding the Remaining Components  <step_4.md>\n5. Controlling Movement  <step_5.md>\n6. Adding the HUD  <step_6.md>\n7. Adding Menus  <step_7.md>\n```\n"
  },
  {
    "path": "doc/tutorials/platformer/step_1.md",
    "content": "# 1. Preparation\n\nBefore you begin any kind of game project, you need an idea of what you want to make and I like to\nthen give it a **name**. For this tutorial and game, Ember will be on a quest to gather as many\n(GitHub) stars as possible and I will call the game, `Ember Quest`.\n\nNow it is time to get started, but first you need to go to the [bare flame game\ntutorial](../bare_flame_game.md) and complete the necessary setup steps. When you come back, you\nshould already have the `main.dart` file with the following content:\n\n```dart\nimport 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart';\n\nvoid main() {\n  final game = FlameGame();\n  runApp(GameWidget(game: game));\n}\n```\n\n\n## Planning\n\nLike in the [klondike](../klondike/klondike.md) tutorial, starting a new game can feel overwhelming.\nI like to first decide what platform I am trying to target. Will this be a mobile game, a desktop\ngame, or maybe a web game, with Flutter and Flame, these are all possible. For this game though, I\nam going to focus on a web game. This means my users will interact with the game using their\nkeyboards.\n\nStarting with a simple sketch (it doesn't have to be perfect as mine is very rough) is\nthe best way to get an understanding of what will need to be accomplished. For the sketch below,\nwe know we will need the following:\n\n- Player Class\n- Enemy Class\n- Star Class\n- Platform Class\n- Ground Class\n- HUD Class (health and stars collected)\n\n![Sketch of Ember Quest](../../images/tutorials/platformer/ember_quest_sketch.png)\n\nAll of these will be brought together in `EmberQuestGame` derived from `FlameGame`.\n\n\n## Assets\n\nEvery game needs assets. Assets are images, sprites, animations, sounds, etc. Now, I am not an\nartist, but because I am basing this game on Ember, the flame mascot, and Ember is already designed,\nit sets the tone that this will be a pixel art game. There are numerous sites available that\nprovide free pixel art that can be used in games, but please check and comply with the licensing and\nalways provide valid creator attribution. For this game though, I am going to take a chance and\nmake my artwork using an online pixel art tool. If you decide to use this tool, multiple online\ntutorials will assist you with the basic operations as well as exporting the assets. Now normally,\nmost games will utilize sprite sheets. These combine many images into one larger image that can be\nsectioned and used as individual images. For this tutorial though, I specifically will save the\nimages individually as I want to demonstrate the Flame engine's caching abilities. Ember and the\nwater enemy are sprite sheets though as they contain multiple images to create animations.\n\nRight-click the images below, choose \"Save as...\", and store them in the `assets/images` folder of the\nproject. At this point our project's structure looks like this:\n\n```text\nemberquest/\n ├─assets/\n │  └─images/\n │     ├─block.png\n │     ├─ember.png\n │     ├─ground.png\n │     ├─heart_half.png\n │     ├─heart.png\n │     ├─star.png\n │     └─water_enemy.png\n ├─lib/\n │  └─main.dart\n └─pubspec.yaml\n```\n\n![Platform Block](app/assets/images/block.png)\n![Ember Animation](app/assets/images/ember.png)\n![Ground Block](app/assets/images/ground.png)\n![HUD Heart Half Opacity](app/assets/images/heart_half.png)\n![HUD Heart Full Opacity](app/assets/images/heart.png)\n![Star](app/assets/images/star.png)\n![Water Enemy Animation](app/assets/images/water_enemy.png)\n\n```{note}\nYou may ask, why are the images different sizes?\n\nAs I was using the online tool to make the assets, I had trouble getting the\ndetail I desired for the game in a 16x16 block. The heart worked out in 32x32 \nand the ground as well as the star were 64x64. Regardless, the asset size does\nnot matter for the game as we will resize as needed.\n```\n\nAlso, you need to tell Flutter about these images (just having them inside the `assets` folder is\nnot enough). To do this, let's add the following lines into the `pubspec.yaml` file:\n\n```yaml\nflutter:\n  assets:\n    - assets/images/\n```\n\nAlright, enough with preparing -- onward to coding!\n"
  },
  {
    "path": "doc/tutorials/platformer/step_2.md",
    "content": "# 2. Start Coding\n\n\n## The Plan\n\nNow that we have the assets loaded and a very rough idea of what classes we will need, we need to\nstart thinking about how we will implement this game and our goals. To do this, let's break down\nwhat the game should do:\n\n- Ember should be able to be controlled to move left, right, and jump.\n- The level will be infinite, so we need a way to randomly load sections of the level.\n- The objective is to collect stars while avoiding enemies.\n- Enemies cannot be killed as you need to use the platforms to avoid them.\n- If Ember is hit by an enemy, it should reduce Ember's health by 1.\n- Ember should have 3 lives to lose.\n- There should be pits that if Ember falls into, it is automatically game over.\n- There should be a main menu and a game-over screen that lets the player start over.\n\nNow that this is planned out, I know you are probably as excited as I am to begin and I just want to\nsee Ember on the screen. So let's do that first.\n\n```{note}\nWhy did I choose to make this game an infinite side scrolling platformer?\n\nWell, I wanted to be able to showcase random level loading. No two game plays\nwill be the same. This exact setup can easily be adapted to be a traditional \nlevel game. As you make your way through this tutorial, you will see how we \ncould modify the level code to have an end. I will add a note in that section\nto explain the appropriate mechanics.\n```\n\n\n## Loading Assets\n\nFor Ember to be displayed, we will need to load the assets. This can be done in `main.dart`, but by\nso doing, we will quickly clutter the file. To keep our game organized, we should create files that\nhave a single focus. So let's create a file in the `lib` folder called `ember_quest.dart`. In that\nfile, we will add:\n\n```dart\nimport 'package:flame/game.dart';\n\nclass EmberQuestGame extends FlameGame {\n  EmberQuestGame();\n\n  @override\n  Future<void> onLoad() async {\n    await images.loadAll([\n      'block.png',\n      'ember.png',\n      'ground.png',\n      'heart_half.png',\n      'heart.png',\n      'star.png',\n      'water_enemy.png',\n    ]);\n\n  }\n}\n```\n\nAs I mentioned in the [assets](step_1.md#assets) section, we are using multiple individual image\nfiles and for performance reasons, we should leverage Flame's built-in caching system which will\nonly load the files once, but allow us to access them as many times as needed without an impact to\nthe game. `await images.loadAll()` takes a list of the file names that are found in `assets/images`\nand loads them to cache.\n\n\n## Scaffolding\n\nSo now that we have our game file, let's prepare the `main.dart` file to receive our newly created\n`FlameGame`. Change your entire `main.dart` file to the following:\n\n```dart\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nimport 'ember_quest.dart';\n\nvoid main() {\n  runApp(\n    const GameWidget<EmberQuestGame>.controlled(\n      gameFactory: EmberQuestGame.new,\n    ),\n  );\n}\n```\n\nYou can run this file and you should just have a blank screen now. Let's get Ember loaded!\n\n\n## CameraComponent and World\n\nTo move around in the world we are going the use the built-in `CameraComponent` and `World` that\nexists on the `FlameGame` class.\nWe are going to add all our components to the `world` and follow the player with the `camera`.\n\n```dart\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\n\nclass EmberQuestGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    await images.loadAll([\n      'block.png',\n      'ember.png',\n      'ground.png',\n      'heart_half.png',\n      'heart.png',\n      'star.png',\n      'water_enemy.png',\n    ]);\n\n    // Everything in this tutorial assumes that the position\n    // of the `CameraComponent`s viewfinder (where the camera is looking)\n    // is in the top left corner, that's why we set the anchor here.\n    camera.viewfinder.anchor = Anchor.topLeft;\n  }\n}\n```\n\n\n## Ember Time\n\nKeeping your game files organized can always be a challenge. I like to keep things logically\norganized by how they will be involved in my game. So for Ember, let's create the following folder,\n`lib/actors` and in that folder, create `ember.dart`. In that file, add the following code:\n\n```dart\nimport 'package:flame/components.dart';\n\nimport '../ember_quest.dart';\n\nclass EmberPlayer extends SpriteAnimationComponent\n    with HasGameReference<EmberQuestGame> {\n  EmberPlayer({\n    required super.position,\n  }) : super(size: Vector2.all(64), anchor: Anchor.center);\n\n  @override\n  void onLoad() {\n    animation = SpriteAnimation.fromFrameData(\n      game.images.fromCache('ember.png'),\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        textureSize: Vector2.all(16),\n        stepTime: 0.12,\n      ),\n    );\n  }\n}\n```\n\nThis file uses the `HasGameRef` mixin which allows us to reach back to `ember_quest.dart` and\nleverage any of the variables or methods that are defined in the game class. You can see this in\nuse with the line `game.images.fromCache('ember.png')`. Earlier, we loaded all the files into\ncache, so to use that file now, we call `fromCache` so it can be leveraged by the `SpriteAnimation`.\nThe `EmberPlayer` class is extending a `SpriteAnimationComponent` which allows us to define\nanimation as well as position it accordingly in our game world. When we construct this class, the\ndefault size of `Vector2.all(64)` is defined as the size of Ember in our game world should be 64x64.\nYou may notice that in the animation `SpriteAnimationData`, the `textureSize` is defined as\n`Vector2.all(16)` or 16x16. This is because the individual frame in our `ember.png` is 16x16 and\nthere are 4 frames in total. To define the speed of the animation, `stepTime` is used and set at\n`0.12` seconds per frame. You can change the `stepTime` to any length that makes the animation seem\ncorrect for your game vision.\n\nNow before you rush to run the game again, we have to add Ember to the game world. To do this, go\nback to `ember_quest.dart` and add the following:\n\n```dart\nimport 'package:flame/game.dart';\n\nimport 'actors/ember.dart';\n\nclass EmberQuestGame extends FlameGame {\n  late EmberPlayer _ember;\n  \n  @override\n  Future<void> onLoad() async {\n    await images.loadAll([\n      'block.png',\n      'ember.png',\n      'ground.png',\n      'heart_half.png',\n      'heart.png',\n      'star.png',\n      'water_enemy.png',\n    ]);\n\n    camera.viewfinder.anchor = Anchor.topLeft;\n    \n    _ember = EmberPlayer(\n      position: Vector2(128, canvasSize.y - 70),\n    );\n    world.add(_ember);\n  }\n}\n```\n\nRun your game now and you should now see Ember flickering in the lower left-hand corner.\n\n\n## Building Blocks\n\nNow that we have Ember showing on screen and we know our basic environment is all working correctly,\nit's time to create a world for Embers Quest! Proceed on to [](step_3.md)!\n"
  },
  {
    "path": "doc/tutorials/platformer/step_3.md",
    "content": "# 3. Building the World\n\n\n## Creating Segments\n\nFor this world to be infinite, the best way to approach this is to create segments that can be\nreloaded over and over. To do this, we need a rough sketch of what our level segments will look\nlike. I have created the following sketch to show what the segments would look like and how they can\nbe repeated:\n\n![Level Segment Sketch](../../images/tutorials/platformer/LevelSegmentSketch.jpg)\n\nEach segment is a 10x10 grid and each block is 64 pixels x 64 pixels. This means Ember Quest has a\nheight of 640 with an infinite width. In my design, there must always be a ground\nblock at the beginning and the end. Additionally, there must be at least 3 ground blocks that come\nbefore an enemy, including if the segment wraps to another segment. This is because the plan is to\nhave the enemies traverse back and forth for 3 blocks. Now that we have a plan for the segments,\nlet's create a segment manager class.\n\n\n### Segment Manager\n\nTo get started, we have to understand that we will be referencing our blocks in the segment manager,\nso first create a new folder called `lib/objects`. In that folder, create 3 files called\n`ground_block.dart`, `platform_block.dart`, and `star.dart`. Those files just need basic\nboilerplate code for the class, so create the following in their respective files:\n\n```dart\nclass GroundBlock {}\n\nclass PlatformBlock {}\n\nclass Star {}\n```\n\nAlso, create `water_enemy.dart` in the `lib/actors` folder using this boilerplate code:\n\n```dart\nclass WaterEnemy {}\n```\n\nNow we can create a file called `segment_manager.dart` which will be placed in a new folder called\n`lib/managers`. The segment manager is the heart and soul, if you will, of Ember Quest. This is\nwhere you can get as creative as you want. You do not have to follow my design, just remember that\nwhatever you design, the segment must follow the rules outlined above. Add the following code to\n`segment_manager.dart`:\n\n```dart\nclass Block {\n  // gridPosition position is always segment based X,Y.\n  // 0,0 is the bottom left corner.\n  // 10,10 is the upper right corner.\n  final Vector2 gridPosition;\n  final Type blockType;\n  Block(this.gridPosition, this.blockType);\n}\n\nfinal segments = [\n  segment0,\n];\n\nfinal segment0 = [\n\n];\n```\n\nSo what this does, is allows us to create segments (segment0, segment1, etc) in a list format that\ngets added to the `segments` list. The individual segments will be made up of multiple entries of the\n`Block` class. This information will allow us to translate the block position from a 10x10 grid to\nthe actual pixel position in the game world. To create a segment, you need to create\nentries for each block that you wish to be rendered from the sketch.\n\nTo understand each segment, if we start in the bottom left corner of the grid in the sketch, we see\nthat we should place a `Block()` in the `segment0` list with a first parameter `gridPosition` of a\n`Vector2(0,0)` and a `blockType` of the `GroundBlock` class that we created earlier. Remember, the\nvery bottom left cell is x=0 and y=0 thus the `Vector2(x,y)` is `Vector2(0,0)`.\n\n![Segment 0 Sketch](../../images/tutorials/platformer/Segment0Sketch.jpg)\n\nThe full segment would look like this:\n\n```dart\nfinal segment0 = [\n  Block(Vector2(0, 0), GroundBlock),\n  Block(Vector2(1, 0), GroundBlock),\n  Block(Vector2(2, 0), GroundBlock),\n  Block(Vector2(3, 0), GroundBlock),\n  Block(Vector2(4, 0), GroundBlock),\n  Block(Vector2(5, 0), GroundBlock),\n  Block(Vector2(5, 1), WaterEnemy),\n  Block(Vector2(5, 3), PlatformBlock),\n  Block(Vector2(6, 0), GroundBlock),\n  Block(Vector2(6, 3), PlatformBlock),\n  Block(Vector2(7, 0), GroundBlock),\n  Block(Vector2(7, 3), PlatformBlock),\n  Block(Vector2(8, 0), GroundBlock),\n  Block(Vector2(8, 3), PlatformBlock),\n  Block(Vector2(9, 0), GroundBlock),\n];\n```\n\nProceed to build the remaining segments. The full segment manager should look like this:\n\n```dart\nimport 'package:flame/components.dart';\n\nimport '../actors/water_enemy.dart';\nimport '../objects/ground_block.dart';\nimport '../objects/platform_block.dart';\nimport '../objects/star.dart';\n\nclass Block {\n  // gridPosition position is always segment based X,Y.\n  // 0,0 is the bottom left corner.\n  // 10,10 is the upper right corner.\n  final Vector2 gridPosition;\n  final Type blockType;\n  Block(this.gridPosition, this.blockType);\n}\n\nfinal segments = [\n  segment0,\n  segment1,\n  segment2,\n  segment3,\n  segment4,\n];\n\nfinal segment0 = [\n  Block(Vector2(0, 0), GroundBlock),\n  Block(Vector2(1, 0), GroundBlock),\n  Block(Vector2(2, 0), GroundBlock),\n  Block(Vector2(3, 0), GroundBlock),\n  Block(Vector2(4, 0), GroundBlock),\n  Block(Vector2(5, 0), GroundBlock),\n  Block(Vector2(5, 1), WaterEnemy),\n  Block(Vector2(5, 3), PlatformBlock),\n  Block(Vector2(6, 0), GroundBlock),\n  Block(Vector2(6, 3), PlatformBlock),\n  Block(Vector2(7, 0), GroundBlock),\n  Block(Vector2(7, 3), PlatformBlock),\n  Block(Vector2(8, 0), GroundBlock),\n  Block(Vector2(8, 3), PlatformBlock),\n  Block(Vector2(9, 0), GroundBlock),\n];\n\nfinal segment1 = [\n  Block(Vector2(0, 0), GroundBlock),\n  Block(Vector2(1, 0), GroundBlock),\n  Block(Vector2(1, 1), PlatformBlock),\n  Block(Vector2(1, 2), PlatformBlock),\n  Block(Vector2(1, 3), PlatformBlock),\n  Block(Vector2(2, 6), PlatformBlock),\n  Block(Vector2(3, 6), PlatformBlock),\n  Block(Vector2(6, 5), PlatformBlock),\n  Block(Vector2(7, 5), PlatformBlock),\n  Block(Vector2(7, 7), Star),\n  Block(Vector2(8, 0), GroundBlock),\n  Block(Vector2(8, 1), PlatformBlock),\n  Block(Vector2(8, 5), PlatformBlock),\n  Block(Vector2(8, 6), WaterEnemy),\n  Block(Vector2(9, 0), GroundBlock),\n];\n\nfinal segment2 = [\n  Block(Vector2(0, 0), GroundBlock),\n  Block(Vector2(1, 0), GroundBlock),\n  Block(Vector2(2, 0), GroundBlock),\n  Block(Vector2(3, 0), GroundBlock),\n  Block(Vector2(3, 3), PlatformBlock),\n  Block(Vector2(4, 0), GroundBlock),\n  Block(Vector2(4, 3), PlatformBlock),\n  Block(Vector2(5, 0), GroundBlock),\n  Block(Vector2(5, 3), PlatformBlock),\n  Block(Vector2(5, 4), WaterEnemy),\n  Block(Vector2(6, 0), GroundBlock),\n  Block(Vector2(6, 3), PlatformBlock),\n  Block(Vector2(6, 4), PlatformBlock),\n  Block(Vector2(6, 5), PlatformBlock),\n  Block(Vector2(6, 7), Star),\n  Block(Vector2(7, 0), GroundBlock),\n  Block(Vector2(8, 0), GroundBlock),\n  Block(Vector2(9, 0), GroundBlock),\n];\n\nfinal segment3 = [\n  Block(Vector2(0, 0), GroundBlock),\n  Block(Vector2(1, 0), GroundBlock),\n  Block(Vector2(1, 1), WaterEnemy),\n  Block(Vector2(2, 0), GroundBlock),\n  Block(Vector2(2, 1), PlatformBlock),\n  Block(Vector2(2, 2), PlatformBlock),\n  Block(Vector2(4, 4), PlatformBlock),\n  Block(Vector2(6, 6), PlatformBlock),\n  Block(Vector2(7, 0), GroundBlock),\n  Block(Vector2(7, 1), PlatformBlock),\n  Block(Vector2(8, 0), GroundBlock),\n  Block(Vector2(8, 8), Star),\n  Block(Vector2(9, 0), GroundBlock),\n];\n\nfinal segment4 = [\n  Block(Vector2(0, 0), GroundBlock),\n  Block(Vector2(1, 0), GroundBlock),\n  Block(Vector2(2, 0), GroundBlock),\n  Block(Vector2(2, 3), PlatformBlock),\n  Block(Vector2(3, 0), GroundBlock),\n  Block(Vector2(3, 1), WaterEnemy),\n  Block(Vector2(3, 3), PlatformBlock),\n  Block(Vector2(4, 0), GroundBlock),\n  Block(Vector2(5, 0), GroundBlock),\n  Block(Vector2(5, 5), PlatformBlock),\n  Block(Vector2(6, 0), GroundBlock),\n  Block(Vector2(6, 5), PlatformBlock),\n  Block(Vector2(6, 7), Star),\n  Block(Vector2(7, 0), GroundBlock),\n  Block(Vector2(8, 0), GroundBlock),\n  Block(Vector2(8, 3), PlatformBlock),\n  Block(Vector2(9, 0), GroundBlock),\n  Block(Vector2(9, 1), WaterEnemy),\n  Block(Vector2(9, 3), PlatformBlock),\n];\n```\n\n\n### Loading the Segments into the World\n\nNow that our segments are defined, we need to create a way to load these blocks into our world. To\ndo that, we are going to start work in the `ember_quest.dart` file. We will create a `loadSegments`\nmethod that when given an index for the segments list, will then loop through that segment from\nour `segment_manager` and we will add the appropriate blocks later. It should look like this:\n\n```dart\nvoid loadGameSegments(int segmentIndex, double xPositionOffset) {\n    for (final block in segments[segmentIndex]) {\n      switch (block.blockType) {\n        case GroundBlock:\n        case PlatformBlock:\n        case Star:\n        case WaterEnemy:\n      }\n    }\n  }\n```\n\nYou will need to add the following imports if they were not auto-imported:\n\n```dart\nimport 'actors/water_enemy.dart';\nimport 'managers/segment_manager.dart';\nimport 'objects/ground_block.dart';\nimport 'objects/platform_block.dart';\nimport 'objects/star.dart';\n```\n\nNow we can refactor our game a bit and create an `initializeGame()` method which will call our\n`loadGameSegments` method.\n\n```dart\n  void initializeGame() {\n    // Assume that size.x < 3200\n    final segmentsToLoad = (size.x / 640).ceil();\n    segmentsToLoad.clamp(0, segments.length);\n\n    for (var i = 0; i <= segmentsToLoad; i++) {\n      loadGameSegments(i, (640 * i).toDouble());\n    }\n\n    _ember = EmberPlayer(\n      position: Vector2(128, canvasSize.y - 70),\n    );\n    world.add(_ember);\n  }\n```\n\nWe simply are taking the width of the game screen, divide that by 640 (10 blocks in a segment times\n64 pixels wide for each block), and round that up. As we only defined 5 segments total, we need to\nrestrict that integer from 0 to the length of the segments list in case the user has a really wide\nscreen. Then we simply loop through the number of `segmentsToLoad` and call `loadGameSegments` with\nthe integer to load and then calculate the offset.\n\nAdditionally, I have moved the Ember-related code from the `onLoad` method to our new\n`initializeGame` method. This means I can now make the call in `onLoad` to `initializeGame` such\nas:\n\n```dart\n@override\n  Future<void> onLoad() async {\n    await images.loadAll([\n      'block.png',\n      'ember.png',\n      'ground.png',\n      'heart_half.png',\n      'heart.png',\n      'star.png',\n      'water_enemy.png',\n    ]);\n    \n    camera.viewfinder.anchor = Anchor.topLeft;\n    initializeGame();\n  }\n```\n\nAt this point, you probably have errors for all the object classes and the enemy class, but don't\nworry, we will solve those right now.\n\n\n### The Platform Block\n\nOne of the easiest blocks to start with is the Platform Block. There are two things that we need to\ndevelop beyond getting the sprite to be displayed; that is, we need to place it in the correct\nposition and as Ember moves across the screen, we need to remove the blocks once they are off the\nscreen. In Ember Quest, the player can only move forward, so this will keep the game lightweight as\nit's an infinite level.\n\nOpen the `lib/objects/platform_block.dart` file and add the following code:\n\n```dart\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\n\nimport '../ember_quest.dart';\n\nclass PlatformBlock extends SpriteComponent\n    with HasGameReference<EmberQuestGame> {\n  final Vector2 gridPosition;\n  double xOffset;\n\n  PlatformBlock({\n    required this.gridPosition,\n    required this.xOffset,\n  }) : super(size: Vector2.all(64), anchor: Anchor.bottomLeft);\n\n  @override\n  void onLoad() {\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n  }\n}\n```\n\nWe are going to extend the Flame `SpriteComponent` and we will need the `HasGameRef` mixin to access\nour game class just like we did before. We are starting with the empty `onLoad` and `update`\nmethods and we will begin adding code to create the functionality that is necessary for the game.\n\nThe secret to any gaming engine is the game loop. This is an infinite loop that calls all the\nobjects in your game so you can provide updates. The `update` method is the hook into this and it\nuses a `double dt` to pass to your method the amount of time in seconds since it was last\ncalled. This `dt` variable then allows you to calculate how far your component needs to move\non-screen.\n\nAll components in our game will need to move at the same speed, so to do this, open\n`lib/ember_quest.dart`, and let's define a global variable called `objectSpeed`. At the top of the\n`EmberQuestGame` class, add:\n\n```dart\n  late EmberPlayer _ember;\n  double objectSpeed = 0.0;\n```\n\nSo to implement that movement, declare a variable at the top of the `PlatformBlock` class and make\nyour `update` method look like this:\n\n```dart\nfinal Vector2 velocity = Vector2.zero();\n```\n\n```dart\n  @override\n  void update(double dt) {\n    velocity.x = game.objectSpeed;\n    position += velocity * dt;\n    if (position.x < -size.x) removeFromParent();\n    super.update(dt);\n  }\n```\n\nAll that is happening is we define a base `velocity` that is instantiated at 0 on both axes and then\nwe update `velocity` using the global `objectSpeed` variable for the x-axis. As this is our\nplatform block, it will only scroll left and right, so our y-axis in the `velocity` will always be 0\nas do not want our blocks jumping.\n\nNext, we update the `position` which is a special variable built into the Flame engine components.\nBy multiplying the `velocity` vector by the `dt` we can move our component to the required amount.\n\nFinally, if `x` value of position is `-size.x` (this means off the left side of the screen by the\nwidth of the image) then remove this platform block from the game entirely.\n\nNow we just need to finish the `onLoad` method. So make your `onLoad` method look like this:\n\n```dart\n  @override\n  void onLoad() {\n    final platformImage = game.images.fromCache('block.png');\n    sprite = Sprite(platformImage);\n    position = Vector2((gridPosition.x * size.x) + xOffset,\n        game.size.y - (gridPosition.y * size.y),\n    );\n    add(RectangleHitbox(collisionType: CollisionType.passive));\n  }\n```\n\nFirst, we retrieve the image from cache as we did before, and because this is a `SpriteComponent`\nwe can use the built-in `sprite` variable to assign the image to the component. Next, we need to\ncalculate its starting position. This is where all the magic happens, so let's break this down.\n\nJust like in the `update` method we will be setting the `position` variable to a `Vector2`. To\ndetermine where it needs to be, we need to calculate the x and y positions. Focusing on the x\nfirst, we can see that we are taking `gridPosition.x` times the width of the image and then we will\nadd that to the `xOffset` that we pass in. With the y-axis, we will take the height of the\ngame and we will subtract the `gridPosition.y` times the height of the image.\n\nLastly, as we want Ember to be able to interact with the platform, we will add a `RectangleHitbox`\nwith a `passive` `CollisionType`. Collisions will be explained more in a later chapter.\n\n\n#### Display the Platform\n\nIn our `loadGameSegments` method from earlier, we will need to add the call to add our block. We\nwill need to define `gridPosition` and `xOffset` to be passed in. `gridPosition` will be a\n`Vector2` and `xOffset` is a double as that will be used to calculate the x-axis offset for\nthe block in a `Vector2`. So add the following to your `loadGameSegments` method:\n\n```dart\ncase PlatformBlock:\n  add(PlatformBlock(\n    gridPosition: block.gridPosition,\n    xOffset: xPositionOffset,\n  ));\n```\n\nIf you run your code, you should now see:\n\n![Platforms Displayed](../../images/tutorials/platformer/Step3Platforms.jpg)\n\nWhile this does run, the black just makes it look like Ember is in a dungeon. Let's change that\nbackground real quick so there is a nice blue sky. Just add the following code to\n`lib/ember_quest.dart`:\n\n```dart\nimport 'package:flutter/material.dart';\n\n@override\nColor backgroundColor() {\n  return const Color.fromARGB(255, 173, 223, 247);\n}\n```\n\nExcellent! Ember is now in front of a blue sky.\n\nOn to [](step_4.md), where we will add the rest of the components now that we have a basic\nunderstanding of what we are going to accomplish.\n"
  },
  {
    "path": "doc/tutorials/platformer/step_4.md",
    "content": "# 4. Adding the Remaining Components\n\n\n## Star\n\nThe star is pretty simple. It is just like the Platform block except we are going to add an effect\nto make it pulse in size. For the effect to look correct, we need to change the object's `Anchor`\nto `center`. This means we will need to adjust the position by half of the image size. For brevity,\nI am going to add the whole class and explain the additional changes after.\n\n```dart\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flutter/material.dart';\n\nimport '../ember_quest.dart';\n\nclass Star extends SpriteComponent\n    with HasGameReference<EmberQuestGame> {\n  final Vector2 gridPosition;\n  double xOffset;\n\n  final Vector2 velocity = Vector2.zero();\n\n  Star({\n    required this.gridPosition,\n    required this.xOffset,\n  }) : super(size: Vector2.all(64), anchor: Anchor.center);\n\n  @override\n  void onLoad() {\n    final starImage = game.images.fromCache('star.png');\n    sprite = Sprite(starImage);\n    position = Vector2(\n      (gridPosition.x * size.x) + xOffset + (size.x / 2),\n      game.size.y - (gridPosition.y * size.y) - (size.y / 2),\n    );\n    add(RectangleHitbox(collisionType: CollisionType.passive));\n    add(\n      SizeEffect.by(\n        Vector2(-24, -24),\n        EffectController(\n          duration: .75,\n          reverseDuration: .5,\n          infinite: true,\n          curve: Curves.easeOut,\n        ),\n      ),\n    );\n  }\n\n  @override\n  void update(double dt) {\n    velocity.x = game.objectSpeed;\n    position += velocity * dt;\n    if (position.x < -size.x) removeFromParent();\n    super.update(dt);\n  }\n}\n```\n\nSo the only change between the Star and the Platform beyond the anchor is simply the following:\n\n```dart\nadd(\n  SizeEffect.by(\n    Vector2(-24, -24),\n    EffectController(\n      duration: .75,\n      reverseDuration: .5,\n      infinite: true,\n      curve: Curves.easeOut,\n    ),\n  ),\n);\n```\n\nThe `SizeEffect` is best explained by going to their\n[docs](../../flame/effects.md#sizeeffectby). In short, we simply reduce the size of the star\nby -24 pixels in both directions and we make it pulse infinitely using the `EffectController`.\n\nDon't forget to add the star to your `lib/ember_quest.dart` file by doing:\n\n```dart\ncase Star:\n  world.add(\n    Star(\n      gridPosition: block.gridPosition,\n      xOffset: xPositionOffset,\n    ),\n  );\n```\n\nIf you run your game, you should now see pulsating stars!\n\n\n## Water Enemy\n\nNow that we understand adding effects to our objects, let's do the same for the water drop enemy.\nOpen `lib/actors/water_enemy.dart` and add the following code:\n\n```dart\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\n\nimport '../ember_quest.dart';\n\nclass WaterEnemy extends SpriteAnimationComponent\n    with HasGameReference<EmberQuestGame> {\n  final Vector2 gridPosition;\n  double xOffset;\n\n  final Vector2 velocity = Vector2.zero();\n\n  WaterEnemy({\n    required this.gridPosition,\n    required this.xOffset,\n  }) : super(size: Vector2.all(64), anchor: Anchor.bottomLeft);\n\n  @override\n  void onLoad() {\n    animation = SpriteAnimation.fromFrameData(\n      game.images.fromCache('water_enemy.png'),\n      SpriteAnimationData.sequenced(\n        amount: 2,\n        textureSize: Vector2.all(16),\n        stepTime: 0.70,\n      ),\n    );\n    position = Vector2(\n      (gridPosition.x * size.x) + xOffset,\n      game.size.y - (gridPosition.y * size.y),\n    );\n    add(RectangleHitbox(collisionType: CollisionType.passive));\n    add(\n      MoveEffect.by(\n        Vector2(-2 * size.x, 0),\n        EffectController(\n          duration: 3,\n          alternate: true,\n          infinite: true,\n        ),\n      ),\n    );\n  }\n\n  @override\n  void update(double dt) {\n    velocity.x = game.objectSpeed;\n    position += velocity * dt;\n    if (position.x < -size.x) removeFromParent();\n    super.update(dt);\n  }\n}\n\n```\n\nThe water drop enemy is an animation just like Ember, so this class is extending the\n`SpriteAnimationComponent` class but it uses all of the previous code we have used for the Star and\nthe Platform. The only difference will be instead of the `SizeEffect`, we are going to use the\n`MoveEffect`. The best resource for information will be their [help\ndocs](../../flame/effects.md#sizeeffectby).\n\nIn short, the `MoveEffect` will last for 3 seconds, alternate directions, and run infinitely. It\nwill move our enemy to the left, 128 pixels (-2 x image width).\n\nDon't forget to add the water enemy to your `lib/ember_quest.dart` file by doing:\n\n```dart\ncase WaterEnemy:\n    world.add(\n      WaterEnemy(\n       gridPosition: block.gridPosition,\n       xOffset: xPositionOffset,\n      ),\n    );\n```\n\nIf you run the game now, the Water Enemy should be displayed and moving!\n\n![Water Enemies](../../images/tutorials/platformer/Step4Enemies.jpg)\n\n\n## Ground Blocks\n\nFinally, the last component that needs to be displayed is the Ground Block! This component is more\ncomplex than the others as we need to identify two times during a block's life cycle.\n\n- When the block is added, if it is the last block in the segment, we need to update a global value\n  as to its position.\n- When the block is removed, if it was the first block in the segment, we need to randomly get the\n  next segment to load.\n\nSo let's start with the basic class which is nothing more than a copy of the Platform Block.\n\n```dart\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flutter/material.dart';\n\nimport '../ember_quest.dart';\n\nclass GroundBlock extends SpriteComponent with HasGameReference<EmberQuestGame> {\n  final Vector2 gridPosition;\n  double xOffset;\n\n  final Vector2 velocity = Vector2.zero();\n\n  GroundBlock({\n    required this.gridPosition,\n    required this.xOffset,\n  }) : super(size: Vector2.all(64), anchor: Anchor.bottomLeft);\n\n  @override\n  void onLoad() {\n    final groundImage = game.images.fromCache('ground.png');\n    sprite = Sprite(groundImage);\n    position = Vector2(\n      gridPosition.x * size.x + xOffset,\n      game.size.y - gridPosition.y * size.y,\n    );\n    add(RectangleHitbox(collisionType: CollisionType.passive));\n  }\n\n  @override\n  void update(double dt) {\n    velocity.x = game.objectSpeed;\n    position += velocity * dt;\n    super.update(dt);\n  }\n}\n```\n\nThe first thing we will tackle is registering the block globally if it is the absolute last block to\nbe loaded. To do this, add two new global variables in `lib/ember_quest.dart` called:\n\n```dart\n  late double lastBlockXPosition = 0.0;\n  late UniqueKey lastBlockKey;\n```\n\nDeclare the following variable at the top of your Ground Block class:\n\n```dart\nfinal UniqueKey _blockKey = UniqueKey();\n```\n\nNow in your Ground Block's `onLoad` method, add the following at the end of the method:\n\n```dart\nif (gridPosition.x == 9 && position.x > game.lastBlockXPosition) {\n  game.lastBlockKey = _blockKey;\n  game.lastBlockXPosition = position.x + size.x;\n}\n```\n\nAll that is happening is if this block is the 10th block (9 as the segment grid is 0 based) AND\nthis block's position is greater than the global `lastBlockXPosition`, set the global block key to be\nthis block's key and set the global `lastBlockXPosition` to be this blocks position plus the width of\nthe image (the anchor is bottom left and we want the next block to align right next to it).\n\nNow we can address updating this information, so in the `update` method, add the following code:\n\n```dart\n  @override\n  void update(double dt) {\n    velocity.x = game.objectSpeed;\n    position += velocity * dt;\n\n    if (gridPosition.x == 9) {\n      if (game.lastBlockKey == _blockKey) {\n        game.lastBlockXPosition = position.x + size.x - 10;\n      }\n    }\n\n    super.update(dt);\n  }\n```\n\n`game.lastBlockXPosition` is being updated by the block's current x-axis position plus its width -\n10 pixels. This will cause a little overlap, but due to the potential variance in `dt` this\nprevents gaps in the map as it loads while a player is moving.\n\n\n### Loading the Next Random Segment\n\nTo load the next random segment, we will use the `Random()` function that is built-in to\n`dart:math`. The following line of code gets a random integer from 0 (inclusive) to the max number\nin the passed parameter (exclusive).\n\n```dart\nRandom().nextInt(segments.length),\n```\n\nBack in our Ground Block, we can now add the following to our 'update' method before\nthe other block we just added:\n\n```dart\nif (position.x < -size.x) {\n  removeFromParent();\n  if (gridPosition.x == 0) {\n    game.loadGameSegments(\n      Random().nextInt(segments.length),\n      game.lastBlockXPosition,\n    );\n  }\n}\n```\n\nThis simply extends the code that we have in our other objects, where once the block is off the\nscreen and if the block is the first block of the segment, we will call the `loadGameSegments`\nmethod in our game class, get a random number between 0 and the number of segments and pass in the\noffset. If `Random()` or `segments.length` does not auto-import, you will need:\n\n```dart\nimport 'dart:math';\n\nimport '../managers/segment_manager.dart';\n```\n\nSo our full Ground Block class should look like this:\n\n```dart\nimport 'dart:math';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flutter/material.dart';\n\nimport '../ember_quest.dart';\nimport '../managers/segment_manager.dart';\n\nclass GroundBlock extends SpriteComponent with HasGameReference<EmberQuestGame> {\n  final Vector2 gridPosition;\n  double xOffset;\n  \n  final UniqueKey _blockKey = UniqueKey();\n  final Vector2 velocity = Vector2.zero();\n\n  GroundBlock({\n    required this.gridPosition,\n    required this.xOffset,\n  }) : super(size: Vector2.all(64), anchor: Anchor.bottomLeft);\n\n  @override\n  void onLoad() {\n    final groundImage = game.images.fromCache('ground.png');\n    sprite = Sprite(groundImage);\n    position = Vector2(\n      gridPosition.x * size.x + xOffset,\n      game.size.y - gridPosition.y * size.y,\n    );\n    add(RectangleHitbox(collisionType: CollisionType.passive));\n    if (gridPosition.x == 9 && position.x > game.lastBlockXPosition) {\n      game.lastBlockKey = _blockKey;\n      game.lastBlockXPosition = position.x + size.x;\n    }\n  }\n\n  @override\n  void update(double dt) {\n    velocity.x = game.objectSpeed;\n    position += velocity * dt;\n\n    if (position.x < -size.x) {\n      removeFromParent();\n      if (gridPosition.x == 0) {\n        game.loadGameSegments(\n          Random().nextInt(segments.length),\n          game.lastBlockXPosition,\n        );\n      }\n    }\n    if (gridPosition.x == 9) {\n      if (game.lastBlockKey == _blockKey) {\n        game.lastBlockXPosition = position.x + size.x - 10;\n      }\n    }\n\n    super.update(dt);\n  }\n}\n\n```\n\nFinally, don't forget to add your Ground Block to `lib/ember_quest.dart` by adding the following:\n\n```dart\ncase GroundBlock:\n  world.add(\n    GroundBlock(\n      gridPosition: block.gridPosition,\n      xOffset: xPositionOffset,\n    ),\n  );\n```\n\nIf you run your code, your game should now look like this:\n\n![Ground Blocks](../../images/tutorials/platformer/Step4Ground.jpg)\n\nYou might say, but wait! Ember is in the middle of the ground and that is correct because Ember's\n`Anchor` is set to center. This is ok and we will be addressing this in [](step_5.md) where we will\nbe adding movement and collisions to Ember!\n"
  },
  {
    "path": "doc/tutorials/platformer/step_5.md",
    "content": "# 5. Controlling Movement\n\nIf you were waiting for some serious coding, this chapter is it. Prepare yourself as we dive in!\n\n\n## Keyboard Controls\n\nThe first step will be to allow control of Ember via the keyboard. We need to start by adding the\nappropriate mixins to the game class and Ember. Add the following:\n\n`lib/ember_quest.dart`\n\n```dart\nimport 'package:flame/events.dart';\n\nclass EmberQuestGame extends FlameGame with HasKeyboardHandlerComponents {\n```\n\n`lib/actors/ember.dart`\n\n```dart\nclass EmberPlayer extends SpriteAnimationComponent\n    with KeyboardHandler, HasGameReference<EmberQuestGame> {\n```\n\nNow we can add a new method:\n\n```dart\n  @override\n  bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {\n    return true;\n  }\n```\n\nLike before, if this did not trigger an auto-import, you will need the following:\n\n```dart\nimport 'package:flutter/services.dart';\n```\n\nTo control Ember's movement, it is easiest to set a variable where we think of the direction of\nmovement like a normalized vector, meaning the value will be restricted to -1, 0, or 1. So let's\nset a variable at the top of the class:\n\n```dart\n  int horizontalDirection = 0;\n```\n\nNow in our `onKeyEvent` method, we can register the key pressed by adding:\n\n```dart\n@override\n  bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {\n    horizontalDirection = 0;\n    horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyA) ||\n            keysPressed.contains(LogicalKeyboardKey.arrowLeft))\n        ? -1\n        : 0;\n    horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyD) ||\n            keysPressed.contains(LogicalKeyboardKey.arrowRight))\n        ? 1\n        : 0;\n\n    return true;\n  }\n```\n\nLet's make Ember move by adding a few lines of code and creating our `update` method. First, we\nneed to define a velocity variable for Ember. Add the following at the top of the `EmberPlayer`\nclass:\n\n```dart\nfinal Vector2 velocity = Vector2.zero();\nfinal double moveSpeed = 200;\n```\n\nThis establishes a base velocity of 0 and stores `moveSpeed` so we can adjust as necessary to suit\nhow the game-play should be. Next, add the `update` method with the following:\n\n\n```dart\n  @override\n  void update(double dt) {\n    velocity.x = horizontalDirection * moveSpeed;\n    position += velocity * dt;\n    super.update(dt);\n  }\n```\n\nIf you run the game now, Ember moves left and right using the arrow keys or the `A` and `D` keys.\nYou may have noticed that Ember doesn't look back if you are going left, to fix that, add the\nfollowing code at the end of your `update` method:\n\n```dart\nif (horizontalDirection < 0 && scale.x > 0) {\n  flipHorizontally();\n} else if (horizontalDirection > 0 && scale.x < 0) {\n  flipHorizontally();\n}\n```\n\nNow Ember looks in the direction they are traveling.\n\n\n## Collisions\n\nIt is time to get into the thick of it with collisions. I highly suggest reading the\n[documentation](../../flame/collision_detection.md) to understand how collisions work in Flame. The\nfirst thing we need to do is make the game aware that collisions are going to occur using the\n`HasCollisionDetection` mixin. Add that to `lib/ember_quest.dart` like:\n\n```dart\nclass EmberQuestGame extends FlameGame\n    with HasCollisionDetection, HasKeyboardHandlerComponents {\n```\n\nNext, add the `CollisionCallbacks` mixin to `lib/actors/ember.dart` like:\n\n```dart\nclass EmberPlayer extends SpriteAnimationComponent\n    with KeyboardHandler, CollisionCallbacks, HasGameReference<EmberQuestGame> {\n```\n\nIf it did not auto-import, you will need the following:\n\n```dart\nimport 'package:flame/collisions.dart';\n```\n\nNow add the following `onCollision` method:\n\n```dart\n@override\nvoid onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {\n  if (other is GroundBlock || other is PlatformBlock) {\n    if (intersectionPoints.length == 2) {\n      // Calculate the collision normal and separation distance.\n      final mid = (intersectionPoints.elementAt(0) +\n        intersectionPoints.elementAt(1)) / 2;\n\n      final collisionNormal = absoluteCenter - mid;\n      final separationDistance = (size.x / 2) - collisionNormal.length;\n      collisionNormal.normalize();\n\n      // If collision normal is almost upwards,\n      // ember must be on ground.\n      if (fromAbove.dot(collisionNormal) > 0.9) {\n        isOnGround = true;\n      }\n\n      // Resolve collision by moving ember along\n      // collision normal by separation distance.\n      position += collisionNormal.scaled(separationDistance);\n      }\n    }\n\n  super.onCollision(intersectionPoints, other);\n}\n```\n\nYou will need to import the following:\n\n```dart\nimport '../objects/ground_block.dart';\nimport '../objects/platform_block.dart';\n```\n\nAs well as create these class variables:\n\n```dart\n  final Vector2 fromAbove = Vector2(0, -1);\n  bool isOnGround = false;\n```\n\nFor the collisions to be activated for Ember, we need to add a `CircleHitbox`, so in the `onLoad`\nmethod, add the following:\n\n```dart\nadd(CircleHitbox());\n```\n\nNow that we have the basic collisions created, we can add gravity so Ember exists in a game world\nwith very basic physics. To do that, we need to create some more variables:\n\n```dart\n  final double gravity = 15;\n  final double jumpSpeed = 600;\n  final double terminalVelocity = 150;\n\n  bool hasJumped = false;\n```\n\nNow we can add Ember's ability to jump by adding the following to our `onKeyEvent` method:\n\n```dart\nhasJumped = keysPressed.contains(LogicalKeyboardKey.space);\n```\n\nFinally, in our `update` method we can tie this all together with:\n\n```dart\n// Apply basic gravity\nvelocity.y += gravity;\n\n// Determine if ember has jumped\nif (hasJumped) {\n  if (isOnGround) {\n    velocity.y = -jumpSpeed;\n    isOnGround = false;\n  }\n  hasJumped = false;\n}\n\n// Prevent ember from jumping to crazy fast as well as descending too fast and \n// crashing through the ground or a platform.\nvelocity.y = velocity.y.clamp(-jumpSpeed, terminalVelocity);\n```\n\nEarlier I mentioned that Ember was in the center of the grass, to solve this and show how collisions\nand gravity work with Ember, I like to add a little drop-in when you start the game. So in\n`lib/ember_quest.dart` in the `initializeGame` method, change the following:\n\n```dart\n_ember = EmberPlayer(\n  position: Vector2(128, canvasSize.y - 128),\n);\n```\n\nIf you run the game now, Ember should be created and fall to the ground; then you can jump around!\n\n\n### Collisions with Objects\n\nAdding the collisions with the other objects is fairly trivial. All we need to do is add the\nfollowing to the bottom of the `onCollision` method:\n\n```dart\nif (other is Star) {\n  other.removeFromParent();\n}\n\nif (other is WaterEnemy) {\n  hit();\n}\n```\n\nWhen Ember collides with a star, the game will remove the star, and to implement the `hit` method for\nwhen Ember collides with an enemy, we need to do the following:\n\nAdd the following variable at the top of the `EmberPlayer` class:\n\n```dart\nbool hitByEnemy = false;\n```\n\nAdditionally, add this method to the `EmberPlayer` class:\n\n```dart\n// This method runs an opacity effect on ember\n// to make it blink.\nvoid hit() {\n  if (!hitByEnemy) {\n    hitByEnemy = true;\n  }\n  add(\n    OpacityEffect.fadeOut(\n    EffectController(\n      alternate: true,\n      duration: 0.1,\n      repeatCount: 6,\n    ),\n    )..onComplete = () {\n      hitByEnemy = false;\n    },\n  );\n}\n```\n\nIf the auto-imports did not occur, you will need to add the following imports to your file:\n\n```dart\nimport 'package:flame/effects.dart';\n\nimport '../objects/star.dart';\nimport 'water_enemy.dart';\n```\n\nIf you run the game now, you should be able to move around, make stars disappear, and if you\ncollide with an enemy, Ember should blink.\n\n\n## Adding the Scrolling\n\nThis is our last task with Ember. We need to restrict Ember's movement because as of now, Ember can\ngo off-screen and we never move the map. So to implement this feature, we simply need to add the\nfollowing to the end of our `update` method:\n\n```dart\ngame.objectSpeed = 0;\n// Prevent ember from going backwards at screen edge.\nif (position.x - 36 <= 0 && horizontalDirection < 0) {\n  velocity.x = 0;\n}\n// Prevent ember from going beyond half screen.\nif (position.x + 64 >= game.size.x / 2 && horizontalDirection > 0) {\n  velocity.x = 0;\n  game.objectSpeed = -moveSpeed;\n}\n\nposition += velocity * dt;\nsuper.update(dt);\n```\n\nIf you run the game now, Ember can't move off-screen to the left, and as Ember moves to the right,\nonce they get to the middle of the screen, the rest of the objects scroll by. This is because we\nare now updating `game.objectSpeed` which we established early on in the series. Additionally,\nyou will see the next random segment be generated and added to the level based on the work we did in\nGround Block.\n\n```{note}\nAs I mentioned earlier, I would add a section on how this game could be adapted\nto a traditional level game. As we built the segments in [](step_3.md), we\ncould add a segment that has a door or a special block. For every `X` number of\nsegments loaded, we could then add that special segment. When Ember reaches that\nobject, we could reload the level and start all over maintaining the stars \ncollected and health.\n```\n\nWe are almost done! In [](step_6.md), we will add the health system, keep track of\nthe score, and provide a HUD to relay that information to the player.\n"
  },
  {
    "path": "doc/tutorials/platformer/step_6.md",
    "content": "# 6. Adding the HUD\n\n\n## Setting up the HUD\n\nNow that the game is up and running, the rest of the code should come fairly easily. To prepare for\nthe hud, we need to add some variables in `lib/ember_quest.dart`. Add the following to the top of\nthe class:\n\n```dart\nint starsCollected = 0;\nint health = 3;\n```\n\nStart by creating a folder called `lib/overlays`, and in that folder, create a component called\n`heart.dart`. This is going to be the health monitoring component in the upper left-hand corner of\nthe game. Add the following code:\n\n```dart\nimport 'package:ember_quest/ember_quest.dart';\nimport 'package:flame/components.dart';\n\nenum HeartState {\n  available,\n  unavailable,\n}\n\nclass HeartHealthComponent extends SpriteGroupComponent<HeartState>\n    with HasGameReference<EmberQuestGame> {\n  final int heartNumber;\n\n  HeartHealthComponent({\n    required this.heartNumber,\n    required super.position,\n    required super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.priority,\n  });\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    final availableSprite = await game.loadSprite(\n      'heart.png',\n      srcSize: Vector2.all(32),\n    );\n\n    final unavailableSprite = await game.loadSprite(\n      'heart_half.png',\n      srcSize: Vector2.all(32),\n    );\n\n    sprites = {\n      HeartState.available: availableSprite,\n      HeartState.unavailable: unavailableSprite,\n    };\n\n    current = HeartState.available;\n  }\n\n  @override\n  void update(double dt) {\n    if (game.health < heartNumber) {\n      current = HeartState.unavailable;\n    } else {\n      current = HeartState.available;\n    }\n    super.update(dt);\n  }\n}\n\n```\n\nThe `HeartHealthComponent` is just a [SpriteGroupComponent](../../flame/components/sprite_components.md#spritegroupcomponent)\nthat uses the heart images that were created early on. The unique thing that is being done, is when\nthe component is created, it requires a `heartNumber`, so in the `update` method, we check to see if\nthe `game.health` is less than the `heartNumber` and if so, change the state of the component to\nunavailable.\n\nTo put this all together, create `hud.dart` in the same folder and add the following code:\n\n```dart\nimport 'package:flame/components.dart';\nimport 'package:flutter/material.dart';\n\nimport '../ember_quest.dart';\nimport 'heart.dart';\n\nclass Hud extends PositionComponent with HasGameReference<EmberQuestGame> {\n  Hud({\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority = 5,\n  });\n\n  late TextComponent _scoreTextComponent;\n\n  @override\n  Future<void> onLoad() async {\n    _scoreTextComponent = TextComponent(\n      text: '${game.starsCollected}',\n      textRenderer: TextPaint(\n        style: const TextStyle(\n          fontSize: 32,\n          color: Color.fromRGBO(10, 10, 10, 1),\n        ),\n      ),\n      anchor: Anchor.center,\n      position: Vector2(game.size.x - 60, 20),\n    );\n    add(_scoreTextComponent);\n\n    final starSprite = await game.loadSprite('star.png');\n    add(\n      SpriteComponent(\n        sprite: starSprite,\n        position: Vector2(game.size.x - 100, 20),\n        size: Vector2.all(32),\n        anchor: Anchor.center,\n      ),\n    );\n\n    for (var i = 1; i <= game.health; i++) {\n      final positionX = 40 * i;\n      await add(\n        HeartHealthComponent(\n          heartNumber: i,\n          position: Vector2(positionX.toDouble(), 20),\n          size: Vector2.all(32),\n        ),\n      );\n    }\n  }\n\n  @override\n  void update(double dt) {\n    _scoreTextComponent.text = '${game.starsCollected}';\n  }\n}\n\n```\n\nIn the `onLoad` method, you can see where we loop from 1 to the `game.health` amount, to create\nthe number of hearts necessary. The last step is to add the hud to the game.\n\nGo to `lib/ember_quest.dart` and add the following code in the `initializeGame` method:\n\n```dart\ncamera.viewport.add(Hud());\n```\n\nIf the auto-import did not occur, you will need to add:\n\n```dart\nimport 'overlays/hud.dart';\n```\n\nIf you run the game now, you should see:\n\n![HUD Loaded](../../images/tutorials/platformer/Step6HUD.jpg)\n\n\n## Updating the HUD Data\n\nThe last thing we need to do before closing out the HUD is to update the data. To do this, we need\nto open `lib/actors/ember.dart` and add the following code:\n\n`onCollision`\n\n```dart\nif (other is Star) {\n  other.removeFromParent();\n  game.starsCollected++;\n}\n```\n\n```dart\nvoid hit() {\n  if (!hitByEnemy) {\n    game.health--;\n    hitByEnemy = true;\n  }\n  add(\n    OpacityEffect.fadeOut(\n      EffectController(\n        alternate: true,\n        duration: 0.1,\n        repeatCount: 5,\n      ),\n    )..onComplete = () {\n      hitByEnemy = false;\n    },\n  );\n}\n```\n\nIf you run the game now, you will see that your health is updated and the stars are incremented as\nappropriate. Finally, in [](step_7), we will finish the game by adding the main menu and the\ngame-over menu.\n"
  },
  {
    "path": "doc/tutorials/platformer/step_7.md",
    "content": "# 7. Adding Menus\n\nTo add menus to the game, we will leverage Flame's built-in\n[overlay](../../flame/overlays.md) system.\n\n\n## Main Menu\n\nIn the `lib/overlays` folder, create `main_menu.dart` and add the following code:\n\n```dart\nimport 'package:flutter/material.dart';\n\nimport '../ember_quest.dart';\n\nclass MainMenu extends StatelessWidget {\n  // Reference to parent game.\n  final EmberQuestGame game;\n\n  const MainMenu({super.key, required this.game});\n\n  @override\n  Widget build(BuildContext context) {\n    const blackTextColor = Color.fromRGBO(0, 0, 0, 1.0);\n    const whiteTextColor = Color.fromRGBO(255, 255, 255, 1.0);\n\n    return Material(\n      color: Colors.transparent,\n      child: Center(\n        child: Container(\n          padding: const EdgeInsets.all(10.0),\n          height: 250,\n          width: 300,\n          decoration: const BoxDecoration(\n            color: blackTextColor,\n            borderRadius: const BorderRadius.all(\n              Radius.circular(20),\n            ),\n          ),\n          child: Column(\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              const Text(\n                'Ember Quest',\n                style: TextStyle(\n                  color: whiteTextColor,\n                  fontSize: 24,\n                ),\n              ),\n              const SizedBox(height: 40),\n              SizedBox(\n                width: 200,\n                height: 75,\n                child: ElevatedButton(\n                  onPressed: () {\n                    game.overlays.remove('MainMenu');\n                  },\n                  style: ElevatedButton.styleFrom(\n                    backgroundColor: whiteTextColor,\n                  ),\n                  child: const Text(\n                    'Play',\n                    style: TextStyle(\n                      fontSize: 40.0,\n                      color: blackTextColor,\n                    ),\n                  ),\n                ),\n              ),\n              const SizedBox(height: 20),\n              const Text(\n'''Use WASD or Arrow Keys for movement.\nSpace bar to jump.\nCollect as many stars as you can and avoid enemies!''',\n                textAlign: TextAlign.center,\n                style: TextStyle(\n                  color: whiteTextColor,\n                  fontSize: 14,\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\n```\n\nThis is a pretty self-explanatory file that just uses standard Flutter widgets to display\ninformation and provide a `Play` button. The only Flame-related line is\n`game.overlays.remove('MainMenu');` which simply removes the overlay so the user can play the\ngame. It should be noted that the user can technically move Ember while this is displayed, but\ntrapping the input is outside the scope of this tutorial as there are multiple ways this can be\naccomplished.\n\n\n## Game Over Menu\n\nNext, create a file called `lib/overlays/game_over.dart` and add the following code:\n\n```dart\nimport 'package:flutter/material.dart';\n\nimport '../ember_quest.dart';\n\nclass GameOver extends StatelessWidget {\n  // Reference to parent game.\n  final EmberQuestGame game;\n  const GameOver({super.key, required this.game});\n\n  @override\n  Widget build(BuildContext context) {\n    const blackTextColor = Color.fromRGBO(0, 0, 0, 1.0);\n    const whiteTextColor = Color.fromRGBO(255, 255, 255, 1.0);\n\n    return Material(\n      color: Colors.transparent,\n      child: Center(\n        child: Container(\n          padding: const EdgeInsets.all(10.0),\n          height: 200,\n          width: 300,\n          decoration: const BoxDecoration(\n            color: blackTextColor,\n            borderRadius: const BorderRadius.all(\n              Radius.circular(20),\n            ),\n          ),\n          child: Column(\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              const Text(\n                'Game Over',\n                style: TextStyle(\n                  color: whiteTextColor,\n                  fontSize: 24,\n                ),\n              ),\n              const SizedBox(height: 40),\n              SizedBox(\n                width: 200,\n                height: 75,\n                child: ElevatedButton(\n                  onPressed: () {\n                    game.reset();\n                    game.overlays.remove('GameOver');\n                  },\n                  style: ElevatedButton.styleFrom(\n                    backgroundColor: whiteTextColor,\n                  ),\n                  child: const Text(\n                    'Play Again',\n                    style: TextStyle(\n                      fontSize: 28.0,\n                      color: blackTextColor,\n                    ),\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n```\n\nAs with the Main Menu, this is all standard Flutter widgets except for the call to remove the\noverlay and also the call to `game.reset()` which we will create now.\n\nOpen `lib/ember_quest.dart` and add / update the following code:\n\n```dart\n@override\nFuture<void> onLoad() async {\n  await images.loadAll([\n      'block.png',\n      'ember.png',\n      'ground.png',\n      'heart_half.png',\n      'heart.png',\n      'star.png',\n      'water_enemy.png',\n  ]);\n  \n  camera.viewfinder.anchor = Anchor.topLeft;\n  initializeGame(true);\n}\n\nvoid initializeGame(bool loadHud) {\n  // Assume that size.x < 3200\n  final segmentsToLoad = (size.x / 640).ceil();\n  segmentsToLoad.clamp(0, segments.length);\n\n  for (var i = 0; i <= segmentsToLoad; i++) {\n    loadGameSegments(i, (640 * i).toDouble());\n  }\n\n  _ember = EmberPlayer(\n    position: Vector2(128, canvasSize.y - 128),\n  );\n  add(_ember);\n  if (loadHud) {\n    add(Hud());\n  }\n}\n\nvoid reset() {\n  starsCollected = 0;\n  health = 3;\n  initializeGame(false);\n}\n```\n\nYou may notice that we have added a parameter to the `initializeGame` method which allows us to\nbypass adding the HUD to the game. This is because in the coming section, when Ember's health drops\nto 0, we will wipe the game, but we do not need to remove the HUD, as we just simply need to reset\nthe values using `reset()`.\n\n\n## Displaying the Menus\n\nTo display the menus, add the following code to `lib/main.dart`:\n\n```dart\nvoid main() {\n  runApp(\n    GameWidget<EmberQuestGame>.controlled(\n      gameFactory: EmberQuestGame.new,\n      overlayBuilderMap: {\n        'MainMenu': (_, game) => MainMenu(game: game),\n        'GameOver': (_, game) => GameOver(game: game),\n      },\n      initialActiveOverlays: const ['MainMenu'],\n    ),\n  );\n}\n```\n\nIf the menus did not auto-import, add the following:\n\n```dart\nimport 'overlays/game_over.dart';\nimport 'overlays/main_menu.dart';\n```\n\nIf you run the game now, you should be greeted with the Main Menu overlay. Pressing play will\nremove it and allow you to start playing the game.\n\n\n### Health Check for Game Over\n\nOur last step to finish Ember Quest is to add a game-over mechanism. This is fairly simple but\nrequires us to place similar code in all of our components. So let's get started!\n\nIn `lib/actors/ember.dart`, in the `update` method, add the following:\n\n```dart\n// If ember fell in pit, then game over.\nif (position.y > game.size.y + size.y) {\n  game.health = 0;\n}\n\nif (game.health <= 0) {\n  removeFromParent();\n}\n```\n\nIn `lib/actors/water_enemy.dart`, in the `update` method update the following code:\n\n```dart\nif (position.x < -size.x || game.health <= 0) {\n  removeFromParent();\n}\n```\n\nIn `lib/objects/ground_block.dart`, in the `update` method update the following code:\n\n```dart\nif (game.health <= 0) {\n  removeFromParent();\n}\n```\n\nIn `lib/objects/platform_block.dart`, in the `update` method update the following code:\n\n```dart\nif (position.x < -size.x || game.health <= 0) {\n  removeFromParent();\n}\n```\n\nIn `lib/objects/star.dart`, in the `update` method update the following code:\n\n```dart\nif (position.x < -size.x || game.health <= 0) {\n  removeFromParent();\n}\n```\n\nFinally, in `lib/ember_quest.dart`, add the following `update` method:\n\n```dart\n@override\nvoid update(double dt) {\n  if (health <= 0) {\n    overlays.add('GameOver');\n  }\n  super.update(dt);\n}\n```\n\n\n## Congratulations\n\nYou made it! You have a working Ember Quest. Press the button below to see what the resulting code\nlooks like or to play it live.\n\n```{flutter-app}\n:sources: ../tutorials/platformer/app\n:show: popup code\n```\n"
  },
  {
    "path": "doc/tutorials/space_shooter/app/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "doc/tutorials/space_shooter/app/lib/main.dart",
    "content": "import 'package:flutter/widgets.dart';\nimport 'package:tutorials_space_shooter/step1/main.dart' as step1;\nimport 'package:tutorials_space_shooter/step2/main.dart' as step2;\nimport 'package:tutorials_space_shooter/step3/main.dart' as step3;\nimport 'package:tutorials_space_shooter/step4/main.dart' as step4;\nimport 'package:tutorials_space_shooter/step5/main.dart' as step5;\nimport 'package:tutorials_space_shooter/step6/main.dart' as step6;\nimport 'package:web/web.dart' as web;\n\nvoid main() {\n  var page = web.window.location.search;\n  if (page.startsWith('?')) {\n    page = page.substring(1);\n  }\n\n  return switch (page) {\n    'step1' => step1.main(),\n    'step2' => step2.main(),\n    'step3' => step3.main(),\n    'step4' => step4.main(),\n    'step5' => step5.main(),\n    'step6' => step6.main(),\n    _ => runApp(\n      Directionality(\n        textDirection: TextDirection.ltr,\n        child: Text('''Error: unknown page. Pass \"step{1,6}\" as a GET param; \ne.g: ${web.window.location}?step1'''),\n      ),\n    ),\n  };\n}\n"
  },
  {
    "path": "doc/tutorials/space_shooter/app/lib/step1/main.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(GameWidget(game: SpaceShooterGame()));\n}\n\nclass Player extends PositionComponent {\n  static final _paint = Paint()..color = Colors.white;\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(size.toRect(), _paint);\n  }\n}\n\nclass SpaceShooterGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    add(\n      Player()\n        ..position = size / 2\n        ..width = 50\n        ..height = 100\n        ..anchor = Anchor.center,\n    );\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/space_shooter/app/lib/step2/main.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(GameWidget(game: SpaceShooterGame()));\n}\n\nclass SpaceShooterGame extends FlameGame with PanDetector {\n  late Player player;\n\n  @override\n  Future<void> onLoad() async {\n    player = Player();\n    add(player);\n  }\n\n  @override\n  void onPanUpdate(DragUpdateInfo info) {\n    player.move(info.delta.global);\n  }\n}\n\nclass Player extends SpriteComponent with HasGameReference<SpaceShooterGame> {\n  Player()\n    : super(\n        size: Vector2(100, 150),\n        anchor: Anchor.center,\n      );\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    sprite = await game.loadSprite('player-sprite.png');\n\n    position = game.size / 2;\n    anchor = Anchor.center;\n  }\n\n  void move(Vector2 delta) {\n    position.add(delta);\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/space_shooter/app/lib/step3/main.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/parallax.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(GameWidget(game: SpaceShooterGame()));\n}\n\nclass SpaceShooterGame extends FlameGame with PanDetector {\n  late Player player;\n\n  @override\n  Future<void> onLoad() async {\n    final parallax = await loadParallaxComponent(\n      [\n        ParallaxImageData('stars_0.png'),\n        ParallaxImageData('stars_1.png'),\n        ParallaxImageData('stars_2.png'),\n      ],\n      baseVelocity: Vector2(0, -5),\n      repeat: ImageRepeat.repeat,\n      velocityMultiplierDelta: Vector2(0, 5),\n    );\n    add(parallax);\n\n    player = Player();\n    add(player);\n  }\n\n  @override\n  void onPanUpdate(DragUpdateInfo info) {\n    player.move(info.delta.global);\n  }\n}\n\nclass Player extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame> {\n  Player()\n    : super(\n        size: Vector2(100, 150),\n        anchor: Anchor.center,\n      );\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    animation = await game.loadSpriteAnimation(\n      'player.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        stepTime: 0.2,\n        textureSize: Vector2(32, 48),\n      ),\n    );\n\n    position = game.size / 2;\n  }\n\n  void move(Vector2 delta) {\n    position.add(delta);\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/space_shooter/app/lib/step4/main.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/parallax.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(GameWidget(game: SpaceShooterGame()));\n}\n\nclass SpaceShooterGame extends FlameGame with PanDetector {\n  late Player player;\n\n  @override\n  Future<void> onLoad() async {\n    final parallax = await loadParallaxComponent(\n      [\n        ParallaxImageData('stars_0.png'),\n        ParallaxImageData('stars_1.png'),\n        ParallaxImageData('stars_2.png'),\n      ],\n      baseVelocity: Vector2(0, -5),\n      repeat: ImageRepeat.repeat,\n      velocityMultiplierDelta: Vector2(0, 5),\n    );\n    add(parallax);\n\n    player = Player();\n    add(player);\n  }\n\n  @override\n  void onPanUpdate(DragUpdateInfo info) {\n    player.move(info.delta.global);\n  }\n\n  @override\n  void onPanStart(DragStartInfo info) {\n    player.startShooting();\n  }\n\n  @override\n  void onPanEnd(DragEndInfo info) {\n    player.stopShooting();\n  }\n}\n\nclass Player extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame> {\n  Player()\n    : super(\n        size: Vector2(100, 150),\n        anchor: Anchor.center,\n      );\n\n  late final SpawnComponent _bulletSpawner;\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    animation = await game.loadSpriteAnimation(\n      'player.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        stepTime: 0.2,\n        textureSize: Vector2(32, 48),\n      ),\n    );\n\n    position = game.size / 2;\n\n    _bulletSpawner = SpawnComponent(\n      period: 0.2,\n      selfPositioning: true,\n      factory: (index) {\n        return Bullet(\n          position:\n              position +\n              Vector2(\n                0,\n                -height / 2,\n              ),\n        );\n      },\n      autoStart: false,\n    );\n\n    game.add(_bulletSpawner);\n  }\n\n  void move(Vector2 delta) {\n    position.add(delta);\n  }\n\n  void startShooting() {\n    _bulletSpawner.timer.start();\n  }\n\n  void stopShooting() {\n    _bulletSpawner.timer.stop();\n  }\n}\n\nclass Bullet extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame> {\n  Bullet({\n    super.position,\n  }) : super(\n         size: Vector2(25, 50),\n         anchor: Anchor.center,\n       );\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    animation = await game.loadSpriteAnimation(\n      'bullet.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        stepTime: 0.2,\n        textureSize: Vector2(8, 16),\n      ),\n    );\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    position.y += dt * -500;\n\n    if (position.y < -height) {\n      removeFromParent();\n    }\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/space_shooter/app/lib/step5/main.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/parallax.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(GameWidget(game: SpaceShooterGame()));\n}\n\nclass SpaceShooterGame extends FlameGame with PanDetector {\n  late Player player;\n\n  @override\n  Future<void> onLoad() async {\n    final parallax = await loadParallaxComponent(\n      [\n        ParallaxImageData('stars_0.png'),\n        ParallaxImageData('stars_1.png'),\n        ParallaxImageData('stars_2.png'),\n      ],\n      baseVelocity: Vector2(0, -5),\n      repeat: ImageRepeat.repeat,\n      velocityMultiplierDelta: Vector2(0, 5),\n    );\n    add(parallax);\n\n    player = Player();\n    add(player);\n\n    add(\n      SpawnComponent(\n        factory: (index) {\n          return Enemy();\n        },\n        period: 1,\n        area: Rectangle.fromLTWH(0, 0, size.x, -Enemy.enemySize),\n      ),\n    );\n  }\n\n  @override\n  void onPanUpdate(DragUpdateInfo info) {\n    player.move(info.delta.global);\n  }\n\n  @override\n  void onPanStart(DragStartInfo info) {\n    player.startShooting();\n  }\n\n  @override\n  void onPanEnd(DragEndInfo info) {\n    player.stopShooting();\n  }\n}\n\nclass Player extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame> {\n  Player()\n    : super(\n        size: Vector2(100, 150),\n        anchor: Anchor.center,\n      );\n\n  late final SpawnComponent _bulletSpawner;\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    animation = await game.loadSpriteAnimation(\n      'player.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        stepTime: 0.2,\n        textureSize: Vector2(32, 48),\n      ),\n    );\n\n    position = game.size / 2;\n\n    _bulletSpawner = SpawnComponent(\n      period: 0.2,\n      selfPositioning: true,\n      factory: (index) {\n        return Bullet(\n          position:\n              position +\n              Vector2(\n                0,\n                -height / 2,\n              ),\n        );\n      },\n      autoStart: false,\n    );\n\n    game.add(_bulletSpawner);\n  }\n\n  void move(Vector2 delta) {\n    position.add(delta);\n  }\n\n  void startShooting() {\n    _bulletSpawner.timer.start();\n  }\n\n  void stopShooting() {\n    _bulletSpawner.timer.stop();\n  }\n}\n\nclass Bullet extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame> {\n  Bullet({\n    super.position,\n  }) : super(\n         size: Vector2(25, 50),\n         anchor: Anchor.center,\n       );\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    animation = await game.loadSpriteAnimation(\n      'bullet.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        stepTime: 0.2,\n        textureSize: Vector2(8, 16),\n      ),\n    );\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    position.y += dt * -500;\n\n    if (position.y < -height) {\n      removeFromParent();\n    }\n  }\n}\n\nclass Enemy extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame> {\n  Enemy({\n    super.position,\n  }) : super(\n         size: Vector2.all(enemySize),\n         anchor: Anchor.center,\n       );\n\n  static const enemySize = 50.0;\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    animation = await game.loadSpriteAnimation(\n      'enemy.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        stepTime: 0.2,\n        textureSize: Vector2.all(16),\n      ),\n    );\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    position.y += dt * 250;\n\n    if (position.y > game.size.y) {\n      removeFromParent();\n    }\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/space_shooter/app/lib/step6/main.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/parallax.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(GameWidget(game: SpaceShooterGame()));\n}\n\nclass SpaceShooterGame extends FlameGame\n    with PanDetector, HasCollisionDetection {\n  late Player player;\n\n  @override\n  Future<void> onLoad() async {\n    final parallax = await loadParallaxComponent(\n      [\n        ParallaxImageData('stars_0.png'),\n        ParallaxImageData('stars_1.png'),\n        ParallaxImageData('stars_2.png'),\n      ],\n      baseVelocity: Vector2(0, -5),\n      repeat: ImageRepeat.repeat,\n      velocityMultiplierDelta: Vector2(0, 5),\n    );\n    add(parallax);\n\n    player = Player();\n    add(player);\n\n    add(\n      SpawnComponent(\n        factory: (index) {\n          return Enemy();\n        },\n        period: 1,\n        area: Rectangle.fromLTWH(0, 0, size.x, -Enemy.enemySize),\n      ),\n    );\n  }\n\n  @override\n  void onPanUpdate(DragUpdateInfo info) {\n    player.move(info.delta.global);\n  }\n\n  @override\n  void onPanStart(DragStartInfo info) {\n    player.startShooting();\n  }\n\n  @override\n  void onPanEnd(DragEndInfo info) {\n    player.stopShooting();\n  }\n}\n\nclass Player extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame> {\n  Player()\n    : super(\n        size: Vector2(100, 150),\n        anchor: Anchor.center,\n      );\n\n  late final SpawnComponent _bulletSpawner;\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    animation = await game.loadSpriteAnimation(\n      'player.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        stepTime: 0.2,\n        textureSize: Vector2(32, 48),\n      ),\n    );\n\n    position = game.size / 2;\n\n    _bulletSpawner = SpawnComponent(\n      period: 0.2,\n      selfPositioning: true,\n      factory: (index) {\n        return Bullet(\n          position:\n              position +\n              Vector2(\n                0,\n                -height / 2,\n              ),\n        );\n      },\n      autoStart: false,\n    );\n\n    game.add(_bulletSpawner);\n  }\n\n  void move(Vector2 delta) {\n    position.add(delta);\n  }\n\n  void startShooting() {\n    _bulletSpawner.timer.start();\n  }\n\n  void stopShooting() {\n    _bulletSpawner.timer.stop();\n  }\n}\n\nclass Bullet extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame> {\n  Bullet({\n    super.position,\n  }) : super(\n         size: Vector2(25, 50),\n         anchor: Anchor.center,\n       );\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    animation = await game.loadSpriteAnimation(\n      'bullet.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        stepTime: 0.2,\n        textureSize: Vector2(8, 16),\n      ),\n    );\n\n    add(\n      RectangleHitbox(\n        collisionType: CollisionType.passive,\n      ),\n    );\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    position.y += dt * -500;\n\n    if (position.y < -height) {\n      removeFromParent();\n    }\n  }\n}\n\nclass Enemy extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame>, CollisionCallbacks {\n  Enemy({\n    super.position,\n  }) : super(\n         size: Vector2.all(enemySize),\n         anchor: Anchor.center,\n       );\n\n  static const enemySize = 50.0;\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    animation = await game.loadSpriteAnimation(\n      'enemy.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        stepTime: 0.2,\n        textureSize: Vector2.all(16),\n      ),\n    );\n\n    add(RectangleHitbox());\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    position.y += dt * 250;\n\n    if (position.y > game.size.y) {\n      removeFromParent();\n    }\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n\n    if (other is Bullet) {\n      removeFromParent();\n      other.removeFromParent();\n      game.add(Explosion(position: position));\n    }\n  }\n}\n\nclass Explosion extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame> {\n  Explosion({\n    super.position,\n  }) : super(\n         size: Vector2.all(150),\n         anchor: Anchor.center,\n         removeOnFinish: true,\n       );\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    animation = await game.loadSpriteAnimation(\n      'explosion.png',\n      SpriteAnimationData.sequenced(\n        amount: 6,\n        stepTime: 0.1,\n        textureSize: Vector2.all(32),\n        loop: false,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "doc/tutorials/space_shooter/app/pubspec.yaml",
    "content": "name: tutorials_space_shooter\nresolution: workspace\ndescription: Flame Space Shooter Tutorial\n\npublish_to: 'none'\n\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  web: ^1.1.0\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n\nflutter:\n  uses-material-design: true\n  assets:\n    - assets/images/\n"
  },
  {
    "path": "doc/tutorials/space_shooter/app/web/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <!--\n    If you are serving your web app in a path other than the root, change the\n    href value below to reflect the base path you are serving from.\n\n    The path provided below has to start and end with a slash \"/\" in order for\n    it to work correctly.\n\n    For more details:\n    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base\n\n    This is a placeholder for base href that will be replaced by the value of\n    the `--base-href` argument provided to `flutter build`.\n  -->\n  <base href=\"$FLUTTER_BASE_HREF\">\n\n  <meta charset=\"UTF-8\">\n  <meta content=\"IE=Edge\" http-equiv=\"X-UA-Compatible\">\n  <meta name=\"description\" content=\"A new Flutter project.\">\n\n  <!-- iOS meta tags & icons -->\n  <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n  <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\">\n  <meta name=\"apple-mobile-web-app-title\" content=\"space_shooter\">\n  <link rel=\"apple-touch-icon\" href=\"icons/Icon-192.png\">\n\n  <title>Flame Tutorial</title>\n  <link rel=\"manifest\" href=\"manifest.json\">\n</head>\n<body>\n  <!-- This script installs service_worker.js to provide PWA functionality to\n       application. For more information, see:\n       https://developers.google.com/web/fundamentals/primers/service-workers -->\n  <script>\n    var serviceWorkerVersion = null;\n    var scriptLoaded = false;\n    function loadMainDartJs() {\n      if (scriptLoaded) {\n        return;\n      }\n      scriptLoaded = true;\n      var scriptTag = document.createElement('script');\n      scriptTag.src = 'main.dart.js';\n      scriptTag.type = 'application/javascript';\n      document.body.append(scriptTag);\n    }\n\n    if ('serviceWorker' in navigator) {\n      // Service workers are supported. Use them.\n      window.addEventListener('load', function () {\n        // Wait for registration to finish before dropping the <script> tag.\n        // Otherwise, the browser will load the script multiple times,\n        // potentially different versions.\n        var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;\n        navigator.serviceWorker.register(serviceWorkerUrl)\n          .then((reg) => {\n            function waitForActivation(serviceWorker) {\n              serviceWorker.addEventListener('statechange', () => {\n                if (serviceWorker.state == 'activated') {\n                  console.log('Installed new service worker.');\n                  loadMainDartJs();\n                }\n              });\n            }\n            if (!reg.active && (reg.installing || reg.waiting)) {\n              // No active web worker and we have installed or are installing\n              // one for the first time. Simply wait for it to activate.\n              waitForActivation(reg.installing || reg.waiting);\n            } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {\n              // When the app updates the serviceWorkerVersion changes, so we\n              // need to ask the service worker to update.\n              console.log('New service worker available.');\n              reg.update();\n              waitForActivation(reg.installing);\n            } else {\n              // Existing service worker is still good.\n              console.log('Loading app from service worker.');\n              loadMainDartJs();\n            }\n          });\n\n        // If service worker doesn't succeed in a reasonable amount of time,\n        // fallback to plaint <script> tag.\n        setTimeout(() => {\n          if (!scriptLoaded) {\n            console.warn(\n              'Failed to load app from service worker. Falling back to plain <script> tag.',\n            );\n            loadMainDartJs();\n          }\n        }, 4000);\n      });\n    } else {\n      // Service workers not supported. Just drop the <script> tag.\n      loadMainDartJs();\n    }\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "doc/tutorials/space_shooter/space_shooter.md",
    "content": "# Space Shooter Game Tutorial\n\nIn this tutorial, we will follow a step-by-step process for coding a game using the Flame\nengine.\n\nThis tutorial assumes that you have at least some familiarity with common programming concepts, and\nwith the [Dart] programming language.\n\n\n[Dart]: https://dart.dev/overview\n\n```{toctree}\n:hidden:\n\n1. Getting Started  <step_1.md>\n2. Controlling the player and adding some graphics <step_2.md>\n3. Adding animations and depth <step_3.md>\n4. Adding Bullets <step_4.md>\n5. Adding Enemies <step_5.md>\n6. Enemy and Bullet collisions <step_6.md>\n```\n"
  },
  {
    "path": "doc/tutorials/space_shooter/step_1.md",
    "content": "# Getting Started\n\nThis tutorial will guide you on the development of a full Flame game, starting from the ground up,\nstep by step. By the end of it, you will have built a classic Space Shooter game, featuring\nanimations, input using gestures, mouse and keyboard controls, collision detections, and so on.\n\nThis first part will introduce you to:\n\n- `FlameGame`: The base class for games using the Flame Component System.\n- `GameWidget`: The `Widget` that will insert your game into the Flutter widget tree.\n- `PositionComponent`: One of the most basic Flame components holds both a position and\ndimension in the game space.\n\nLet's start by creating our game class and the `GameWidget` that will run it.\n\n```dart\nimport 'package:flutter/material.dart';\nimport 'package:flame/game.dart';\n\nclass SpaceShooterGame extends FlameGame {\n}\n\nvoid main() {\n  runApp(GameWidget(game: SpaceShooterGame()));\n}\n```\n\nThat is it! If you run this, you will only see an empty black screen for now, from that, we can\nstart implementing our game.\n\nNext, let's create our player component. To do so, we will create a new class based on Flame's\n`PositionComponent`. This component is the base for all components that have a position and a size\non the game screen. For now, our component will only render a white square; it could be\nimplemented as follows:\n\n```dart\nimport 'package:flame/components.dart';\nimport 'package:flutter/material.dart';\n\nclass Player extends PositionComponent {\n  static final _paint = Paint()..color = Colors.white;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(size.toRect(), _paint);\n  }\n}\n```\n\nNow, let's add our new component to the game. Adding any component on game startup should be done\nin the `onLoad` method, so let's override `FlameGame.onLoad` and add our logic there. The modified\ncode will look like the following:\n\n```dart\nclass SpaceShooterGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    add(\n      Player()\n        ..position = size / 2\n        ..width = 50\n        ..height = 100\n        ..anchor = Anchor.center,\n    );\n  }\n}\n```\n\nIf you run this, you will now see a white rectangle being rendered in the center of the screen.\n\nA couple of points worth commenting:\n\n- `size` is a `Vector2` variable from the game class and it holds the current dimension of the game\narea, where `x` is the horizontal dimension or the width, and `y` is the vertical dimension or the\nheight.\n- By default, Flame follows Flutter's canvas anchoring, which means that (0, 0) is anchored on the\ntop left corner of the canvas. So the game and all components use that same anchor by default. We\ncan change this by changing our component's `anchor` attribute to `Anchor.center`, which will make\nour life way easier if you want to center the component on the screen.\n\nAnd that is it for this first part! In this first step, we learned the basics of how to create a\ngame class, insert it into the Flutter widget tree, and render a simple component.\n\n\n## Preparing the assets folder\n\nBefore we move on, let's prepare our project for the graphics we will be using in the next steps.\nGames need assets such as images, sprites, and animations, and our Space Shooter is no exception.\n\nFirst, create an `assets/images/` folder at the root of your project. Then tell Flutter about it\nby adding the following lines to your `pubspec.yaml`:\n\n```yaml\nflutter:\n  assets:\n    - assets/images/\n```\n\nYour project structure should look like this:\n\n```text\nspace_shooter/\n ├─assets/\n │  └─images/\n ├─lib/\n │  └─main.dart\n └─pubspec.yaml\n```\n\nIn the following steps, we will provide the image files that need to be saved into the\n`assets/images/` folder. Make sure to save each one as you encounter it.\n\n```{flutter-app}\n:sources: ../tutorials/space_shooter/app\n:page: step1\n:show: popup code\n```\n\n[Next step: Controlling the player and adding some graphics](./step_2.md)\n"
  },
  {
    "path": "doc/tutorials/space_shooter/step_2.md",
    "content": "# Controlling the player and adding some graphics\n\nNow that we have the base for our game and a component for our player, let's add some interactivity\nto it. We can begin by allowing the player to be controlled by mouse/touch gestures.\n\nThere are a couple of ways of doing that in Flame. For this tutorial, we will do that by using one\nof Flame's gesture detectors: `PanDetector`.\n\nThis detector will make our game class receive pan (or drag) events. To do so, we just need to add\nthe `PanDetector` mixin to our game class and override its listener methods; in our case, we will\nuse the `onPanUpdate` method. The updated code will look like the following:\n\n```dart\nimport 'package:flame/input.dart';\nimport 'package:flame/events.dart';\n\nclass SpaceShooterGame extends FlameGame with PanDetector {\n  late Player player;\n\n  @override\n  void onLoad() {\n    // omitted\n  }\n\n  @override\n  void onPanUpdate(DragUpdateInfo info) {\n  }\n}\n\n```\n\nAt this point, our game should be receiving all the pan update inputs, but we are not doing anything\nwith these events.\n\nWe now need a way to move our player. That can be achieved by simply saving our `Player` component\nto a variable inside our game class, adding a method `move` to our `Player`, and just connect\nthem:\n\n```dart\nclass Player extends PositionComponent { \n  static final _paint = Paint()..color = Colors.white;\n  \n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(size.toRect(), _paint);\n  }\n\n  void move(Vector2 delta) {\n    position.add(delta);\n  }\n}\n\nclass SpaceShooterGame extends FlameGame with PanDetector {\n  late Player player;\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    player = Player()\n      ..position = size / 2\n      ..width = 50\n      ..height = 100\n      ..anchor = Anchor.center;\n\n    add(player);\n  }\n\n  @override\n  void onPanUpdate(DragUpdateInfo info) {\n    player.move(info.delta.global);\n  }\n}\n```\n\nThat is it! If you drag the screen, the player should follow your movement and we have just\nimplemented our very first interactive game!\n\nBefore we move to our next step, let's replace that boring white rectangle with some cool graphics.\n\nFlame provides many classes to help us with graphical rendering. For this step, we are going to use\nthe `Sprite` class.\n\n`Sprite`s are used in Flame to render static images or portions of them in the game. To render a\n`Sprite` inside a `FlameGame`, we should use the `SpriteComponent` class, which wraps the `Sprite`\nfeatures into a component.\n\nBefore we write the code, we need the player sprite image. Right-click the image below, choose\n\"Save as...\", and store it as `player-sprite.png` in your `assets/images/` folder:\n\n![player-sprite](app/assets/images/player-sprite.png)\n\nNow let's refactor our current implementation, first, we can replace our inheritance from\n`PositionComponent` to `SpriteComponent` (which is a component that extends from\n`PositionComponent`) and load the sprite:\n\n```dart\nclass Player extends SpriteComponent {\n  void move(Vector2 delta) {\n    position.add(delta);\n  }\n}\n\nclass SpaceShooterGame extends FlameGame with PanDetector {\n  late Player player;\n\n  @override\n  Future<void>? onLoad() async {\n    await super.onLoad();\n\n    final playerSprite = await loadSprite('player-sprite.png');\n    player = Player()\n      ..sprite = playerSprite\n      ..x = size.x / 2\n      ..y = size.y / 2\n      ..width = 50\n      ..height = 100\n      ..anchor = Anchor.center;\n\n    add(player);\n  }\n\n  @override\n  void onPanUpdate(DragUpdateInfo info) {\n    player.move(info.delta.global);\n  }\n}\n```\n\nAnd now, you should see a small blue spaceship on the screen!\n\nA couple of notes worth mentioning:\n\n- Unlike `PositionComponent`, `SpriteComponent` has an implementation for the `render` method, so we\ncan delete the previous override.\n- `FlameGame` has a couple of methods for loading assets, like `loadSprite`. Those methods are\nquite useful, because when used, `FlameGame` will take care of cleaning any cache when the game is\nremoved from the Flutter widget tree.\n\nBefore we close this step, there is one small improvement that we can do. Right now, we are loading\nthe sprite and passing it to our component. For now, this may seem fine, but imagine a game with\na lot of components; if the game is responsible for loading assets for all components, our code can\nbecome a mess quite fast.\n\nJust like `FlameGame`, components also have an `onLoad` method that can be overridden to do\ninitializations. But before we implement our player's load method, note that we use an attribute and\nthe `loadSprite` method from the `FlameGame` class.\n\nThat is not a problem! Every time our component needs to access things from its game class, we can\nmix our component with the `HasGameReference` mixin; that will add a new variable to our component called\n`game` which will point to the game instance where the component is running. Now, let's refactor\nour game a little bit:\n\n```dart\nclass Player extends SpriteComponent with HasGameReference<SpaceShooterGame> {\n\n  Player() : super(\n    size: Vector2(100, 150),\n    anchor: Anchor.center,\n  );\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    sprite = await game.loadSprite('player-sprite.png');\n\n    position = game.size / 2;\n  }\n\n  void move(Vector2 delta) {\n    position.add(delta);\n  }\n}\n\nclass SpaceShooterGame extends FlameGame with PanDetector {\n  late Player player;\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    player = Player();\n\n    add(player);\n  }\n\n  @override\n  void onPanUpdate(DragUpdateInfo info) {\n    player.move(info.delta.global);\n  }\n}\n```\n\nIf you run the game now, you will not notice any visual differences, but now we have a more scalable\nstructure for developing our game. And that closes this step!\n\n```{flutter-app}\n:sources: ../tutorials/space_shooter/app\n:page: step2\n:show: popup code\n```\n\n[Next step: Adding animations and depth](./step_3.md)\n"
  },
  {
    "path": "doc/tutorials/space_shooter/step_3.md",
    "content": "# Adding animations and depth\n\nWe now have something that looks more like a game, having graphics for our spaceship and being\nable to directly control it.\n\nBut our game so far is too boring, the starship is just a static sprite and the background is\njust a black screen.\n\nIn this step we will look at how to improve that, we will replace the static graphics of the player\nwith an animation and create a cool sense of depth and movement by adding a parallax to the\nbackground of the game.\n\nSo lets start by adding the animation to the player ship! For that, we will something that we\ncall Sprite Animations, which is an animation that is composed by a collection of sprites, each\none representing one frame, and the animation effect is achieved by rendering one sprite after\nthe other over a time frame.\n\nTo better visualize this, this is the animation that we will be using, note how the image holds 4\nindividual images (or frames). Right-click the image below, choose \"Save as...\", and store it as\n`player.png` in your `assets/images/` folder (replacing the static `player-sprite.png` we used\nbefore):\n\n![player](app/assets/images/player.png)\n\nFlame provides us with a specialized classes to deal with such images: `SpriteAnimation` and its component\nwrapper `SpriteAnimationComponent` and changing our `Player` component to be an animation is quite\nsimple, take a look at how the component will look like now:\n\n```dart\nclass Player extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame> {\n\n  Player() : super(\n    size: Vector2(100, 150),\n    anchor: Anchor.center,\n  );\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    animation = await game.loadSpriteAnimation(\n      'player.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        stepTime: .2,\n        textureSize: Vector2(32, 48),\n      ),\n    );\n\n    position = game.size / 2;\n  }\n\n  // Other methods omitted\n}\n```\n\nSo lets break down the changes:\n\n- First we changed our `Player` component to extend from `SpriteAnimationComponent` instead of\n`SpriteComponent`\n- In the `onLoad` method we are now using the `game.loadSpriteAnimation` helper instead of the\n `loadSprite` one, and setting the `animation` attribute with its returned value.\n\nThe `SpriteAnimationData` class might look complicated at first glance, but it is actually quite\nsimple, note how we used the `sequenced` constructor, which is a helper to load animation images\nwhere the frames are already laid down in the sequence that they will play, then:\n\n- `amount` defines how many frames the animation has, in this case `4`\n- `stepTime` is the time in seconds that each frame will be rendered, before it gets replaced\nwith the next one.\n- `textureSize` is the size in pixels which defines each frame of the image.\n\nWith all of this information, the `SpriteAnimationComponent` will now automatically play the\nanimation!\n\nNow lets add some depth and energy to our game background. Of course there are many ways of\ndoing so, in this tutorial we will explore the idea of parallax scrolling. If you never heard\nabout it, it consist of a technique where background images move past the camera with different\nspeeds, this not only creates the sensation of depth but also improves the movement feeling\nof the game a lot. If you want to read more about Parallax Scrolling, check this article\nfrom [Wikipedia](https://en.wikipedia.org/wiki/Parallax_scrolling).\n\nFlame provides classes to implement parallax scrolling out of the box, these classes are `Parallax` and\n`ParallaxComponent`. We will need three star layer images for our parallax background. Right-click\neach image below, choose \"Save as...\", and store them in your `assets/images/` folder:\n\n- `stars_0.png` (farthest layer): ![stars_0](app/assets/images/stars_0.png)\n- `stars_1.png` (middle layer): ![stars_1](app/assets/images/stars_1.png)\n- `stars_2.png` (closest layer): ![stars_2](app/assets/images/stars_2.png)\n\nNow lets take a look at how we can add that new feature to the game:\n\n```dart\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/parallax.dart';\nimport 'package:flutter/material.dart';\n\nclass SpaceShooterGame extends FlameGame with PanDetector {\n  late Player player;\n\n  @override\n  Future<void> onLoad() async {\n    final parallax = await loadParallaxComponent(\n      [\n        ParallaxImageData('stars_0.png'),\n        ParallaxImageData('stars_1.png'),\n        ParallaxImageData('stars_2.png'),\n      ],\n      baseVelocity: Vector2(0, -5),\n      repeat: ImageRepeat.repeat,\n      velocityMultiplierDelta: Vector2(0, 5),\n    );\n    add(parallax);\n\n    player = Player();\n    add(player);\n  }\n\n  @override\n  void onPanUpdate(DragUpdateInfo info) {\n    player.move(info.delta.global);\n  }\n}\n```\n\nLooking at the code above we notice that we are now using the `loadParallaxComponent` helper\nmethod from the `FlameGame` class to directly load a `ParallaxComponent` and add it to our game.\n\nThe arguments used there are as follows:\n\n- The first argument is a positional one, which should be a list of `ParallaxData`s. There are a\ncouple of types of `ParallaxData`s in Flame, in this tutorial we are using the `ParallaxImageData`\nwhich describes a layer in the parallax scrolling effect that is an `image`. This list will tell\nFlame about all the layers that we want in our parallax.\n- `baseVelocity` is the base value for all the values, so by passing a `Vector2(0, -5)` to it\nmeans that the slower of the layers will move at 0 pixels per second on the `x` axis and `-5`\npixels per second on the `y` axis.\n- Finally `velocityMultiplierDelta` is a vector that is applied to the base value for each layer,\nand in our example the multiplication rate is `5` on only the `y` axis.\n\n\nGive it a try by running the game now, you will notice that it looks way more dynamic now, giving a\nmore convincing feeling to the player that the spaceship is really crossing the stars!\n\n```{flutter-app}\n:sources: ../tutorials/space_shooter/app\n:page: step3\n:show: popup code\n```\n\n[Next step: Adding bullets](./step_4.md)\n"
  },
  {
    "path": "doc/tutorials/space_shooter/step_4.md",
    "content": "# Adding bullets\n\nFor this next step we will add a very important feature to any space shooter game, shooting!\n\nHere is how we will implement it: since we already control our space ship by dragging on the screen\nwith the mouse/fingers, we will make the ship auto shoot when the player starts dragging and\nstop shooting when the gesture/input has ended.\n\nFirst, let's create a `Bullet` component that will represent the shots in the game. We need a\nbullet sprite for it. Right-click the image below, choose \"Save as...\", and store it as\n`bullet.png` in your `assets/images/` folder:\n\n![bullet](app/assets/images/bullet.png)\n\n```dart\nclass Bullet extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame> {\n  Bullet({\n    super.position,\n  }) : super(\n          size: Vector2(25, 50),\n          anchor: Anchor.center,\n        );\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    animation = await game.loadSpriteAnimation(\n      'bullet.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        stepTime: .2,\n        textureSize: Vector2(8, 16),\n      ),\n    );\n  }\n}\n```\n\nSo far, this does not introduce any new concepts, we just created a component and set\nup its animations attributes.\n\nThe `Bullet` behavior is a simple one, it always moves towards the top of the screen and should\nbe removed from the game if it is not visible anymore, so let's add an `update` method to it\nand make it happen:\n\n```dart\nclass Bullet extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame> {\n  Bullet({\n    super.position,\n  }) : super(\n          size: Vector2(25, 50),\n          anchor: Anchor.center,\n        );\n\n  @override\n  Future<void> onLoad() async {\n    // Omitted\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    position.y += dt * -500;\n\n    if (position.y < -height) {\n      removeFromParent();\n    }\n  }\n}\n```\n\nThe above code should be straight forward, but lets break it down:\n\n- We add to the bullet's y axis position at a rate of -500 pixels per second. Remember going up\nin the y axis means getting closer to `0` since the top left corner of the screen is `0, 0`.\n- If the y is smaller than the negative value of the bullet's height, means that the component is\ncompletely off the screen and it can be removed.\n\nRight, we now have a `Bullet` class ready, so lets start to implement the action of shooting.\nFirst thing, let's create two empty methods in the `Player` class, `startShooting()` and\n`stopShooting()`.\n\n```dart\nclass Player extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame> {\n\n  // Rest of implementation omitted\n\n  void startShooting() {\n    // TODO\n  }\n\n  void stopShooting() {\n    // TODO\n  }\n}\n```\n\nAnd let's hook into those methods from the game class by using the `onPanStart()`\nand `onPanEnd()` methods from the `PanDetector` mixin that we already have been using for the ship\nmovement:\n\n```dart\nclass SpaceShooterGame extends FlameGame with PanDetector {\n  late Player player;\n\n  // Rest of implementation omitted\n\n  @override\n  void onPanUpdate(DragUpdateInfo info) {\n    player.move(info.delta.global);\n  }\n\n  @override\n  void onPanStart(DragStartInfo info) {\n    player.startShooting();\n  }\n\n  @override\n  void onPanEnd(DragEndInfo info) {\n    player.stopShooting();\n  }\n}\n```\n\nWe now have everything set up, so let's write the shooting routine in our player class.\n\nRemember, the shooting behavior will be adding bullets through time intervals when the player is\ndragging the starship.\n\nWe could implement the time interval code and the spawning manually, but Flame\nprovides a component out of the box for that, the `SpawnComponent`, so let's take advantage of it:\n\n\n```dart\nclass Player extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame> {\n  late final SpawnComponent _bulletSpawner;\n\n  @override\n  Future<void> onLoad() async {\n    // Loading animation omitted\n\n    _bulletSpawner = SpawnComponent(\n      period: .2,\n      selfPositioning: true,\n      factory: (index) {\n        return Bullet(position: position + Vector2(0, -height / 2));\n      },\n      autoStart: false,\n    );\n\n    game.add(_bulletSpawner);\n  }\n\n  void move(Vector2 delta) {\n    position.add(delta);\n  }\n\n  void startShooting() {\n    _bulletSpawner.timer.start();\n  }\n\n  void stopShooting() {\n    _bulletSpawner.timer.stop();\n  }\n}\n```\n\nHopefully the code above speaks for itself, but let's look at it in more detail:\n\n- First we declared a `SpawnComponent` called `_bulletSpawner` in our game class, we needed it\nto be an variable accessible to the whole component since we will be accessing it in the\n`startShooting` and `stopShooting` methods.\n- We initialize our `_bulletSpawner` in the `onLoad` method. In the first argument, `period`, we set\nhow much time in seconds it will take between calls, and we choose `.2` seconds for now.\n- We set `selfPositioning: true` so the spawn component doesn't try to position the created component\nsince we want to handle that ourselves to make the bullets spawn out of the ship.\n- The `factory` attribute receives a function that will be called every time the `period` is  \nreached and return the created component.\n- We set `autoStart: false` so it does not start by default.\n- Finally we add the `_bulletSpawner` to our component, so it can be processed in the game loop.\n- Note how the `_bulletSpawner` is added to the game instead of the player, since the bullets\nare part of the whole game and not the player itself.\n\nWith the `_bulletSpawner` all set up, the only missing piece now is starting the\n`_bulletSpawner.timer` in `startShooting()` and stopping it in the `stopShooting()`!\n\nAnd that closes this step, putting us real close to a real game!\n\n```{flutter-app}\n:sources: ../tutorials/space_shooter/app\n:page: step4\n:show: popup code\n```\n\n[Next step: Adding Enemies](./step_5.md)\n"
  },
  {
    "path": "doc/tutorials/space_shooter/step_5.md",
    "content": "# Adding Enemies\n\nNow that the starship is able to shoot, we need something for the player to shoot at! So for\nthis step we will work on adding enemies to the game.\n\nFirst, let's create an `Enemy` class that will represent the enemies in game. Right-click the\nimage below, choose \"Save as...\", and store it as `enemy.png` in your `assets/images/` folder:\n\n![enemy](app/assets/images/enemy.png)\n\n```dart\nclass Enemy extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame> {\n\n  Enemy({\n    super.position,\n  }) : super(\n          size: Vector2.all(enemySize),\n          anchor: Anchor.center,\n        );\n\n\n  static const enemySize = 50.0;\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    animation = await game.loadSpriteAnimation(\n      'enemy.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        stepTime: .2,\n        textureSize: Vector2.all(16),\n      ),\n    );\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    position.y += dt * 250;\n\n    if (position.y > game.size.y) {\n      removeFromParent();\n    }\n  }\n}\n```\n\nNote that for now, the `Enemy` class is super similar to the `Bullet` one, the only differences are\ntheir sizes, animation information and that bullets travel from bottom to top, while enemies travel from\ntop to bottom, so nothing new here.\n\nNext we need to make the enemies spawn in the game, the logic here will be simple:\nwe will make enemies spawn from the top of the screen at a random position on the `x` axis.\n\nOnce again, we could manually add all the time based events in the game's `update()` method, maintain\na random instance to get the enemy x position and so on and so forth, but Flame provides us with a\nway to avoid having to write all that by ourselves: we can use the `SpawnComponent`! So in the\n`SpaceShooterGame.onLoad()` method let's add the following code:\n\n```dart\n    add(\n      SpawnComponent(\n        factory: (index) {\n          return Enemy();\n        },\n        period: 1,\n        area: Rectangle.fromLTWH(0, 0, size.x, -Enemy.enemySize),\n      ),\n    );\n```\n\nThe `SpawnComponent` will take a couple of arguments, let's review them as they appear in the code:\n\n- `factory` receives a function which has the index of the component that should be created. We\ndon't use the index in our code, but it is useful to create more advanced spawn routines.\nThis function should return the created component, in our case a new instance of `Enemy`.\n- `period` simply define the interval in which a new component will be spawned.\n- `area` defines the possible area where the components can be placed once created. In our case they\nshould be placed in the area above the screen top, so they can be seen as they are arriving into the\nplayable area.\n\nAnd this concludes this short step!\n\n```{flutter-app}\n:sources: ../tutorials/space_shooter/app\n:page: step5\n:show: popup code\n```\n\n[Next step: Collision Detection](./step_6.md)\n"
  },
  {
    "path": "doc/tutorials/space_shooter/step_6.md",
    "content": "# Enemies and Bullets collision\n\nRight, we are really close to a playable game, we have enemies and we have the ability to shoot bullets\nat them! We now need to do something when a bullet hits an enemy.\n\nFlame provides a collision detection system out of the box, which we will use to implement our\nlogic when a bullet and an enemy come into contact. The result will be that both are removed!\n\nFirst we need to let our `FlameGame` know that we want collisions between components to\nbe checked. In order to do so, simply add the `HasCollisionDetection` mixin to the declaration\nof the game class:\n\n```dart\nclass SpaceShooterGame extends FlameGame\n    with PanDetector, HasCollisionDetection {\n    // ...\n}\n```\n\nWith that, Flame now will start to check if components have collided with each other. Next we need to\nidentify which components can cause collisions.\n\nIn our case those are the `Bullet` and `Enemy` components and we need to add hitboxes to them.\n\nA hitbox is nothing more than a defined part of the component's area that can hit\nother objects. Flame offers a collection of classes to define a hitbox, the simplest of them is\nthe `RectangleHitbox`, which like the name implies, will set a rectangular area as the component's\nhitbox.\n\nHitboxes are also components, so we can simply add them to the components that we want to have hitboxes.\nLet's start by adding the following line to the `Enemy` class:\n\n```dart\nadd(RectangleHitbox());\n```\n\nFor the bullet we will do the same, but with a slight difference:\n\n```dart\nadd(\n  RectangleHitbox(\n    collisionType: CollisionType.passive,\n  ),\n);\n```\n\nThe `collisionType`s are very important to understand, since they can directly impact the game\nperformance!\n\nThere are three types of collisions in Flame:\n\n- `active` collides with other hitboxes of type active or passive\n- `passive` collides with other hitboxes of type active\n- `inactive` will not collide with any other hitbox\n\nUsually it is smart to mark `hitboxes` from components that will have a higher number of instances\nas passive, so they will be taken into account for collision, but they themselves will not check\ntheir own collisions, drastically reducing the number of checking, giving a better performance\nto the game!\n\nAnd since in this game we anticipate that there will be more bullets than enemies, we set the\nbullets to have a passive collision type!\n\nFrom this point on, Flame will take care of checking the collision between those two components and\nwe now need to do something when this occurs.\n\nWe start by receiving the collision events in one of the classes. Since `Bullet`s have a\npassive collision type, we will also add the collision checking logic to the `Enemy` class.\n\nTo listen for collision events we need to add the `CollisionCallbacks` mixin to the component.\nBy doing so we will be able to override some methods like `onCollisionStart()` and `onCollisionEnd()`.\n\nSo let's make a few changes to the `Enemy` class:\n\n```dart\nclass Enemy extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame>, CollisionCallbacks {\n\n  // Other methods omitted\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n\n    if (other is Bullet) {\n      removeFromParent();\n      other.removeFromParent();\n    }\n  }\n}\n```\n\nAs you can see, we added the mixin to the class, overrode the `onCollisionStart` method,\nwhere we check whether the component that collided with us was a `Bullet` and if it was, then\nwe remove both the current `Enemy` instance and the `Bullet`.\n\nIf you run the game now you will finally be able to defeat the enemies crawling down the screen!\n\nTo add some final touches, let's add some explosion animations to introduce more action to the game!\n\nFirst, we need an explosion sprite sheet. Right-click the image below, choose \"Save as...\", and\nstore it as `explosion.png` in your `assets/images/` folder:\n\n![explosion](app/assets/images/explosion.png)\n\nNow let's create the explosion class:\n\n```dart\nclass Explosion extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame> {\n  Explosion({\n    super.position,\n  }) : super(\n          size: Vector2.all(150),\n          anchor: Anchor.center,\n          removeOnFinish: true,\n        );\n\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    animation = await game.loadSpriteAnimation(\n      'explosion.png',\n      SpriteAnimationData.sequenced(\n        amount: 6,\n        stepTime: .1,\n        textureSize: Vector2.all(32),\n        loop: false,\n      ),\n    );\n  }\n}\n```\n\nThere is not much new in it, the biggest difference compared to the other animation components is\nthat we are passing `loop: false` in the `SpriteAnimationData.sequenced` constructor and that we are\nsetting `removeOnFinish: true;`. We do that so that when the animation is finished, it will\nautomatically be removed from the game!\n\nAnd finally, we make a small change in the `onCollisionStart()` method in the `Enemy` class\nin order to add the explosion to the game:\n\n```dart\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n\n    if (other is Bullet) {\n      removeFromParent();\n      other.removeFromParent();\n      game.add(Explosion(position: position));\n    }\n  }\n```\n\nAnd that is it! We finally have a game which provides all the minimum necessary elements for a space\nshooter, from here you can use what you learned to build more features in the game like making\nthe player suffer damage if it clashes with an enemy, or make the enemies shoot back, or maybe\nboth?\n\nGood hunting pilot, and happy coding!\n\n```{flutter-app}\n:sources: ../tutorials/space_shooter/app\n:page: step6\n:show: popup code\n```\n"
  },
  {
    "path": "doc/tutorials/tutorials.md",
    "content": "# Tutorials\n\nThe following are tutorials that are maintained with every release of Flame.\n\n- [](bare_flame_game.md) -- this tutorial focuses on setting up your environment for making a new\n  Flame game. This \"initial state\" is assumed as a starting point for all other tutorials.\n\n- [](basic_shader/basic_shader.md) -- in this tutorial, we will build and apply an outline\n  shader on sprite components.\n\n- [](klondike/klondike.md) -- in this tutorial, we will build the Klondike\n  solitaire card game.\n\n- [](platformer/platformer.md) -- in this tutorial, we will build Ember Quest, a simple\n  side-scrolling platformer.\n\n- [](space_shooter/space_shooter.md) -- in this tutorial, we will build Space Shooter, a classic\n  top-down shooting game.\n\n```{toctree}\n:hidden:\n\nBare Flame game  <bare_flame_game.md>\nBasic Shader     <basic_shader/basic_shader.md>\nKlondike         <klondike/klondike.md>\nEmber Quest      <platformer/platformer.md>\nSpace Shooter    <space_shooter/space_shooter.md>\n```\n"
  },
  {
    "path": "examples/README.md",
    "content": "[![Powered by Flame](https://img.shields.io/badge/Powered%20by-%F0%9F%94%A5-orange.svg)](https://flame-engine.org)\n[![Discord](https://img.shields.io/discord/509714518008528896.svg)](https://discord.gg/pxrBmy4)\n\n\n# Flame Examples\n\nThis is a set of small examples showcasing specific features of the Flame Engine; it's a great\nsource of learning how to use certain things.\n[See it live here](https://examples.flame-engine.org/).\n\nThis app is composed of a main menu in which you can select one of the examples and play with it.\nEach example is a standalone game and is contained within its own file, so you can easily checkout\nthe code and see how it works.\n\nFor a very simple, but complete game in Flame, check the\n[example folder inside the Flame package](https://github.com/flame-engine/flame/tree/main/packages/flame/example).\n\n\n## Help\n\nIf you have questions about this:\n\n- Check the source code, the examples are meant to be simple, short, and easy to read.\n- Check our extensive documentation, links to which can be found\n [on the main repo](https://github.com/flame-engine/flame) (faq, docs folder, code/api docs,\n tutorials, flame-awesome).\n- Join [Blue Fire's Discord](https://discord.gg/5unKpdQD78), we have a #flame channel where you can\n find lots of people to help and get help from.\n- Use the `flame` tag on StackOverflow.\n"
  },
  {
    "path": "examples/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "examples/assets/images/animations/chopper.json",
    "content": "{ \"frames\": {\n   \"chopper 0.aseprite\": {\n    \"frame\": { \"x\": 0, \"y\": 0, \"w\": 48, \"h\": 48 },\n    \"rotated\": false,\n    \"trimmed\": false,\n    \"spriteSourceSize\": { \"x\": 0, \"y\": 0, \"w\": 48, \"h\": 48 },\n    \"sourceSize\": { \"w\": 48, \"h\": 48 },\n    \"duration\": 100\n   },\n   \"chopper 1.aseprite\": {\n    \"frame\": { \"x\": 48, \"y\": 0, \"w\": 48, \"h\": 48 },\n    \"rotated\": false,\n    \"trimmed\": false,\n    \"spriteSourceSize\": { \"x\": 0, \"y\": 0, \"w\": 48, \"h\": 48 },\n    \"sourceSize\": { \"w\": 48, \"h\": 48 },\n    \"duration\": 100\n   },\n   \"chopper 2.aseprite\": {\n    \"frame\": { \"x\": 96, \"y\": 0, \"w\": 48, \"h\": 48 },\n    \"rotated\": false,\n    \"trimmed\": false,\n    \"spriteSourceSize\": { \"x\": 0, \"y\": 0, \"w\": 48, \"h\": 48 },\n    \"sourceSize\": { \"w\": 48, \"h\": 48 },\n    \"duration\": 100\n   },\n   \"chopper 3.aseprite\": {\n    \"frame\": { \"x\": 144, \"y\": 0, \"w\": 48, \"h\": 48 },\n    \"rotated\": false,\n    \"trimmed\": false,\n    \"spriteSourceSize\": { \"x\": 0, \"y\": 0, \"w\": 48, \"h\": 48 },\n    \"sourceSize\": { \"w\": 48, \"h\": 48 },\n    \"duration\": 100\n   }\n },\n \"meta\": {\n  \"app\": \"http://www.aseprite.org/\",\n  \"version\": \"1.3-dev\",\n  \"image\": \"/home/erick/projetos/gamedev/airplane-resource-pack/chopper.png\",\n  \"format\": \"RGBA8888\",\n  \"size\": { \"w\": 192, \"h\": 48 },\n  \"scale\": \"1\",\n  \"frameTags\": [\n  ],\n  \"layers\": [\n   { \"name\": \"body\", \"opacity\": 255, \"blendMode\": \"normal\" },\n   { \"name\": \"rotor\", \"opacity\": 255, \"blendMode\": \"normal\" },\n   { \"name\": \"stabilizer\", \"opacity\": 255, \"blendMode\": \"normal\" }\n  ],\n  \"slices\": [\n  ]\n }\n}\n"
  },
  {
    "path": "examples/assets/images/animations/lottieLogo.json",
    "content": "{\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":0,\"ty\":1,\"nm\":\"MASTER\",\"ks\":{\"o\":{\"k\":0},\"r\":{\"k\":0},\"p\":{\"k\":[214.457,347.822,0]},\"a\":{\"k\":[60,60,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"sw\":120,\"sh\":120,\"sc\":\"#ffffff\",\"ip\":12,\"op\":179,\"st\":0,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"S5-Y 4\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":-89.1},\"p\":{\"k\":[53.205,131.606,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[142.038,29.278],[131.282,21.807]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":76,\"s\":[87],\"e\":[50.633]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":79,\"s\":[50.633],\"e\":[0]},{\"t\":83}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":76,\"s\":[100],\"e\":[75.856]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":79,\"s\":[75.856],\"e\":[0]},{\"t\":83}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":76,\"op\":84,\"st\":40,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"S4-Y 4\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":-89.1},\"p\":{\"k\":[53.205,131.606,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[142.183,-5.112],[130.029,5.016]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":76,\"s\":[87],\"e\":[43.833]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":79,\"s\":[43.833],\"e\":[0]},{\"t\":83}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":76,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":79,\"s\":[66.356],\"e\":[0]},{\"t\":83}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":76,\"op\":84,\"st\":40,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"S3-Y 4\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":-89.1},\"p\":{\"k\":[53.205,131.606,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[147.699,13.025],[133.195,13.21]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":76,\"s\":[87],\"e\":[42.133]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":79,\"s\":[42.133],\"e\":[0]},{\"t\":83}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":76,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":79,\"s\":[66.356],\"e\":[0]},{\"t\":83}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":76,\"op\":84,\"st\":40,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"S5-Y 3\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":97.9},\"p\":{\"k\":[58.205,-39.394,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[145.677,22.22],[134.922,14.749]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":75,\"s\":[87],\"e\":[50.633]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":78,\"s\":[50.633],\"e\":[0]},{\"t\":82}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":75,\"s\":[100],\"e\":[75.856]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":78,\"s\":[75.856],\"e\":[0]},{\"t\":82}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":75,\"op\":83,\"st\":39,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"S4-Y 3\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":97.9},\"p\":{\"k\":[58.205,-39.394,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[144.429,-5.397],[132.275,4.731]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":75,\"s\":[87],\"e\":[43.833]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":78,\"s\":[43.833],\"e\":[0]},{\"t\":82}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":75,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":78,\"s\":[66.356],\"e\":[0]},{\"t\":82}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":75,\"op\":83,\"st\":39,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"S3-Y 3\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":97.9},\"p\":{\"k\":[58.205,-39.394,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[149.624,8.244],[136.648,10.156]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":75,\"s\":[87],\"e\":[42.133]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":78,\"s\":[42.133],\"e\":[0]},{\"t\":82}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":75,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":78,\"s\":[66.356],\"e\":[0]},{\"t\":82}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":75,\"op\":83,\"st\":39,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":7,\"ty\":4,\"nm\":\"S13\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[128,3.65],[78.25,3.5]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":85,\"s\":[87],\"e\":[21.233]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":90,\"s\":[21.233],\"e\":[0]},{\"t\":94}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":85,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":90,\"s\":[66.356],\"e\":[0]},{\"t\":94}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":1.5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":85,\"op\":95,\"st\":49,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":8,\"ty\":4,\"nm\":\"S12\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[119.25,-20.05],[63.5,-20.5]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":84,\"s\":[87],\"e\":[21.233]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":87,\"s\":[21.233],\"e\":[0]},{\"t\":91}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":84,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":87,\"s\":[66.356],\"e\":[0]},{\"t\":91}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":1.5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":84,\"op\":94,\"st\":48,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":9,\"ty\":4,\"nm\":\"S11\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[119.5,-45.05],[82.75,-44.75]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":80,\"s\":[87],\"e\":[21.233]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":83,\"s\":[21.233],\"e\":[0]},{\"t\":87}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":80,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":83,\"s\":[66.356],\"e\":[0]},{\"t\":87}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":1.5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":80,\"op\":90,\"st\":44,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":10,\"ty\":4,\"nm\":\"S5-Y 2\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[169.5,18.073],[137.481,11.365]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":97,\"s\":[87],\"e\":[50.633]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":100,\"s\":[50.633],\"e\":[0]},{\"t\":107}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":97,\"s\":[100],\"e\":[75.856]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":100,\"s\":[75.856],\"e\":[0]},{\"t\":107}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":97,\"op\":107,\"st\":61,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":11,\"ty\":4,\"nm\":\"S4-Y 2\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[156.45,-23.05],[132,2.75]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":97,\"s\":[87],\"e\":[43.833]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":100,\"s\":[43.833],\"e\":[0]},{\"t\":107}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":97,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":100,\"s\":[66.356],\"e\":[0]},{\"t\":107}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":97,\"op\":107,\"st\":61,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":12,\"ty\":4,\"nm\":\"S3-Y 2\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[166.731,-7.927],[136.731,7.115]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":97,\"s\":[87],\"e\":[42.133]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":100,\"s\":[42.133],\"e\":[0]},{\"t\":107}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":97,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":100,\"s\":[66.356],\"e\":[0]},{\"t\":107}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":97,\"op\":107,\"st\":61,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":13,\"ty\":4,\"nm\":\"S6-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-87.5,20.95],[-48.75,54.75]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[87],\"e\":[43.933]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":57,\"s\":[43.933],\"e\":[0]},{\"t\":64}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[100],\"e\":[70.456]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":57,\"s\":[70.456],\"e\":[0]},{\"t\":64}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":54,\"op\":64,\"st\":18,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":14,\"ty\":4,\"nm\":\"S5-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-94.5,37.073],[-48.769,55.365]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[87],\"e\":[50.633]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":57,\"s\":[50.633],\"e\":[0]},{\"t\":64}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[100],\"e\":[75.856]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":57,\"s\":[75.856],\"e\":[0]},{\"t\":64}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":54,\"op\":64,\"st\":18,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":15,\"ty\":4,\"nm\":\"S4-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[7.45,21.95],[-32.75,55.75]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[87],\"e\":[43.833]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":57,\"s\":[43.833],\"e\":[0]},{\"t\":64}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":57,\"s\":[66.356],\"e\":[0]},{\"t\":64}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":54,\"op\":64,\"st\":18,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":16,\"ty\":4,\"nm\":\"S3-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[16.231,39.073],[-32.769,57.365]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[87],\"e\":[42.133]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":57,\"s\":[42.133],\"e\":[0]},{\"t\":64}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":57,\"s\":[66.356],\"e\":[0]},{\"t\":64}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":54,\"op\":64,\"st\":18,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":17,\"ty\":4,\"nm\":\"S8\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[-0.148,14.256],[10.476,0],[0,0]],\"o\":[[0,0],[-8.551,-8.263],[-21.454,0],[0,0]],\"v\":[[-3,35.95],[-1.352,-6.756],[-32.046,-20.579],[-42.25,4.25]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":65,\"s\":[87],\"e\":[21.233]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":70,\"s\":[21.233],\"e\":[0]},{\"t\":75}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":65,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":70,\"s\":[66.356],\"e\":[0]},{\"t\":75}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":1.5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":65,\"op\":75,\"st\":29,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":18,\"ty\":4,\"nm\":\"S7\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[27,1.45],[31.046,-1.421],[0,0]],\"o\":[[-27,-1.45],[-26.426,1.21],[0,0]],\"v\":[[34.5,-13.05],[-35.046,-35.579],[-62.25,-5.75]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":65,\"s\":[87],\"e\":[21.233]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":70,\"s\":[21.233],\"e\":[0]},{\"t\":75}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":65,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":70,\"s\":[66.356],\"e\":[0]},{\"t\":75}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":1.5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":65,\"op\":75,\"st\":29,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":19,\"ty\":4,\"nm\":\"S2-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[1.9,-10.768],[1,-19]],\"o\":[[0,0],[-3.167,17.951],[-1,19]],\"v\":[[-67.25,-105.5],[-72.333,-84.201],[-76.5,-37.75]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":29,\"s\":[87],\"e\":[25.333]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":33,\"s\":[25.333],\"e\":[0]},{\"t\":36}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":29,\"s\":[100],\"e\":[69.056]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":33,\"s\":[69.056],\"e\":[0]},{\"t\":36}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":1.5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":30,\"op\":37,\"st\":-7,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":20,\"ty\":4,\"nm\":\"S1-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[1.9,-10.768],[1,-19]],\"o\":[[0,0],[-3.167,17.951],[-1,19]],\"v\":[[-67.125,-112],[-75.458,-89.951],[-80.375,-39.25]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":29,\"s\":[87],\"e\":[37.533]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":33,\"s\":[37.533],\"e\":[0]},{\"t\":36}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":29,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":33,\"s\":[66.356],\"e\":[0]},{\"t\":36}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":1.5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":30,\"op\":37,\"st\":-7,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":21,\"ty\":4,\"nm\":\"Dot1\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.823,\"y\":0},\"n\":\"0p833_0p833_0p823_0\",\"t\":-3,\"s\":[295.771,108.994,0],\"e\":[35.771,108.994,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":16}]},\"a\":{\"k\":[196.791,266.504,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"k\":[9.4,9.4]},\"p\":{\"k\":[0.8,-0.5]},\"nm\":\"Ellipse Path 1\"},{\"ty\":\"fl\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"nm\":\"Fill 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[196,267],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\"}],\"ip\":-5,\"op\":17,\"st\":-36,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":22,\"ty\":4,\"nm\":\"L-B\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[39.043,45.678,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[25.671,-4.167],[1.456,6.902],[-8.481,1.863],[-47.562,13.01],[-0.501,0.133],[-71.423,-2.315]],\"o\":[[0,0],[-8.224,1.335],[-1.456,-6.903],[23.817,-5.233],[0.16,-0.044],[0.501,-0.133],[0,0]],\"v\":[[-8.837,-58.229],[-35.834,33.662],[-51.688,23.148],[-41.174,7.293],[51.797,44.178],[53.188,43.741],[140.394,43.672]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[166.029,270.643],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 8\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.703],\"y\":[0.821]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p703_0p821_0p167_0p167\"],\"t\":18,\"s\":[80],\"e\":[50]},{\"i\":{\"x\":[0.263],\"y\":[1]},\"o\":{\"x\":[0.037],\"y\":[0.168]},\"n\":[\"0p263_1_0p037_0p168\"],\"t\":23,\"s\":[50],\"e\":[30]},{\"t\":55}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.337],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p337_1_0p167_0p167\"],\"t\":18,\"s\":[81],\"e\":[73.4]},{\"t\":29}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"}],\"ip\":18,\"op\":179,\"st\":8,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":23,\"ty\":4,\"nm\":\"L-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[39.043,45.678,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[25.671,-4.167],[1.456,6.902],[-8.481,1.863],[-47.562,13.01],[-0.501,0.133],[-71.423,-2.315]],\"o\":[[0,0],[-8.224,1.335],[-1.456,-6.903],[23.817,-5.233],[0.16,-0.044],[0.501,-0.133],[0,0]],\"v\":[[-8.837,-58.229],[-35.834,33.662],[-51.688,23.148],[-41.174,7.293],[51.797,44.178],[53.188,43.741],[140.394,43.672]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":8.4},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[166.029,270.643],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 8\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.703],\"y\":[0.857]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p703_0p857_0p167_0p167\"],\"t\":16,\"s\":[80],\"e\":[50]},{\"i\":{\"x\":[0.938],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0.202]},\"n\":[\"0p938_1_0p333_0p202\"],\"t\":20,\"s\":[50],\"e\":[0]},{\"t\":28}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.337],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p337_1_0p167_0p167\"],\"t\":16,\"s\":[81],\"e\":[73.4]},{\"t\":27}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"}],\"ip\":16,\"op\":179,\"st\":8,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":24,\"ty\":1,\"nm\":\"N\",\"parent\":0,\"ks\":{\"o\":{\"k\":0},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.26,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p26_1_0p167_0p167\",\"t\":28,\"s\":[-33.667,8.182,0],\"e\":[-33.667,-72.818,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.74,\"y\":0},\"n\":\"0p833_0p833_0p74_0\",\"t\":40,\"s\":[-33.667,-72.818,0],\"e\":[-33.667,102.057,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":54}]},\"a\":{\"k\":[60,60,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"sw\":120,\"sh\":120,\"sc\":\"#ffffff\",\"ip\":28,\"op\":54,\"st\":0,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":25,\"ty\":4,\"nm\":\"Dot-Y\",\"parent\":24,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p833_0p833_0p167_0p167\",\"t\":28,\"s\":[39.875,60,0],\"e\":[79.375,60,0],\"to\":[6.58333349227905,0,0],\"ti\":[-6.58333349227905,0,0]},{\"t\":54}]},\"a\":{\"k\":[196.791,266.504,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"k\":[9.4,9.4]},\"p\":{\"k\":[0.8,-0.5]},\"nm\":\"Ellipse Path 1\"},{\"ty\":\"fl\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"nm\":\"Fill 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[196,267],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\"}],\"ip\":28,\"op\":54,\"st\":4,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":26,\"ty\":4,\"nm\":\"T1a-B\",\"parent\":36,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[250,250,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[-0.5,9.501],[-0.048,5.655],[0.054,0.06],[0.946,1.486],[-9.967,8.05],[-40.546,0]],\"o\":[[0.031,-0.594],[0.076,-8.978],[-1.161,-1.3],[-5.939,-9.327],[24.677,-19.929],[0,0]],\"v\":[[-30.72,63.761],[-30.741,45.192],[-37.397,27.014],[-40.698,22.661],[-37.873,-7.117],[49.506,11.559]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":24.9,\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.673],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p673_1_0p167_0p167\"],\"t\":70,\"s\":[24.9],\"e\":[89.1]},{\"t\":84}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[227.677,234.375],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 9\"}],\"ip\":70,\"op\":179,\"st\":17,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":27,\"ty\":4,\"nm\":\"T2a-B\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[39.043,45.678,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[1.681,-29.992],[-1.681,29.992]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.06],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p06_1_0p167_0p167\"],\"t\":75,\"s\":[50],\"e\":[0]},{\"t\":85}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.06],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p06_1_0p167_0p167\"],\"t\":75,\"s\":[50],\"e\":[100]},{\"t\":85}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":3,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[277.698,247.258],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 7\"}],\"ip\":75,\"op\":179,\"st\":15,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":28,\"ty\":4,\"nm\":\"T1a-Y 2\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p667_1_0p167_0p167\",\"t\":56,\"s\":[39.043,48.678,0],\"e\":[39.043,45.678,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":64}]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[-0.5,9.501],[-0.048,5.655],[0.054,0.06],[0.946,1.486],[-9.967,8.05],[-40.546,0]],\"o\":[[0.031,-0.594],[0.076,-8.978],[-1.161,-1.3],[-5.939,-9.327],[24.677,-19.929],[0,0]],\"v\":[[-30.72,63.761],[-30.741,45.192],[-37.397,27.014],[-40.698,22.661],[-37.873,-7.117],[49.506,11.559]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[1]},\"o\":{\"x\":[0.301],\"y\":[0]},\"n\":[\"0p833_1_0p301_0\"],\"t\":54,\"s\":[0],\"e\":[24.9]},{\"t\":70}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.301],\"y\":[0]},\"n\":[\"0p667_1_0p301_0\"],\"t\":54,\"s\":[0],\"e\":[100]},{\"t\":78}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":8.4},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[227.677,234.375],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 9\"}],\"ip\":59,\"op\":179,\"st\":12,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":29,\"ty\":4,\"nm\":\"O-B\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p833_0p833_0p167_0p167\",\"t\":31,\"s\":[-62.792,73.057,0],\"e\":[-53.792,7.557,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.638,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.198},\"n\":\"0p638_1_0p167_0p198\",\"t\":35.257,\"s\":[-53.792,7.557,0],\"e\":[-33.667,-72.818,0],\"to\":[0,0,0],\"ti\":[-19.1562919616699,1.73831975460052,0]},{\"i\":{\"x\":0.795,\"y\":1},\"o\":{\"x\":0.523,\"y\":0},\"n\":\"0p795_1_0p523_0\",\"t\":44,\"s\":[-33.667,-72.818,0],\"e\":[-14.167,102.182,0],\"to\":[16.2075271606445,-1.47073686122894,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.348,\"y\":1},\"o\":{\"x\":0.18,\"y\":0},\"n\":\"0p348_1_0p18_0\",\"t\":54,\"s\":[-14.167,102.182,0],\"e\":[-14.167,59.182,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.27,\"y\":1},\"o\":{\"x\":0.693,\"y\":0},\"n\":\"0p27_1_0p693_0\",\"t\":63,\"s\":[-14.167,59.182,0],\"e\":[-14.167,62.182,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":73}]},\"a\":{\"k\":[196.791,266.504,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"k\":[{\"i\":{\"x\":[0.667,0.667],\"y\":[1,1]},\"o\":{\"x\":[0.333,0.333],\"y\":[0,0]},\"n\":[\"0p667_1_0p333_0\",\"0p667_1_0p333_0\"],\"t\":54,\"s\":[3,3],\"e\":[44.6,44.6]},{\"t\":61}]},\"p\":{\"k\":[0.8,-0.5]},\"nm\":\"Ellipse Path 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[196,267],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[0],\"e\":[30]},{\"i\":{\"x\":[0.432],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[1.124]},\"n\":[\"0p432_1_0p167_1p124\"],\"t\":63,\"s\":[30],\"e\":[39.9]},{\"t\":91}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[100],\"e\":[88]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":63,\"s\":[88],\"e\":[88]},{\"t\":91}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"}],\"ip\":54,\"op\":179,\"st\":4,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":30,\"ty\":4,\"nm\":\"O-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p833_0p833_0p167_0p167\",\"t\":31,\"s\":[-62.792,73.057,0],\"e\":[-53.792,7.557,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.638,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.198},\"n\":\"0p638_1_0p167_0p198\",\"t\":35.257,\"s\":[-53.792,7.557,0],\"e\":[-33.667,-72.818,0],\"to\":[0,0,0],\"ti\":[-19.1562919616699,1.73831975460052,0]},{\"i\":{\"x\":0.795,\"y\":1},\"o\":{\"x\":0.523,\"y\":0},\"n\":\"0p795_1_0p523_0\",\"t\":44,\"s\":[-33.667,-72.818,0],\"e\":[-14.167,102.182,0],\"to\":[16.2075271606445,-1.47073686122894,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.348,\"y\":1},\"o\":{\"x\":0.18,\"y\":0},\"n\":\"0p348_1_0p18_0\",\"t\":54,\"s\":[-14.167,102.182,0],\"e\":[-14.167,59.182,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.27,\"y\":1},\"o\":{\"x\":0.693,\"y\":0},\"n\":\"0p27_1_0p693_0\",\"t\":63,\"s\":[-14.167,59.182,0],\"e\":[-14.167,62.182,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":73}]},\"a\":{\"k\":[196.791,266.504,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"k\":[{\"i\":{\"x\":[0.667,0.667],\"y\":[1,1]},\"o\":{\"x\":[0.333,0.333],\"y\":[0,0]},\"n\":[\"0p667_1_0p333_0\",\"0p667_1_0p333_0\"],\"t\":54,\"s\":[3,3],\"e\":[44.6,44.6]},{\"t\":61}]},\"p\":{\"k\":[0.8,-0.5]},\"nm\":\"Ellipse Path 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":8.8},\"lc\":1,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[196,267],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\"}],\"ip\":54,\"op\":179,\"st\":4,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":31,\"ty\":4,\"nm\":\"T1b-B\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[39.043,45.678,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[1.768,-25.966],[-1.768,25.966]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":0,\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.21],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p21_1_0p167_0p167\"],\"t\":81,\"s\":[11.7],\"e\":[100]},{\"t\":88}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":2,\"lj\":2,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[242.756,265.581],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 10\"}],\"ip\":81,\"op\":179,\"st\":26,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":32,\"ty\":4,\"nm\":\"T1b-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[39.043,45.678,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[1.768,-25.966],[-1.768,25.966]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":70,\"s\":[0],\"e\":[0]},{\"t\":75}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":70,\"s\":[11.7],\"e\":[100]},{\"t\":75}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":8.4},\"lc\":2,\"lj\":2,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[242.756,265.581],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 10\"}],\"ip\":70,\"op\":161,\"st\":15,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":33,\"ty\":4,\"nm\":\"T2b-B\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[39.043,45.678,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[246.65,213.814],[340.956,213.628]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":82,\"s\":[29],\"e\":[0]},{\"t\":91}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":82,\"s\":[41.1],\"e\":[66.5]},{\"t\":91}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 5\"}],\"ip\":82,\"op\":179,\"st\":-17,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":34,\"ty\":4,\"nm\":\"T2a-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[39.043,45.678,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[1.681,-29.992],[-1.681,29.992]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.06],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p06_1_0p167_0p167\"],\"t\":72,\"s\":[50],\"e\":[0]},{\"t\":82}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.06],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p06_1_0p167_0p167\"],\"t\":72,\"s\":[50],\"e\":[100]},{\"t\":82}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":3,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[277.698,247.258],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 7\"}],\"ip\":72,\"op\":89,\"st\":12,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":35,\"ty\":4,\"nm\":\"T2b-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[39.043,45.678,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[246.65,213.814],[340.956,213.628]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":76,\"s\":[29],\"e\":[0]},{\"t\":85}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":76,\"s\":[41.1],\"e\":[66.5]},{\"t\":85}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 5\"}],\"ip\":76,\"op\":92,\"st\":-23,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":36,\"ty\":4,\"nm\":\"T1a-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p667_1_0p167_0p167\",\"t\":56,\"s\":[39.043,48.678,0],\"e\":[39.043,45.678,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":64}]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[-0.5,9.501],[-0.048,5.655],[0.054,0.06],[0.946,1.486],[-9.967,8.05],[-40.546,0]],\"o\":[[0.031,-0.594],[0.076,-8.978],[-1.161,-1.3],[-5.939,-9.327],[24.677,-19.929],[0,0]],\"v\":[[-30.72,63.761],[-30.741,45.192],[-37.397,27.014],[-40.698,22.661],[-37.873,-7.117],[49.506,11.559]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[1]},\"o\":{\"x\":[0.301],\"y\":[0]},\"n\":[\"0p833_1_0p301_0\"],\"t\":54,\"s\":[0],\"e\":[24.9]},{\"t\":70}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.301],\"y\":[0]},\"n\":[\"0p667_1_0p301_0\"],\"t\":54,\"s\":[0],\"e\":[100]},{\"t\":74}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":8.4},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[227.677,234.375],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 9\"}],\"ip\":59,\"op\":156,\"st\":12,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":37,\"ty\":4,\"nm\":\"E1-B\",\"parent\":38,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[344.672,214.842,0]},\"a\":{\"k\":[344.672,214.842,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-13.664,-0.145],[62.163,0.29]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.562},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[344.672,214.842],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[0.12]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_0p12_0p167_0p167\"],\"t\":84,\"s\":[0],\"e\":[0]},{\"t\":93}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":84,\"s\":[0],\"e\":[37.5]},{\"t\":93}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"}],\"ip\":84,\"op\":179,\"st\":84,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":38,\"ty\":4,\"nm\":\"E1-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.12,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p12_1_0p167_0p167\",\"t\":79,\"s\":[113.715,9.146,0],\"e\":[137.715,9.146,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.12,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"n\":\"0p12_1_0p167_0\",\"t\":88,\"s\":[137.715,9.146,0],\"e\":[133.715,9.146,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":92}]},\"a\":{\"k\":[344.672,214.842,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-13.664,-0.145],[62.163,0.29]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":8.4},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[344.672,214.842],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[0.12]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_0p12_0p167_0p167\"],\"t\":79,\"s\":[0],\"e\":[0]},{\"t\":88}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":79,\"s\":[0],\"e\":[37.5]},{\"t\":88}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"}],\"ip\":79,\"op\":94,\"st\":79,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":39,\"ty\":4,\"nm\":\"E2-B\",\"parent\":40,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[332.05,237.932,0]},\"a\":{\"k\":[332.05,237.932,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-26.67,-0.283],[99.171,0.066]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[0.12]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_0p12_0p167_0p167\"],\"t\":86,\"s\":[0],\"e\":[0]},{\"t\":95}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":86,\"s\":[0],\"e\":[43]},{\"t\":95}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.562},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[331.664,238.14],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 3\"}],\"ip\":86,\"op\":179,\"st\":86,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":40,\"ty\":4,\"nm\":\"E2-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.12,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p12_1_0p167_0p167\",\"t\":83,\"s\":[109.092,33.61,0],\"e\":[121.092,33.61,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.12,\"y\":0.12},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p12_0p12_0p167_0p167\",\"t\":92,\"s\":[121.092,33.61,0],\"e\":[121.092,33.61,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":96}]},\"a\":{\"k\":[332.05,237.932,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-26.67,-0.283],[99.171,0.066]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[0.12]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_0p12_0p167_0p167\"],\"t\":83,\"s\":[0],\"e\":[0]},{\"t\":92}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":83,\"s\":[0],\"e\":[43]},{\"t\":92}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":8.4},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[331.664,238.14],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 3\"}],\"ip\":83,\"op\":96,\"st\":83,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":41,\"ty\":4,\"nm\":\"I-B\",\"parent\":42,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[303.802,282.182,0]},\"a\":{\"k\":[303.802,282.182,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[0.859,-21.143],[-4.359,70.392]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[0.12]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_0p12_0p167_0p167\"],\"t\":81,\"s\":[0],\"e\":[0]},{\"t\":91}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":81,\"s\":[0],\"e\":[45.7]},{\"t\":91}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":3,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[304.135,282.409],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 6\"}],\"ip\":81,\"op\":179,\"st\":18,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":42,\"ty\":4,\"nm\":\"I-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.12,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p12_1_0p167_0p167\",\"t\":78,\"s\":[93.594,62.861,0],\"e\":[92.626,82.829,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.12,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"n\":\"0p12_1_0p167_0\",\"t\":88,\"s\":[92.626,82.829,0],\"e\":[92.844,77.861,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":92}]},\"a\":{\"k\":[303.802,282.182,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[0.859,-21.143],[-4.359,70.392]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[0.12]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_0p12_0p167_0p167\"],\"t\":78,\"s\":[0],\"e\":[0]},{\"t\":88}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":78,\"s\":[0],\"e\":[45.7]},{\"t\":88}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":8.4},\"lc\":3,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[304.135,282.409],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 6\"}],\"ip\":78,\"op\":93,\"st\":15,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":43,\"ty\":4,\"nm\":\"E3-B\",\"parent\":44,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[345.189,261.801,0]},\"a\":{\"k\":[345.124,261.801,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-13.664,-0.145],[75.663,0.29]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":92,\"s\":[0],\"e\":[0]},{\"t\":97}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":92,\"s\":[0],\"e\":[31.6]},{\"t\":97}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 2\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.562},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[344.674,261.877],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\"}],\"ip\":92,\"op\":179,\"st\":29,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":44,\"ty\":4,\"nm\":\"E3-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p667_1_0p167_0p167\",\"t\":84,\"s\":[119.167,57.479,0],\"e\":[137.167,57.479,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"n\":\"0p667_1_0p167_0\",\"t\":92,\"s\":[137.167,57.479,0],\"e\":[134.167,57.479,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":96}]},\"a\":{\"k\":[345.124,261.801,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-13.664,-0.145],[75.663,0.29]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":84,\"s\":[0],\"e\":[0]},{\"t\":92}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":84,\"s\":[0],\"e\":[31.6]},{\"t\":92}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 2\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.562},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[344.674,261.877],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\"}],\"ip\":84,\"op\":102,\"st\":21,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":45,\"ty\":4,\"nm\":\"Dot-Y\",\"parent\":46,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0,\"y\":0.812},\"o\":{\"x\":0,\"y\":0},\"n\":\"0_0p812_0_0\",\"t\":96,\"s\":[43.263,59.75,0],\"e\":[62.513,59.75,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.708,\"y\":1},\"o\":{\"x\":0.39,\"y\":0.707},\"n\":\"0p708_1_0p39_0p707\",\"t\":108,\"s\":[62.513,59.75,0],\"e\":[63.763,59.75,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":115}]},\"a\":{\"k\":[196.791,266.504,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"k\":[9.2,9.2]},\"p\":{\"k\":[0.8,-0.5]},\"nm\":\"Ellipse Path 1\"},{\"ty\":\"fl\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"nm\":\"Fill 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[196,267],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\"}],\"ip\":96,\"op\":182,\"st\":65,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":46,\"ty\":1,\"nm\":\"Bncr\",\"parent\":0,\"ks\":{\"o\":{\"k\":0},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.18,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p18_1_0p167_0p167\",\"t\":96,\"s\":[164.782,57.473,0],\"e\":[164.782,55.473,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.82,\"y\":0},\"n\":\"0p833_0p833_0p82_0\",\"t\":99,\"s\":[164.782,55.473,0],\"e\":[164.782,57.473,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.18,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p18_1_0p167_0p167\",\"t\":102,\"s\":[164.782,57.473,0],\"e\":[164.782,56.909,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.82,\"y\":0},\"n\":\"0p833_0p833_0p82_0\",\"t\":105,\"s\":[164.782,56.909,0],\"e\":[164.782,57.473,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":108}]},\"a\":{\"k\":[60,60,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"sw\":120,\"sh\":120,\"sc\":\"#ffffff\",\"ip\":96,\"op\":182,\"st\":15,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":47,\"ty\":4,\"nm\":\"BG\",\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[187.5,333.5,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"k\":[375,667]},\"p\":{\"k\":[0,0]},\"r\":{\"k\":0},\"nm\":\"Rectangle Path 1\"},{\"ty\":\"fl\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.82,0.76,1]},\"o\":{\"k\":100},\"nm\":\"Fill 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle 1\"}],\"ip\":0,\"op\":179,\"st\":0,\"bm\":0,\"sr\":1}],\"v\":\"4.4.26\",\"ddd\":0,\"ip\":0,\"op\":179,\"fr\":30,\"w\":375,\"h\":667}"
  },
  {
    "path": "examples/assets/images/parallax/license.txt",
    "content": "Artwork created by Luis Zuno (@ansimuz)\r\n\r\nLicense (CC0) You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission: http://creativecommons.org/publicdomain/zero/1.0/\r\n\r\nGet more resources at pixelgameart.org, Spread the word!\r\n\r\n"
  },
  {
    "path": "examples/assets/spine/LICENSE",
    "content": "Copyright (c) 2013, Esoteric Software\n\nThe images in this project may be redistributed as long as they are accompanied\nby this license file. The images may not be used for commercial use of any\nkind.\n\nThe project file is released into the public domain. It may be used as the basis\nfor derivative work."
  },
  {
    "path": "examples/assets/spine/spineboy-pro.json",
    "content": "{\n\"skeleton\": {\n\t\"hash\": \"F9h6lQBU32U\",\n\t\"spine\": \"4.3.39-beta\",\n\t\"x\": -188.63379,\n\t\"y\": -7.9395638,\n\t\"width\": 418.44992,\n\t\"height\": 686.20233,\n\t\"images\": \"./images/\",\n\t\"audio\": null\n},\n\"bones\": [\n\t{ \"name\": \"root\", \"rotation\": 0.04774475 },\n\t{ \"name\": \"hip\", \"parent\": \"root\", \"y\": 247.26994 },\n\t{ \"name\": \"crosshair\", \"parent\": \"root\", \"x\": 302.83084, \"y\": 569.4487, \"color\": \"ff3f00ff\", \"icon\": \"circle\" },\n\t{\n\t\t\"name\": \"aim-constraint-target\",\n\t\t\"parent\": \"hip\",\n\t\t\"length\": 26.241163,\n\t\t\"rotation\": 19.610935,\n\t\t\"x\": 1.0154866,\n\t\t\"y\": 5.6218414,\n\t\t\"color\": \"abe323ff\"\n\t},\n\t{ \"name\": \"rear-foot-target\", \"parent\": \"root\", \"x\": 61.908287, \"y\": 0.42114258, \"color\": \"ff3f00ff\", \"icon\": \"ik\" },\n\t{ \"name\": \"rear-leg-target\", \"parent\": \"rear-foot-target\", \"x\": -33.908966, \"y\": 37.33794, \"color\": \"ff3f00ff\", \"icon\": \"ik\" },\n\t{\n\t\t\"name\": \"rear-thigh\",\n\t\t\"parent\": \"hip\",\n\t\t\"length\": 85.71959,\n\t\t\"rotation\": -72.541,\n\t\t\"x\": 8.913322,\n\t\t\"y\": -5.6266937,\n\t\t\"color\": \"ff000dff\"\n\t},\n\t{\n\t\t\"name\": \"rear-shin\",\n\t\t\"parent\": \"rear-thigh\",\n\t\t\"length\": 121.87728,\n\t\t\"rotation\": -19.833338,\n\t\t\"x\": 86.100006,\n\t\t\"y\": -1.3253326,\n\t\t\"color\": \"ff000dff\"\n\t},\n\t{\n\t\t\"name\": \"rear-foot\",\n\t\t\"parent\": \"rear-shin\",\n\t\t\"length\": 51.581825,\n\t\t\"rotation\": 45.778595,\n\t\t\"x\": 121.45904,\n\t\t\"y\": -0.7551935,\n\t\t\"color\": \"ff000dff\"\n\t},\n\t{\n\t\t\"name\": \"back-foot-tip\",\n\t\t\"parent\": \"rear-foot\",\n\t\t\"length\": 50.301598,\n\t\t\"rotation\": -0.85201645,\n\t\t\"x\": 51.16588,\n\t\t\"y\": 0.23815536,\n\t\t\"inherit\": \"noRotationOrReflection\",\n\t\t\"color\": \"ff000dff\"\n\t},\n\t{ \"name\": \"board-ik\", \"parent\": \"root\", \"x\": -131.77533, \"y\": 69.08683, \"color\": \"4c56ffff\", \"icon\": \"arrows\" },\n\t{ \"name\": \"clipping\", \"parent\": \"root\" },\n\t{\n\t\t\"name\": \"hoverboard-controller\",\n\t\t\"parent\": \"root\",\n\t\t\"rotation\": -0.28445435,\n\t\t\"x\": -329.6916,\n\t\t\"y\": 69.82341,\n\t\t\"color\": \"ff0004ff\",\n\t\t\"icon\": \"arrowsB\"\n\t},\n\t{ \"name\": \"exhaust1\", \"parent\": \"hoverboard-controller\", \"rotation\": 3.0205064, \"x\": -249.68484, \"y\": 53.390816 },\n\t{ \"name\": \"exhaust2\", \"parent\": \"hoverboard-controller\", \"rotation\": 26.335474, \"x\": -191.6049, \"y\": -22.92239 },\n\t{\n\t\t\"name\": \"exhaust3\",\n\t\t\"parent\": \"hoverboard-controller\",\n\t\t\"rotation\": -12.341045,\n\t\t\"x\": -236.02995,\n\t\t\"y\": 80.54244,\n\t\t\"scaleX\": 0.78467643,\n\t\t\"scaleY\": 0.78467643\n\t},\n\t{ \"name\": \"portal-root\", \"parent\": \"root\", \"x\": 12.901611, \"y\": 328.53928, \"scaleX\": 2.0333827, \"scaleY\": 2.0333827 },\n\t{ \"name\": \"flare1\", \"parent\": \"portal-root\", \"x\": -6.3449063, \"y\": -161.57274, \"icon\": \"particles\" },\n\t{ \"name\": \"flare10\", \"parent\": \"portal-root\", \"x\": -6.3449063, \"y\": -161.57274, \"icon\": \"particles\" },\n\t{ \"name\": \"flare2\", \"parent\": \"portal-root\", \"x\": -6.3449063, \"y\": -161.57274, \"icon\": \"particles\" },\n\t{ \"name\": \"flare3\", \"parent\": \"portal-root\", \"x\": -6.3449063, \"y\": -161.57274, \"icon\": \"particles\" },\n\t{ \"name\": \"flare4\", \"parent\": \"portal-root\", \"x\": -6.3449063, \"y\": -161.57274, \"icon\": \"particles\" },\n\t{ \"name\": \"flare5\", \"parent\": \"portal-root\", \"x\": -6.3449063, \"y\": -161.57274, \"icon\": \"particles\" },\n\t{ \"name\": \"flare6\", \"parent\": \"portal-root\", \"x\": -6.3449063, \"y\": -161.57274, \"icon\": \"particles\" },\n\t{ \"name\": \"flare7\", \"parent\": \"portal-root\", \"x\": -6.3449063, \"y\": -161.57274, \"icon\": \"particles\" },\n\t{ \"name\": \"flare8\", \"parent\": \"portal-root\", \"x\": -6.3449063, \"y\": -161.57274, \"icon\": \"particles\" },\n\t{ \"name\": \"flare9\", \"parent\": \"portal-root\", \"x\": -6.3449063, \"y\": -161.57274, \"icon\": \"particles\" },\n\t{\n\t\t\"name\": \"torso\",\n\t\t\"parent\": \"hip\",\n\t\t\"length\": 42.519165,\n\t\t\"rotation\": 103.82454,\n\t\t\"x\": -1.6175156,\n\t\t\"y\": 4.904114,\n\t\t\"color\": \"e0da19ff\"\n\t},\n\t{ \"name\": \"torso2\", \"parent\": \"torso\", \"length\": 42.519165, \"x\": 42.519165, \"color\": \"e0da19ff\" },\n\t{ \"name\": \"torso3\", \"parent\": \"torso2\", \"length\": 42.519165, \"x\": 42.519165, \"color\": \"e0da19ff\" },\n\t{ \"name\": \"front-shoulder\", \"parent\": \"torso3\", \"rotation\": 255.89099, \"x\": 18.721344, \"y\": 19.329266, \"color\": \"00ff04ff\" },\n\t{ \"name\": \"front-upper-arm\", \"parent\": \"front-shoulder\", \"length\": 69.452255, \"rotation\": -87.51462, \"color\": \"00ff04ff\" },\n\t{\n\t\t\"name\": \"front-bracer\",\n\t\t\"parent\": \"front-upper-arm\",\n\t\t\"length\": 40.570087,\n\t\t\"rotation\": 18.2995,\n\t\t\"x\": 68.80316,\n\t\t\"y\": -0.68387413,\n\t\t\"color\": \"00ff04ff\"\n\t},\n\t{\n\t\t\"name\": \"front-fist\",\n\t\t\"parent\": \"front-bracer\",\n\t\t\"length\": 65.3853,\n\t\t\"rotation\": 12.433234,\n\t\t\"x\": 40.56961,\n\t\t\"y\": 0.19889641,\n\t\t\"color\": \"00ff04ff\"\n\t},\n\t{ \"name\": \"front-foot-target\", \"parent\": \"root\", \"x\": -13.529625, \"y\": 0.040145874, \"color\": \"ff3f00ff\", \"icon\": \"ik\" },\n\t{ \"name\": \"front-leg-target\", \"parent\": \"front-foot-target\", \"x\": -28.399971, \"y\": 29.062645, \"color\": \"ff3f00ff\", \"icon\": \"ik\" },\n\t{\n\t\t\"name\": \"front-thigh\",\n\t\t\"parent\": \"hip\",\n\t\t\"length\": 74.80652,\n\t\t\"rotation\": -95.511925,\n\t\t\"x\": -17.458992,\n\t\t\"y\": -11.644302,\n\t\t\"color\": \"00ff04ff\"\n\t},\n\t{\n\t\t\"name\": \"front-shin\",\n\t\t\"parent\": \"front-thigh\",\n\t\t\"length\": 128.76756,\n\t\t\"rotation\": -2.213562,\n\t\t\"x\": 78.690125,\n\t\t\"y\": 1.6017075,\n\t\t\"color\": \"00ff04ff\"\n\t},\n\t{\n\t\t\"name\": \"front-foot\",\n\t\t\"parent\": \"front-shin\",\n\t\t\"length\": 41.00884,\n\t\t\"rotation\": 51.26577,\n\t\t\"x\": 128.7557,\n\t\t\"y\": -0.33932877,\n\t\t\"color\": \"00ff04ff\"\n\t},\n\t{\n\t\t\"name\": \"front-foot-tip\",\n\t\t\"parent\": \"front-foot\",\n\t\t\"length\": 56.02933,\n\t\t\"rotation\": -1.6757278,\n\t\t\"x\": 41.424618,\n\t\t\"y\": -0.08879948,\n\t\t\"inherit\": \"noRotationOrReflection\",\n\t\t\"color\": \"00ff04ff\"\n\t},\n\t{ \"name\": \"back-shoulder\", \"parent\": \"torso3\", \"rotation\": -104.10899, \"x\": 7.318115, \"y\": -19.220879, \"color\": \"ff000dff\" },\n\t{ \"name\": \"rear-upper-arm\", \"parent\": \"back-shoulder\", \"length\": 51.93936, \"rotation\": -65.446434, \"color\": \"ff000dff\" },\n\t{ \"name\": \"rear-bracer\", \"parent\": \"rear-upper-arm\", \"length\": 34.55841, \"rotation\": 23.15295, \"x\": 51.357986, \"color\": \"ff000dff\" },\n\t{\n\t\t\"name\": \"gun\",\n\t\t\"parent\": \"rear-bracer\",\n\t\t\"length\": 43.10579,\n\t\t\"rotation\": -5.4277267,\n\t\t\"x\": 34.421886,\n\t\t\"y\": -0.4539795,\n\t\t\"color\": \"ff000dff\"\n\t},\n\t{ \"name\": \"gun-tip\", \"parent\": \"gun\", \"rotation\": 7.0998316, \"x\": 200.77574, \"y\": 52.498734, \"color\": \"ff0000ff\" },\n\t{\n\t\t\"name\": \"neck\",\n\t\t\"parent\": \"torso3\",\n\t\t\"length\": 25.453945,\n\t\t\"rotation\": -31.53788,\n\t\t\"x\": 42.458893,\n\t\t\"y\": -0.30856323,\n\t\t\"color\": \"e0da19ff\"\n\t},\n\t{\n\t\t\"name\": \"head\",\n\t\t\"parent\": \"neck\",\n\t\t\"length\": 131.79367,\n\t\t\"rotation\": 26.09855,\n\t\t\"x\": 27.663269,\n\t\t\"y\": -0.25990295,\n\t\t\"color\": \"e0da19ff\"\n\t},\n\t{\n\t\t\"name\": \"hair1\",\n\t\t\"parent\": \"head\",\n\t\t\"length\": 47.229755,\n\t\t\"rotation\": -49.102795,\n\t\t\"x\": 149.82922,\n\t\t\"y\": -59.770554,\n\t\t\"color\": \"e0da19ff\"\n\t},\n\t{\n\t\t\"name\": \"hair2\",\n\t\t\"parent\": \"hair1\",\n\t\t\"length\": 55.56698,\n\t\t\"rotation\": 50.41702,\n\t\t\"x\": 47.22934,\n\t\t\"y\": 0.18682861,\n\t\t\"color\": \"e0da19ff\"\n\t},\n\t{\n\t\t\"name\": \"hair3\",\n\t\t\"parent\": \"head\",\n\t\t\"length\": 62.22058,\n\t\t\"rotation\": -32.167114,\n\t\t\"x\": 164.13812,\n\t\t\"y\": 3.684761,\n\t\t\"color\": \"e0da19ff\"\n\t},\n\t{\n\t\t\"name\": \"hair4\",\n\t\t\"parent\": \"hair3\",\n\t\t\"length\": 80.28074,\n\t\t\"rotation\": 83.70933,\n\t\t\"x\": 62.22061,\n\t\t\"y\": -0.03564453,\n\t\t\"color\": \"e0da19ff\"\n\t},\n\t{ \"name\": \"hoverboard-thruster-front\", \"parent\": \"hoverboard-controller\", \"rotation\": -29.204151, \"x\": 95.77119, \"y\": -2.9910965, \"inherit\": \"noRotationOrReflection\" },\n\t{ \"name\": \"hoverboard-thruster-rear\", \"parent\": \"hoverboard-controller\", \"rotation\": -29.204151, \"x\": -76.471695, \"y\": -4.877556, \"inherit\": \"noRotationOrReflection\" },\n\t{ \"name\": \"hoverglow-front\", \"parent\": \"hoverboard-thruster-front\", \"rotation\": 0.17448044, \"x\": -1.775383, \"y\": -37.785416 },\n\t{ \"name\": \"hoverglow-rear\", \"parent\": \"hoverboard-thruster-rear\", \"rotation\": 0.17448044, \"x\": 1.0581818, \"y\": -35.656784 },\n\t{\n\t\t\"name\": \"muzzle\",\n\t\t\"parent\": \"rear-bracer\",\n\t\t\"rotation\": 3.0575485,\n\t\t\"x\": 242.3435,\n\t\t\"y\": 34.26361,\n\t\t\"color\": \"ffb900ff\",\n\t\t\"icon\": \"muzzleFlash\"\n\t},\n\t{ \"name\": \"muzzle-ring\", \"parent\": \"muzzle\", \"color\": \"ffb900ff\" },\n\t{ \"name\": \"muzzle-ring2\", \"parent\": \"muzzle\", \"color\": \"ffb900ff\" },\n\t{ \"name\": \"muzzle-ring3\", \"parent\": \"muzzle\", \"color\": \"ffb900ff\" },\n\t{ \"name\": \"muzzle-ring4\", \"parent\": \"muzzle\", \"color\": \"ffb900ff\" },\n\t{ \"name\": \"portal\", \"parent\": \"portal-root\" },\n\t{ \"name\": \"portal-shade\", \"parent\": \"portal-root\" },\n\t{ \"name\": \"portal-streaks1\", \"parent\": \"portal-root\" },\n\t{ \"name\": \"portal-streaks2\", \"parent\": \"portal-root\" },\n\t{ \"name\": \"side-glow1\", \"parent\": \"hoverboard-controller\", \"x\": -110.559875, \"y\": 2.6199722, \"color\": \"000effff\" },\n\t{\n\t\t\"name\": \"side-glow2\",\n\t\t\"parent\": \"hoverboard-controller\",\n\t\t\"x\": -110.559875,\n\t\t\"y\": 2.6199722,\n\t\t\"scaleX\": 0.73804706,\n\t\t\"scaleY\": 0.73804706,\n\t\t\"color\": \"000effff\"\n\t},\n\t{ \"name\": \"head-control\", \"parent\": \"head\", \"x\": 110.211395, \"color\": \"00a220ff\", \"icon\": \"arrows\" }\n],\n\"slots\": [\n\t{ \"name\": \"portal-bg\", \"bone\": \"portal\" },\n\t{ \"name\": \"portal-shade\", \"bone\": \"portal-shade\" },\n\t{ \"name\": \"portal-streaks2\", \"bone\": \"portal-streaks2\", \"blend\": \"additive\" },\n\t{ \"name\": \"portal-streaks1\", \"bone\": \"portal-streaks1\", \"blend\": \"additive\" },\n\t{ \"name\": \"portal-flare8\", \"bone\": \"flare8\", \"color\": \"c3cbffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"portal-flare9\", \"bone\": \"flare9\", \"color\": \"c3cbffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"portal-flare10\", \"bone\": \"flare10\", \"color\": \"c3cbffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"clipping\", \"bone\": \"clipping\" },\n\t{ \"name\": \"exhaust3\", \"bone\": \"exhaust3\", \"color\": \"5eb4ffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"hoverboard-thruster-rear\", \"bone\": \"hoverboard-thruster-rear\" },\n\t{ \"name\": \"hoverboard-thruster-front\", \"bone\": \"hoverboard-thruster-front\" },\n\t{ \"name\": \"hoverboard-board\", \"bone\": \"hoverboard-controller\" },\n\t{ \"name\": \"side-glow1\", \"bone\": \"side-glow1\", \"color\": \"ff8686ff\", \"blend\": \"additive\" },\n\t{ \"name\": \"side-glow3\", \"bone\": \"side-glow1\", \"color\": \"ff8686ff\", \"blend\": \"additive\" },\n\t{ \"name\": \"side-glow2\", \"bone\": \"side-glow2\", \"color\": \"ff8686ff\", \"blend\": \"additive\" },\n\t{ \"name\": \"hoverglow-front\", \"bone\": \"hoverglow-front\", \"color\": \"5eb4ffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"hoverglow-rear\", \"bone\": \"hoverglow-rear\", \"color\": \"5eb4ffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"exhaust1\", \"bone\": \"exhaust2\", \"color\": \"5eb4ffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"exhaust2\", \"bone\": \"exhaust1\", \"color\": \"5eb4ffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"rear-upper-arm\", \"bone\": \"rear-upper-arm\", \"attachment\": \"rear-upper-arm\" },\n\t{ \"name\": \"rear-bracer\", \"bone\": \"rear-bracer\", \"attachment\": \"rear-bracer\" },\n\t{ \"name\": \"gun\", \"bone\": \"gun\", \"attachment\": \"gun\" },\n\t{ \"name\": \"rear-foot\", \"bone\": \"rear-foot\", \"attachment\": \"rear-foot\" },\n\t{ \"name\": \"rear-thigh\", \"bone\": \"rear-thigh\", \"attachment\": \"rear-thigh\" },\n\t{ \"name\": \"rear-shin\", \"bone\": \"rear-shin\", \"attachment\": \"rear-shin\" },\n\t{ \"name\": \"neck\", \"bone\": \"neck\", \"attachment\": \"neck\" },\n\t{ \"name\": \"torso\", \"bone\": \"torso\", \"attachment\": \"torso\" },\n\t{ \"name\": \"front-upper-arm\", \"bone\": \"front-upper-arm\", \"attachment\": \"front-upper-arm\" },\n\t{ \"name\": \"head\", \"bone\": \"head\", \"attachment\": \"head\" },\n\t{ \"name\": \"eye\", \"bone\": \"head\", \"attachment\": \"eye-indifferent\" },\n\t{ \"name\": \"front-thigh\", \"bone\": \"front-thigh\", \"attachment\": \"front-thigh\" },\n\t{ \"name\": \"front-foot\", \"bone\": \"front-foot\", \"attachment\": \"front-foot\" },\n\t{ \"name\": \"front-shin\", \"bone\": \"front-shin\", \"attachment\": \"front-shin\" },\n\t{ \"name\": \"mouth\", \"bone\": \"head\", \"attachment\": \"mouth-smile\" },\n\t{ \"name\": \"goggles\", \"bone\": \"head\", \"attachment\": \"goggles\" },\n\t{ \"name\": \"front-bracer\", \"bone\": \"front-bracer\", \"attachment\": \"front-bracer\" },\n\t{ \"name\": \"front-fist\", \"bone\": \"front-fist\", \"attachment\": \"front-fist-closed\" },\n\t{ \"name\": \"muzzle\", \"bone\": \"muzzle\" },\n\t{ \"name\": \"head-bb\", \"bone\": \"head\" },\n\t{ \"name\": \"portal-flare1\", \"bone\": \"flare1\", \"color\": \"c3cbffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"portal-flare2\", \"bone\": \"flare2\", \"color\": \"c3cbffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"portal-flare3\", \"bone\": \"flare3\", \"color\": \"c3cbffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"portal-flare4\", \"bone\": \"flare4\", \"color\": \"c3cbffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"portal-flare5\", \"bone\": \"flare5\", \"color\": \"c3cbffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"portal-flare6\", \"bone\": \"flare6\", \"color\": \"c3cbffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"portal-flare7\", \"bone\": \"flare7\", \"color\": \"c3cbffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"crosshair\", \"bone\": \"crosshair\" },\n\t{ \"name\": \"muzzle-glow\", \"bone\": \"gun-tip\", \"color\": \"ffffff00\", \"blend\": \"additive\" },\n\t{ \"name\": \"muzzle-ring\", \"bone\": \"muzzle-ring\", \"color\": \"d8baffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"muzzle-ring2\", \"bone\": \"muzzle-ring2\", \"color\": \"d8baffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"muzzle-ring3\", \"bone\": \"muzzle-ring3\", \"color\": \"d8baffff\", \"blend\": \"additive\" },\n\t{ \"name\": \"muzzle-ring4\", \"bone\": \"muzzle-ring4\", \"color\": \"d8baffff\", \"blend\": \"additive\" }\n],\n\"constraints\": [\n\t{\n\t\t\"type\": \"transform\",\n\t\t\"name\": \"shoulder\",\n\t\t\"source\": \"front-shoulder\",\n\t\t\"bones\": [ \"back-shoulder\" ],\n\t\t\"x\": 40.166965,\n\t\t\"y\": -1.6619568,\n\t\t\"properties\": {\n\t\t\t\"x\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"x\": { \"max\": 100 }\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"y\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"y\": { \"max\": 100 }\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"mixX\": -1\n\t},\n\t{\n\t\t\"type\": \"ik\",\n\t\t\"name\": \"board-ik\",\n\t\t\"target\": \"board-ik\",\n\t\t\"bones\": [ \"hoverboard-controller\" ]\n\t},\n\t{\n\t\t\"type\": \"transform\",\n\t\t\"name\": \"front-foot-board-transform\",\n\t\t\"source\": \"hoverboard-controller\",\n\t\t\"bones\": [ \"front-foot-target\" ],\n\t\t\"x\": -69.8,\n\t\t\"y\": 20.7,\n\t\t\"properties\": {\n\t\t\t\"rotate\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"rotate\": { \"max\": 100 }\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"x\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"x\": { \"max\": 100 }\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"y\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"y\": { \"max\": 100 }\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"scaleX\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"scaleX\": {}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"scaleY\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"scaleY\": {}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"shearY\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"shearY\": { \"max\": 100 }\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"mixRotate\": 0,\n\t\t\"mixX\": 0,\n\t\t\"mixScaleX\": 0,\n\t\t\"mixShearY\": 0\n\t},\n\t{\n\t\t\"type\": \"transform\",\n\t\t\"name\": \"rear-foot-board-transform\",\n\t\t\"source\": \"hoverboard-controller\",\n\t\t\"bones\": [ \"rear-foot-target\" ],\n\t\t\"x\": 86.6,\n\t\t\"y\": 21.3,\n\t\t\"properties\": {\n\t\t\t\"rotate\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"rotate\": { \"max\": 100 }\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"x\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"x\": { \"max\": 100 }\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"y\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"y\": { \"max\": 100 }\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"scaleX\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"scaleX\": {}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"scaleY\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"scaleY\": {}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"shearY\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"shearY\": { \"max\": 100 }\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"mixRotate\": 0,\n\t\t\"mixX\": 0,\n\t\t\"mixScaleX\": 0,\n\t\t\"mixShearY\": 0\n\t},\n\t{\n\t\t\"type\": \"ik\",\n\t\t\"name\": \"front-leg-ik\",\n\t\t\"target\": \"front-leg-target\",\n\t\t\"bones\": [ \"front-thigh\", \"front-shin\" ],\n\t\t\"bendPositive\": false\n\t},\n\t{\n\t\t\"type\": \"ik\",\n\t\t\"name\": \"rear-leg-ik\",\n\t\t\"target\": \"rear-leg-target\",\n\t\t\"bones\": [ \"rear-thigh\", \"rear-shin\" ],\n\t\t\"bendPositive\": false\n\t},\n\t{\n\t\t\"type\": \"ik\",\n\t\t\"name\": \"front-foot-ik\",\n\t\t\"target\": \"front-foot-target\",\n\t\t\"bones\": [ \"front-foot\" ]\n\t},\n\t{\n\t\t\"type\": \"ik\",\n\t\t\"name\": \"rear-foot-ik\",\n\t\t\"target\": \"rear-foot-target\",\n\t\t\"bones\": [ \"rear-foot\" ]\n\t},\n\t{\n\t\t\"type\": \"ik\",\n\t\t\"name\": \"aim-torso-ik\",\n\t\t\"target\": \"crosshair\",\n\t\t\"bones\": [ \"aim-constraint-target\" ]\n\t},\n\t{\n\t\t\"type\": \"transform\",\n\t\t\"name\": \"aim-torso-transform\",\n\t\t\"source\": \"aim-constraint-target\",\n\t\t\"bones\": [ \"torso\" ],\n\t\t\"rotation\": 69.5,\n\t\t\"properties\": {\n\t\t\t\"rotate\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"rotate\": { \"max\": 100 }\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"mixRotate\": 0\n\t},\n\t{\n\t\t\"type\": \"transform\",\n\t\t\"name\": \"aim-head-transform\",\n\t\t\"source\": \"aim-constraint-target\",\n\t\t\"bones\": [ \"head\" ],\n\t\t\"rotation\": 84.3,\n\t\t\"properties\": {\n\t\t\t\"rotate\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"rotate\": { \"max\": 100 }\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"mixRotate\": 0\n\t},\n\t{\n\t\t\"type\": \"transform\",\n\t\t\"name\": \"aim-front-arm-transform\",\n\t\t\"source\": \"aim-constraint-target\",\n\t\t\"bones\": [ \"front-upper-arm\" ],\n\t\t\"rotation\": -180,\n\t\t\"properties\": {\n\t\t\t\"rotate\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"rotate\": { \"max\": 100 }\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"mixRotate\": 0\n\t},\n\t{\n\t\t\"type\": \"ik\",\n\t\t\"name\": \"aim-ik\",\n\t\t\"target\": \"crosshair\",\n\t\t\"bones\": [ \"rear-upper-arm\" ],\n\t\t\"mix\": 0\n\t},\n\t{\n\t\t\"type\": \"transform\",\n\t\t\"name\": \"toes-board\",\n\t\t\"source\": \"hoverboard-controller\",\n\t\t\"bones\": [ \"front-foot-tip\", \"back-foot-tip\" ],\n\t\t\"properties\": {\n\t\t\t\"rotate\": {\n\t\t\t\t\"to\": {\n\t\t\t\t\t\"rotate\": { \"max\": 100 }\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"mixRotate\": 0\n\t}\n],\n\"skins\": [\n\t{\n\t\t\"name\": \"default\",\n\t\t\"attachments\": {\n\t\t\t\"clipping\": {\n\t\t\t\t\"clipping\": {\n\t\t\t\t\t\"type\": \"clipping\",\n\t\t\t\t\t\"end\": \"head-bb\",\n\t\t\t\t\t\"vertexCount\": 3,\n\t\t\t\t\t\"vertices\": [ 18.89, -228.46, 1471.52, 140.96, 34.01, 930.06 ],\n\t\t\t\t\t\"color\": \"ce3a3aff\"\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"crosshair\": {\n\t\t\t\t\"crosshair\": { \"width\": 89, \"height\": 89 }\n\t\t\t},\n\t\t\t\"exhaust1\": {\n\t\t\t\t\"hoverglow-small\": { \"scaleX\": 0.4629046, \"scaleY\": 0.81293726, \"rotation\": -83.070786, \"width\": 274, \"height\": 75 }\n\t\t\t},\n\t\t\t\"exhaust2\": {\n\t\t\t\t\"hoverglow-small\": {\n\t\t\t\t\t\"x\": 0.011459351,\n\t\t\t\t\t\"y\": -0.76220703,\n\t\t\t\t\t\"scaleX\": 0.42083138,\n\t\t\t\t\t\"scaleY\": 0.8402699,\n\t\t\t\t\t\"rotation\": -89.24866,\n\t\t\t\t\t\"width\": 274,\n\t\t\t\t\t\"height\": 75\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"exhaust3\": {\n\t\t\t\t\"hoverglow-small\": { \"scaleX\": 0.4629046, \"scaleY\": 0.81293726, \"rotation\": -83.070786, \"width\": 274, \"height\": 75 }\n\t\t\t},\n\t\t\t\"eye\": {\n\t\t\t\t\"eye-indifferent\": {\n\t\t\t\t\t\"type\": \"mesh\",\n\t\t\t\t\t\"uvs\": [ 1, 1, 0, 1, 0, 0, 1, 0 ],\n\t\t\t\t\t\"triangles\": [ 1, 3, 0, 1, 2, 3 ],\n\t\t\t\t\t\"vertices\": [ 2, 66, -36.8, -91.35, 0.3, 46, 73.41, -91.35, 0.7, 2, 66, -87.05, -13.11, 0.70968, 46, 23.16, -13.11, 0.29032, 2, 66, -12.18, 34.99, 0.82818, 46, 98.03, 34.99, 0.17182, 2, 66, 38.07, -43.25, 0.59781, 46, 148.28, -43.25, 0.40219 ],\n\t\t\t\t\t\"hull\": 4,\n\t\t\t\t\t\"edges\": [ 0, 2, 2, 4, 4, 6, 0, 6 ],\n\t\t\t\t\t\"width\": 93,\n\t\t\t\t\t\"height\": 89\n\t\t\t\t},\n\t\t\t\t\"eye-surprised\": {\n\t\t\t\t\t\"type\": \"mesh\",\n\t\t\t\t\t\"uvs\": [ 1, 1, 0, 1, 0, 0, 1, 0 ],\n\t\t\t\t\t\"triangles\": [ 1, 2, 3, 1, 3, 0 ],\n\t\t\t\t\t\"vertices\": [ 2, 66, -46.74, -89.7, 0.3, 46, 63.47, -89.7, 0.7, 2, 66, -77.58, -1.97, 0.71, 46, 32.63, -1.97, 0.29, 2, 66, 6.38, 27.55, 0.83, 46, 116.59, 27.55, 0.17, 2, 66, 37.22, -60.19, 0.6, 46, 147.44, -60.19, 0.4 ],\n\t\t\t\t\t\"hull\": 4,\n\t\t\t\t\t\"edges\": [ 0, 2, 2, 4, 4, 6, 0, 6 ],\n\t\t\t\t\t\"width\": 93,\n\t\t\t\t\t\"height\": 89\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"front-bracer\": {\n\t\t\t\t\"front-bracer\": { \"x\": 12.031342, \"y\": -1.6766033, \"rotation\": 79.59598, \"width\": 58, \"height\": 80 }\n\t\t\t},\n\t\t\t\"front-fist\": {\n\t\t\t\t\"front-fist-closed\": { \"x\": 35.49588, \"y\": 6.0047035, \"rotation\": 67.16275, \"width\": 75, \"height\": 82 },\n\t\t\t\t\"front-fist-open\": { \"x\": 39.566483, \"y\": 7.764904, \"rotation\": 67.16275, \"width\": 86, \"height\": 87 }\n\t\t\t},\n\t\t\t\"front-foot\": {\n\t\t\t\t\"front-foot\": {\n\t\t\t\t\t\"type\": \"mesh\",\n\t\t\t\t\t\"uvs\": [ 0.5941728, 0.23421869, 0.6225744, 0.30335644, 0.65009886, 0.370359, 0.67637414, 0.3840358, 0.7206807, 0.4070982, 0.76264495, 0.4289414, 1, 0.70375, 1, 1, 0.65517044, 1, 0.46923247, 0.99999, 0, 1, 0, 0.39196765, 0.17845653, 0, 0.49795625, 0 ],\n\t\t\t\t\t\"triangles\": [ 8, 9, 3, 4, 8, 3, 5, 8, 4, 6, 8, 5, 8, 6, 7, 11, 1, 10, 0, 12, 13, 0, 11, 12, 0, 1, 11, 9, 2, 3, 1, 2, 10, 9, 10, 2 ],\n\t\t\t\t\t\"vertices\": [ 2, 38, 18.17, 41.57, 0.7896, 39, 12.46, 46.05, 0.2104, 2, 38, 24.08, 40.76, 0.71228, 39, 16.12, 41.34, 0.28772, 2, 38, 29.81, 39.98, 0.55344, 39, 19.67, 36.78, 0.44656, 2, 38, 32.81, 41.67, 0.38554, 39, 23, 35.89, 0.61446, 2, 38, 37.86, 44.52, 0.25567, 39, 28.61, 34.4, 0.74433, 2, 38, 42.65, 47.22, 0.17384, 39, 33.92, 32.99, 0.82616, 1, 39, 64.15, 14.56, 1, 1, 39, 64.51, -5.87, 1, 1, 39, 21.08, -6.64, 1, 2, 38, 44.67, -6.77, 0.5684, 39, -2.34, -6.97, 0.4316, 1, 38, 3.1, -48.81, 1, 1, 38, -26.73, -19.31, 1, 1, 38, -30.15, 15.69, 1, 1, 38, -1.84, 44.32, 1 ],\n\t\t\t\t\t\"hull\": 14,\n\t\t\t\t\t\"edges\": [ 14, 16, 16, 18, 18, 20, 4, 18, 20, 22, 24, 26, 22, 24, 12, 14, 10, 12, 2, 4, 2, 20, 4, 6, 6, 16, 2, 0, 0, 26, 6, 8, 8, 10 ],\n\t\t\t\t\t\"width\": 126,\n\t\t\t\t\t\"height\": 69\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"front-shin\": {\n\t\t\t\t\"front-shin\": {\n\t\t\t\t\t\"type\": \"mesh\",\n\t\t\t\t\t\"uvs\": [ 0.90030664, 0.057846487, 1, 0.1282812, 1, 0.21618849, 0.90250385, 0.31002015, 0.78736234, 0.35683703, 0.7808133, 0.39873743, 0.7721526, 0.45414773, 0.77098155, 0.51572186, 0.8409413, 0.6375107, 0.9309526, 0.74909943, 0.95530945, 0.7792951, 0.78126425, 0.876786, 0.56129694, 1, 0.26869547, 1, 0, 1, 0.0027854783, 0.9611154, 0.013583113, 0.8103831, 0.028220676, 0.6060464, 0.083240554, 0.45142215, 0.18908401, 0.3188221, 0.2957713, 0.23980266, 0.30235705, 0.14941014, 0.3787467, 0.05902038, 0.53284377, 0, 0.7053801, 0, 0.41093943, 0.7196821, 0.40742734, 0.54750824, 0.41093948, 0.4535952, 0.47240293, 0.35185593, 0.33367118, 0.27829072, 0.5022565, 0.3166385, 0.6532811, 0.6750735, 0.6076225, 0.5271604, 0.60059804, 0.4512473, 0.6274673, 0.3754273, 0.65729713, 0.33849847, 0.27843028, 0.3292395, 0.18967116, 0.45202994, 0.16508563, 0.585856, 0.18264666, 0.768204, 0.50531954, 0.24634285, 0.59473395, 0.17967449, 0.6016121, 0.106109366, 0.51391697, 0.04327252, 0.7219777, 0.28848955, 0.82342887, 0.20266359, 0.86813617, 0.11377233, 0.7959167, 0.046337623, 0.44857568, 0.15515275, 0.25465828, 0.962187, 0.5316866, 0.9447962, 0.7531003, 0.83239716 ],\n\t\t\t\t\t\"triangles\": [ 24, 0, 47, 43, 23, 24, 47, 43, 24, 43, 22, 23, 42, 43, 47, 46, 47, 0, 42, 47, 46, 46, 0, 1, 48, 22, 43, 48, 43, 42, 21, 22, 48, 41, 48, 42, 45, 42, 46, 41, 42, 45, 46, 1, 2, 45, 46, 2, 40, 48, 41, 48, 20, 21, 29, 48, 40, 29, 20, 48, 44, 41, 45, 40, 41, 44, 3, 45, 2, 44, 45, 3, 30, 29, 40, 35, 30, 40, 36, 19, 20, 36, 20, 29, 44, 35, 40, 28, 29, 30, 4, 44, 3, 35, 44, 4, 34, 30, 35, 5, 35, 4, 34, 28, 30, 33, 28, 34, 37, 19, 36, 18, 19, 37, 27, 29, 28, 27, 28, 33, 36, 29, 27, 37, 36, 27, 5, 34, 35, 6, 34, 5, 33, 34, 6, 6, 32, 33, 7, 32, 6, 26, 37, 27, 38, 18, 37, 38, 37, 26, 17, 18, 38, 31, 32, 7, 31, 7, 8, 32, 25, 26, 38, 26, 25, 27, 33, 32, 32, 26, 27, 39, 38, 25, 17, 38, 39, 16, 17, 39, 51, 31, 8, 51, 8, 9, 11, 51, 9, 11, 9, 10, 31, 50, 25, 31, 25, 32, 50, 31, 51, 49, 39, 25, 49, 25, 50, 15, 16, 39, 49, 15, 39, 13, 49, 50, 14, 15, 49, 13, 14, 49, 12, 50, 51, 12, 51, 11, 13, 50, 12 ],\n\t\t\t\t\t\"vertices\": [ -23.66, 19.37, -11.73, 28.98, 4.34, 30.83, 22.41, 24.87, 32.05, 16.48, 39.77, 16.83, 49.98, 17.3, 61.25, 18.5, 82.85, 26.78, 102.4, 36.46, 107.69, 39.09, 127.15, 26.97, 151.74, 11.65, 154.49, -12.18, 157.02, -34.07, 149.89, -34.66, 122.23, -36.97, 84.75, -40.09, 55.97, -38.88, 30.73, -33.05, 15.29, -26.03, -1.3, -27.41, -18.54, -23.09, -30.78, -11.79, -32.4, 2.27, 101.92, -6.52, 70.48, -10.44, 53.28, -12.14, 34.11, -9.28, 21.96, -22.13, 27.39, -7.59, 91.48, 12.28, 64.88, 5.44, 51.07, 3.26, 36.95, 3.85, 29.92, 5.5, 31.8, -25.56, 55.08, -30.19, 79.77, -29.37, 112.93, -24.09, 14.51, -8.83, 1.48, -2.95, -12.03, -3.94, -22.69, -12.41, 20.17, 9.71, 3.53, 16.16, -13.14, 17.93, -24.78, 10.62, -1.62, -15.37, 147.71, -14.13, 141.93, 8.07, 119.3, 23.74 ],\n\t\t\t\t\t\"hull\": 25,\n\t\t\t\t\t\"edges\": [ 8, 6, 6, 4, 4, 2, 2, 0, 0, 48, 46, 48, 46, 44, 44, 42, 42, 40, 40, 38, 38, 36, 36, 34, 32, 34, 50, 52, 52, 54, 54, 56, 40, 58, 58, 60, 8, 10, 20, 22, 22, 24, 62, 64, 64, 66, 66, 68, 8, 70, 70, 60, 68, 70, 58, 72, 72, 74, 74, 76, 76, 78, 24, 26, 26, 28, 58, 80, 80, 82, 82, 84, 84, 86, 86, 44, 70, 88, 88, 90, 90, 92, 92, 94, 94, 48, 80, 88, 88, 6, 82, 90, 90, 4, 84, 92, 92, 2, 86, 94, 94, 0, 56, 60, 10, 12, 12, 14, 14, 16, 28, 30, 30, 32, 26, 98, 98, 78, 30, 98, 24, 100, 100, 50, 98, 100, 22, 102, 102, 62, 100, 102, 16, 18, 18, 20, 102, 18 ],\n\t\t\t\t\t\"width\": 82,\n\t\t\t\t\t\"height\": 184\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"front-thigh\": {\n\t\t\t\t\"front-thigh\": { \"x\": 42.4751, \"y\": 4.449414, \"rotation\": 84.86587, \"width\": 45, \"height\": 112 }\n\t\t\t},\n\t\t\t\"front-upper-arm\": {\n\t\t\t\t\"front-upper-arm\": { \"x\": 28.306147, \"y\": 7.3712473, \"rotation\": 97.89548, \"width\": 46, \"height\": 97 }\n\t\t\t},\n\t\t\t\"goggles\": {\n\t\t\t\t\"goggles\": {\n\t\t\t\t\t\"type\": \"mesh\",\n\t\t\t\t\t\"uvs\": [ 0.53653324, 0.041139666, 0.72921944, 0.16035686, 0.91666967, 0.33222657, 0.9704648, 0.31329066, 1, 0.48053038, 0.9575552, 0.57329917, 0.8882532, 0.6327984, 0.86877507, 0.78962344, 0.7740368, 0.86749506, 0.72628045, 1, 0.6071384, 0.9386266, 0.4960078, 0.8813801, 0.41557792, 0.75026965, 0.32547185, 0.70084494, 0.27820104, 0.58256817, 0.17209916, 0.6328146, 0.17228541, 0.7507118, 0.10780662, 0.798978, 0, 0.32304054, 0, 0.12475555, 0.07372671, 0.07343647, 0.15423247, 0.10733794, 0.23165262, 0.13994002, 0.3031334, 0.022563254, 0.34802386, 0, 0.42978895, 0.6918342, 0.39475954, 0.5104154, 0.39488173, 0.31511822, 0.45878187, 0.2319774, 0.5650132, 0.2810853, 0.6996078, 0.39216098, 0.82039356, 0.5420369, 0.8573792, 0.623428, 0.91107106, 0.51407266, 0.7263879, 0.3214668, 0.58763665, 0.19609463, 0.48074514, 0.11268945, 0.37822992, 0.05501315, 0.3286954, 0.17866458, 0.31899738, 0.30499753, 0.36035678, 0.5379894, 0.40327474, 0.70072436, 0.30058587, 0.5583792, 0.21956658, 0.28150323, 0.09963368, 0.2894289, 0.5686289, 0.43680266, 0.49110016, 0.37156153, 0.51184726, 0.5209291, 0.67018056, 0.5930374, 0.76190466, 0.6857483, 0.7329629, 0.43354937 ],\n\t\t\t\t\t\"triangles\": [ 18, 44, 15, 21, 19, 20, 17, 18, 15, 44, 19, 21, 2, 3, 4, 18, 19, 44, 2, 33, 34, 33, 2, 4, 5, 33, 4, 5, 6, 33, 7, 32, 6, 31, 50, 33, 32, 31, 33, 6, 32, 33, 31, 49, 50, 49, 31, 32, 49, 32, 7, 8, 49, 7, 33, 50, 34, 17, 15, 16, 9, 48, 8, 49, 48, 50, 50, 48, 45, 47, 45, 48, 50, 45, 30, 45, 47, 46, 45, 46, 29, 30, 45, 29, 30, 29, 34, 30, 34, 50, 47, 26, 46, 25, 10, 11, 12, 25, 11, 41, 12, 42, 42, 44, 43, 43, 21, 22, 41, 40, 25, 41, 42, 40, 29, 35, 34, 40, 26, 25, 25, 26, 47, 37, 24, 0, 36, 37, 0, 42, 43, 39, 42, 39, 40, 28, 38, 36, 40, 39, 26, 28, 27, 38, 26, 39, 27, 37, 38, 23, 39, 43, 38, 38, 37, 36, 27, 39, 38, 43, 22, 38, 37, 23, 24, 22, 23, 38, 36, 0, 35, 28, 36, 35, 29, 28, 35, 27, 28, 46, 26, 27, 46, 35, 0, 1, 34, 35, 1, 12, 41, 25, 47, 10, 25, 44, 21, 43, 42, 14, 44, 14, 15, 44, 13, 14, 42, 12, 13, 42, 46, 28, 29, 47, 48, 10, 48, 9, 10, 49, 8, 48, 2, 34, 1 ],\n\t\t\t\t\t\"vertices\": [ 2, 66, 61.88, 22.81, 0.832, 46, 172.09, 22.81, 0.168, 2, 66, 59.89, -31.19, 0.6855, 46, 170.1, -31.19, 0.3145, 2, 66, 49.2, -86.8, 0.32635, 46, 159.41, -86.8, 0.67365, 2, 66, 56.82, -99.01, 0.01217, 46, 167.03, -99.01, 0.98783, 1, 46, 143.4, -115.48, 1, 2, 66, 15, -110.14, 0.0041, 46, 125.21, -110.14, 0.9959, 2, 66, -0.32, -96.36, 0.07948, 46, 109.89, -96.36, 0.92052, 2, 66, -26.56, -100.19, 0.01905, 46, 83.65, -100.19, 0.98095, 2, 66, -46.96, -81.16, 0.4921, 46, 63.26, -81.16, 0.50791, 2, 66, -71.84, -76.69, 0.56923, 46, 38.37, -76.69, 0.43077, 2, 66, -72.54, -43.98, 0.74145, 46, 37.67, -43.98, 0.25855, 2, 66, -73.2, -13.47, 0.87929, 46, 37.01, -13.47, 0.12071, 2, 66, -59.63, 13.55, 0.864, 46, 50.58, 13.55, 0.136, 2, 66, -59.69, 38.45, 0.85289, 46, 50.52, 38.45, 0.14711, 2, 66, -45.26, 56.6, 0.74392, 46, 64.95, 56.6, 0.25608, 2, 66, -62.31, 79.96, 0.624, 46, 47.9, 79.96, 0.376, 2, 66, -80.76, 73.42, 0.616, 46, 29.45, 73.42, 0.384, 2, 66, -93.9, 86.64, 0.288, 46, 16.31, 86.64, 0.712, 1, 46, 81.51, 139.38, 1, 1, 46, 112.56, 150.3, 1, 2, 66, 16.76, 134.97, 0.02942, 46, 126.97, 134.97, 0.97058, 2, 66, 18.42, 113.28, 0.36147, 46, 128.63, 113.28, 0.63853, 2, 66, 20.02, 92.43, 0.7135, 46, 130.23, 92.43, 0.2865, 2, 66, 44.58, 81.29, 0.69603, 46, 154.79, 81.29, 0.30397, 2, 66, 52, 71.48, 0.848, 46, 162.21, 71.48, 0.152, 2, 66, -49.25, 13.27, 0.8, 46, 60.96, 13.27, 0.2, 2, 66, -23.88, 31.88, 0.896, 46, 86.33, 31.88, 0.104, 2, 66, 6.72, 42.6, 0.928, 46, 116.93, 42.6, 0.072, 2, 66, 25.26, 31.44, 0.8, 46, 135.47, 31.44, 0.2, 2, 66, 26.77, 2.59, 0.75, 46, 136.98, 2.59, 0.25, 2, 66, 21.02, -36.66, 0.54887, 46, 131.23, -36.66, 0.45113, 2, 66, 8.01, -74.65, 0.36029, 46, 118.22, -74.65, 0.63971, 2, 66, -1.52, -88.24, 0.1253, 46, 108.69, -88.24, 0.8747, 2, 66, 20.25, -95.44, 0.08687, 46, 130.46, -95.44, 0.91313, 2, 66, 34.42, -39.36, 0.72613, 46, 144.63, -39.36, 0.27387, 2, 66, 42.03, 1.7, 0.824, 46, 152.25, 1.7, 0.176, 2, 66, 45.85, 32.6, 0.856, 46, 156.06, 32.6, 0.144, 1, 66, 46.01, 61.02, 1, 1, 66, 22.35, 66.41, 1, 1, 66, 1.73, 61.84, 1, 2, 66, -31.17, 38.83, 0.928, 46, 79.04, 38.83, 0.072, 2, 66, -52.94, 19.31, 0.79073, 46, 57.27, 19.31, 0.20927, 2, 66, -39.54, 52.42, 0.912, 46, 70.67, 52.42, 0.088, 2, 66, -3.2, 87.61, 0.744, 46, 107.02, 87.61, 0.256, 2, 66, -14.82, 116.7, 0.6368, 46, 95.4, 116.7, 0.3632, 2, 66, 2.7, -6.87, 0.856, 46, 112.91, -6.87, 0.144, 2, 66, 6.21, 15.8, 0.744, 46, 116.42, 15.8, 0.256, 2, 66, -15.39, 2.47, 0.856, 46, 94.82, 2.47, 0.144, 2, 66, -12.98, -40.48, 0.72102, 46, 97.24, -40.48, 0.27898, 2, 66, -19.55, -68.16, 0.59162, 46, 90.66, -68.16, 0.40838, 2, 66, 17.44, -47.15, 0.53452, 46, 127.65, -47.15, 0.46548 ],\n\t\t\t\t\t\"hull\": 25,\n\t\t\t\t\t\"edges\": [ 36, 34, 34, 32, 32, 30, 30, 28, 28, 26, 26, 24, 24, 22, 18, 16, 16, 14, 14, 12, 12, 10, 10, 8, 8, 6, 6, 4, 4, 2, 2, 0, 0, 48, 48, 46, 46, 44, 36, 38, 40, 38, 24, 50, 50, 52, 52, 54, 54, 56, 56, 58, 58, 60, 62, 64, 64, 12, 8, 66, 66, 68, 68, 70, 70, 72, 72, 74, 74, 76, 76, 78, 78, 80, 80, 82, 82, 24, 24, 84, 84, 86, 86, 44, 40, 42, 42, 44, 42, 88, 88, 30, 58, 90, 90, 92, 92, 94, 18, 20, 20, 22, 94, 20, 18, 96, 96, 98, 60, 100, 100, 62, 98, 100 ],\n\t\t\t\t\t\"width\": 261,\n\t\t\t\t\t\"height\": 166\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"gun\": {\n\t\t\t\t\"gun\": { \"x\": 77.30423, \"y\": 16.404907, \"rotation\": 60.825897, \"width\": 210, \"height\": 203 }\n\t\t\t},\n\t\t\t\"head\": {\n\t\t\t\t\"head\": {\n\t\t\t\t\t\"type\": \"mesh\",\n\t\t\t\t\t\"uvs\": [ 0.75918776, 0.06107302, 0.88392204, 0.17892529, 0.9017411, 0.3085628, 0.9422393, 0.19660294, 1, 0.26584148, 1, 0.42199564, 0.9586356, 0.4699281, 0.92118025, 0.51333076, 0.8595705, 0.5346962, 0.78388083, 0.6560519, 0.7438444, 0.74838394, 0.85115933, 0.7515119, 0.84828365, 0.8256396, 0.817809, 0.85367495, 0.7559887, 0.859065, 0.76236886, 0.9046774, 0.65875244, 1, 0.38336512, 1, 0.18579765, 0.8540391, 0.12741707, 0.8109081, 0.060247906, 0.6920902, 0, 0.5855156, 0, 0.4102103, 0.085298665, 0.20691523, 0.24243174, 0.14504284, 0.49999988, 0.14209673, 0.5032388, 0.0743304, 0.4173836, 0, 0.57613623, 0, 0.8505914, 0.3608683, 0.734311, 0.43206275, 0.6848055, 0.31270725, 0.7216468, 0.1671771, 0.55931485, 0.04153975, 0.44763958, 0.2289488, 0.23925595, 0.26559323, 0.7127154, 0.440357, 0.5699295, 0.3829953, 0.41678238, 0.3351074, 0.29299957, 0.31496972, 0.7080164, 0.4450182, 0.5667631, 0.3897649, 0.41520515, 0.34416312, 0.2875353, 0.3301711, 0.88988125, 0.501767, 0.30388862, 0.7346256, 0.26460257, 0.65674764, 0.21414289, 0.61583686, 0.14612561, 0.6219387, 0.103157386, 0.66636086, 0.103577375, 0.72556865, 0.14505458, 0.7916364, 0.20263426, 0.8135515, 0.2787257, 0.80158734, 0.34946802, 0.73760104, 0.23073055, 0.57073057, 0.08878042, 0.60706705, 0.29460678, 0.8129002, 0.7300557, 0.8788308, 0.6980495, 0.87348306, 0.66165507, 0.7968106, 0.2246777, 0.698241, 0.14552385, 0.6740475 ],\n\t\t\t\t\t\"triangles\": [ 50, 49, 62, 34, 25, 31, 39, 35, 34, 38, 39, 34, 37, 38, 34, 42, 39, 38, 43, 39, 42, 32, 2, 31, 31, 37, 34, 42, 38, 37, 41, 42, 37, 43, 22, 39, 30, 31, 29, 36, 37, 31, 30, 36, 31, 40, 41, 37, 36, 40, 37, 36, 30, 44, 55, 22, 43, 55, 48, 56, 47, 48, 55, 46, 55, 54, 42, 55, 43, 47, 55, 46, 62, 49, 48, 61, 47, 46, 62, 48, 47, 61, 62, 47, 46, 54, 45, 42, 41, 55, 61, 46, 45, 55, 41, 54, 61, 51, 50, 61, 50, 62, 60, 41, 40, 54, 41, 60, 53, 61, 45, 52, 51, 61, 57, 53, 45, 57, 45, 54, 53, 52, 61, 52, 19, 51, 57, 18, 52, 57, 52, 53, 17, 54, 60, 57, 54, 17, 18, 57, 17, 19, 50, 51, 33, 27, 28, 26, 27, 33, 0, 33, 28, 32, 33, 0, 32, 0, 1, 33, 25, 26, 33, 32, 25, 31, 25, 32, 2, 32, 1, 2, 3, 4, 29, 31, 2, 2, 4, 5, 29, 2, 5, 6, 29, 5, 30, 29, 6, 44, 30, 6, 18, 19, 52, 49, 56, 48, 34, 24, 25, 35, 23, 24, 35, 24, 34, 39, 22, 35, 22, 23, 35, 7, 44, 6, 8, 36, 44, 40, 36, 8, 8, 44, 7, 56, 21, 22, 55, 56, 22, 9, 40, 8, 20, 21, 56, 20, 56, 49, 9, 60, 40, 10, 60, 9, 20, 50, 19, 12, 10, 11, 13, 10, 12, 14, 60, 10, 13, 14, 10, 59, 60, 14, 58, 59, 14, 58, 14, 15, 16, 17, 60, 59, 16, 60, 15, 16, 59, 15, 59, 58, 20, 49, 50 ],\n\t\t\t\t\t\"vertices\": [ 2, 50, 41.97, -41.8, 0.94074, 66, 165.41, -22.6, 0.05926, 4, 48, 73.47, 27.54, 0.26795, 50, -5.75, -51.71, 0.4738, 49, 112.99, -11.41, 0.12255, 66, 143.5, -66.13, 0.1357, 4, 48, 38.23, 10.99, 0.6831, 50, -41.01, -35.22, 0.07866, 49, 92.73, -44.66, 0.04872, 66, 108.65, -83.49, 0.18952, 2, 48, 73.35, 10.89, 0.8455, 66, 143.77, -82.78, 0.1545, 2, 48, 58.59, -10.38, 0.91607, 66, 129.5, -104.39, 0.08393, 3, 46, 195.82, -119.82, 0.104, 47, 75.49, -4.55, 0.09191, 48, 14.36, -24.8, 0.80409, 4, 46, 178.62, -113.98, 0.19022, 47, 59.82, -13.72, 0.33409, 48, -2.7, -18.57, 0.46643, 66, 68.41, -113.98, 0.00926, 4, 46, 163.06, -108.69, 0.18724, 47, 45.64, -22.03, 0.3133, 48, -18.14, -12.93, 0.47469, 66, 52.84, -108.69, 0.02477, 2, 46, 151.52, -95.05, 0.91122, 66, 41.31, -95.05, 0.08878, 2, 46, 110.61, -87.69, 0.70564, 66, 0.4, -87.69, 0.29436, 2, 46, 81.05, -86.58, 0.63951, 66, -29.16, -86.58, 0.36049, 2, 46, 89.82, -114.32, 0.57, 66, -20.39, -114.32, 0.43, 2, 46, 68.72, -120.91, 0.57, 66, -41.49, -120.91, 0.43, 2, 46, 58.1, -115.9, 0.57, 66, -52.11, -115.9, 0.43, 2, 46, 51.03, -100.63, 0.64242, 66, -59.18, -100.63, 0.35758, 2, 46, 38.79, -106.76, 0.81659, 66, -71.43, -106.76, 0.18341, 2, 46, 2.68, -89.7, 0.77801, 66, -107.53, -89.7, 0.22199, 2, 46, -22.07, -19.3, 0.823, 66, -132.28, -19.3, 0.177, 2, 46, 1.2, 45.63, 0.51204, 66, -109.01, 45.63, 0.48796, 2, 46, 8.07, 64.81, 0.60869, 66, -102.14, 64.81, 0.39131, 2, 46, 35.44, 93.73, 0.80009, 66, -74.77, 93.73, 0.19991, 2, 46, 59.98, 119.66, 0.93554, 66, -50.23, 119.66, 0.06446, 2, 46, 109.26, 136.99, 0.99895, 66, -0.95, 136.99, 0.00105, 1, 46, 174.07, 135.27, 1, 3, 46, 205.59, 101.22, 0.80778, 49, -16.84, 104.63, 0.15658, 66, 95.38, 101.22, 0.03564, 3, 50, 58.94, 30.5, 0.43491, 49, 38.36, 61.89, 0.28116, 66, 119.35, 35.65, 0.28393, 2, 50, 75.56, 19.01, 0.92164, 66, 138.68, 41.52, 0.07836, 1, 50, 106.7, 26.9, 1, 1, 50, 83.79, -9.51, 1, 5, 47, 44.51, 27.24, 0.15139, 48, 19.12, 19.33, 0.44847, 50, -46.82, -15.19, 0.05757, 49, 72.19, -48.24, 0.1149, 66, 89.35, -75.58, 0.22767, 3, 47, 7.42, 19.08, 0.37772, 49, 34.32, -45.24, 0.09918, 66, 58.9, -52.89, 0.52311, 2, 49, 45.94, -9.07, 0.4826, 66, 87.99, -28.45, 0.5174, 2, 50, 20.62, -16.35, 0.7435, 66, 132.21, -23.49, 0.2565, 2, 50, 75.74, 0.94, 0.97172, 66, 152.95, 30.42, 0.02828, 4, 46, 200.45, 40.46, 0.18809, 50, 44.6, 56.29, 0.05831, 49, 11.15, 50.46, 0.14366, 66, 90.24, 40.46, 0.60994, 2, 46, 171.41, 90.12, 0.48644, 66, 61.2, 90.12, 0.51356, 2, 46, 164.84, -48.18, 0.43217, 66, 54.62, -48.18, 0.56783, 4, 46, 168.13, -6.02, 0.01949, 47, -28.65, 49.02, 0.02229, 49, 8.54, -6.09, 0.12791, 66, 57.92, -6.02, 0.83031, 2, 46, 167.84, 37.87, 0.15, 66, 57.63, 37.87, 0.85, 2, 46, 162.36, 71.5, 0.24107, 66, 52.15, 71.5, 0.75893, 2, 46, 163.11, -47.44, 0.41951, 66, 52.9, -47.44, 0.58049, 2, 46, 165.94, -5.87, 0.16355, 66, 55.73, -5.87, 0.83645, 2, 46, 165.14, 37.38, 0.15, 66, 54.93, 37.38, 0.85, 2, 46, 157.6, 71.4, 0.21735, 66, 47.39, 71.4, 0.78265, 3, 46, 163.5, -99.54, 0.61812, 47, 39.01, -15.71, 0.30445, 66, 53.29, -99.54, 0.07744, 2, 46, 45.38, 27.24, 0.16741, 66, -64.83, 27.24, 0.83259, 2, 46, 63.74, 44.98, 0.15, 66, -46.47, 44.98, 0.85, 2, 46, 70.7, 61.92, 0.22175, 66, -39.51, 61.92, 0.77825, 2, 46, 62.88, 78.71, 0.38, 66, -47.34, 78.71, 0.62, 2, 46, 46.53, 85.3, 0.51, 66, -63.68, 85.3, 0.49, 2, 46, 29.92, 79.34, 0.388, 66, -80.29, 79.34, 0.612, 2, 46, 15.08, 62.21, 0.38, 66, -95.13, 62.21, 0.62, 2, 46, 14.09, 45.32, 0.41, 66, -96.12, 45.32, 0.59, 2, 46, 24.3, 27.06, 0.192, 66, -85.91, 27.06, 0.808, 1, 66, -61.57, 15.3, 1, 2, 46, 84.87, 62.14, 0.16757, 66, -25.34, 62.14, 0.83243, 2, 46, 61.9, 94.84, 0.68145, 66, -48.31, 94.84, 0.31855, 2, 46, 22.54, 21.88, 0.16, 66, -87.67, 21.88, 0.84, 2, 46, 43.15, -95.95, 0.73445, 66, -67.06, -95.95, 0.26555, 2, 46, 41.77, -87.24, 0.67858, 66, -68.44, -87.24, 0.32142, 2, 46, 60.05, -70.36, 0.50195, 66, -50.16, -70.36, 0.49805, 2, 46, 48.49, 51.09, 0.25, 66, -61.72, 51.09, 0.75, 2, 46, 48.17, 73.71, 0.15634, 66, -62.04, 73.71, 0.84366 ],\n\t\t\t\t\t\"hull\": 29,\n\t\t\t\t\t\"edges\": [ 10, 8, 8, 6, 6, 4, 4, 2, 2, 0, 0, 56, 54, 56, 54, 52, 52, 50, 50, 48, 48, 46, 46, 44, 42, 44, 32, 34, 4, 58, 58, 60, 62, 64, 64, 66, 66, 54, 50, 68, 68, 70, 70, 44, 60, 72, 62, 74, 72, 74, 74, 76, 76, 78, 78, 44, 16, 80, 80, 82, 82, 84, 84, 86, 86, 44, 14, 88, 88, 72, 14, 16, 10, 12, 12, 14, 12, 60, 90, 92, 92, 94, 94, 96, 96, 98, 98, 100, 100, 102, 102, 104, 104, 106, 106, 90, 108, 110, 110, 112, 38, 40, 40, 42, 112, 40, 34, 36, 36, 38, 36, 114, 114, 108, 30, 32, 30, 28, 24, 26, 28, 26, 22, 24, 22, 20, 20, 18, 18, 16, 28, 116, 116, 118, 118, 120, 120, 20 ],\n\t\t\t\t\t\"width\": 271,\n\t\t\t\t\t\"height\": 298\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"head-bb\": {\n\t\t\t\t\"head\": {\n\t\t\t\t\t\"type\": \"boundingbox\",\n\t\t\t\t\t\"vertexCount\": 6,\n\t\t\t\t\t\"vertices\": [ -19.14, -70.3, 40.8, -118.08, 257.78, -115.62, 285.17, 57.18, 120.77, 164.95, -5.07, 76.95 ]\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"hoverboard-board\": {\n\t\t\t\t\"hoverboard-board\": {\n\t\t\t\t\t\"type\": \"mesh\",\n\t\t\t\t\t\"uvs\": [ 0.13864607, 0.5662393, 0.1142763, 0.5146078, 0.07618786, 0.5210682, 0.023639273, 0.52998126, 0.012808947, 0.5318183, 0, 0.37979233, 0, 0.2205953, 0.0051917746, 0.10824845, 0.010375012, 0.10726268, 0.03834171, 0.10194385, 0.050914608, 0, 0.083260015, 0, 0.10932894, 0.042061206, 0.13820295, 0.08864832, 0.18915813, 0.24067438, 0.22233906, 0.40630206, 0.23886204, 0.44063473, 0.83412385, 0.44034415, 0.88443774, 0.38295734, 0.9259101, 0.32638615, 0.959957, 0.28841007, 0.9861191, 0.28541666, 1, 0.38675195, 0.9949444, 0.47104198, 0.9788285, 0.5325082, 0.9440907, 0.62135005, 0.9020624, 0.6949223, 0.8656917, 0.7109357, 0.82821923, 0.7079088, 0.81285924, 0.7712708, 0.6293069, 0.77265894, 0.61364007, 0.7064525, 0.47166333, 0.7066431, 0.4590079, 0.7782668, 0.27746817, 0.76985896, 0.2657999, 0.7037184, 0.24976107, 0.7138066, 0.24600902, 0.7782668, 0.23041801, 0.8493069, 0.20926188, 0.90955544, 0.17298914, 1, 0.15076564, 0.99967355, 0.1290571, 0.90192485, 0.10369129, 0.736925, 0.10198391, 0.6248198, 0.0913133, 0.47272092, 0.09133468, 0.41324735, 0.15081826, 0.41867685, 0.21991451, 0.51855695, 0.0633089, 0.10816091, 0.083826385, 0.21695572, 0.08905153, 0.3753241, 0.15903135, 0.58725834, 0.17538019, 0.6570573, 0.20118096, 0.8029049, 0.1791791, 0.5564352, 0.22166404, 0.5802012, 0.862594, 0.57961524, 0.9234568, 0.4853438, 0.9669095, 0.36881113, 0.094500154, 0.13259096, 0.12687987, 0.17830575, 0.15985525, 0.24681903, 0.18035573, 0.31267944, 0.20607181, 0.42350072, 0.16074103, 0.85403216, 0.13623522, 0.7012158, 0.12096038, 0.6404868, 0.023959922, 0.21811189, 0.027316555, 0.37838712, 0.025566524, 0.4971959, 0.14475645, 0.45736033, 0.18019417, 0.51688933, 0.19692187, 0.5663605 ],\n\t\t\t\t\t\"triangles\": [ 10, 11, 12, 9, 10, 12, 49, 9, 12, 60, 49, 12, 13, 60, 12, 61, 60, 13, 50, 49, 60, 50, 60, 61, 68, 8, 9, 68, 9, 49, 68, 49, 50, 7, 8, 68, 6, 7, 68, 61, 13, 14, 62, 61, 14, 50, 61, 62, 63, 62, 14, 59, 20, 21, 19, 20, 59, 51, 50, 62, 51, 62, 63, 51, 69, 68, 51, 68, 50, 6, 68, 69, 5, 6, 69, 18, 19, 59, 15, 63, 14, 59, 21, 22, 47, 51, 63, 47, 46, 51, 47, 63, 64, 15, 64, 63, 64, 15, 16, 71, 46, 47, 23, 59, 22, 69, 51, 70, 45, 46, 71, 70, 51, 2, 58, 18, 59, 58, 59, 23, 17, 18, 58, 70, 5, 69, 2, 51, 46, 1, 45, 71, 47, 48, 71, 47, 64, 48, 48, 72, 71, 1, 71, 72, 16, 48, 64, 45, 2, 46, 2, 45, 1, 70, 4, 5, 3, 70, 2, 3, 4, 70, 24, 58, 23, 72, 0, 1, 73, 55, 72, 55, 0, 72, 48, 73, 72, 57, 17, 58, 25, 57, 58, 56, 48, 16, 73, 48, 56, 56, 16, 17, 56, 17, 57, 52, 0, 55, 24, 25, 58, 44, 0, 52, 67, 44, 52, 52, 56, 53, 73, 52, 55, 56, 52, 73, 67, 52, 53, 26, 57, 25, 66, 67, 53, 56, 32, 35, 53, 56, 35, 56, 57, 32, 28, 31, 57, 57, 31, 32, 57, 27, 28, 26, 27, 57, 36, 53, 35, 43, 44, 67, 43, 67, 66, 34, 35, 32, 29, 31, 28, 30, 31, 29, 53, 54, 66, 53, 36, 54, 33, 34, 32, 37, 54, 36, 65, 43, 66, 38, 54, 37, 54, 65, 66, 39, 65, 54, 42, 43, 65, 38, 39, 54, 40, 42, 65, 40, 41, 42, 65, 39, 40 ],\n\t\t\t\t\t\"vertices\": [ -189.36, 15.62, -201.35, 23.47, -220.09, 22.49, -245.95, 21.13, -251.28, 20.86, -257.58, 43.96, -257.57, 68.16, -255.02, 85.24, -252.47, 85.39, -238.71, 86.2, -232.52, 101.69, -216.61, 101.69, -203.78, 95.3, -189.58, 88.21, -164.51, 65.1, -148.19, 39.93, -140.06, 34.71, 152.82, 34.73, 177.57, 43.45, 197.97, 52.05, 214.72, 57.82, 227.6, 58.27, 234.42, 42.87, 231.94, 30.06, 224.01, 20.72, 206.91, 7.21, 186.23, -3.97, 168.34, -6.4, 149.9, -5.94, 142.35, -15.57, 52.04, -15.77, 44.33, -5.71, -25.52, -5.73, -31.75, -16.62, -121.07, -15.34, -126.81, -5.28, -134.7, -6.81, -136.54, -16.61, -144.22, -27.41, -154.63, -36.57, -172.47, -50.31, -183.41, -50.26, -194.09, -35.4, -206.56, -10.32, -207.4, 6.72, -212.65, 29.84, -212.64, 38.88, -183.37, 38.05, -149.38, 22.86, -226.43, 85.25, -216.33, 68.71, -213.76, 44.64, -179.34, 12.42, -171.29, 1.81, -158.6, -20.36, -169.42, 17.11, -148.52, 13.49, 166.82, 13.56, 196.76, 27.89, 218.14, 45.6, -211.08, 81.54, -195.15, 74.59, -178.93, 64.17, -168.84, 54.16, -156.19, 37.31, -178.5, -28.13, -190.55, -4.9, -198.07, 4.33, -245.79, 68.54, -244.14, 44.18, -245, 26.12, -186.36, 32.17, -168.92, 23.12, -160.69, 15.6 ],\n\t\t\t\t\t\"hull\": 45,\n\t\t\t\t\t\"edges\": [ 0, 2, 8, 10, 10, 12, 12, 14, 18, 20, 20, 22, 26, 28, 28, 30, 30, 32, 32, 34, 34, 36, 36, 38, 38, 40, 40, 42, 42, 44, 44, 46, 46, 48, 48, 50, 50, 52, 52, 54, 54, 56, 56, 58, 58, 60, 60, 62, 62, 64, 64, 66, 66, 68, 68, 70, 70, 72, 72, 74, 80, 82, 82, 84, 84, 86, 86, 88, 0, 88, 2, 90, 90, 92, 92, 94, 94, 96, 96, 32, 18, 98, 98, 100, 100, 102, 2, 4, 102, 4, 92, 102, 0, 104, 104, 106, 106, 108, 78, 80, 108, 78, 74, 76, 76, 78, 62, 56, 64, 70, 0, 110, 112, 114, 114, 116, 116, 118, 118, 42, 50, 116, 114, 34, 98, 120, 120, 122, 22, 24, 24, 26, 120, 24, 122, 124, 124, 126, 126, 128, 128, 96, 80, 130, 130, 132, 132, 134, 134, 88, 14, 16, 16, 18, 136, 16, 136, 138, 138, 140, 4, 6, 6, 8, 140, 6, 96, 112, 92, 142, 142, 144, 110, 146, 146, 112, 144, 146 ],\n\t\t\t\t\t\"width\": 492,\n\t\t\t\t\t\"height\": 152\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"hoverboard-thruster-front\": {\n\t\t\t\t\"hoverboard-thruster\": { \"x\": 0.021476746, \"y\": -7.079941, \"rotation\": 0.17448044, \"width\": 60, \"height\": 64 }\n\t\t\t},\n\t\t\t\"hoverboard-thruster-rear\": {\n\t\t\t\t\"hoverboard-thruster\": { \"x\": 1.099083, \"y\": -6.2866554, \"rotation\": 0.17448044, \"width\": 60, \"height\": 64 }\n\t\t\t},\n\t\t\t\"hoverglow-front\": {\n\t\t\t\t\"hoverglow-small\": {\n\t\t\t\t\t\"x\": 2.1299858,\n\t\t\t\t\t\"y\": -1.9999886,\n\t\t\t\t\t\"scaleX\": 0.303,\n\t\t\t\t\t\"scaleY\": 0.495,\n\t\t\t\t\t\"rotation\": 0.15000382,\n\t\t\t\t\t\"width\": 274,\n\t\t\t\t\t\"height\": 75\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"hoverglow-rear\": {\n\t\t\t\t\"hoverglow-small\": {\n\t\t\t\t\t\"x\": 1.3899841,\n\t\t\t\t\t\"y\": -2.0899887,\n\t\t\t\t\t\"scaleX\": 0.303,\n\t\t\t\t\t\"scaleY\": 0.495,\n\t\t\t\t\t\"rotation\": 0.60982704,\n\t\t\t\t\t\"width\": 274,\n\t\t\t\t\t\"height\": 75\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"mouth\": {\n\t\t\t\t\"mouth-grind\": {\n\t\t\t\t\t\"type\": \"mesh\",\n\t\t\t\t\t\"uvs\": [ 1, 1, 0, 1, 0, 0, 1, 0 ],\n\t\t\t\t\t\"triangles\": [ 1, 3, 0, 1, 2, 3 ],\n\t\t\t\t\t\"vertices\": [ 2, 66, -98.93, -85.88, 0.22, 46, 11.28, -85.88, 0.78, 2, 66, -129.77, 1.84, 0.6, 46, -19.56, 1.84, 0.4, 2, 66, -74.12, 21.41, 0.6, 46, 36.09, 21.41, 0.4, 2, 66, -43.28, -66.32, 0.4, 46, 66.93, -66.32, 0.6 ],\n\t\t\t\t\t\"hull\": 4,\n\t\t\t\t\t\"edges\": [ 0, 2, 2, 4, 4, 6, 0, 6 ],\n\t\t\t\t\t\"width\": 93,\n\t\t\t\t\t\"height\": 59\n\t\t\t\t},\n\t\t\t\t\"mouth-oooo\": {\n\t\t\t\t\t\"type\": \"mesh\",\n\t\t\t\t\t\"uvs\": [ 1, 1, 0, 1, 0, 0, 1, 0 ],\n\t\t\t\t\t\"triangles\": [ 1, 3, 0, 1, 2, 3 ],\n\t\t\t\t\t\"vertices\": [ 2, 46, 11.28, -85.89, 0.22, 66, -98.93, -85.89, 0.78, 2, 46, -19.56, 1.85, 0.6, 66, -129.78, 1.85, 0.4, 2, 46, 36.1, 21.42, 0.6, 66, -74.12, 21.42, 0.4, 2, 46, 66.94, -66.32, 0.4, 66, -43.27, -66.32, 0.6 ],\n\t\t\t\t\t\"hull\": 4,\n\t\t\t\t\t\"edges\": [ 0, 2, 2, 4, 4, 6, 0, 6 ],\n\t\t\t\t\t\"width\": 93,\n\t\t\t\t\t\"height\": 59\n\t\t\t\t},\n\t\t\t\t\"mouth-smile\": {\n\t\t\t\t\t\"type\": \"mesh\",\n\t\t\t\t\t\"uvs\": [ 1, 1, 0, 1, 0, 0, 1, 0 ],\n\t\t\t\t\t\"triangles\": [ 1, 3, 0, 1, 2, 3 ],\n\t\t\t\t\t\"vertices\": [ 2, 66, -98.93, -85.89, 0.21075, 46, 11.28, -85.89, 0.78925, 2, 66, -129.77, 1.85, 0.6, 46, -19.56, 1.85, 0.4, 2, 66, -74.11, 21.42, 0.6, 46, 36.1, 21.42, 0.4, 2, 66, -43.27, -66.32, 0.40772, 46, 66.94, -66.32, 0.59228 ],\n\t\t\t\t\t\"hull\": 4,\n\t\t\t\t\t\"edges\": [ 0, 2, 2, 4, 4, 6, 0, 6 ],\n\t\t\t\t\t\"width\": 93,\n\t\t\t\t\t\"height\": 59\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"muzzle\": {\n\t\t\t\t\"muzzle01\": {\n\t\t\t\t\t\"x\": 151.9714,\n\t\t\t\t\t\"y\": 5.807556,\n\t\t\t\t\t\"scaleX\": 3.7360818,\n\t\t\t\t\t\"scaleY\": 3.7360818,\n\t\t\t\t\t\"rotation\": 0.15336847,\n\t\t\t\t\t\"width\": 133,\n\t\t\t\t\t\"height\": 79\n\t\t\t\t},\n\t\t\t\t\"muzzle02\": {\n\t\t\t\t\t\"x\": 187.2479,\n\t\t\t\t\t\"y\": 5.9019775,\n\t\t\t\t\t\"scaleX\": 4.0622835,\n\t\t\t\t\t\"scaleY\": 4.0622835,\n\t\t\t\t\t\"rotation\": 0.15336847,\n\t\t\t\t\t\"width\": 135,\n\t\t\t\t\t\"height\": 84\n\t\t\t\t},\n\t\t\t\t\"muzzle03\": {\n\t\t\t\t\t\"x\": 231.96396,\n\t\t\t\t\t\"y\": 6.0216675,\n\t\t\t\t\t\"scaleX\": 4.132498,\n\t\t\t\t\t\"scaleY\": 4.132498,\n\t\t\t\t\t\"rotation\": 0.15336847,\n\t\t\t\t\t\"width\": 166,\n\t\t\t\t\t\"height\": 106\n\t\t\t\t},\n\t\t\t\t\"muzzle04\": {\n\t\t\t\t\t\"x\": 231.96399,\n\t\t\t\t\t\"y\": 6.0216675,\n\t\t\t\t\t\"scaleX\": 4.004575,\n\t\t\t\t\t\"scaleY\": 4.004575,\n\t\t\t\t\t\"rotation\": 0.15336847,\n\t\t\t\t\t\"width\": 149,\n\t\t\t\t\t\"height\": 90\n\t\t\t\t},\n\t\t\t\t\"muzzle05\": {\n\t\t\t\t\t\"x\": 293.80118,\n\t\t\t\t\t\"y\": 6.187192,\n\t\t\t\t\t\"scaleX\": 4.4672766,\n\t\t\t\t\t\"scaleY\": 4.4672766,\n\t\t\t\t\t\"rotation\": 0.15336847,\n\t\t\t\t\t\"width\": 135,\n\t\t\t\t\t\"height\": 75\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"muzzle-glow\": {\n\t\t\t\t\"muzzle-glow\": { \"width\": 50, \"height\": 50 }\n\t\t\t},\n\t\t\t\"muzzle-ring\": {\n\t\t\t\t\"muzzle-ring\": { \"x\": -1.304802, \"y\": 0.32229614, \"scaleX\": 0.3147263, \"scaleY\": 0.3147263, \"width\": 49, \"height\": 209 }\n\t\t\t},\n\t\t\t\"muzzle-ring2\": {\n\t\t\t\t\"muzzle-ring\": { \"x\": -1.304802, \"y\": 0.32229614, \"scaleX\": 0.3147263, \"scaleY\": 0.3147263, \"width\": 49, \"height\": 209 }\n\t\t\t},\n\t\t\t\"muzzle-ring3\": {\n\t\t\t\t\"muzzle-ring\": { \"x\": -1.304802, \"y\": 0.32229614, \"scaleX\": 0.3147263, \"scaleY\": 0.3147263, \"width\": 49, \"height\": 209 }\n\t\t\t},\n\t\t\t\"muzzle-ring4\": {\n\t\t\t\t\"muzzle-ring\": { \"x\": -1.304802, \"y\": 0.32229614, \"scaleX\": 0.3147263, \"scaleY\": 0.3147263, \"width\": 49, \"height\": 209 }\n\t\t\t},\n\t\t\t\"neck\": {\n\t\t\t\t\"neck\": { \"x\": 9.769501, \"y\": -3.0127716, \"rotation\": -55.223812, \"width\": 36, \"height\": 41 }\n\t\t\t},\n\t\t\t\"portal-bg\": {\n\t\t\t\t\"portal-bg\": { \"x\": -3.0979004, \"y\": 7.2471313, \"scaleX\": 1.0491699, \"scaleY\": 1.0491699, \"width\": 266, \"height\": 266 }\n\t\t\t},\n\t\t\t\"portal-flare1\": {\n\t\t\t\t\"portal-flare1\": { \"width\": 111, \"height\": 60 },\n\t\t\t\t\"portal-flare2\": { \"width\": 114, \"height\": 61 },\n\t\t\t\t\"portal-flare3\": { \"width\": 115, \"height\": 59 }\n\t\t\t},\n\t\t\t\"portal-flare2\": {\n\t\t\t\t\"portal-flare1\": { \"width\": 111, \"height\": 60 },\n\t\t\t\t\"portal-flare2\": { \"width\": 114, \"height\": 61 },\n\t\t\t\t\"portal-flare3\": { \"width\": 115, \"height\": 59 }\n\t\t\t},\n\t\t\t\"portal-flare3\": {\n\t\t\t\t\"portal-flare1\": { \"width\": 111, \"height\": 60 },\n\t\t\t\t\"portal-flare2\": { \"width\": 114, \"height\": 61 },\n\t\t\t\t\"portal-flare3\": { \"width\": 115, \"height\": 59 }\n\t\t\t},\n\t\t\t\"portal-flare4\": {\n\t\t\t\t\"portal-flare1\": { \"width\": 111, \"height\": 60 },\n\t\t\t\t\"portal-flare2\": { \"width\": 114, \"height\": 61 },\n\t\t\t\t\"portal-flare3\": { \"width\": 115, \"height\": 59 }\n\t\t\t},\n\t\t\t\"portal-flare5\": {\n\t\t\t\t\"portal-flare1\": { \"width\": 111, \"height\": 60 },\n\t\t\t\t\"portal-flare2\": { \"width\": 114, \"height\": 61 },\n\t\t\t\t\"portal-flare3\": { \"width\": 115, \"height\": 59 }\n\t\t\t},\n\t\t\t\"portal-flare6\": {\n\t\t\t\t\"portal-flare1\": { \"width\": 111, \"height\": 60 },\n\t\t\t\t\"portal-flare2\": { \"width\": 114, \"height\": 61 },\n\t\t\t\t\"portal-flare3\": { \"width\": 115, \"height\": 59 }\n\t\t\t},\n\t\t\t\"portal-flare7\": {\n\t\t\t\t\"portal-flare1\": { \"width\": 111, \"height\": 60 },\n\t\t\t\t\"portal-flare2\": { \"width\": 114, \"height\": 61 },\n\t\t\t\t\"portal-flare3\": { \"width\": 115, \"height\": 59 }\n\t\t\t},\n\t\t\t\"portal-flare8\": {\n\t\t\t\t\"portal-flare1\": { \"width\": 111, \"height\": 60 },\n\t\t\t\t\"portal-flare2\": { \"width\": 114, \"height\": 61 },\n\t\t\t\t\"portal-flare3\": { \"width\": 115, \"height\": 59 }\n\t\t\t},\n\t\t\t\"portal-flare9\": {\n\t\t\t\t\"portal-flare1\": { \"width\": 111, \"height\": 60 },\n\t\t\t\t\"portal-flare2\": { \"width\": 114, \"height\": 61 },\n\t\t\t\t\"portal-flare3\": { \"width\": 115, \"height\": 59 }\n\t\t\t},\n\t\t\t\"portal-flare10\": {\n\t\t\t\t\"portal-flare1\": { \"width\": 111, \"height\": 60 },\n\t\t\t\t\"portal-flare2\": { \"width\": 114, \"height\": 61 },\n\t\t\t\t\"portal-flare3\": { \"width\": 115, \"height\": 59 }\n\t\t\t},\n\t\t\t\"portal-shade\": {\n\t\t\t\t\"portal-shade\": { \"width\": 266, \"height\": 266 }\n\t\t\t},\n\t\t\t\"portal-streaks1\": {\n\t\t\t\t\"portal-streaks1\": { \"scaleX\": 0.97737765, \"scaleY\": 0.97737765, \"width\": 252, \"height\": 256 }\n\t\t\t},\n\t\t\t\"portal-streaks2\": {\n\t\t\t\t\"portal-streaks2\": { \"x\": -1.6380005, \"y\": 2.7937927, \"width\": 250, \"height\": 249 }\n\t\t\t},\n\t\t\t\"rear-bracer\": {\n\t\t\t\t\"rear-bracer\": { \"x\": 11.151097, \"y\": -2.2033694, \"rotation\": 66.173065, \"width\": 56, \"height\": 72 }\n\t\t\t},\n\t\t\t\"rear-foot\": {\n\t\t\t\t\"rear-foot\": {\n\t\t\t\t\t\"type\": \"mesh\",\n\t\t\t\t\t\"uvs\": [ 0.48368493, 0.13870415, 0.51990783, 0.2142364, 0.55099833, 0.27906656, 0.588384, 0.2981607, 0.63488865, 0.32191223, 0.773423, 0.39266646, 1, 0.73346615, 1, 1, 0.5483075, 0.99883115, 0.3116092, 1, 0, 1, 0, 0.41396642, 0.13630842, 0, 0.41716674, 0 ],\n\t\t\t\t\t\"triangles\": [ 8, 3, 4, 8, 4, 5, 8, 5, 6, 8, 6, 7, 11, 1, 10, 3, 9, 2, 2, 10, 1, 12, 13, 0, 0, 11, 12, 1, 11, 0, 2, 9, 10, 3, 8, 9 ],\n\t\t\t\t\t\"vertices\": [ 2, 8, 10.45, 29.41, 0.90802, 9, -6.74, 49.62, 0.09198, 2, 8, 16.56, 29.27, 0.84259, 9, -2.65, 45.09, 0.15741, 2, 8, 21.8, 29.15, 0.69807, 9, 0.85, 41.2, 0.30193, 2, 8, 25.53, 31.43, 0.52955, 9, 5.08, 40.05, 0.47045, 2, 8, 30.18, 34.27, 0.39303, 9, 10.33, 38.62, 0.60697, 2, 8, 44.02, 42.73, 0.27525, 9, 25.98, 34.36, 0.72475, 2, 8, 76.47, 47.28, 0.21597, 9, 51.56, 13.9, 0.78403, 2, 8, 88.09, 36.29, 0.28719, 9, 51.55, -2.09, 0.71281, 2, 8, 52.94, -0.73, 0.47576, 9, 0.52, -1.98, 0.52424, 2, 8, 34.63, -20.23, 0.68757, 9, -26.23, -2.03, 0.31243, 2, 8, 10.44, -45.81, 0.84141, 9, -61.43, -2, 0.15859, 2, 8, -15.11, -21.64, 0.93283, 9, -61.4, 33.15, 0.06717, 1, 8, -22.57, 6.61, 1, 1, 8, -0.76, 29.67, 1 ],\n\t\t\t\t\t\"hull\": 14,\n\t\t\t\t\t\"edges\": [ 14, 12, 10, 12, 14, 16, 16, 18, 18, 20, 4, 18, 20, 22, 24, 26, 22, 24, 4, 2, 2, 20, 4, 6, 6, 16, 6, 8, 8, 10, 2, 0, 0, 26 ],\n\t\t\t\t\t\"width\": 113,\n\t\t\t\t\t\"height\": 60\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"rear-shin\": {\n\t\t\t\t\"rear-shin\": { \"x\": 58.290283, \"y\": -2.7543373, \"rotation\": 92.37433, \"width\": 75, \"height\": 178 }\n\t\t\t},\n\t\t\t\"rear-thigh\": {\n\t\t\t\t\"rear-thigh\": { \"x\": 33.10798, \"y\": -4.11312, \"rotation\": 72.541, \"width\": 55, \"height\": 94 }\n\t\t\t},\n\t\t\t\"rear-upper-arm\": {\n\t\t\t\t\"rear-upper-arm\": { \"x\": 21.125124, \"y\": 4.0870357, \"rotation\": 89.326004, \"width\": 40, \"height\": 87 }\n\t\t\t},\n\t\t\t\"side-glow1\": {\n\t\t\t\t\"hoverglow-small\": { \"x\": 2.0941744, \"scaleX\": 0.23527138, \"scaleY\": 0.41317555, \"width\": 274, \"height\": 75 }\n\t\t\t},\n\t\t\t\"side-glow2\": {\n\t\t\t\t\"hoverglow-small\": { \"x\": 2.0941744, \"scaleX\": 0.23527138, \"scaleY\": 0.41317555, \"width\": 274, \"height\": 75 }\n\t\t\t},\n\t\t\t\"side-glow3\": {\n\t\t\t\t\"hoverglow-small\": { \"x\": 2.0941744, \"scaleX\": 0.35858902, \"scaleY\": 0.6297418, \"width\": 274, \"height\": 75 }\n\t\t\t},\n\t\t\t\"torso\": {\n\t\t\t\t\"torso\": {\n\t\t\t\t\t\"type\": \"mesh\",\n\t\t\t\t\t\"uvs\": [ 0.6250965, 0.12672493, 1, 0.26360828, 1, 0.28870577, 1, 0.66021097, 1, 0.68245256, 0.92323905, 0.6925871, 0.95115805, 0.8496525, 0.7712399, 1, 0.49654746, 1, 0.27181146, 1, 0.1384236, 0.7719636, 0.09886348, 0.68170476, 0.056349717, 0.584707, 0, 0.45614165, 0, 0.3377818, 0, 0.19436389, 0.14462778, 0, 0.27801543, 0, 0.7252479, 0.27835009, 0.7609111, 0.46216103, 0.8488804, 0.67962754, 0.68257374, 0.63249457, 0.53985965, 0.38469505, 0.25443152, 0.3216951, 0.30062926, 0.5517359, 0.39552706, 0.79506904, 0.26389468, 0.17006968, 0.5240986, 0.18673615, 0.7149173, 0.7665529, 0.82151175, 0.7295553, 0.2762614, 0.4303966, 0.62327, 0.5295234, 0.34549934, 0.66678995, 0.53243136, 0.29139623 ],\n\t\t\t\t\t\"triangles\": [ 18, 1, 2, 19, 2, 3, 18, 0, 1, 23, 15, 26, 27, 26, 16, 14, 15, 23, 15, 16, 26, 17, 27, 16, 13, 14, 23, 0, 27, 17, 13, 23, 30, 11, 12, 24, 21, 31, 19, 12, 13, 30, 24, 22, 31, 31, 22, 19, 12, 30, 24, 32, 24, 31, 24, 30, 22, 3, 20, 19, 32, 31, 21, 11, 24, 32, 4, 5, 3, 8, 28, 7, 7, 29, 6, 7, 28, 29, 9, 25, 8, 8, 25, 28, 9, 10, 25, 29, 5, 6, 10, 32, 25, 25, 21, 28, 25, 32, 21, 10, 11, 32, 28, 21, 29, 29, 20, 5, 29, 21, 20, 5, 20, 3, 20, 21, 19, 33, 26, 27, 22, 18, 19, 19, 18, 2, 33, 27, 18, 30, 23, 22, 22, 33, 18, 23, 33, 22, 33, 23, 26, 27, 0, 18 ],\n\t\t\t\t\t\"vertices\": [ 2, 29, 44.59, -10.39, 0.88, 40, -17.65, 33.99, 0.12, 3, 28, 59.65, -45.08, 0.12189, 29, 17.13, -45.08, 0.26811, 40, 22.68, 15.82, 0.61, 3, 28, 55.15, -44.72, 0.1345, 29, 12.63, -44.72, 0.2555, 40, 23.43, 11.37, 0.61, 3, 27, 31.01, -39.45, 0.51133, 28, -11.51, -39.45, 0.30867, 40, 34.58, -54.57, 0.18, 3, 27, 27.01, -39.14, 0.53492, 28, -15.5, -39.14, 0.28508, 40, 35.25, -58.52, 0.18, 2, 27, 25.79, -31.5, 0.75532, 28, -16.73, -31.5, 0.24468, 1, 27, -2.61, -32, 1, 1, 27, -28.2, -12.29, 1, 1, 27, -26.08, 14.55, 1, 1, 27, -24.35, 36.5, 1, 2, 27, 17.6, 46.3, 0.8332, 28, -24.92, 46.3, 0.1668, 2, 27, 34.1, 48.89, 0.59943, 28, -8.42, 48.89, 0.40058, 3, 27, 51.83, 51.67, 0.29262, 28, 9.32, 51.67, 0.63181, 29, -33.2, 51.67, 0.07557, 3, 27, 75.34, 55.35, 0.06656, 28, 32.82, 55.35, 0.62298, 29, -9.7, 55.35, 0.31046, 2, 28, 54.06, 53.67, 0.37296, 29, 11.54, 53.67, 0.62704, 2, 28, 79.79, 51.64, 0.10373, 29, 37.27, 51.64, 0.89627, 1, 29, 71.04, 34.76, 1, 1, 29, 70.01, 21.72, 1, 1, 30, 36.74, 7.06, 1, 3, 30, 45.7, -24.98, 0.67, 28, 25.87, -18.9, 0.3012, 29, -16.65, -18.9, 0.0288, 2, 27, 28.69, -24.42, 0.77602, 28, -13.83, -24.42, 0.22398, 3, 30, 43.24, -56.49, 0.064, 27, 38.43, -8.84, 0.67897, 28, -4.09, -8.84, 0.25703, 3, 30, 22.02, -14.85, 0.29, 28, 41.48, 1.59, 0.53368, 29, -1.04, 1.59, 0.17632, 3, 30, -7.45, -8.33, 0.76, 28, 54.98, 28.59, 0.06693, 29, 12.46, 28.59, 0.17307, 3, 30, 3.91, -48.4, 0.25, 27, 55.87, 27.33, 0.15843, 28, 13.35, 27.33, 0.59157, 1, 27, 11.47, 21.51, 1, 2, 30, -11.09, 18.74, 0.416, 29, 39.6, 25.51, 0.584, 2, 30, 14.56, 20.03, 0.53, 29, 34.6, 0.33, 0.47, 1, 27, 14.12, -10.1, 1, 2, 27, 19.94, -21.03, 0.92029, 28, -22.58, -21.03, 0.07971, 3, 30, -2.08, -27.26, 0.29, 28, 35.31, 27.99, 0.49582, 29, -7.21, 27.99, 0.21418, 2, 30, 34.42, -39.19, 0.25, 28, 14.84, -4.5, 0.75, 2, 27, 34.87, 24.58, 0.67349, 28, -7.64, 24.58, 0.32651, 2, 30, 18.5, 1.59, 0.76, 29, 15.76, 1, 0.24 ],\n\t\t\t\t\t\"hull\": 18,\n\t\t\t\t\t\"edges\": [ 14, 12, 12, 10, 10, 8, 18, 20, 32, 34, 30, 32, 2, 4, 36, 4, 36, 38, 38, 40, 4, 6, 6, 8, 40, 6, 40, 42, 14, 16, 16, 18, 50, 16, 46, 52, 54, 36, 2, 0, 0, 34, 54, 0, 54, 32, 20, 50, 14, 56, 56, 42, 50, 56, 56, 58, 58, 40, 58, 10, 46, 60, 60, 48, 26, 60, 60, 44, 24, 26, 24, 48, 42, 62, 62, 44, 48, 62, 48, 64, 64, 50, 42, 64, 20, 22, 22, 24, 64, 22, 26, 28, 28, 30, 28, 46, 44, 66, 66, 54, 46, 66, 66, 36, 62, 38 ],\n\t\t\t\t\t\"width\": 98,\n\t\t\t\t\t\"height\": 180\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n],\n\"events\": {\n\t\"footstep\": {}\n},\n\"animations\": {\n\t\"aim\": {\n\t\t\"slots\": {\n\t\t\t\"crosshair\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"crosshair\" }\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t\"bones\": {\n\t\t\t\"front-fist\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": 36.07917 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": -26.552755 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": 62.305367 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": 9.111553 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"gun\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": -0.30511093 }\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t\"ik\": {\n\t\t\t\"aim-ik\": [\n\t\t\t\t{ \"mix\": 0.995 }\n\t\t\t]\n\t\t},\n\t\t\"transform\": {\n\t\t\t\"aim-front-arm-transform\": [\n\t\t\t\t{ \"mixRotate\": 0.78400004 }\n\t\t\t],\n\t\t\t\"aim-head-transform\": [\n\t\t\t\t{ \"mixRotate\": 0.65900004 }\n\t\t\t],\n\t\t\t\"aim-torso-transform\": [\n\t\t\t\t{ \"mixRotate\": 0.42299998 }\n\t\t\t]\n\t\t}\n\t},\n\t\"death\": {\n\t\t\"slots\": {\n\t\t\t\"eye\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"eye-surprised\" },\n\t\t\t\t\t{ \"time\": 0.53333336, \"name\": \"eye-indifferent\" },\n\t\t\t\t\t{ \"time\": 2.2, \"name\": \"eye-surprised\" },\n\t\t\t\t\t{ \"time\": 4.6000004, \"name\": \"eye-indifferent\" }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-fist\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"front-fist-open\" }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"mouth\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"mouth-oooo\" },\n\t\t\t\t\t{ \"time\": 0.53333336, \"name\": \"mouth-grind\" },\n\t\t\t\t\t{ \"time\": 1.4000001, \"name\": \"mouth-oooo\" },\n\t\t\t\t\t{ \"time\": 2.1666667, \"name\": \"mouth-grind\" },\n\t\t\t\t\t{ \"time\": 4.533334, \"name\": \"mouth-oooo\" }\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t\"bones\": {\n\t\t\t\"head\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -2.8279877,\n\t\t\t\t\t\t\"curve\": [ 0.015444445, -2.8279877, 0.035690706, 12.7193165 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": 12.186468,\n\t\t\t\t\t\t\"curve\": [ 0.09600131, 11.681858, 0.11911736, -1.144228 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": -6.8585644,\n\t\t\t\t\t\t\"curve\": [ 0.14927696, -13.267348, 0.21048638, -37.27945 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.3,\n\t\t\t\t\t\t\"value\": -36.86384,\n\t\t\t\t\t\t\"curve\": [ 0.3539913, -36.613155, 0.41202274, -32.35048 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"value\": -23.489603,\n\t\t\t\t\t\t\"curve\": [ 0.48994896, -19.870373, 0.5120965, -3.2874317 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.53333336,\n\t\t\t\t\t\t\"value\": -3.2377663,\n\t\t\t\t\t\t\"curve\": [ 0.55961764, -3.3898296, 0.6143612, -67.24913 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": -74.402985,\n\t\t\t\t\t\t\"curve\": [ 0.6523558, -81.57579, 0.70216596, -88.94219 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": -88.93178,\n\t\t\t\t\t\t\"curve\": [ 0.8048824, -88.907875, 0.83801675, -80.874954 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"value\": -81.02708,\n\t\t\t\t\t\t\"curve\": [ 0.92162764, -81.3189, 0.9755557, -85.288284 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": -85.288284, \"curve\": \"stepped\" },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.2333333,\n\t\t\t\t\t\t\"value\": -85.288284,\n\t\t\t\t\t\t\"curve\": [ 2.3143334, -85.288284, 2.3823261, -68.06154 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.4666667,\n\t\t\t\t\t\t\"value\": -63.483845,\n\t\t\t\t\t\t\"curve\": [ 2.5701094, -57.869366, 2.9157326, -55.24182 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.2000003,\n\t\t\t\t\t\t\"value\": -55.103165,\n\t\t\t\t\t\t\"curve\": [ 3.4471326, -54.982624, 4.134757, -56.60572 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.266667,\n\t\t\t\t\t\t\"value\": -58.2344,\n\t\t\t\t\t\t\"curve\": [ 4.672121, -63.24052, 4.64639, -82.692894 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 4.9333334, \"value\": -85.288284 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"curve\": [ 0.469, 1.005, 0.4916667, 1.065, 0.47500002, 1.0178572, 0.4916667, 0.94 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"x\": 1.065,\n\t\t\t\t\t\t\"y\": 0.94,\n\t\t\t\t\t\t\"curve\": [ 0.5166667, 1.065, 0.5410838, 0.9910153, 0.5166667, 0.94, 0.5423502, 1.0256574 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"x\": 0.98957354,\n\t\t\t\t\t\t\"y\": 1.0247642,\n\t\t\t\t\t\t\"curve\": [ 0.5933728, 0.9880685, 0.6089289, 1.0015051, 0.59484184, 1.0237292, 0.6074599, 1.0010352 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6333334 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"neck\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -2.8279877,\n\t\t\t\t\t\t\"curve\": [ 0.11415304, 1.3304386, 0.19498403, 4.1300545 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": 4.132889,\n\t\t\t\t\t\t\"curve\": [ 0.35123327, 4.13624, 0.44404563, -24.501444 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": -24.686066,\n\t\t\t\t\t\t\"curve\": [ 0.57063705, -23.89392, 0.5499005, 34.22126 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"value\": 35.127342,\n\t\t\t\t\t\t\"curve\": [ 0.7131321, 34.811573, 0.75557166, 22.764702 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8333334,\n\t\t\t\t\t\t\"value\": 22.81864,\n\t\t\t\t\t\t\"curve\": [ 0.8679167, 22.842625, 0.9156493, 47.949654 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.9666667, \"value\": 47.949654, \"curve\": \"stepped\" },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.2333333,\n\t\t\t\t\t\t\"value\": 47.949654,\n\t\t\t\t\t\t\"curve\": [ 2.3000002, 47.949654, 2.6166525, 18.71746 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.6666667,\n\t\t\t\t\t\t\"value\": 18.505486,\n\t\t\t\t\t\t\"curve\": [ 3.1719964, 16.579683, 4.060462, 16.790627 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.533334,\n\t\t\t\t\t\t\"value\": 18.505486,\n\t\t\t\t\t\t\"curve\": [ 4.7065263, 19.133564, 4.7758894, 41.105133 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 4.8, \"value\": 47.949654 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -8.615379,\n\t\t\t\t\t\t\"curve\": [ 0.010259144, -16.712029, 0.032228872, -33.600677 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": -33.372116,\n\t\t\t\t\t\t\"curve\": [ 0.18178345, -32.608078, 0.29769447, 123.07062 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"value\": 122.77002,\n\t\t\t\t\t\t\"curve\": [ 0.5109518, 122.69125, 0.52041817, 100.200424 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": 88.96379,\n\t\t\t\t\t\t\"curve\": [ 0.5875576, 83.88809, 0.6666667, 75.34303 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.70000005,\n\t\t\t\t\t\t\"value\": 75.34303,\n\t\t\t\t\t\t\"curve\": [ 0.7666667, 75.34303, 0.90000004, 76.03314 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.9666667, \"value\": 76.03314 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -38.855072,\n\t\t\t\t\t\t\"curve\": [ 0.021926392, -40.375153, 0.09562778, -41.91797 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": -41.91797,\n\t\t\t\t\t\t\"curve\": [ 0.17629863, -41.917984, 0.21647093, -16.920395 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": -4.34639,\n\t\t\t\t\t\t\"curve\": [ 0.2575247, 13.692627, 0.30759022, 60.34659 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"value\": 60.16684,\n\t\t\t\t\t\t\"curve\": [ 0.49595165, 59.98024, 0.538565, 33.628464 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": 23.061996,\n\t\t\t\t\t\t\"curve\": [ 0.59455496, 32.711044, 0.6754875, 53.71147 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": 53.61354,\n\t\t\t\t\t\t\"curve\": [ 0.79691344, 53.50589, 0.92648214, 30.978836 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.9333334, \"value\": 19.57402, \"curve\": \"stepped\" },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.9666668,\n\t\t\t\t\t\t\"value\": 19.57402,\n\t\t\t\t\t\t\"curve\": [ 2.245216, 19.57402, 2.7021878, 77.02501 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.0666668,\n\t\t\t\t\t\t\"value\": 77.06021,\n\t\t\t\t\t\t\"curve\": [ 3.208887, 77.327965, 3.2910693, 67.992966 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.4333334,\n\t\t\t\t\t\t\"value\": 67.959595,\n\t\t\t\t\t\t\"curve\": [ 3.6084769, 68.34179, 3.7293818, 73.87673 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.8333335,\n\t\t\t\t\t\t\"value\": 73.41716,\n\t\t\t\t\t\t\"curve\": [ 4.152479, 73.91438, 4.460341, 71.980255 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.6333337,\n\t\t\t\t\t\t\"value\": 64.76924,\n\t\t\t\t\t\t\"curve\": [ 4.6877923, 62.49916, 4.8468027, 26.42305 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 4.866667, \"value\": 10.940689 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -44.695557,\n\t\t\t\t\t\t\"curve\": [ 0.033333335, -44.695557, 0.11970831, 54.886444 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 64.6217,\n\t\t\t\t\t\t\"curve\": [ 0.15370962, 79.180824, 0.21412581, 79.422104 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": 63.395065,\n\t\t\t\t\t\t\"curve\": [ 0.29276425, 55.185246, 0.33189163, 30.129349 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": 30.129349,\n\t\t\t\t\t\t\"curve\": [ 0.40001768, 30.129349, 0.44129986, 39.874146 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"value\": 55.12625,\n\t\t\t\t\t\t\"curve\": [ 0.48838428, 68.18418, 0.52040523, 100.72226 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.53333336,\n\t\t\t\t\t\t\"value\": 111.95866,\n\t\t\t\t\t\t\"curve\": [ 0.550502, 126.88067, 0.627324, 185.96957 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"value\": 185.96957,\n\t\t\t\t\t\t\"curve\": [ 0.6916668, 185.96957, 0.73579437, 162.4288 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8000001,\n\t\t\t\t\t\t\"value\": 158.00668,\n\t\t\t\t\t\t\"curve\": [ 0.9000577, 151.11525, 1.0166668, 144.01282 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.1, \"value\": 144.01282, \"curve\": \"stepped\" },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.3666668,\n\t\t\t\t\t\t\"value\": 144.01282,\n\t\t\t\t\t\t\"curve\": [ 2.4916668, 144.01282, 2.7416668, 138.6282 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.8666668,\n\t\t\t\t\t\t\"value\": 138.6282,\n\t\t\t\t\t\t\"curve\": [ 3.0666668, 138.6282, 3.466667, 138.63403 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.6666667,\n\t\t\t\t\t\t\"value\": 138.63403,\n\t\t\t\t\t\t\"curve\": [ 3.8833334, 138.63403, 4.316667, 135.18347 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.533334,\n\t\t\t\t\t\t\"value\": 135.18347,\n\t\t\t\t\t\t\"curve\": [ 4.5750003, 135.18347, 4.691667, 131.594 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.7333336,\n\t\t\t\t\t\t\"value\": 131.594,\n\t\t\t\t\t\t\"curve\": [ 4.7583337, 131.594, 4.516667, 144.01282 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 4.8333335, \"value\": 144.01282 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"curve\": [ 0.5166667, 0, 0.6166667, -34.96278, 0.5166667, 0, 0.6166667, -16.58711 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"x\": -35.022476, \"y\": -16.615433 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 21.880772,\n\t\t\t\t\t\t\"curve\": [ 0.033333335, 21.880772, 0.09858582, 20.435394 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 9.429705,\n\t\t\t\t\t\t\"curve\": [ 0.16401747, -0.2889843, 0.16229573, -38.259872 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"value\": -38.051247,\n\t\t\t\t\t\t\"curve\": [ 0.23978901, -37.962395, 0.2278192, -17.819233 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": -9.72551,\n\t\t\t\t\t\t\"curve\": [ 0.37194034, -6.7640696, 0.43129396, -0.7419243 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"value\": 6.467783,\n\t\t\t\t\t\t\"curve\": [ 0.48914683, 11.049698, 0.503116, 19.088676 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.53333336,\n\t\t\t\t\t\t\"value\": 19.08868,\n\t\t\t\t\t\t\"curve\": [ 0.5710945, 19.08868, 0.55432826, -42.67358 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": -42.67358,\n\t\t\t\t\t\t\"curve\": [ 0.65266836, -42.67358, 0.69084185, -13.799316 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.70000005,\n\t\t\t\t\t\t\"value\": -3.5362797,\n\t\t\t\t\t\t\"curve\": [ 0.70654225, 3.7950516, 0.7194211, 24.93827 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8000001,\n\t\t\t\t\t\t\"value\": 25.306828,\n\t\t\t\t\t\t\"curve\": [ 0.9015829, 24.746063, 0.9921239, -0.34114075 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": -32.163086, \"curve\": \"stepped\" },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.2333333,\n\t\t\t\t\t\t\"value\": -32.163086,\n\t\t\t\t\t\t\"curve\": [ 2.6000001, -32.163086, 2.6377406, -5.2954884 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.7,\n\t\t\t\t\t\t\"value\": -1.9566765,\n\t\t\t\t\t\t\"curve\": [ 2.7074482, -1.5572681, 2.775, 1.6656933 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.8000002,\n\t\t\t\t\t\t\"value\": 1.6656933,\n\t\t\t\t\t\t\"curve\": [ 2.825, 1.6656933, 2.8750002, -0.39156532 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.9,\n\t\t\t\t\t\t\"value\": -0.39156532,\n\t\t\t\t\t\t\"curve\": [ 2.9250002, -0.39156532, 2.9750001, 0.25940514 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.0000002,\n\t\t\t\t\t\t\"value\": 0.25940514,\n\t\t\t\t\t\t\"curve\": [ 3.025, 0.25940514, 3.075, -1.8061695 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.1000001,\n\t\t\t\t\t\t\"value\": -1.8061695,\n\t\t\t\t\t\t\"curve\": [ 3.1250002, -1.8061695, 3.1750002, -0.51751614 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.2000003,\n\t\t\t\t\t\t\"value\": -0.51751614,\n\t\t\t\t\t\t\"curve\": [ 3.2250001, -0.51751614, 3.275, -2.4079523 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.3000002,\n\t\t\t\t\t\t\"value\": -2.4079523,\n\t\t\t\t\t\t\"curve\": [ 3.3333335, -2.4079523, 3.4, -0.38046455 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.4333334,\n\t\t\t\t\t\t\"value\": -0.38046455,\n\t\t\t\t\t\t\"curve\": [ 3.466667, -0.38046455, 3.5333335, -2.253333 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.5666668,\n\t\t\t\t\t\t\"value\": -2.253333,\n\t\t\t\t\t\t\"curve\": [ 3.591667, -2.253333, 3.641667, -0.32653618 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.6666667,\n\t\t\t\t\t\t\"value\": -0.32653618,\n\t\t\t\t\t\t\"curve\": [ 3.7000003, -0.32653618, 3.766667, -1.3411007 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.8000002,\n\t\t\t\t\t\t\"value\": -1.3411007,\n\t\t\t\t\t\t\"curve\": [ 3.8250003, -1.3411007, 3.8619134, -0.76776505 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.9,\n\t\t\t\t\t\t\"value\": -0.77316666,\n\t\t\t\t\t\t\"curve\": [ 3.941953, -0.76688385, 3.9907715, -1.4800053 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4,\n\t\t\t\t\t\t\"value\": -1.866457,\n\t\t\t\t\t\t\"curve\": [ 4.166667, -1.866457, 4.5, -1.9566765 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.666667,\n\t\t\t\t\t\t\"value\": -1.9566765,\n\t\t\t\t\t\t\"curve\": [ 4.709498, 18.048933, 4.766667, 34.55217 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.8,\n\t\t\t\t\t\t\"value\": 34.55217,\n\t\t\t\t\t\t\"curve\": [ 4.8396654, 34.244392, 4.901584, 12.027884 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 4.9333334, \"value\": -18.749977 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-fist\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -2.333191,\n\t\t\t\t\t\t\"curve\": [ 0.018520074, 4.426531, 0.06852007, 10.824462 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"value\": 10.599137,\n\t\t\t\t\t\t\"curve\": [ 0.14767976, 10.599137, 0.12264547, -15.241027 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"value\": -15.346755,\n\t\t\t\t\t\t\"curve\": [ 0.26631907, -15.4374, 0.31596848, -6.48421 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": -3.8969297,\n\t\t\t\t\t\t\"curve\": [ 0.36239058, 0.43245602, 0.4787541, 22.36063 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": 22.010826,\n\t\t\t\t\t\t\"curve\": [ 0.6100949, 21.83802, 0.62726235, 12.848224 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": 9.047508,\n\t\t\t\t\t\t\"curve\": [ 0.6433533, 2.774735, 0.62234885, -39.428562 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.70000005,\n\t\t\t\t\t\t\"value\": -39.499332,\n\t\t\t\t\t\t\"curve\": [ 0.7731915, -39.56604, 0.81386966, 14.771628 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"value\": 14.809445,\n\t\t\t\t\t\t\"curve\": [ 0.9652193, 14.88003, 1.1, 5.6350155 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.1, \"value\": -6.0795293, \"curve\": \"stepped\" },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.2333333,\n\t\t\t\t\t\t\"value\": -6.0795293,\n\t\t\t\t\t\t\"curve\": [ 2.3068335, -6.0795293, 2.4266386, -25.888779 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.5333335,\n\t\t\t\t\t\t\"value\": -22.420551,\n\t\t\t\t\t\t\"curve\": [ 2.5976954, -20.383656, 2.6568336, 5.728967 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.7,\n\t\t\t\t\t\t\"value\": 5.728967,\n\t\t\t\t\t\t\"curve\": [ 2.7704723, 5.728967, 2.8510456, -5.3770056 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.9333334,\n\t\t\t\t\t\t\"value\": -5.3770056,\n\t\t\t\t\t\t\"curve\": [ 3.0075505, -5.3770056, 3.0867047, -4.5399537 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.1666667,\n\t\t\t\t\t\t\"value\": -4.1655493,\n\t\t\t\t\t\t\"curve\": [ 3.222622, -3.9125588, 4.4855742, 5.7289667 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.666667,\n\t\t\t\t\t\t\"value\": 5.7289667,\n\t\t\t\t\t\t\"curve\": [ 4.7333336, 5.7289667, 4.885903, -2.4747167 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 4.9333334, \"value\": -6.5237675 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 10.362316,\n\t\t\t\t\t\t\"curve\": [ 0.033333335, 10.362316, 0.10000001, -32.88954 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": -32.88954,\n\t\t\t\t\t\t\"curve\": [ 0.18333334, -32.88954, 0.28333336, -4.4547806 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": -4.4547806,\n\t\t\t\t\t\t\"curve\": [ 0.36666667, -4.4547806, 0.4379563, -6.8640804 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"value\": -8.989485,\n\t\t\t\t\t\t\"curve\": [ 0.5292276, -13.620812, 0.6053304, -20.580513 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": -23.203642,\n\t\t\t\t\t\t\"curve\": [ 0.7078339, -30.182356, 0.7583334, -35.555008 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8000001,\n\t\t\t\t\t\t\"value\": -35.555008,\n\t\t\t\t\t\t\"curve\": [ 0.87500006, -35.555008, 1.0250001, -23.203642 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.1, \"value\": -23.203642 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"gun\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -2.7885742,\n\t\t\t\t\t\t\"curve\": [ 0.033333335, -2.7885742, 0.11987464, -7.2157125 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": -8.516563,\n\t\t\t\t\t\t\"curve\": [ 0.16800459, -11.867717, 0.29027477, -23.711977 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": -26.241997,\n\t\t\t\t\t\t\"curve\": [ 0.36859605, -28.313953, 0.43559977, -29.74596 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": -29.65575,\n\t\t\t\t\t\t\"curve\": [ 0.5524791, -29.582237, 0.611462, -25.47485 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": -22.682713,\n\t\t\t\t\t\t\"curve\": [ 0.6562597, -19.755901, 0.67987, -10.021171 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.70000005,\n\t\t\t\t\t\t\"value\": -6.4856453,\n\t\t\t\t\t\t\"curve\": [ 0.72211426, -2.6016312, 0.75000006, -1.2165899 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.7666667,\n\t\t\t\t\t\t\"value\": -1.3502121,\n\t\t\t\t\t\t\"curve\": [ 0.7916667, -1.5506454, 0.8416667, -19.737776 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.86666673, \"value\": -19.796799 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hip\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.097746626, -42.62397, 0.16644299, -79.848206, 0.028903658, 84.97473, 0.1091842, 155.92613 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"x\": -133.79216,\n\t\t\t\t\t\t\"y\": 152.4395,\n\t\t\t\t\t\t\"curve\": [ 0.3611138, -184.62695, 0.3924851, -203.6922, 0.42022148, 149.11559, 0.4666667, -15.698044 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"x\": -230.01918,\n\t\t\t\t\t\t\"y\": -113.87448,\n\t\t\t\t\t\t\"curve\": [ 0.52257174, -249.85983, 0.5651282, -261.7036, 0.47333974, -133.10252, 0.5833334, -203.43018 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"x\": -268.57004,\n\t\t\t\t\t\t\"y\": -203.43018,\n\t\t\t\t\t\t\"curve\": [ 0.66301024, -280.97705, 0.81634724, -290.05045, 0.7083334, -203.43018, 0.8916667, -203.49619 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": -290.41855, \"y\": -203.49619 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-thigh\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.05976329, 1.0151215, 0.15099992, 45.23327 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"value\": 54.00554,\n\t\t\t\t\t\t\"curve\": [ 0.18961369, 66.85423, 0.35798755, 169.85176 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": 169.5134,\n\t\t\t\t\t\t\"curve\": [ 0.62758785, 169.85176, 0.69241035, 108.84532 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.70000005,\n\t\t\t\t\t\t\"value\": 97.73543,\n\t\t\t\t\t\t\"curve\": [ 0.72329974, 102.60121, 0.8053221, 111.599594 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"value\": 111.694626,\n\t\t\t\t\t\t\"curve\": [ 0.89914715, 111.82616, 1.0152105, 109.1455 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.0666667, \"value\": 95.797714 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-shin\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.08635837, -0.01806736, 0.19055389, -24.251423 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"value\": -26.495842,\n\t\t\t\t\t\t\"curve\": [ 0.21439338, -29.915737, 0.24939966, -40.509525 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": -40.57034,\n\t\t\t\t\t\t\"curve\": [ 0.43134087, -40.703438, 0.4589271, -11.343845 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"value\": -8.711919,\n\t\t\t\t\t\t\"curve\": [ 0.47711343, -5.159385, 0.5239129, 17.128399 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": 16.982323,\n\t\t\t\t\t\t\"curve\": [ 0.63176244, 17.085365, 0.62499994, 2.7644572 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": 2.7644577,\n\t\t\t\t\t\t\"curve\": [ 0.64845496, 2.7644577, 0.6529771, 2.7465897 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"value\": 2.591426,\n\t\t\t\t\t\t\"curve\": [ 0.67848855, 2.385952, 0.73333335, 2.531887 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": -9.426415,\n\t\t\t\t\t\t\"curve\": [ 0.7452464, -2.4766083, 0.7820335, 3.1247478 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8000001,\n\t\t\t\t\t\t\"value\": 4.278675,\n\t\t\t\t\t\t\"curve\": [ 0.8318293, 6.3229613, 0.8954211, 8.461233 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9333334,\n\t\t\t\t\t\t\"value\": 8.49048,\n\t\t\t\t\t\t\"curve\": [ 0.9860934, 8.531183, 1.0511497, 6.381635 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"value\": 2.28483,\n\t\t\t\t\t\t\"curve\": [ 1.078323, 4.166202, 1.1031494, 5.861723 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1333334,\n\t\t\t\t\t\t\"value\": 5.884948,\n\t\t\t\t\t\t\"curve\": [ 1.1908876, 5.92923, 1.208822, 4.5605817 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.2333333, \"value\": 2.524644 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-thigh\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.033333335, 0, 0.12001532, 50.26316 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 57.30254,\n\t\t\t\t\t\t\"curve\": [ 0.16367699, 73.34101, 0.2737021, 147.17735 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": 147.10303,\n\t\t\t\t\t\t\"curve\": [ 0.47515628, 146.44525, 0.582564, 95.71753 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": 79.66113,\n\t\t\t\t\t\t\"curve\": [ 0.61982745, 94.742165, 0.7321241, 103.15384 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.7666667,\n\t\t\t\t\t\t\"value\": 103.022545,\n\t\t\t\t\t\t\"curve\": [ 0.8121769, 102.849556, 0.8968628, 95.747345 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.9333334, \"value\": 83.01085 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-shin\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.021343872, -16.653662, 0.09115682, -54.816345 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"value\": -55.288445,\n\t\t\t\t\t\t\"curve\": [ 0.18699843, -55.415558, 0.2134717, -52.517345 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": -45.982357,\n\t\t\t\t\t\t\"curve\": [ 0.2420925, -43.10037, 0.31057096, -12.726774 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": -6.3172607,\n\t\t\t\t\t\t\"curve\": [ 0.35621873, 0.12688255, 0.4666666, 24.495005 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": 24.495005,\n\t\t\t\t\t\t\"curve\": [ 0.5425889, 24.495005, 0.55970895, 3.782467 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": -3.5259838,\n\t\t\t\t\t\t\"curve\": [ 0.5851009, 3.8576965, 0.6593202, 16.627193 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.70000005,\n\t\t\t\t\t\t\"value\": 16.561085,\n\t\t\t\t\t\t\"curve\": [ 0.7823284, 16.427292, 0.8960851, 8.438449 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9333334,\n\t\t\t\t\t\t\"value\": 4.0412264,\n\t\t\t\t\t\t\"curve\": [ 0.9562265, 6.8361187, 1.0083334, 8.412511 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0333334,\n\t\t\t\t\t\t\"value\": 8.412511,\n\t\t\t\t\t\t\"curve\": [ 1.0666667, 8.412511, 1.1224275, 8.136743 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.1666667, \"value\": 5.799962 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-foot\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -0.2811966,\n\t\t\t\t\t\t\"curve\": [ 0.033333335, -0.2811966, 0.25623202, -66.705505 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": -66.84443,\n\t\t\t\t\t\t\"curve\": [ 0.4184119, -66.90952, 0.49857256, -21.794205 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": -21.523298,\n\t\t\t\t\t\t\"curve\": [ 0.65191686, -21.384632, 0.66457033, -53.961082 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.70000005,\n\t\t\t\t\t\t\"value\": -54.26296,\n\t\t\t\t\t\t\"curve\": [ 0.75746447, -53.961082, 0.84294355, -2.072342 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9333334,\n\t\t\t\t\t\t\"value\": -1.4685898,\n\t\t\t\t\t\t\"curve\": [ 0.9684308, -2.072342, 0.9754345, -19.96207 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1,\n\t\t\t\t\t\t\"value\": -19.96207,\n\t\t\t\t\t\t\"curve\": [ 1.0250001, -19.96207, 1.075, -12.4205475 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1,\n\t\t\t\t\t\t\"value\": -12.4205475,\n\t\t\t\t\t\t\"curve\": [ 1.1333334, -12.4205475, 1.2, -18.34373 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.2333333, \"value\": -18.34373 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-foot\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.008333334, -11.325378, 0.10833334, -57.71149 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": -57.71149,\n\t\t\t\t\t\t\"curve\": [ 0.17500001, -57.71149, 0.22882804, 19.729279 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.3,\n\t\t\t\t\t\t\"value\": 19.341759,\n\t\t\t\t\t\t\"curve\": [ 0.35398707, 19.341759, 0.40000004, -57.76014 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"value\": -57.76014,\n\t\t\t\t\t\t\"curve\": [ 0.45833334, -57.76014, 0.510723, -3.5613594 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.53333336,\n\t\t\t\t\t\t\"value\": 3.6987648,\n\t\t\t\t\t\t\"curve\": [ 0.56319785, 13.288166, 0.6333334, 15.792763 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"value\": 15.792763,\n\t\t\t\t\t\t\"curve\": [ 0.70000005, 15.792763, 0.7666667, -48.75049 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8000001,\n\t\t\t\t\t\t\"value\": -48.75049,\n\t\t\t\t\t\t\"curve\": [ 0.8416667, -48.75049, 0.9250001, 4.7043076 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9666667,\n\t\t\t\t\t\t\"value\": 4.7043076,\n\t\t\t\t\t\t\"curve\": [ 1, 4.7043076, 1.0666667, -22.897013 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1,\n\t\t\t\t\t\t\"value\": -22.897013,\n\t\t\t\t\t\t\"curve\": [ 1.1416668, -22.897013, 1.225, -13.277081 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.2666668, \"value\": -13.277081 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-foot-target\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": -0.2811985 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-foot-tip\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -0.2811985,\n\t\t\t\t\t\t\"curve\": [ 0.008333334, -0.2811985, 0.0028173854, -66.61513 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": -65.75365,\n\t\t\t\t\t\t\"curve\": [ 0.16552001, -64.419876, 0.2337835, 14.352765 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": 38.24919,\n\t\t\t\t\t\t\"curve\": [ 0.29371846, 57.90787, 0.39242426, 89.78588 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"value\": 90.73152,\n\t\t\t\t\t\t\"curve\": [ 0.48333335, 90.73152, 0.55, 177.66196 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": 177.66196,\n\t\t\t\t\t\t\"curve\": [ 0.7326746, 176.2435, 0.75000006, 11.348257 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8000001,\n\t\t\t\t\t\t\"value\": 11.348257,\n\t\t\t\t\t\t\"curve\": [ 0.8859026, 12.293889, 0.9114623, 47.878242 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9333334,\n\t\t\t\t\t\t\"value\": 56.76897,\n\t\t\t\t\t\t\"curve\": [ 0.96733403, 70.590485, 1.0500001, 86.457405 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1,\n\t\t\t\t\t\t\"value\": 86.457405,\n\t\t\t\t\t\t\"curve\": [ 1.1867261, 86.457405, 1.2140646, 66.44207 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": 64.550804 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"back-foot-tip\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -0.2811985,\n\t\t\t\t\t\t\"curve\": [ 8.2126266E-5, -7.9678493, 0.027242552, -18.692642 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": -18.996307,\n\t\t\t\t\t\t\"curve\": [ 0.16596521, -19.30436, 0.20791966, 15.583389 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": 45.95023,\n\t\t\t\t\t\t\"curve\": [ 0.3060278, 66.24353, 0.37753627, 99.08093 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"value\": 99.08093,\n\t\t\t\t\t\t\"curve\": [ 0.4966522, 98.61508, 0.48828924, -1.1972253 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": -1.3168106,\n\t\t\t\t\t\t\"curve\": [ 0.63690794, -0.8404943, 0.68687534, 94.40564 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": 94.3284,\n\t\t\t\t\t\t\"curve\": [ 0.8318345, 94.16463, 0.8951556, 29.597832 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9666667,\n\t\t\t\t\t\t\"value\": 28.666117,\n\t\t\t\t\t\t\"curve\": [ 1.0263189, 28.666117, 1.0445411, 53.142174 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.1, \"value\": 53.375107 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair4\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.011168385, 4.5032196, 0.050000004, 11.416817 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": 11.416817,\n\t\t\t\t\t\t\"curve\": [ 0.10000001, 11.416817, 0.13615881, -5.9184036 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"value\": -10.537537,\n\t\t\t\t\t\t\"curve\": [ 0.20614353, -16.514656, 0.32748863, -21.9986 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": -24.471233,\n\t\t\t\t\t\t\"curve\": [ 0.41259408, -27.36985, 0.4666667, -43.9946 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": -43.9946,\n\t\t\t\t\t\t\"curve\": [ 0.53333336, -43.9946, 0.55222243, 12.119652 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": 11.853188,\n\t\t\t\t\t\t\"curve\": [ 0.7141786, 11.587608, 0.7583334, -34.132217 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8000001,\n\t\t\t\t\t\t\"value\": -34.132217,\n\t\t\t\t\t\t\"curve\": [ 0.85833335, -34.132217, 1.0149959, -12.471024 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"value\": -8.848976,\n\t\t\t\t\t\t\"curve\": [ 1.1205678, -5.070595, 1.2191709, -0.015052795 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.3333334,\n\t\t\t\t\t\t\"value\": 1.2907867,\n\t\t\t\t\t\t\"curve\": [ 1.5088931, 3.2989044, 1.7627755, 2.7490234 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.8666668,\n\t\t\t\t\t\t\"value\": 2.7768707,\n\t\t\t\t\t\t\"curve\": [ 1.9742383, 2.805704, 2.1083546, 2.808632 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.2,\n\t\t\t\t\t\t\"value\": 2.7768707,\n\t\t\t\t\t\t\"curve\": [ 2.3152533, 2.7369308, 2.3741817, 1.219162 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.4666667,\n\t\t\t\t\t\t\"value\": 1.1792221,\n\t\t\t\t\t\t\"curve\": [ 2.525, 1.1792221, 2.6083336, 10.794548 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.6666667,\n\t\t\t\t\t\t\"value\": 10.794548,\n\t\t\t\t\t\t\"curve\": [ 2.7250001, 10.794548, 2.893151, 4.721054 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.0333335,\n\t\t\t\t\t\t\"value\": 4.721054,\n\t\t\t\t\t\t\"curve\": [ 3.1166668, 4.721054, 3.2833335, 7.9320755 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.3666668,\n\t\t\t\t\t\t\"value\": 7.9320755,\n\t\t\t\t\t\t\"curve\": [ 3.4916668, 7.9320755, 3.775, 6.927864 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.9,\n\t\t\t\t\t\t\"value\": 6.927864,\n\t\t\t\t\t\t\"curve\": [ 3.9809446, 6.927864, 4.093641, 6.8972015 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.2000003,\n\t\t\t\t\t\t\"value\": 8.442413,\n\t\t\t\t\t\t\"curve\": [ 4.2674913, 9.422943, 4.401258, 16.605806 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.5,\n\t\t\t\t\t\t\"value\": 16.334618,\n\t\t\t\t\t\t\"curve\": [ 4.581609, 16.11781, 4.7089415, 9.9449005 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.7333336,\n\t\t\t\t\t\t\"value\": 6.5065994,\n\t\t\t\t\t\t\"curve\": [ 4.747064, 4.5712214, 4.779033, -1.7636824 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.8,\n\t\t\t\t\t\t\"value\": -1.7486115,\n\t\t\t\t\t\t\"curve\": [ 4.8233595, -1.7325964, 4.819983, 4.467903 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.866667,\n\t\t\t\t\t\t\"value\": 6.0432816,\n\t\t\t\t\t\t\"curve\": [ 4.899199, 7.1411057, 4.9127226, 6.927864 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 4.9333334, \"value\": 6.927864 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 10.609882,\n\t\t\t\t\t\t\"curve\": [ 0.075, 10.609882, 0.050000004, 12.669956 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": 12.669956,\n\t\t\t\t\t\t\"curve\": [ 0.1233677, 12.669956, 0.19394414, -16.505566 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"value\": -19.867872,\n\t\t\t\t\t\t\"curve\": [ 0.20651405, -23.48457, 0.23617339, -31.682863 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.3,\n\t\t\t\t\t\t\"value\": -31.800272,\n\t\t\t\t\t\t\"curve\": [ 0.35643864, -31.90409, 0.4369411, -25.611557 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"value\": -19.291225,\n\t\t\t\t\t\t\"curve\": [ 0.48529798, -15.329792, 0.52894795, 6.481163 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": 6.667576,\n\t\t\t\t\t\t\"curve\": [ 0.62790054, 6.9702034, 0.6498716, -46.393135 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": -46.298576,\n\t\t\t\t\t\t\"curve\": [ 0.8430757, -46.17424, 0.9410482, -33.368584 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9666667,\n\t\t\t\t\t\t\"value\": -23.173223,\n\t\t\t\t\t\t\"curve\": [ 0.97218823, -20.975813, 1.0466495, 15.210228 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1,\n\t\t\t\t\t\t\"value\": 15.210228,\n\t\t\t\t\t\t\"curve\": [ 1.1416668, 15.210228, 1.1829382, 10.734215 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2666668,\n\t\t\t\t\t\t\"value\": 10.609882,\n\t\t\t\t\t\t\"curve\": [ 1.45, 10.337642, 1.8166667, 10.609882 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2,\n\t\t\t\t\t\t\"value\": 10.609882,\n\t\t\t\t\t\t\"curve\": [ 2.075, 10.609882, 2.2250001, 16.902882 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.3000002,\n\t\t\t\t\t\t\"value\": 16.902882,\n\t\t\t\t\t\t\"curve\": [ 2.3271947, 16.902882, 2.3465943, 6.813179 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.4,\n\t\t\t\t\t\t\"value\": 6.834305,\n\t\t\t\t\t\t\"curve\": [ 2.4923768, 6.8708344, 2.6021945, 17.392544 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.6666667,\n\t\t\t\t\t\t\"value\": 17.392544,\n\t\t\t\t\t\t\"curve\": [ 2.7416668, 17.392544, 2.8916988, 10.665386 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.966667,\n\t\t\t\t\t\t\"value\": 10.640583,\n\t\t\t\t\t\t\"curve\": [ 3.1869025, 10.567711, 3.3436024, 10.731342 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.6000001,\n\t\t\t\t\t\t\"value\": 11.395809,\n\t\t\t\t\t\t\"curve\": [ 3.7658198, 11.825535, 3.873757, 14.87431 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.9333336,\n\t\t\t\t\t\t\"value\": 14.827503,\n\t\t\t\t\t\t\"curve\": [ 4.0221586, 14.757721, 4.208427, 9.491276 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.3,\n\t\t\t\t\t\t\"value\": 9.53775,\n\t\t\t\t\t\t\"curve\": [ 4.391111, 9.583996, 4.4407206, 14.816524 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.533334,\n\t\t\t\t\t\t\"value\": 14.844669,\n\t\t\t\t\t\t\"curve\": [ 4.6423426, 14.877789, 4.692118, 1.1734428 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.766667,\n\t\t\t\t\t\t\"value\": 1.2434273,\n\t\t\t\t\t\t\"curve\": [ 4.8227067, 1.2960372, 4.818162, 18.354725 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.866667,\n\t\t\t\t\t\t\"value\": 18.384907,\n\t\t\t\t\t\t\"curve\": [ 4.9045296, 18.408459, 4.901353, 10.6075535 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 4.9333334, \"value\": 10.609882 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.048333332, 0, 0.12943684, -12.732102 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"value\": -15.947126,\n\t\t\t\t\t\t\"curve\": [ 0.22118534, -20.65514, 0.25443134, -21.623718 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.3,\n\t\t\t\t\t\t\"value\": -21.586037,\n\t\t\t\t\t\t\"curve\": [ 0.45750102, -21.455812, 0.4595645, -1.671394 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": -1.7090368,\n\t\t\t\t\t\t\"curve\": [ 0.7095563, -1.7255486, 0.7151743, -4.000723 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.7666667,\n\t\t\t\t\t\t\"value\": -3.9727337,\n\t\t\t\t\t\t\"curve\": [ 0.8663325, -3.918559, 0.83992106, 0.016319573 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"curve\": \"stepped\" },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2,\n\t\t\t\t\t\t\"curve\": [ 2.275, 0, 2.8669045, -5.7971454 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.1000001,\n\t\t\t\t\t\t\"value\": -6.4372787,\n\t\t\t\t\t\t\"curve\": [ 3.327306, -7.0615106, 3.7103267, -6.232035 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.9333336,\n\t\t\t\t\t\t\"value\": -5.405311,\n\t\t\t\t\t\t\"curve\": [ 4.1684337, -4.533755, 4.488472, -2.8298516 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 4.8 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.025000002, 0, 0.09008592, -3.6559749 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"value\": -4.551666,\n\t\t\t\t\t\t\"curve\": [ 0.14257248, -8.397885, 0.22345564, -17.068499 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": -18.311,\n\t\t\t\t\t\t\"curve\": [ 0.2820652, -24.440912, 0.35016996, -28.995378 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": -30.069881,\n\t\t\t\t\t\t\"curve\": [ 0.4051797, -32.578415, 0.44173232, -33.03234 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"value\": -32.988052,\n\t\t\t\t\t\t\"curve\": [ 0.4914639, -33.040646, 0.5048739, -23.56439 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.53333336,\n\t\t\t\t\t\t\"value\": -23.546326,\n\t\t\t\t\t\t\"curve\": [ 0.57087785, -23.665419, 0.59879434, -27.209892 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": -27.206652,\n\t\t\t\t\t\t\"curve\": [ 0.669014, -27.203304, 0.7422721, -10.427679 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.7666667,\n\t\t\t\t\t\t\"value\": -7.788528,\n\t\t\t\t\t\t\"curve\": [ 0.78753144, -5.531253, 0.79603463, -4.417817 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8333334,\n\t\t\t\t\t\t\"value\": -2.9019608,\n\t\t\t\t\t\t\"curve\": [ 0.8749381, -1.2111093, 0.9333334, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.9666667, \"curve\": \"stepped\" },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.4333334,\n\t\t\t\t\t\t\"curve\": [ 2.516667, 0, 2.6833556, 4.6301155 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.766667,\n\t\t\t\t\t\t\"value\": 4.6568756,\n\t\t\t\t\t\t\"curve\": [ 3.0843723, 4.7589417, 3.248143, 4.367768 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.4,\n\t\t\t\t\t\t\"value\": 3.736271,\n\t\t\t\t\t\t\"curve\": [ 3.596353, 2.919743, 3.7547882, 2.1818871 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.8666668,\n\t\t\t\t\t\t\"value\": 1.7159004,\n\t\t\t\t\t\t\"curve\": [ 4.1362214, 0.59317493, 4.4710636, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 4.8 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0, 0, 0.040569127, 10.742979 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": 14.159859,\n\t\t\t\t\t\t\"curve\": [ 0.07479884, 15.224585, 0.14791568, 18.036913 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"value\": 18.13369,\n\t\t\t\t\t\t\"curve\": [ 0.2513522, 18.229107, 0.30682567, -4.7526207 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": -5.062149,\n\t\t\t\t\t\t\"curve\": [ 0.41201928, -5.2967377, 0.46981028, -0.9579954 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": 2.2096653,\n\t\t\t\t\t\t\"curve\": [ 0.5121225, 3.4816265, 0.59482247, 20.305944 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": 24.872202,\n\t\t\t\t\t\t\"curve\": [ 0.64728093, 26.525967, 0.7191795, 29.326023 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8000001,\n\t\t\t\t\t\t\"value\": 29.219383,\n\t\t\t\t\t\t\"curve\": [ 0.85861564, 29.142044, 0.9000328, 28.480516 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9333334,\n\t\t\t\t\t\t\"value\": 26.110369,\n\t\t\t\t\t\t\"curve\": [ 0.9809245, 22.72311, 0.99809396, 2.0561347 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.1, \"value\": 2.2096653 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair1\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.046892468, -0.21306229, 0.047901608, 7.863083 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": 13.272148,\n\t\t\t\t\t\t\"curve\": [ 0.083257586, 18.054514, 0.13504568, 24.443092 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"value\": 24.016968,\n\t\t\t\t\t\t\"curve\": [ 0.22500001, 24.016968, 0.28034237, 6.321228 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.3,\n\t\t\t\t\t\t\"value\": 3.1026344,\n\t\t\t\t\t\t\"curve\": [ 0.32250878, -0.5827751, 0.3818797, -7.1210938 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"value\": -7.4477234,\n\t\t\t\t\t\t\"curve\": [ 0.51158553, -7.655552, 0.5376558, 12.13205 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": 16.463089,\n\t\t\t\t\t\t\"curve\": [ 0.6085694, 22.718737, 0.67165947, 27.3962 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": 27.54968,\n\t\t\t\t\t\t\"curve\": [ 0.8266794, 27.3962, 0.93293107, 23.229742 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9666667,\n\t\t\t\t\t\t\"value\": 19.106976,\n\t\t\t\t\t\t\"curve\": [ 0.9980442, 15.272388, 1.0916667, -2.528984 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1333334,\n\t\t\t\t\t\t\"value\": -2.528984,\n\t\t\t\t\t\t\"curve\": [ 1.1583334, -2.528984, 1.2083334, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.2333333, \"curve\": \"stepped\" },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2,\n\t\t\t\t\t\t\"curve\": [ 2.075, 0, 2.247674, 0.34612274 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.3333335,\n\t\t\t\t\t\t\"value\": 0.7803421,\n\t\t\t\t\t\t\"curve\": [ 2.5849822, 2.0559807, 2.8047395, 3.4637756 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.2000003,\n\t\t\t\t\t\t\"value\": 3.503952,\n\t\t\t\t\t\t\"curve\": [ 3.5925977, 3.5438652, 3.9791408, 2.3555145 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.166667,\n\t\t\t\t\t\t\"value\": 1.553379,\n\t\t\t\t\t\t\"curve\": [ 4.39082, 0.594574, 4.447003, 0.036182404 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.6000004,\n\t\t\t\t\t\t\"value\": 0.036182404,\n\t\t\t\t\t\t\"curve\": [ 4.641668, 0.036182404, 4.741667, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 4.9333334 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"head-control\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.025000002, 0, 0.08982688, 1.433876, 0.025000002, 0, 0.075, -34.764725 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"x\": 1.5892944,\n\t\t\t\t\t\t\"y\": -34.764725,\n\t\t\t\t\t\t\"curve\": [ 0.21390621, 3.3294754, 0.37500003, 5.3352356, 0.19166668, -34.764725, 0.44133055, -21.166029 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"x\": 5.3352356,\n\t\t\t\t\t\t\"y\": -12.572044,\n\t\t\t\t\t\t\"curve\": [ 0.4916667, 5.3352356, 0.54978555, 5.2354965, 0.48202398, -7.362872, 0.5036168, 4.027279 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"x\": 5.1124954,\n\t\t\t\t\t\t\"y\": 4.00758,\n\t\t\t\t\t\t\"curve\": [ 0.6580277, 4.4467545, 0.6791786, 3.1944351, 0.6491876, 3.9818003, 0.64173335, -16.840157 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.70000005,\n\t\t\t\t\t\t\"x\": 2.7987366,\n\t\t\t\t\t\t\"y\": -16.740479,\n\t\t\t\t\t\t\"curve\": [ 0.78651315, 1.1546326, 0.8806581, -1.2923965, 0.7720639, -16.617199, 0.8195241, 8.945714 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.90000004,\n\t\t\t\t\t\t\"x\": -1.7211609,\n\t\t\t\t\t\t\"y\": 8.90686,\n\t\t\t\t\t\t\"curve\": [ 0.9605303, -3.062973, 1.0250001, -3.5791626, 0.974895, 8.870702, 0.9505929, -1.3719251 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1,\n\t\t\t\t\t\t\"x\": -3.5791626,\n\t\t\t\t\t\t\"y\": -1.4544678,\n\t\t\t\t\t\t\"curve\": [ 1.2916667, -3.5791626, 2.0017653, -2.4029236, 1.291651, -1.5603487, 1.9750001, -1.4544678 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.1666667,\n\t\t\t\t\t\t\"x\": -1.3947296,\n\t\t\t\t\t\t\"y\": -1.4544678,\n\t\t\t\t\t\t\"curve\": [ 2.250246, -0.8837433, 2.503161, 1.3765717, 2.2833335, -1.4544678, 2.6026857, -12.435727 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.6666667,\n\t\t\t\t\t\t\"x\": 2.1327133,\n\t\t\t\t\t\t\"y\": -14.447431,\n\t\t\t\t\t\t\"curve\": [ 2.7657928, 2.5911255, 2.9986973, 2.8127213, 2.834681, -19.73016, 3.002592, -25.203335 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 3.1333334,\n\t\t\t\t\t\t\"x\": 2.9122925,\n\t\t\t\t\t\t\"y\": -26.08423,\n\t\t\t\t\t\t\"curve\": [ 3.3917341, 3.1033936, 4.1986957, 4.047333, 3.48327, -28.441994, 4.1285806, -27.230576 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.366667,\n\t\t\t\t\t\t\"x\": 4.813339,\n\t\t\t\t\t\t\"y\": -19.586075,\n\t\t\t\t\t\t\"curve\": [ 4.4292784, 5.0988617, 4.5936823, 8.537651, 4.5381665, -14.0795355, 4.583333, -7.8750296 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 4.666667,\n\t\t\t\t\t\t\"x\": 8.654564,\n\t\t\t\t\t\t\"y\": -4.557136,\n\t\t\t\t\t\t\"curve\": [ 4.7938237, 8.858269, 4.8057714, 5.9251223, 4.6908545, -3.5941138, 4.800488, -1.6082137 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 4.9333334, \"x\": 5.803871, \"y\": -1.9929583 }\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t\"ik\": {\n\t\t\t\"front-foot-ik\": [\n\t\t\t\t{ \"mix\": 0 }\n\t\t\t],\n\t\t\t\"front-leg-ik\": [\n\t\t\t\t{ \"mix\": 0, \"bendPositive\": false }\n\t\t\t],\n\t\t\t\"rear-foot-ik\": [\n\t\t\t\t{ \"mix\": 0.005 }\n\t\t\t],\n\t\t\t\"rear-leg-ik\": [\n\t\t\t\t{ \"mix\": 0.005, \"bendPositive\": false }\n\t\t\t]\n\t\t}\n\t},\n\t\"hoverboard\": {\n\t\t\"slots\": {\n\t\t\t\"exhaust1\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"hoverglow-small\" }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"exhaust2\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"hoverglow-small\" }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"exhaust3\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"hoverglow-small\" }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-fist\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"front-fist-open\" }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hoverboard-board\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"hoverboard-board\" }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hoverboard-thruster-front\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"hoverboard-thruster\" }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hoverboard-thruster-rear\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"hoverboard-thruster\" }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hoverglow-front\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"hoverglow-small\" }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hoverglow-rear\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"hoverglow-small\" }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"side-glow1\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"hoverglow-small\" },\n\t\t\t\t\t{ \"time\": 0.9666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"side-glow2\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"time\": 0.06666667, \"name\": \"hoverglow-small\" },\n\t\t\t\t\t{ \"time\": 1 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"side-glow3\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"hoverglow-small\" },\n\t\t\t\t\t{ \"time\": 0.9666667 }\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t\"bones\": {\n\t\t\t\"hoverboard-controller\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 319.55005,\n\t\t\t\t\t\t\"y\": -1.5864716,\n\t\t\t\t\t\t\"curve\": [ 0.063661434, 319.55005, 0.20047697, 347.85352, 0.057994243, -1.1977158, 0.20033486, 23.109634 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"x\": 347.65765,\n\t\t\t\t\t\t\"y\": 39.621246,\n\t\t\t\t\t\t\"curve\": [ 0.34991896, 347.41132, 0.47592065, 341.4658, 0.3227371, 53.578537, 0.43954828, 85.8174 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.53333336,\n\t\t\t\t\t\t\"x\": 338.47452,\n\t\t\t\t\t\t\"y\": 85.7157,\n\t\t\t\t\t\t\"curve\": [ 0.6032626, 334.83112, 0.9125737, 319.64664, 0.6208322, 85.620804, 0.87999594, -1.529808 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": 319.55005, \"y\": -1.5864716 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hip\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -53.488518,\n\t\t\t\t\t\t\"y\": 32.142715,\n\t\t\t\t\t\t\"curve\": [ 0.061164066, -53.765244, 0.09322592, -51.811993, 0.044356536, 16.338673, 0.06267086, 9.66658 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"x\": -49.313904,\n\t\t\t\t\t\t\"y\": 7.007004,\n\t\t\t\t\t\t\"curve\": [ 0.29993045, -35.26631, 0.4609025, -20.05555, 0.31350458, 9.521679, 0.4075428, 121.0916 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"x\": -20.05555,\n\t\t\t\t\t\t\"y\": 122.72142,\n\t\t\t\t\t\t\"curve\": [ 0.7164097, -20.092588, 0.91221154, -53.29204, 0.75252014, 121.79715, 0.94577616, 51.854942 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": -53.488518, \"y\": 32.142715 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"exhaust1\": {\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 1.5925673,\n\t\t\t\t\t\t\"y\": 0.9635573,\n\t\t\t\t\t\t\"curve\": [ 0.033333335, 1.5925673, 0.10000001, 1, 0.033333335, 0.9635573, 0.10000001, 0.71298504 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"y\": 0.71298504,\n\t\t\t\t\t\t\"curve\": [ 0.15, 1, 0.18333334, 1.7740201, 0.15, 0.71298504, 0.18333334, 0.8827183 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"x\": 1.7740201,\n\t\t\t\t\t\t\"y\": 0.8827183,\n\t\t\t\t\t\t\"curve\": [ 0.24166667, 1.7740201, 0.32500002, 1.1814528, 0.24166667, 0.8827183, 0.32500002, 0.6494452 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"x\": 1.1814528,\n\t\t\t\t\t\t\"y\": 0.6494452,\n\t\t\t\t\t\t\"curve\": [ 0.40833336, 1.1814528, 0.4916667, 1.8928039, 0.40833336, 0.6494452, 0.4916667, 0.81917846 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.53333336,\n\t\t\t\t\t\t\"x\": 1.8928039,\n\t\t\t\t\t\t\"y\": 0.81917846,\n\t\t\t\t\t\t\"curve\": [ 0.55833334, 1.8928039, 0.60833335, 1.1802405, 0.55833334, 0.81917846, 0.60833335, 0.6857325 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"x\": 1.1802405,\n\t\t\t\t\t\t\"y\": 0.6857325,\n\t\t\t\t\t\t\"curve\": [ 0.65833336, 1.1802405, 0.7083334, 1.9033748, 0.65833336, 0.6857325, 0.7083334, 0.85546577 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"x\": 1.9033748,\n\t\t\t\t\t\t\"y\": 0.85546577,\n\t\t\t\t\t\t\"curve\": [ 0.7666667, 1.9033748, 0.8333334, 1.3108075, 0.7666667, 0.85546577, 0.8333334, 0.6222546 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"x\": 1.3108075,\n\t\t\t\t\t\t\"y\": 0.6222546,\n\t\t\t\t\t\t\"curve\": [ 0.90000004, 1.3108075, 0.9666667, 1.5925673, 0.90000004, 0.6222546, 0.9666667, 0.9635573 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": 1.5925673, \"y\": 0.9635573 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"exhaust2\": {\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 1.8796126,\n\t\t\t\t\t\t\"y\": 0.8323539,\n\t\t\t\t\t\t\"curve\": [ 0.025000002, 1.8796126, 0.075, 1.3108075, 0.025000002, 0.8323539, 0.075, 0.6857325 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"x\": 1.3108075,\n\t\t\t\t\t\t\"y\": 0.6857325,\n\t\t\t\t\t\t\"curve\": [ 0.13333334, 1.3108075, 0.20000002, 2.0101795, 0.13333334, 0.6857325, 0.20776251, 0.7363072 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"x\": 2.0101795,\n\t\t\t\t\t\t\"y\": 0.768876,\n\t\t\t\t\t\t\"curve\": [ 0.26666668, 2.0101795, 0.33333334, 1, 0.2818951, 0.8307277, 0.33333334, 0.90979606 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"y\": 0.90979606,\n\t\t\t\t\t\t\"curve\": [ 0.40000004, 1, 0.4666667, 1.699372, 0.40000004, 0.90979606, 0.47360024, 0.8906031 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"x\": 1.699372,\n\t\t\t\t\t\t\"y\": 0.85960644,\n\t\t\t\t\t\t\"curve\": [ 0.5166667, 1.699372, 0.55, 1.1814528, 0.53950936, 0.81321764, 0.55, 0.71298504 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"x\": 1.1814528,\n\t\t\t\t\t\t\"y\": 0.71298504,\n\t\t\t\t\t\t\"curve\": [ 0.6166667, 1.1814528, 0.7166667, 1.8808248, 0.6166667, 0.71298504, 0.7166667, 0.7960666 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.7666667,\n\t\t\t\t\t\t\"x\": 1.8808248,\n\t\t\t\t\t\t\"y\": 0.7960666,\n\t\t\t\t\t\t\"curve\": [ 0.8000001, 1.8808248, 0.86666673, 1.3002366, 0.8000001, 0.7960666, 0.86666673, 0.6494452 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.90000004,\n\t\t\t\t\t\t\"x\": 1.3002366,\n\t\t\t\t\t\t\"y\": 0.6494452,\n\t\t\t\t\t\t\"curve\": [ 0.9250001, 1.3002366, 0.975, 1.8796126, 0.9250001, 0.6494452, 0.975, 0.8323539 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": 1.8796126, \"y\": 0.8323539 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hoverboard-thruster-front\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.125, 0, 0.37500003, 24.064991 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": 24.064991,\n\t\t\t\t\t\t\"curve\": [ 0.62500006, 24.064991, 0.87500006, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hoverglow-front\": {\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 0.84913766,\n\t\t\t\t\t\t\"y\": 1.763755,\n\t\t\t\t\t\t\"curve\": [ 0.016666668, 0.84913766, 0.050000004, 0.83469284, 0.016666668, 1.763755, 0.050000004, 2.033342 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"x\": 0.83469284,\n\t\t\t\t\t\t\"y\": 2.033342,\n\t\t\t\t\t\t\"curve\": [ 0.09166667, 0.83469284, 0.14166668, 0.75202346, 0.09166667, 2.033342, 0.14166668, 1.583745 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"x\": 0.75202346,\n\t\t\t\t\t\t\"y\": 1.583745,\n\t\t\t\t\t\t\"curve\": [ 0.18333334, 0.75202346, 0.21666668, 0.8093396, 0.18333334, 1.583745, 0.21666668, 1.710419 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"x\": 0.8093396,\n\t\t\t\t\t\t\"y\": 1.710419,\n\t\t\t\t\t\t\"curve\": [ 0.25, 0.8093396, 0.28333336, 0.7172279, 0.25, 1.710419, 0.28333336, 1.4495454 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.3,\n\t\t\t\t\t\t\"x\": 0.7172279,\n\t\t\t\t\t\t\"y\": 1.4495454,\n\t\t\t\t\t\t\"curve\": [ 0.3166667, 0.7172279, 0.35000002, 0.777262, 0.3166667, 1.4495454, 0.35000002, 1.6981606 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"x\": 0.777262,\n\t\t\t\t\t\t\"y\": 1.6981606,\n\t\t\t\t\t\t\"curve\": [ 0.40033963, 0.7808548, 0.45000002, 0.68498915, 0.37500003, 1.6981606, 0.45000002, 1.1725577 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"x\": 0.68498915,\n\t\t\t\t\t\t\"y\": 1.1725577,\n\t\t\t\t\t\t\"curve\": [ 0.4916667, 0.68498915, 0.5416667, 0.82510686, 0.4916667, 1.1725577, 0.5416667, 1.5723042 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"x\": 0.82510686,\n\t\t\t\t\t\t\"y\": 1.5723042,\n\t\t\t\t\t\t\"curve\": [ 0.6112109, 0.8156652, 0.6300903, 0.7267164, 0.6112109, 1.5770249, 0.60616595, 1.2548331 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"x\": 0.72477967,\n\t\t\t\t\t\t\"y\": 1.2406707,\n\t\t\t\t\t\t\"curve\": [ 0.6916667, 0.72477967, 0.74166673, 0.8953859, 0.6916667, 1.2406707, 0.7494155, 1.7993612 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.7666667,\n\t\t\t\t\t\t\"x\": 0.8953859,\n\t\t\t\t\t\t\"y\": 1.8571669,\n\t\t\t\t\t\t\"curve\": [ 0.78333336, 0.8953859, 0.7963973, 0.8921125, 0.7960031, 1.9554677, 0.8166667, 1.9618536 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8333334,\n\t\t\t\t\t\t\"x\": 0.84526765,\n\t\t\t\t\t\t\"y\": 1.9618536,\n\t\t\t\t\t\t\"curve\": [ 0.8445584, 0.83103126, 0.8833334, 0.8022892, 0.85, 1.9618536, 0.8715096, 1.704042 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.90000004,\n\t\t\t\t\t\t\"x\": 0.8022892,\n\t\t\t\t\t\t\"y\": 1.4913216,\n\t\t\t\t\t\t\"curve\": [ 0.9166667, 0.8022892, 0.95000005, 0.8449259, 0.90676165, 1.4408371, 0.93648726, 1.507996 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9666667,\n\t\t\t\t\t\t\"x\": 0.8449259,\n\t\t\t\t\t\t\"y\": 1.6267049,\n\t\t\t\t\t\t\"curve\": [ 0.975, 0.8449259, 0.99166673, 0.84913766, 0.9730493, 1.6518108, 0.99166673, 1.763755 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": 0.84913766, \"y\": 1.763755 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hoverboard-thruster-rear\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.125, 0, 0.37500003, 24.064991 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": 24.064991,\n\t\t\t\t\t\t\"curve\": [ 0.62500006, 24.064991, 0.87500006, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hoverglow-rear\": {\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 0.8449259,\n\t\t\t\t\t\t\"y\": 1.3101475,\n\t\t\t\t\t\t\"curve\": [ 0.016666668, 0.8449259, 0.116666675, 0.89917237, 0.016666668, 1.3101475, 0.116666675, 2.033342 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"x\": 0.89917237,\n\t\t\t\t\t\t\"y\": 2.033342,\n\t\t\t\t\t\t\"curve\": [ 0.15, 0.89917237, 0.18333334, 0.75202346, 0.15, 2.033342, 0.18333334, 1.5736133 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"x\": 0.75202346,\n\t\t\t\t\t\t\"y\": 1.5736133,\n\t\t\t\t\t\t\"curve\": [ 0.22500001, 0.75202346, 0.275, 0.8093396, 0.22500001, 1.5736133, 0.275, 1.710419 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.3,\n\t\t\t\t\t\t\"x\": 0.8093396,\n\t\t\t\t\t\t\"y\": 1.710419,\n\t\t\t\t\t\t\"curve\": [ 0.3166667, 0.8093396, 0.35000002, 0.7172279, 0.3166667, 1.710419, 0.35000002, 1.3971558 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"x\": 0.7172279,\n\t\t\t\t\t\t\"y\": 1.3971558,\n\t\t\t\t\t\t\"curve\": [ 0.38333336, 0.7172279, 0.4166667, 0.777262, 0.38333336, 1.3971558, 0.4166667, 1.4500636 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"x\": 0.777262,\n\t\t\t\t\t\t\"y\": 1.4500636,\n\t\t\t\t\t\t\"curve\": [ 0.45000002, 0.777262, 0.49636215, 0.68901914, 0.45000002, 1.4500636, 0.48116708, 1.1675577 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.53333336,\n\t\t\t\t\t\t\"x\": 0.68498915,\n\t\t\t\t\t\t\"y\": 1.1725577,\n\t\t\t\t\t\t\"curve\": [ 0.56515354, 0.68152064, 0.6166667, 0.7576283, 0.57538074, 1.1765877, 0.6166667, 1.2969007 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"x\": 0.7576283,\n\t\t\t\t\t\t\"y\": 1.2969007,\n\t\t\t\t\t\t\"curve\": [ 0.65833336, 0.7576283, 0.7083334, 0.72477967, 0.65833336, 1.2969007, 0.7083334, 1.2406707 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"x\": 0.72477967,\n\t\t\t\t\t\t\"y\": 1.2406707,\n\t\t\t\t\t\t\"curve\": [ 0.7721236, 0.73160404, 0.79636216, 0.89311117, 0.78227574, 1.2383958, 0.77834535, 1.8535308 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8333334,\n\t\t\t\t\t\t\"x\": 0.8953859,\n\t\t\t\t\t\t\"y\": 1.8571669,\n\t\t\t\t\t\t\"curve\": [ 0.87791884, 0.89993554, 0.99166673, 0.8449259, 0.87971604, 1.8602344, 0.99166673, 1.3101475 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": 0.8449259, \"y\": 1.3101475 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -85.92003,\n\t\t\t\t\t\t\"curve\": [ 0.07966432, -85.58839, 0.28378937, -62.701607 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": -55.13999,\n\t\t\t\t\t\t\"curve\": [ 0.43779933, -48.64994, 0.55142397, -43.208664 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": -43.21428,\n\t\t\t\t\t\t\"curve\": [ 0.7163255, -43.21998, 0.90833336, -85.92003 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": -85.92003 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -0.59210205,\n\t\t\t\t\t\t\"y\": -2.9409485,\n\t\t\t\t\t\t\"curve\": [ 0.100465566, -1.2054043, 0.275, -1.7364502, 0.09166667, -2.9409485, 0.275, -6.385315 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"x\": -1.7364502,\n\t\t\t\t\t\t\"y\": -6.385315,\n\t\t\t\t\t\t\"curve\": [ 0.43333337, -1.7364502, 0.5666667, 0.72280884, 0.43333337, -6.385315, 0.5872843, -4.478018 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"x\": 0.72280884,\n\t\t\t\t\t\t\"y\": -4.2092066,\n\t\t\t\t\t\t\"curve\": [ 0.725, 0.72280884, 0.90833336, -0.07692909, 0.7425979, -3.571373, 0.90833336, -2.9409485 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": -0.59210205, \"y\": -2.9409485 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-fist\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 7.6139,\n\t\t\t\t\t\t\"curve\": [ 0.14280485, 7.615801, 0.24749966, -23.172117 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": -26.562366,\n\t\t\t\t\t\t\"curve\": [ 0.28090018, -29.079971, 0.35126793, -37.357285 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"value\": -37.204018,\n\t\t\t\t\t\t\"curve\": [ 0.51315707, -37.050312, 0.5616712, -29.884733 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": -25.176544,\n\t\t\t\t\t\t\"curve\": [ 0.62112856, -22.581184, 0.69402313, -3.9839792 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8000001,\n\t\t\t\t\t\t\"value\": 3.6281843,\n\t\t\t\t\t\t\"curve\": [ 0.8612923, 8.030727, 0.9464212, 7.569488 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": 7.6139 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.116666675, 0, 0.35000002, 0.52316284, 0.116666675, 0, 0.35000002, -3.2719727 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"x\": 0.52316284,\n\t\t\t\t\t\t\"y\": -3.2719727,\n\t\t\t\t\t\t\"curve\": [ 0.6, 0.52316284, 0.86666673, 0, 0.6, -3.2719727, 0.86666673, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1 }\n\t\t\t\t],\n\t\t\t\t\"shear\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"y\": 19.83303,\n\t\t\t\t\t\t\"curve\": [ 0.116666675, 0, 0.35000002, 15.27623, 0.116666675, 19.83303, 0.35000002, 28.307434 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"x\": 15.27623,\n\t\t\t\t\t\t\"y\": 28.307434,\n\t\t\t\t\t\t\"curve\": [ 0.6, 15.27623, 0.86666673, 0, 0.6, 28.307434, 0.86666673, 19.83303 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"y\": 19.83303 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"board-ik\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 393.62268,\n\t\t\t\t\t\t\"curve\": [ 0.083333336, 393.62268, 0.25, 393.47745, 0.083333336, 0, 0.25, 117.69192 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"x\": 393.47745,\n\t\t\t\t\t\t\"y\": 117.69192,\n\t\t\t\t\t\t\"curve\": [ 0.37500003, 393.47745, 0.45833334, 393.62268, 0.37500003, 117.69192, 0.45833334, 83.820915 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.5, \"x\": 393.62268, \"y\": 83.820915 },\n\t\t\t\t\t{ \"time\": 0.6666667, \"x\": 393.62265, \"y\": 30.147125 },\n\t\t\t\t\t{ \"time\": 1, \"x\": 393.62268 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-thigh\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": -7.48584, \"y\": 8.506332 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-leg-target\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"curve\": [ 0.4278075, 10.829493, 0.5666667, 12.77742, 0.41390374, 7.2933292, 0.5666667, 8.790497 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"x\": 12.77742,\n\t\t\t\t\t\t\"y\": 8.790497,\n\t\t\t\t\t\t\"curve\": [ 0.6916667, 12.77742, 0.77157444, 11.271518, 0.6916667, 8.790497, 0.7664361, 8.619392 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.86666673 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-leg-target\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"curve\": [ 0.4916667, 0, 0.5342914, 4.4684753, 0.4916667, 0, 0.54184526, 1.6264877 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"x\": 4.5323563,\n\t\t\t\t\t\t\"y\": 1.7728882,\n\t\t\t\t\t\t\"curve\": [ 0.6222882, 4.642107, 0.7166667, 3.3122807, 0.6148122, 2.0568504, 0.71035635, 2.09515 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.8000001 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"exhaust3\": {\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 1.8815429,\n\t\t\t\t\t\t\"y\": 0.8103436,\n\t\t\t\t\t\t\"curve\": [ 0.016666668, 1.8815429, 0.16666667, 1.3002366, 0.016666668, 0.8103436, 0.16666667, 0.6494452 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"x\": 1.3002366,\n\t\t\t\t\t\t\"y\": 0.6494452,\n\t\t\t\t\t\t\"curve\": [ 0.22500001, 1.3002366, 0.275, 2.0505826, 0.22500001, 0.6494452, 0.275, 0.98424345 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.3,\n\t\t\t\t\t\t\"x\": 2.0505826,\n\t\t\t\t\t\t\"y\": 0.98424345,\n\t\t\t\t\t\t\"curve\": [ 0.32500002, 2.0505826, 0.37500003, 1.3108075, 0.32500002, 0.98424345, 0.38387057, 0.7154094 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"x\": 1.3108075,\n\t\t\t\t\t\t\"y\": 0.6857325,\n\t\t\t\t\t\t\"curve\": [ 0.43333337, 1.3108075, 0.5, 1.860152, 0.42571914, 0.63841134, 0.5, 0.53709126 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.53333336,\n\t\t\t\t\t\t\"x\": 1.860152,\n\t\t\t\t\t\t\"y\": 0.53709126,\n\t\t\t\t\t\t\"curve\": [ 0.5666667, 1.860152, 0.6333334, 1.1865239, 0.5666667, 0.53709126, 0.6043685, 0.85394675 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"x\": 1.1865239,\n\t\t\t\t\t\t\"y\": 0.85385764,\n\t\t\t\t\t\t\"curve\": [ 0.70000005, 1.1865239, 0.7666667, 1.5493445, 0.7065524, 0.8538005, 0.7735074, 0.7750741 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8000001,\n\t\t\t\t\t\t\"x\": 1.5493445,\n\t\t\t\t\t\t\"y\": 0.7456156,\n\t\t\t\t\t\t\"curve\": [ 0.8166667, 1.5493445, 0.85, 1.1814528, 0.8145784, 0.7294049, 0.85, 0.71298504 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"x\": 1.1814528,\n\t\t\t\t\t\t\"y\": 0.71298504,\n\t\t\t\t\t\t\"curve\": [ 0.90000004, 1.1814528, 0.9666667, 1.8815429, 0.90000004, 0.71298504, 0.9666667, 0.8103436 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": 1.8815429, \"y\": 0.8103436 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"side-glow1\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": 51.122334, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.06666667, \"value\": 43.8174, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.10000001, \"value\": 40.95424, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.16666667, \"value\": 27.781216, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.20000002, \"value\": 10.243, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.26666668, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.8000001, \"value\": -25.811462 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": 338.2833, \"y\": 40.22265, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.06666667, \"x\": 331.19852, \"y\": 30.388908, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.10000001, \"x\": 318.63272, \"y\": 20.588142, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.16666667, \"x\": 302.44952, \"y\": 9.637321, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.20000002, \"x\": 276.87427, \"y\": 1.1328201, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.26666668, \"x\": 248.1646, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.3, \"x\": 221.35808, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.36666667, \"x\": 195.69295, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.40000004, \"x\": 171.07834, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.4666667, \"x\": 144.8389, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.5, \"x\": 121.218796, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.5666667, \"x\": 91.9805, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.6, \"x\": 62.632572, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.6666667, \"x\": 30.775017, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.70000005, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.7666667, \"x\": -28.448898, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.8000001, \"x\": -67.48724, \"y\": 16.81865, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.86666673, \"x\": -83.07391, \"y\": 24.357399, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.90000004, \"x\": -93.80559, \"y\": 29.547943 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{ \"x\": 0.5346727, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.06666667, \"x\": 0.5943282, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.10000001, \"x\": 0.84373015, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.16666667, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.8000001, \"x\": 0.5339128, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.86666673, \"x\": 0.42787904, \"y\": 0.8014025, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.90000004, \"x\": 0.34898117, \"y\": 0.65362954 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"side-glow2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 0.06666667, \"value\": 51.122334, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.10000001, \"value\": 43.8174, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.16666667, \"value\": 40.95424, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.20000002, \"value\": 27.781216, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.26666668, \"value\": 10.243, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.3, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.86666673, \"value\": -25.811462 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"time\": 0.06666667, \"x\": 338.2833, \"y\": 40.22265, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.10000001, \"x\": 331.19852, \"y\": 30.388908, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.16666667, \"x\": 318.63272, \"y\": 20.588142, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.20000002, \"x\": 302.44952, \"y\": 9.637321, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.26666668, \"x\": 276.87427, \"y\": 1.1328201, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.3, \"x\": 248.1646, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.36666667, \"x\": 221.35808, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.40000004, \"x\": 195.69295, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.4666667, \"x\": 171.07834, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.5, \"x\": 144.8389, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.5666667, \"x\": 121.218796, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.6, \"x\": 91.9805, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.6666667, \"x\": 62.632572, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.70000005, \"x\": 30.775017, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.7666667, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.8000001, \"x\": -28.448898, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.86666673, \"x\": -67.48724, \"y\": 16.81865, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.90000004, \"x\": -83.07391, \"y\": 24.357399, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.9666667, \"x\": -93.80559, \"y\": 29.547943 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{ \"time\": 0.06666667, \"x\": 0.5346727, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.10000001, \"x\": 0.5943282, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.16666667, \"x\": 0.84373015, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.20000002, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.86666673, \"x\": 0.5339128, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.90000004, \"x\": 0.42787904, \"y\": 0.8014025, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 0.9666667, \"x\": 0.34898117, \"y\": 0.65362954 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -34.730927,\n\t\t\t\t\t\t\"curve\": [ 0.033957515, -36.31231, 0.16164774, -39.33425 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": -39.369553,\n\t\t\t\t\t\t\"curve\": [ 0.38382354, -39.369553, 0.49142906, -29.519127 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": -28.856522,\n\t\t\t\t\t\t\"curve\": [ 0.5246124, -26.953812, 0.57093024, -21.009949 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": -21.009949,\n\t\t\t\t\t\t\"curve\": [ 0.72499996, -21.009949, 0.96918166, -33.3546 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": -34.730927 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"neck\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 10.198809,\n\t\t\t\t\t\t\"curve\": [ 0.06957364, 12.087551, 0.18930483, 16.032207 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": 16.143192,\n\t\t\t\t\t\t\"curve\": [ 0.3330214, 16.143192, 0.44935408, 8.034576 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": 5.831173,\n\t\t\t\t\t\t\"curve\": [ 0.54162014, 4.0204487, 0.6, 2.6821098 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": 2.6821098,\n\t\t\t\t\t\t\"curve\": [ 0.725, 2.6821098, 0.9432171, 8.572393 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": 10.198809 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"head\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 10.198807,\n\t\t\t\t\t\t\"curve\": [ 0.04406569, 11.515038, 0.20002422, 16.124762 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": 16.143196,\n\t\t\t\t\t\t\"curve\": [ 0.37456274, 16.173042, 0.49214864, 2.6543446 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": 2.6821194,\n\t\t\t\t\t\t\"curve\": [ 0.7254709, 2.700241, 0.9627052, 9.262859 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": 10.198807 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.030124608, -0.24282837, 0.20000002, -4.2181396, 0.051385146, -1.064109, 0.20000002, -3.622406 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"x\": -4.2181396,\n\t\t\t\t\t\t\"y\": -3.622406,\n\t\t\t\t\t\t\"curve\": [ 0.35833335, -4.2181396, 0.5416667, 0.835022, 0.35833335, -3.622406, 0.5416667, 6.0112305 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"x\": 0.835022,\n\t\t\t\t\t\t\"y\": 6.0112305,\n\t\t\t\t\t\t\"curve\": [ 0.725, 0.835022, 0.9392235, 0.3238945, 0.725, 6.0112305, 0.9453954, 1.1418962 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -11.184067,\n\t\t\t\t\t\t\"curve\": [ 0.064068936, -14.818895, 0.25, -20.008562 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": -20.008562,\n\t\t\t\t\t\t\"curve\": [ 0.4290698, -20.11992, 0.5796532, 5.1212215 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": 8.669727,\n\t\t\t\t\t\t\"curve\": [ 0.6174707, 11.716629, 0.68726665, 20.524658 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.7666667,\n\t\t\t\t\t\t\"value\": 20.55344,\n\t\t\t\t\t\t\"curve\": [ 0.84837854, 20.69685, 0.96325815, -9.425582 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": -11.184067 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 9.614861,\n\t\t\t\t\t\t\"curve\": [ 0.014304811, 8.505188, 0.075, 2.63373 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"value\": 2.63373,\n\t\t\t\t\t\t\"curve\": [ 0.15, 2.63373, 0.25, 13.522522 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.3,\n\t\t\t\t\t\t\"value\": 13.522522,\n\t\t\t\t\t\t\"curve\": [ 0.35000002, 13.522522, 0.45000002, 11.27948 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": 11.27948,\n\t\t\t\t\t\t\"curve\": [ 0.57500005, 11.27948, 0.725, 18.13115 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8000001,\n\t\t\t\t\t\t\"value\": 18.13115,\n\t\t\t\t\t\t\"curve\": [ 0.85, 18.13115, 0.97799134, 11.069231 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": 9.614861 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair4\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -17.701607,\n\t\t\t\t\t\t\"curve\": [ 0.008333334, -17.701607, 0.025000002, -23.727055 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.033333335,\n\t\t\t\t\t\t\"value\": -23.727055,\n\t\t\t\t\t\t\"curve\": [ 0.06666667, -23.727055, 0.15414421, -4.4044647 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"value\": -1.9229889,\n\t\t\t\t\t\t\"curve\": [ 0.19701427, 4.0907516, 0.23571353, 12.90876 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": 17.55626,\n\t\t\t\t\t\t\"curve\": [ 0.3007945, 22.680435, 0.3416667, 27.974327 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": 27.974327,\n\t\t\t\t\t\t\"curve\": [ 0.40000004, 27.974327, 0.4666667, -1.4496689 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": -1.4496689,\n\t\t\t\t\t\t\"curve\": [ 0.5166667, -1.4496689, 0.55, 3.1607742 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": 3.1607742,\n\t\t\t\t\t\t\"curve\": [ 0.5833334, 3.1607742, 0.6166667, -8.9045105 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": -8.9045105,\n\t\t\t\t\t\t\"curve\": [ 0.6416667, -8.9045105, 0.65833336, -5.4047775 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"value\": -5.4047775,\n\t\t\t\t\t\t\"curve\": [ 0.6833334, -5.4047775, 0.7166667, -15.321167 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": -15.321167,\n\t\t\t\t\t\t\"curve\": [ 0.75000006, -15.321167, 0.78333336, -9.186089 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8000001,\n\t\t\t\t\t\t\"value\": -9.186089,\n\t\t\t\t\t\t\"curve\": [ 0.8166667, -9.186089, 0.85, -23.603443 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"value\": -23.603443,\n\t\t\t\t\t\t\"curve\": [ 0.8833334, -23.603443, 0.9166667, -17.379684 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9333334,\n\t\t\t\t\t\t\"value\": -17.379684,\n\t\t\t\t\t\t\"curve\": [ 0.9416667, -17.379684, 0.9583334, -20.461502 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9666667,\n\t\t\t\t\t\t\"value\": -20.461502,\n\t\t\t\t\t\t\"curve\": [ 0.975, -20.461502, 0.99166673, -17.701607 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": -17.701607 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair1\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 9.614861,\n\t\t\t\t\t\t\"curve\": [ 0.0597572, 9.040993, 0.25, 8.903637 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": 8.903637,\n\t\t\t\t\t\t\"curve\": [ 0.39166668, 8.903637, 0.5083334, 14.578968 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": 14.578968,\n\t\t\t\t\t\t\"curve\": [ 0.675, 14.578968, 0.9562208, 10.283142 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": 9.614861 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -3.820675,\n\t\t\t\t\t\t\"curve\": [ 0.016666668, -3.820675, 0.06448884, -9.157619 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": -9.085266,\n\t\t\t\t\t\t\"curve\": [ 0.17791708, -9.03841, 0.23369505, 1.2894592 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": 5.978203,\n\t\t\t\t\t\t\"curve\": [ 0.27576363, 7.2718353, 0.33648264, 17.103916 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": 17.104008,\n\t\t\t\t\t\t\"curve\": [ 0.4125646, 17.10416, 0.4666667, 1.5926285 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": 1.5926285,\n\t\t\t\t\t\t\"curve\": [ 0.53333336, 1.5926285, 0.5666667, 13.625595 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": 13.625595,\n\t\t\t\t\t\t\"curve\": [ 0.6166667, 13.625595, 0.6833334, 0.78372574 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.70000005,\n\t\t\t\t\t\t\"value\": 0.78372574,\n\t\t\t\t\t\t\"curve\": [ 0.7166667, 0.78372574, 0.75010437, 12.009415 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.7666667,\n\t\t\t\t\t\t\"value\": 11.899349,\n\t\t\t\t\t\t\"curve\": [ 0.79226184, 11.729248, 0.8166667, -0.8489189 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8333334,\n\t\t\t\t\t\t\"value\": -0.8489151,\n\t\t\t\t\t\t\"curve\": [ 0.85029763, -0.8489151, 0.8801588, 1.9889565 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.90000004,\n\t\t\t\t\t\t\"value\": 1.8188553,\n\t\t\t\t\t\t\"curve\": [ 0.9164943, 1.6774521, 0.95000005, -6.8954697 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9666667,\n\t\t\t\t\t\t\"value\": -6.8954697,\n\t\t\t\t\t\t\"curve\": [ 0.975, -6.8954697, 0.99166673, -3.820675 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": -3.820675 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 31.653091,\n\t\t\t\t\t\t\"curve\": [ 0.10833334, 31.653091, 0.32500002, 13.013519 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"value\": 13.013519,\n\t\t\t\t\t\t\"curve\": [ 0.7098432, 13.013519, 0.9166667, 31.653091 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": 31.653091 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 31.00085,\n\t\t\t\t\t\t\"curve\": [ 0.10833334, 31.00085, 0.32499242, 12.759247 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"value\": 12.785236,\n\t\t\t\t\t\t\"curve\": [ 0.58666086, 12.822018, 0.9166667, 31.00085 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": 31.00085 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"gun\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 1.9487133,\n\t\t\t\t\t\t\"curve\": [ 0.083333336, 1.9487133, 0.24508634, 36.732033 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": 36.71154,\n\t\t\t\t\t\t\"curve\": [ 0.4394591, 36.6869, 0.58874005, 10.679077 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": 8.750355,\n\t\t\t\t\t\t\"curve\": [ 0.7013947, 5.806615, 0.9166667, 1.9487133 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": 1.9487133 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.033333335, 0, 0.10000001, 2.3487778 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 2.3487778,\n\t\t\t\t\t\t\"curve\": [ 0.22500001, 2.3487778, 0.40833336, -2.3988113 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": -2.3988113,\n\t\t\t\t\t\t\"curve\": [ 0.5666667, -2.3988113, 0.70000005, 1.444809 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.7666667,\n\t\t\t\t\t\t\"value\": 1.444809,\n\t\t\t\t\t\t\"curve\": [ 0.82500005, 1.444809, 0.9416667, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.06324985, 0.7667523, 0.10601111, 1.4170189 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"value\": 1.4170189,\n\t\t\t\t\t\t\"curve\": [ 0.25933838, 1.4170189, 0.34433287, -1.252899 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"value\": -1.2558479,\n\t\t\t\t\t\t\"curve\": [ 0.6558634, -1.2604079, 0.9167232, -0.779322 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"head-control\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 0.37347412,\n\t\t\t\t\t\t\"y\": -11.169981,\n\t\t\t\t\t\t\"curve\": [ 0.13250001, 0.37347412, 0.335225, -10.232269, 0.13250001, -11.169981, 0.335225, 3.1505556 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.53333336,\n\t\t\t\t\t\t\"x\": -10.232269,\n\t\t\t\t\t\t\"y\": 3.1505556,\n\t\t\t\t\t\t\"curve\": [ 0.70977503, -10.232269, 0.8833334, 0.37347412, 0.70977503, 3.1505556, 0.8833334, -11.169981 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": 0.37347412, \"y\": -11.169981 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-shoulder\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 1.461792,\n\t\t\t\t\t\t\"y\": 10.149414,\n\t\t\t\t\t\t\"curve\": [ 0.10333334, 1.461792, 0.24937779, 1.3603516, 0.10333334, 10.149414, 0.24937779, -4.3946743 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"x\": 1.3603516,\n\t\t\t\t\t\t\"y\": -4.3946743,\n\t\t\t\t\t\t\"curve\": [ 0.6206223, 1.3603516, 0.85, 1.461792, 0.6206223, -4.3946743, 0.85, 10.149414 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": 1.461792, \"y\": 10.149414 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"back-shoulder\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 1.4024963,\n\t\t\t\t\t\t\"y\": 0.4371624,\n\t\t\t\t\t\t\"curve\": [ 0.08833333, 1.4024963, 0.20787777, -2.4733276, 0.08833333, 0.4371624, 0.20787777, 8.614532 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"x\": -2.4733276,\n\t\t\t\t\t\t\"y\": 8.614532,\n\t\t\t\t\t\t\"curve\": [ 0.5721222, -2.4733276, 0.8333334, 1.4024963, 0.5721222, 8.614532, 0.8333334, 0.4371624 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": 1.4024963, \"y\": 0.4371624 }\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t\"transform\": {\n\t\t\t\"front-foot-board-transform\": [\n\t\t\t\t{ \"mixRotate\": 0.9969877 }\n\t\t\t],\n\t\t\t\"rear-foot-board-transform\": [\n\t\t\t\t{}\n\t\t\t],\n\t\t\t\"toes-board\": [\n\t\t\t\t{}\n\t\t\t]\n\t\t},\n\t\t\"attachments\": {\n\t\t\t\"default\": {\n\t\t\t\t\"front-foot\": {\n\t\t\t\t\t\"front-foot\": {\n\t\t\t\t\t\t\"deform\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"offset\": 26,\n\t\t\t\t\t\t\t\t\"vertices\": [ -0.028320312, -5.3702393, -0.028320312, -5.3702393, 3.8188019, -3.7757006, -0.028320312, -5.3702393, -3.8215942, 3.7784686 ]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"front-shin\": {\n\t\t\t\t\t\"front-shin\": {\n\t\t\t\t\t\t\"deform\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"offset\": 14,\n\t\t\t\t\t\t\t\t\"vertices\": [ 0.5298004, -1.12677, -0.855072, -4.2058716, -11.351578, -10.1922455, -10.798645, -8.437653, -6.0644684, -6.8975677, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.5489197, -3.0602112, 1.4846344, -2.2966309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4.804367, -7.018173 ]\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\t\t\"offset\": 14,\n\t\t\t\t\t\t\t\t\"vertices\": [ 0.5298004, -1.12677, -11.66571, -9.072113, -25.65866, -17.537354, -25.532166, -16.50978, -11.782318, -11.260971, 0, 0, 0.60487366, -1.6358948, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.60487366, -1.6358948, 0, 0, -2.645218, -7.3573914, 1.4846344, -2.2966309, 0, 0, 0, 0, 0, 0, 0.60487366, -1.6358948, 0.60487366, -1.6358948, 0.60487366, -1.6358948, 0.60487366, -1.6358948, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.60487366, -1.6358948, 0, 0, -10.068726, -12.099899 ]\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"time\": 0.53333336,\n\t\t\t\t\t\t\t\t\"offset\": 14,\n\t\t\t\t\t\t\t\t\"vertices\": [ 0.5298004, -1.12677, -0.855072, -4.2058716, -7.0077515, -8.247711, -6.4548187, -6.4931183, -6.0644684, -6.8975677, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.5489197, -3.0602112, 1.4846344, -2.2966309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4.804367, -7.018173 ]\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"time\": 1,\n\t\t\t\t\t\t\t\t\"offset\": 14,\n\t\t\t\t\t\t\t\t\"vertices\": [ 0.5298004, -1.12677, -0.855072, -4.2058716, -11.351578, -10.1922455, -10.798645, -8.437653, -6.0644684, -6.8975677, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.5489197, -3.0602112, 1.4846344, -2.2966309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4.804367, -7.018173 ]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"hoverboard-board\": {\n\t\t\t\t\t\"hoverboard-board\": {\n\t\t\t\t\t\t\"deform\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"curve\": [ 0.06666667, 0, 0.20000002, 1 ]\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\t\t\"offset\": 1,\n\t\t\t\t\t\t\t\t\"vertices\": [ 2.4585571, 0, 0, 0, 0, 0, 0, 0, 0, 3.5567284, -2.975464E-4, 3.5567284, -2.975464E-4, 0, 0, 0, 0, 0, 0, -7.5912476E-4, -9.841583, -7.5912476E-4, -9.841583, -7.5912476E-4, -9.841583, -7.5912476E-4, -9.841583, -7.5912476E-4, -9.841583, -7.5912476E-4, -9.841583, -7.5912476E-4, -9.841583, -7.5912476E-4, -9.841583, -7.5912476E-4, -9.841583, -7.5912476E-4, -9.841583, -7.5912476E-4, -9.841583, -7.5912476E-4, -9.841583, 0, 0, 0, 0, 0, 0, 0, 0, -4.9055824, 0.11214447, -9.407063, 6.1798096E-4, -6.348713, 4.348755E-4, -6.349247, -6.570183, -6.349247, -6.570183, -6.348713, 4.348755E-4, -2.3307953, 1.6784668E-4, -2.3313293, -6.57045, -2.3313293, -6.57045, -2.3307953, 1.6784668E-4, 0, 0, 1.2207031E-4, 2.4585571, 1.2207031E-4, 2.4585571, 1.2207031E-4, 2.4585571, 1.2207031E-4, 2.4585571, 3.3297005, 4.440048, 3.3297005, 4.440048, 3.3297005, 4.440048, 1.2207031E-4, 2.4585571, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2.4622688, 1.6784668E-4, -2.4622688, 1.6784668E-4, -2.5231628, 1.1313019, -2.5231628, 1.1313019, -2.5231628, 1.1313019, 1.2207031E-4, 2.4585571, 1.2207031E-4, 2.4585571, -9.40694, 2.459175, 1.8806305, 0.44197083, -2.89917E-4, -3.5480804, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2.5231628, 1.1313019, -2.5231628, 1.1313019, -2.5231628, 1.1313019, -2.4622688, 1.6784668E-4, -2.4622688, 1.6784668E-4, -2.4622688, 1.6784668E-4, 0, 0, 0, 0, 1.2207031E-4, 2.4585571 ],\n\t\t\t\t\t\t\t\t\"curve\": [ 0.45000002, 0, 0.8166667, 1 ]\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{ \"time\": 1 }\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"rear-foot\": {\n\t\t\t\t\t\"rear-foot\": {\n\t\t\t\t\t\t\"deform\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"offset\": 28,\n\t\t\t\t\t\t\t\t\"vertices\": [ -1.9307785, 1.3478165, -0.31417084, 2.3336258, 3.0512238, 0.33946228, 2.3147202, -2.0167847, 2.1758347, -2.057953, -0.042770386, -2.9945908, 1.1542892, 0.26328278, 0.97501373, -0.6716919 ]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t\"idle\": {\n\t\t\"slots\": {\n\t\t\t\"front-fist\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"front-fist-open\" }\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t\"bones\": {\n\t\t\t\"front-foot-target\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": -69.06195 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hip\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.073313296, 0.34827155, 0.30330667, 1.2702631 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"value\": 1.2787018,\n\t\t\t\t\t\t\"curve\": [ 0.6146915, 1.2974385, 0.84705806, -1.4075624 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2,\n\t\t\t\t\t\t\"value\": -1.3809776,\n\t\t\t\t\t\t\"curve\": [ 1.3437006, -1.3701534, 1.6023359, -0.28238234 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -11.972925,\n\t\t\t\t\t\t\"y\": -23.154968,\n\t\t\t\t\t\t\"curve\": [ 0.05875295, -12.964105, 0.2579626, -15.19205, 0.14166668, -23.154968, 0.34119925, -24.893784 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"x\": -15.136561,\n\t\t\t\t\t\t\"y\": -26.739166,\n\t\t\t\t\t\t\"curve\": [ 0.62038213, -15.095693, 0.78841615, -13.2845, 0.5973517, -28.661255, 0.75000006, -30.010971 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.90000004,\n\t\t\t\t\t\t\"x\": -12.016165,\n\t\t\t\t\t\t\"y\": -30.010971,\n\t\t\t\t\t\t\"curve\": [ 0.9780125, -11.129423, 1.174976, -9.052921, 1.0355475, -29.93872, 1.233996, -28.080444 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.3333334,\n\t\t\t\t\t\t\"x\": -9.058169,\n\t\t\t\t\t\t\"y\": -26.638184,\n\t\t\t\t\t\t\"curve\": [ 1.5008559, -9.063721, 1.6144931, -10.950818, 1.4538441, -24.888504, 1.6087501, -23.154968 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667, \"x\": -11.972925, \"y\": -23.154968 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-foot-target\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": 48.869934 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -60.869286,\n\t\t\t\t\t\t\"curve\": [ 0.15379426, -60.854332, 0.45208335, -68.650444 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8333334,\n\t\t\t\t\t\t\"value\": -68.650444,\n\t\t\t\t\t\t\"curve\": [ 1.2208334, -68.650444, 1.5423342, -60.869286 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667, \"value\": -60.869286 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 42.46212,\n\t\t\t\t\t\t\"curve\": [ 0.02905485, 42.9721, 0.1343778, 45.282276 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": 45.274235,\n\t\t\t\t\t\t\"curve\": [ 0.5781398, 45.26433, 0.7984327, 40.07414 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8333334,\n\t\t\t\t\t\t\"value\": 39.74404,\n\t\t\t\t\t\t\"curve\": [ 0.8783342, 39.318417, 1.0192738, 38.22513 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2,\n\t\t\t\t\t\t\"value\": 38.221344,\n\t\t\t\t\t\t\"curve\": [ 1.3766922, 38.217648, 1.6194141, 41.681515 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667, \"value\": 42.46212 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 39.198883,\n\t\t\t\t\t\t\"curve\": [ 0.185123, 39.215454, 0.5, 29.370453 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"value\": 29.370453,\n\t\t\t\t\t\t\"curve\": [ 0.9166667, 29.370453, 1.4166667, 39.198883 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667, \"value\": 39.198883 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"head\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -6.7476807,\n\t\t\t\t\t\t\"curve\": [ 0.17644171, -7.8818083, 0.34916663, -8.947651 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"value\": -8.947651,\n\t\t\t\t\t\t\"curve\": [ 0.55, -8.947651, 0.69701374, -6.771097 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8333334,\n\t\t\t\t\t\t\"value\": -5.4363976,\n\t\t\t\t\t\t\"curve\": [ 0.8796374, -4.9830365, 1.0500001, -4.122652 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1333334,\n\t\t\t\t\t\t\"value\": -4.122652,\n\t\t\t\t\t\t\"curve\": [ 1.2658334, -4.122652, 1.4685583, -5.477994 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667, \"value\": -6.7476807 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-fist\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.08611111, 0, 0.23336044, 2.4826431 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": 4.127198,\n\t\t\t\t\t\t\"curve\": [ 0.42875406, 5.69687, 0.7111112, 10.064902 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8333334,\n\t\t\t\t\t\t\"value\": 10.064902,\n\t\t\t\t\t\t\"curve\": [ 0.9263889, 10.064902, 1.0922854, 4.210019 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2,\n\t\t\t\t\t\t\"value\": 2.781331,\n\t\t\t\t\t\t\"curve\": [ 1.3494837, 0.7986326, 1.551389, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.063113466, 0.5412674, 0.36666667, 3.3939 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.53333336,\n\t\t\t\t\t\t\"value\": 3.3939,\n\t\t\t\t\t\t\"curve\": [ 0.6958334, 3.3939, 0.93889207, -1.6320496 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2,\n\t\t\t\t\t\t\"value\": -1.6092262,\n\t\t\t\t\t\t\"curve\": [ 1.4195797, -1.5900345, 1.5741305, -0.6726322 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"gun\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.0990061, 0.27257442, 0.36666667, 1.2272491 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.53333336,\n\t\t\t\t\t\t\"value\": 1.2272491,\n\t\t\t\t\t\t\"curve\": [ 0.6650001, 1.2272491, 0.93724144, -0.56491375 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1333334,\n\t\t\t\t\t\t\"value\": -0.55424786,\n\t\t\t\t\t\t\"curve\": [ 1.3156897, -0.55391216, 1.5823047, -0.20918465 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -22.875427,\n\t\t\t\t\t\t\"curve\": [ 0.09948181, -23.449928, 0.36319366, -24.73999 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.53333336,\n\t\t\t\t\t\t\"value\": -24.740707,\n\t\t\t\t\t\t\"curve\": [ 0.7060638, -24.7416, 0.96080565, -20.966827 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1333334,\n\t\t\t\t\t\t\"value\": -20.967194,\n\t\t\t\t\t\t\"curve\": [ 1.3550801, -20.967667, 1.5666925, -22.280136 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667, \"value\": -22.875427 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"neck\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 3.7834435,\n\t\t\t\t\t\t\"curve\": [ 0.16666667, 3.7834435, 0.5, 5.449173 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"value\": 5.449173,\n\t\t\t\t\t\t\"curve\": [ 0.9166667, 5.449173, 1.4166667, 3.7834435 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667, \"value\": 3.7834435 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.066815846, 0.33432382, 0.34125456, 2.5357144 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.53333336,\n\t\t\t\t\t\t\"value\": 2.5443554,\n\t\t\t\t\t\t\"curve\": [ 0.73404145, 2.553385, 0.98162, -0.94336116 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1333334,\n\t\t\t\t\t\t\"value\": -0.9299526,\n\t\t\t\t\t\t\"curve\": [ 1.3652151, -0.9094587, 1.548701, -0.561897 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -2.1479998,\n\t\t\t\t\t\t\"curve\": [ 0.051982526, -1.9036858, 0.38399264, -0.14695562 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.53333336,\n\t\t\t\t\t\t\"value\": -0.1397686,\n\t\t\t\t\t\t\"curve\": [ 0.761939, -0.12876725, 0.8954165, -3.104005 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1333334,\n\t\t\t\t\t\t\"value\": -3.1014395,\n\t\t\t\t\t\t\"curve\": [ 1.348143, -3.098699, 1.5921899, -2.4576647 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667, \"value\": -2.1479998 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair1\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.06666667, 0, 0.21343653, 2.8597527 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": 3.6469727,\n\t\t\t\t\t\t\"curve\": [ 0.35760662, 4.9918785, 0.535274, 7.9163513 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"value\": 7.9163513,\n\t\t\t\t\t\t\"curve\": [ 0.80927515, 7.916355, 1.0674845, 5.494446 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1333334,\n\t\t\t\t\t\t\"value\": 4.69825,\n\t\t\t\t\t\t\"curve\": [ 1.2453741, 3.3435402, 1.5250001, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.06666667, 0, 0.22483166, -7.9657555 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": -9.751606,\n\t\t\t\t\t\t\"curve\": [ 0.3156868, -11.84417, 0.51888704, -16.655235 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"value\": -16.65524,\n\t\t\t\t\t\t\"curve\": [ 0.81718457, -16.65524, 1.0289435, -11.4347725 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1333334,\n\t\t\t\t\t\t\"value\": -9.13718,\n\t\t\t\t\t\t\"curve\": [ 1.2504922, -6.558552, 1.5250001, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.10000001, 0, 0.3, 1.3225384 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"value\": 1.3225384,\n\t\t\t\t\t\t\"curve\": [ 0.55, 1.3225384, 0.8664853, 0.925766 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1,\n\t\t\t\t\t\t\"value\": 0.7331009,\n\t\t\t\t\t\t\"curve\": [ 1.189498, 0.45964813, 1.5000001, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair4\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.11836932, -0.44361877, 0.3, -8.519379 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"value\": -8.519379,\n\t\t\t\t\t\t\"curve\": [ 0.55, -8.519379, 0.85, 1.9588852 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1,\n\t\t\t\t\t\t\"value\": 1.9588852,\n\t\t\t\t\t\t\"curve\": [ 1.1666667, 1.9588852, 1.5774639, 0.3826828 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"head-control\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.097740225, 1.4601135, 0.29985186, 4.4907684, 0.16993237, 0.13331592, 0.31626847, -3.280374 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"x\": 4.5544586,\n\t\t\t\t\t\t\"y\": -5.9516397,\n\t\t\t\t\t\t\"curve\": [ 0.5295718, 4.6368484, 0.77592766, 2.589203, 0.49205798, -8.888537, 0.6681139, -14.213062 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"x\": 1.4223175,\n\t\t\t\t\t\t\"y\": -14.264099,\n\t\t\t\t\t\t\"curve\": [ 0.9656826, 0.14897919, 1.1093842, -2.909935, 0.99393606, -14.264099, 1.1439168, -10.576373 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2333333,\n\t\t\t\t\t\t\"x\": -3.0218582,\n\t\t\t\t\t\t\"y\": -8.259235,\n\t\t\t\t\t\t\"curve\": [ 1.3416667, -3.0218582, 1.5675944, -1.4818802, 1.3165369, -6.1031046, 1.5583334, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-shoulder\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.20972222, 0, 0.5250047, -1.7180977, 0.20972222, 0, 0.5250047, 4.0759735 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8333334,\n\t\t\t\t\t\t\"x\": -1.7180977,\n\t\t\t\t\t\t\"y\": 4.0759735,\n\t\t\t\t\t\t\"curve\": [ 1.1499954, -1.7180977, 1.4597224, 0, 1.1499954, 4.0759735, 1.4597224, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667 }\n\t\t\t\t]\n\t\t\t}\n\t\t}\n\t},\n\t\"idle-turn\": {\n\t\t\"slots\": {\n\t\t\t\"front-fist\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"front-fist-open\" }\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t\"bones\": {\n\t\t\t\"front-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -302.77197,\n\t\t\t\t\t\t\"curve\": [ 0, -406.90015, 0.124568395, -420.8693 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668, \"value\": -420.8693 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 2.2368546,\n\t\t\t\t\t\t\"y\": -4.9815826,\n\t\t\t\t\t\t\"curve\": [ 0.06666667, 2.2368546, 0.11140643, 0, 0.06666667, -4.9815826, 0.11140643, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 248.56125,\n\t\t\t\t\t\t\"curve\": [ 0, 371.2775, 0.062284198, 399.19888 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.13333334, \"value\": 399.19888 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -2.839569,\n\t\t\t\t\t\t\"y\": 37.28061,\n\t\t\t\t\t\t\"curve\": [ 0.033333335, -2.839569, 0.06943522, 0, 0.033333335, 37.28061, 0.06943522, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.13333334 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"gun\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -3.9546013,\n\t\t\t\t\t\t\"curve\": [ 0, -10.403942, 0.019096278, -20.42602 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.033333335,\n\t\t\t\t\t\t\"value\": -20.454044,\n\t\t\t\t\t\t\"curve\": [ 0.043844357, -20.474735, 0.12498683, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.20000002 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"neck\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 17.19942,\n\t\t\t\t\t\t\"curve\": [ 0, 6.270158, 0.124568395, 3.7834435 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668, \"value\": 3.7834435 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hip\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -2.6938133,\n\t\t\t\t\t\t\"y\": -6.786743,\n\t\t\t\t\t\t\"curve\": [ 0.06666667, -2.6938133, 0.20000002, -11.972925, 0.06666667, -6.786743, 0.20000002, -23.154968 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668, \"x\": -11.972925, \"y\": -23.154968 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-fist\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -15.535895,\n\t\t\t\t\t\t\"curve\": [ 0, -3.0764246, 0.03372228, 18.443924 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": 19.023037,\n\t\t\t\t\t\t\"curve\": [ 0.107971475, 19.749111, 0.16942663, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 0.9399196,\n\t\t\t\t\t\t\"curve\": [ 0, 0.96210766, 0.024154918, 1.2368381, 0, 1, 0.025600193, 0.94741195 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"x\": 1.2361622,\n\t\t\t\t\t\t\"y\": 0.94741195,\n\t\t\t\t\t\t\"curve\": [ 0.117257625, 1.235358, 0.18898629, 1, 0.116544604, 0.94741195, 0.18898629, 1 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 11.752888,\n\t\t\t\t\t\t\"curve\": [ 0, -7.9748516, 0.016757889, -33.39882 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.033333335,\n\t\t\t\t\t\t\"value\": -33.385704,\n\t\t\t\t\t\t\"curve\": [ 0.04859936, -33.373623, 0.1310863, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.20000002 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -18.249893,\n\t\t\t\t\t\t\"curve\": [ 0, -10.59406, 0.124568395, -22.87542 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668, \"value\": -22.87542 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"y\": 1.03,\n\t\t\t\t\t\t\"curve\": [ 0.06666667, 1, 0.13178295, 1, 0.06666667, 1.03, 0.13178295, 1 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"head\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 5.1160355,\n\t\t\t\t\t\t\"curve\": [ 0, -6.3408804, 0.124568395, -6.7476826 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668, \"value\": -6.7476826 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"y\": 1.03,\n\t\t\t\t\t\t\"curve\": [ 0.06666667, 1, 0.106976755, 1, 0.06666667, 1.03, 0.106976755, 1 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-foot-target\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -58.393997,\n\t\t\t\t\t\t\"y\": 30.476856,\n\t\t\t\t\t\t\"curve\": [ 0, -7.151085, 0.046600904, 16.621117, 0, 12.7073765, 0.03919706, 0.22035736 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"x\": 34.13688,\n\t\t\t\t\t\t\"y\": -0.18829882,\n\t\t\t\t\t\t\"curve\": [ 0.135537, 45.79357, 0.16338928, 48.869934, 0.13250703, -0.4067766, 0.16338928, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.20000002, \"x\": 48.869934 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 6.690201,\n\t\t\t\t\t\t\"curve\": [ 0, 19.76432, 0.03859885, 56.53068 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": 56.6253,\n\t\t\t\t\t\t\"curve\": [ 0.11433946, 56.786015, 0.18898629, 42.462116 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668, \"value\": 42.462116 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-foot-target\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -1.8527799,\n\t\t\t\t\t\t\"curve\": [ 0.014102792, -8.914391, 0.04653653, -28.39679 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"value\": -28.887407,\n\t\t\t\t\t\t\"curve\": [ 0.14351569, -29.286743, 0.2624582, -21.772501 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 9.968124,\n\t\t\t\t\t\t\"y\": 0.8236809,\n\t\t\t\t\t\t\"curve\": [ 0, -54.413353, 0.07785525, -69.06195, 0, 0.15267313, 0.07785525, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.16666667, \"x\": -69.06195 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -9.007214,\n\t\t\t\t\t\t\"curve\": [ 0.043777782, -9.007214, 0.071606, 7.4105954 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 10.077616,\n\t\t\t\t\t\t\"curve\": [ 0.16629143, 11.472048, 0.20818976, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair4\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -16.49289,\n\t\t\t\t\t\t\"curve\": [ 0.043777782, -16.49289, 0.100582, -5.9775696 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": -2.9526367,\n\t\t\t\t\t\t\"curve\": [ 0.16164672, -0.3376007, 0.20818976, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair1\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -3.8499641,\n\t\t\t\t\t\t\"curve\": [ 0.043777782, -3.8499641, 0.071606, 6.912709 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 8.052677,\n\t\t\t\t\t\t\"curve\": [ 0.16629143, 8.648701, 0.20818976, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 1.249958,\n\t\t\t\t\t\t\"curve\": [ 0.043777782, 1.249958, 0.071606, 8.967916 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 8.597805,\n\t\t\t\t\t\t\"curve\": [ 0.16629143, 8.404295, 0.20818976, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-thigh\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 12.209518,\n\t\t\t\t\t\t\"y\": 1.8914032,\n\t\t\t\t\t\t\"curve\": [ 0.033333335, 12.209518, 0.10000001, 0, 0.033333335, 1.8914032, 0.10000001, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.13333334 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-thigh\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -16.111008,\n\t\t\t\t\t\t\"y\": -1.3840027,\n\t\t\t\t\t\t\"curve\": [ 0.033333335, -16.111008, 0.10000001, 0, 0.033333335, -1.3840027, 0.10000001, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.13333334 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 0.26666668, \"value\": -2.147999 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"head-control\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -13.715668,\n\t\t\t\t\t\t\"y\": -34.70318,\n\t\t\t\t\t\t\"curve\": [ 0.06666667, -13.715668, 0.20000002, 0, 0.06666667, -34.70318, 0.20000002, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-shoulder\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 1.1284485,\n\t\t\t\t\t\t\"y\": -14.31152,\n\t\t\t\t\t\t\"curve\": [ 0.06666667, 1.1284485, 0.20000002, 0, 0.06666667, -14.31152, 0.20000002, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t}\n\t\t}\n\t},\n\t\"jump\": {\n\t\t\"slots\": {\n\t\t\t\"front-fist\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"front-fist-open\" },\n\t\t\t\t\t{ \"time\": 0.10000001, \"name\": \"front-fist-closed\" },\n\t\t\t\t\t{ \"time\": 0.8333334, \"name\": \"front-fist-open\" }\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t\"bones\": {\n\t\t\t\"front-thigh\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 55.07567,\n\t\t\t\t\t\t\"curve\": [ 0.0071301255, 46.656803, 0.043178868, 26.299644 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": 22.836548,\n\t\t\t\t\t\t\"curve\": [ 0.099546395, 17.988686, 0.16537325, 15.78083 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": 15.708641,\n\t\t\t\t\t\t\"curve\": [ 0.30917448, 15.628075, 0.40807107, 46.6661 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": 63.598656,\n\t\t\t\t\t\t\"curve\": [ 0.56039745, 74.72335, 0.7619598, 91.47623 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9666667,\n\t\t\t\t\t\t\"value\": 91.811035,\n\t\t\t\t\t\t\"curve\": [ 1.0678477, 92.01352, 1.095855, 22.050758 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1666667,\n\t\t\t\t\t\t\"value\": 22.250694,\n\t\t\t\t\t\t\"curve\": [ 1.1796616, 22.287384, 1.1757436, 56.166813 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2,\n\t\t\t\t\t\t\"value\": 56.16073,\n\t\t\t\t\t\t\"curve\": [ 1.2463567, 56.149105, 1.2631979, 54.9383 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": 55.07567 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": -5.1345825, \"y\": 11.5529785 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -45.56914,\n\t\t\t\t\t\t\"curve\": [ 0.0224493, -44.6064, 0.030192606, -39.063038 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": -35.290848,\n\t\t\t\t\t\t\"curve\": [ 0.12008707, -29.766033, 0.2801849, -19.952888 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"value\": -19.951965,\n\t\t\t\t\t\t\"curve\": [ 0.6730828, -19.950523, 0.8707537, -22.384499 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9666667,\n\t\t\t\t\t\t\"value\": -27.082253,\n\t\t\t\t\t\t\"curve\": [ 1.0941433, -33.325966, 1.1762462, -44.925877 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": -45.56914 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": -3.7868073, \"y\": -0.7673931 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-thigh\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 12.807907,\n\t\t\t\t\t\t\"curve\": [ 0.06666667, 12.807907, 0.2416681, 67.879326 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": 74.10752,\n\t\t\t\t\t\t\"curve\": [ 0.3144626, 86.0155, 0.45448682, 92.227356 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": 92.23942,\n\t\t\t\t\t\t\"curve\": [ 0.7529982, 92.25944, 0.96590084, 67.94411 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1,\n\t\t\t\t\t\t\"value\": 61.318653,\n\t\t\t\t\t\t\"curve\": [ 1.0389385, 53.752964, 1.2178437, 12.682728 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": 12.807907 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-shin\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -115.639915,\n\t\t\t\t\t\t\"curve\": [ 0.06674182, -117.17092, 0.125, -117.14553 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"value\": -117.14553,\n\t\t\t\t\t\t\"curve\": [ 0.22500001, -117.14553, 0.33238536, -108.763664 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"value\": -107.15474,\n\t\t\t\t\t\t\"curve\": [ 0.4798223, -105.25534, 0.68485916, -103.48536 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.7666667,\n\t\t\t\t\t\t\"value\": -101.96748,\n\t\t\t\t\t\t\"curve\": [ 0.8257901, -100.87048, 0.9185746, -92.302505 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1,\n\t\t\t\t\t\t\"value\": -92.2841,\n\t\t\t\t\t\t\"curve\": [ 1.1131337, -92.258545, 1.2974606, -114.21733 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": -115.639915 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -40.207535,\n\t\t\t\t\t\t\"curve\": [ 0.0536731, -35.456406, 0.15, -31.119186 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"value\": -31.119186,\n\t\t\t\t\t\t\"curve\": [ 0.30833334, -31.119186, 0.5466946, -80.11781 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": -96.56404,\n\t\t\t\t\t\t\"curve\": [ 0.69652027, -108.5585, 0.7966081, -112.53702 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"value\": -112.598526,\n\t\t\t\t\t\t\"curve\": [ 1.1365902, -112.83551, 1.2737077, -49.192924 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": -40.207535 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 20.536366,\n\t\t\t\t\t\t\"curve\": [ 0.054479003, 32.228794, 0.19202872, 55.84296 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": 62.577724,\n\t\t\t\t\t\t\"curve\": [ 0.29034674, 71.873825, 0.37502524, 79.28102 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"value\": 79.18405,\n\t\t\t\t\t\t\"curve\": [ 0.55467916, 78.98224, 0.68372726, 27.53711 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": 13.280796,\n\t\t\t\t\t\t\"curve\": [ 0.785987, -1.8513222, 0.87448627, -24.76372 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1,\n\t\t\t\t\t\t\"value\": -25.451767,\n\t\t\t\t\t\t\"curve\": [ 1.1648662, -26.355537, 1.303476, 9.098476 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": 20.536366 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-fist\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -36.16143,\n\t\t\t\t\t\t\"curve\": [ 0.11426025, -39.590572, 0.3, -45.608555 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"value\": -45.608555,\n\t\t\t\t\t\t\"curve\": [ 0.4416667, -45.608555, 0.53670037, -21.53749 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": -15.396365,\n\t\t\t\t\t\t\"curve\": [ 0.59187967, -10.229349, 0.69160116, 11.892562 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": 11.732259,\n\t\t\t\t\t\t\"curve\": [ 0.7830128, 11.541427, 0.8309314, 1.8034153 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"value\": -5.7830496,\n\t\t\t\t\t\t\"curve\": [ 0.89699674, -12.221985, 0.9008169, -14.216885 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9333334,\n\t\t\t\t\t\t\"value\": -14.51377,\n\t\t\t\t\t\t\"curve\": [ 0.9742596, -14.887441, 0.97593594, 10.380578 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1,\n\t\t\t\t\t\t\"value\": 10.547044,\n\t\t\t\t\t\t\"curve\": [ 1.0273728, 10.736399, 1.0228404, -8.4398575 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0333334,\n\t\t\t\t\t\t\"value\": -8.417142,\n\t\t\t\t\t\t\"curve\": [ 1.0594763, -8.360548, 1.0735712, 10.116536 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1,\n\t\t\t\t\t\t\"value\": 10.219346,\n\t\t\t\t\t\t\"curve\": [ 1.168007, 10.483893, 1.2696968, -36.06642 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": -36.16143 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 40.503326,\n\t\t\t\t\t\t\"curve\": [ 0.048025712, 36.09633, 0.16843164, 20.44992 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.3,\n\t\t\t\t\t\t\"value\": 20.44989,\n\t\t\t\t\t\t\"curve\": [ 0.47593588, 20.449875, 0.5709889, 33.757965 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": 38.67331,\n\t\t\t\t\t\t\"curve\": [ 0.64206403, 45.800163, 0.68065536, 57.440735 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": 62.90692,\n\t\t\t\t\t\t\"curve\": [ 0.828676, 72.80023, 0.9955255, 77.61066 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0333334,\n\t\t\t\t\t\t\"value\": 80.3669,\n\t\t\t\t\t\t\"curve\": [ 1.0823637, 83.94128, 1.1476829, 90.60106 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2,\n\t\t\t\t\t\t\"value\": 90.60106,\n\t\t\t\t\t\t\"curve\": [ 1.2475936, 90.46378, 1.316745, 53.066353 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": 49.062782 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 28.284866,\n\t\t\t\t\t\t\"curve\": [ 0.022121392, 25.120243, 0.18745898, -0.8935051 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"value\": -2.518879,\n\t\t\t\t\t\t\"curve\": [ 0.2570979, -9.918992, 0.37233594, -17.379135 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"value\": -17.410477,\n\t\t\t\t\t\t\"curve\": [ 0.5404287, -17.465504, 0.65918607, -16.914978 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.7666667,\n\t\t\t\t\t\t\"value\": -12.098708,\n\t\t\t\t\t\t\"curve\": [ 0.90748173, -5.7887077, 1.0247703, 14.578896 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1,\n\t\t\t\t\t\t\"value\": 20.577023,\n\t\t\t\t\t\t\"curve\": [ 1.1912643, 27.85358, 1.2833334, 29.665577 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": 29.665577 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"neck\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 11.884876,\n\t\t\t\t\t\t\"curve\": [ 0.10369816, 11.816757, 0.17891632, 11.152155 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"value\": 10.077812,\n\t\t\t\t\t\t\"curve\": [ 0.25466025, 7.2925415, 0.40455413, -8.148415 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"value\": -9.350786,\n\t\t\t\t\t\t\"curve\": [ 0.5083334, -12.484213, 0.59507793, -13.135471 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"value\": -12.605598,\n\t\t\t\t\t\t\"curve\": [ 0.71370536, -12.257435, 0.81489563, -5.572529 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8333334,\n\t\t\t\t\t\t\"value\": -4.081579,\n\t\t\t\t\t\t\"curve\": [ 0.88292503, -0.07140541, 1.0454745, 12.765446 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1,\n\t\t\t\t\t\t\"value\": 15.060447,\n\t\t\t\t\t\t\"curve\": [ 1.2079054, 19.60223, 1.2786734, 20.643595 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": 20.730635 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"head\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 13.143766,\n\t\t\t\t\t\t\"curve\": [ 0.0080890395, 12.187704, 0.19672124, -23.529558 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": -23.954443,\n\t\t\t\t\t\t\"curve\": [ 0.50926924, -23.954443, 0.6666667, -2.6586437 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": -2.6586437,\n\t\t\t\t\t\t\"curve\": [ 0.7916667, -2.6586437, 0.90833336, -13.318874 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9666667,\n\t\t\t\t\t\t\"value\": -13.318874,\n\t\t\t\t\t\t\"curve\": [ 1.1580662, -13.106431, 1.2405527, -1.5812836 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": -1.5812836 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.04056497, 1, 0.05244635, 0.9616517, 0.04056497, 1, 0.05244635, 1.1370772 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"x\": 0.9541517,\n\t\t\t\t\t\t\"y\": 1.1370772,\n\t\t\t\t\t\t\"curve\": [ 0.20215653, 0.9616517, 0.3177854, 1, 0.20215653, 1.1370772, 0.2518122, 1.0023715 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.4666667 },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"x\": 1.0017799,\n\t\t\t\t\t\t\"curve\": [ 1.0916667, 1.0017799, 1.1260852, 1.1427599, 1.0916667, 1, 1.128311, 0.97452027 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1666667,\n\t\t\t\t\t\t\"x\": 1.1436756,\n\t\t\t\t\t\t\"y\": 0.97339934,\n\t\t\t\t\t\t\"curve\": [ 1.2036642, 1.1445105, 1.233349, 0.9585149, 1.2061642, 0.97224504, 1.2266711, 1.0624334 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2666668,\n\t\t\t\t\t\t\"x\": 0.95826054,\n\t\t\t\t\t\t\"y\": 1.0626047,\n\t\t\t\t\t\t\"curve\": [ 1.283691, 0.95813054, 1.2915703, 1.0012144, 1.2876703, 1.0626947, 1.2879131, 1.0006912 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hip\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"y\": -45.45778,\n\t\t\t\t\t\t\"curve\": [ 0.041666668, -0.085293904, 0.15, 15.219021, 0.03129127, 44.982628, 0.12330208, 289.72595 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"x\": 15.219021,\n\t\t\t\t\t\t\"y\": 415.85382,\n\t\t\t\t\t\t\"curve\": [ 0.33152714, 15.219021, 0.5392024, -34.517242, 0.2711933, 532.92944, 0.4828083, 720.49945 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.7666667,\n\t\t\t\t\t\t\"x\": -34.517242,\n\t\t\t\t\t\t\"y\": 721.5962,\n\t\t\t\t\t\t\"curve\": [ 0.8875865, -34.517242, 1.0574986, -21.949486, 1.0490158, 721.17474, 1.0975218, 379.83627 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1333334,\n\t\t\t\t\t\t\"x\": -15.665609,\n\t\t\t\t\t\t\"y\": 266.76974,\n\t\t\t\t\t\t\"curve\": [ 1.1441567, -14.768756, 1.1879733, -10.528233, 1.1501372, 213.71597, 1.1717159, -61.31535 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2333333,\n\t\t\t\t\t\t\"x\": -6.5328007,\n\t\t\t\t\t\t\"y\": -61.338684,\n\t\t\t\t\t\t\"curve\": [ 1.2715178, -3.2195106, 1.3106667, 0.04639988, 1.2905278, -61.360336, 1.296268, -44.802708 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"y\": -45.45778 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-shin\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -74.188835,\n\t\t\t\t\t\t\"curve\": [ 0, -51.13796, 0.042270645, -12.5382805 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"value\": -12.284243,\n\t\t\t\t\t\t\"curve\": [ 0.28451803, -12.320733, 0.3699448, -74.43774 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"value\": -92.91604,\n\t\t\t\t\t\t\"curve\": [ 0.49831668, -111.85921, 0.6173692, -140.28201 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.90000004,\n\t\t\t\t\t\t\"value\": -140.8386,\n\t\t\t\t\t\t\"curve\": [ 1.0041902, -141.04378, 1.0896565, -47.86548 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1,\n\t\t\t\t\t\t\"value\": -37.437576,\n\t\t\t\t\t\t\"curve\": [ 1.1075488, -29.827354, 1.1403931, -21.183039 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1666667,\n\t\t\t\t\t\t\"value\": -21.081932,\n\t\t\t\t\t\t\"curve\": [ 1.1795068, -21.032516, 1.1907092, -50.650448 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2,\n\t\t\t\t\t\t\"value\": -53.17039,\n\t\t\t\t\t\t\"curve\": [ 1.2200129, -58.525276, 1.2707276, -73.38457 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": -74.188835 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-foot\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 7.3513794,\n\t\t\t\t\t\t\"curve\": [ 0, 4.8016586, 0.050000004, -26.644875 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": -26.644875,\n\t\t\t\t\t\t\"curve\": [ 0.19166668, -26.644875, 0.4416667, -11.77327 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": -11.77327,\n\t\t\t\t\t\t\"curve\": [ 0.6916667, -11.77327, 0.9416667, -19.36478 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"value\": -19.36478,\n\t\t\t\t\t\t\"curve\": [ 1.1333334, -19.36478, 1.3201784, 3.8209915 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": 7.3513794 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-foot\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": -7.1411285 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"gun\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 12.364543,\n\t\t\t\t\t\t\"curve\": [ 0.022356797, 16.282055, 0.15, 30.810171 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"value\": 30.810171,\n\t\t\t\t\t\t\"curve\": [ 0.25833336, 30.810171, 0.37500003, 13.257755 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"value\": 13.257755,\n\t\t\t\t\t\t\"curve\": [ 0.5083334, 13.257755, 0.65849364, 15.046124 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": 14.9829235,\n\t\t\t\t\t\t\"curve\": [ 0.78941685, 14.935562, 0.82796144, 13.620555 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"value\": 12.72069,\n\t\t\t\t\t\t\"curve\": [ 0.88687545, 12.250854, 0.9843206, 9.831252 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0333334,\n\t\t\t\t\t\t\"value\": 8.599857,\n\t\t\t\t\t\t\"curve\": [ 1.0447482, 8.31307, 1.0826218, 7.5548005 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1333334,\n\t\t\t\t\t\t\"value\": 7.1285486,\n\t\t\t\t\t\t\"curve\": [ 1.1752198, 6.7764764, 1.2833334, 6.182274 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": 6.182274 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-leg-target\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": -13.945175, \"y\": -30.341328 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-leg-target\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": -38.43331 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": 85.00298, \"y\": -33.593994 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-foot-target\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": -62.538223 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": 16.340538, \"y\": 0.18180084 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-foot-target\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": 18.552212 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": -176.39333, \"y\": 134.11803 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"back-foot-tip\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -143.72617,\n\t\t\t\t\t\t\"curve\": [ 0.08347858, -144.23846, 0.166992, -74.255035 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": -52.758675,\n\t\t\t\t\t\t\"curve\": [ 0.34171122, -36.574184, 0.51295114, -36.57154 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": -30.972921,\n\t\t\t\t\t\t\"curve\": [ 0.7235831, -26.775673, 0.848118, -17.061863 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9666667,\n\t\t\t\t\t\t\"value\": -16.741772,\n\t\t\t\t\t\t\"curve\": [ 1.1670359, -16.200747, 1.2718494, -144.17041 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": -143.72617 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-foot-tip\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -1.5731831,\n\t\t\t\t\t\t\"curve\": [ 0, -24.709229, 0.16180633, -60.87512 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": -60.83403,\n\t\t\t\t\t\t\"curve\": [ 0.34203652, -60.804497, 0.58167833, -43.50105 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.70000005,\n\t\t\t\t\t\t\"value\": -39.450535,\n\t\t\t\t\t\t\"curve\": [ 0.77320904, -36.944363, 0.83162963, -36.776226 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9666667,\n\t\t\t\t\t\t\"value\": -36.601685,\n\t\t\t\t\t\t\"curve\": [ 1.0543914, -36.488297, 1.0922754, -37.3679 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1666667,\n\t\t\t\t\t\t\"value\": -33.26124,\n\t\t\t\t\t\t\"curve\": [ 1.2371241, -29.371746, 1.146854, -1.413369 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.2, \"value\": -1.5731831 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -6.81118,\n\t\t\t\t\t\t\"curve\": [ 0, 13.591494, 0.116666675, 18.211311 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 18.211311,\n\t\t\t\t\t\t\"curve\": [ 0.16666667, 18.211311, 0.26007572, 12.954233 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.3,\n\t\t\t\t\t\t\"value\": 11.559322,\n\t\t\t\t\t\t\"curve\": [ 0.38192096, 8.697079, 0.5500177, 9.432354 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"value\": 9.31987,\n\t\t\t\t\t\t\"curve\": [ 0.8425522, 9.150269, 0.9184877, -7.3388023 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": -6.81118 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"curve\": [ 0.78055555, 0, 0.9722686, 16.029846, 0.78055555, 0, 0.9722686, 0.9185066 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1333334,\n\t\t\t\t\t\t\"x\": 16.029846,\n\t\t\t\t\t\t\"y\": 0.9185066,\n\t\t\t\t\t\t\"curve\": [ 1.2110649, 16.029846, 1.2805556, 0, 1.2110649, 0.9185066, 1.2805556, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair4\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -6.81118,\n\t\t\t\t\t\t\"curve\": [ 5.1568303E-4, -3.8773804, 0.062997445, 16.181213 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"value\": 16.136948,\n\t\t\t\t\t\t\"curve\": [ 0.24208306, 16.104736, 0.24888138, 16.071587 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": 13.456192,\n\t\t\t\t\t\t\"curve\": [ 0.44215032, 10.08622, 0.57313555, -2.199707 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": -6.0415115,\n\t\t\t\t\t\t\"curve\": [ 0.61402094, -8.046585, 0.7166667, -33.44294 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.7666667,\n\t\t\t\t\t\t\"value\": -33.44294,\n\t\t\t\t\t\t\"curve\": [ 0.8094167, -33.44294, 0.8348421, -31.31959 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"value\": -27.36391,\n\t\t\t\t\t\t\"curve\": [ 0.87383133, -26.473354, 0.90314054, -14.2814865 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9333334,\n\t\t\t\t\t\t\"value\": -14.470863,\n\t\t\t\t\t\t\"curve\": [ 0.9563124, -14.6150055, 0.94355834, -25.907421 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1,\n\t\t\t\t\t\t\"value\": -25.96328,\n\t\t\t\t\t\t\"curve\": [ 1.0621781, -26.024826, 1.0510793, -1.867569 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"value\": -1.867569,\n\t\t\t\t\t\t\"curve\": [ 1.0963418, -1.867569, 1.096107, -16.08857 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1333334,\n\t\t\t\t\t\t\"value\": -16.081871,\n\t\t\t\t\t\t\"curve\": [ 1.1694174, -16.07538, 1.1533196, -3.3775558 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2,\n\t\t\t\t\t\t\"value\": -3.3797913,\n\t\t\t\t\t\t\"curve\": [ 1.2339215, -3.3826141, 1.2706476, -6.0748444 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": -6.0748444 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -6.81118,\n\t\t\t\t\t\t\"curve\": [ 0, -3.171306, 0.041721012, 16.333447 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": 16.333447,\n\t\t\t\t\t\t\"curve\": [ 0.21039502, 15.738186, 0.2083106, -12.064487 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": -12.206696,\n\t\t\t\t\t\t\"curve\": [ 0.41660592, -12.3014145, 0.55184597, -3.9846573 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"value\": 1.5201721,\n\t\t\t\t\t\t\"curve\": [ 0.7256674, 4.3488274, 0.8166667, 4.9923897 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"value\": 4.9923897,\n\t\t\t\t\t\t\"curve\": [ 0.9006667, 4.9923897, 0.91241914, -29.046799 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9666667,\n\t\t\t\t\t\t\"value\": -27.445984,\n\t\t\t\t\t\t\"curve\": [ 0.98744994, -26.832684, 1.0178804, -5.424507 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"value\": -5.462139,\n\t\t\t\t\t\t\"curve\": [ 1.1068223, -5.215767, 1.0952008, -33.512318 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1333334,\n\t\t\t\t\t\t\"value\": -33.27906,\n\t\t\t\t\t\t\"curve\": [ 1.1622324, -33.567158, 1.1919858, 8.044712 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2666668,\n\t\t\t\t\t\t\"value\": 7.86092,\n\t\t\t\t\t\t\"curve\": [ 1.3024504, 7.77285, 1.3126389, 2.7002945 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": 2.7002945 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair1\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -6.81118,\n\t\t\t\t\t\t\"curve\": [ 6.198327E-4, -3.118248, 0.074073784, 14.658455 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 14.658455,\n\t\t\t\t\t\t\"curve\": [ 0.18796311, 14.802738, 0.29325193, 9.556835 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": 5.994396,\n\t\t\t\t\t\t\"curve\": [ 0.38137624, 1.7243423, 0.55, -11.111454 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"value\": -11.111454,\n\t\t\t\t\t\t\"curve\": [ 0.8333334, -11.111454, 0.9333334, 22.544525 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1,\n\t\t\t\t\t\t\"value\": 22.544525,\n\t\t\t\t\t\t\"curve\": [ 1.1583334, 22.544525, 1.2750001, -6.81118 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": -6.81118 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 4.5222607,\n\t\t\t\t\t\t\"curve\": [ 0.013146169, 2.3257527, 0.09166667, -9.747728 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": -9.747728,\n\t\t\t\t\t\t\"curve\": [ 0.17500001, -9.747728, 0.29143158, -1.2561474 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": 0.96451324,\n\t\t\t\t\t\t\"curve\": [ 0.35854283, 2.3005335, 0.542815, 4.246187 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": 4.6770706,\n\t\t\t\t\t\t\"curve\": [ 0.6832888, 5.304644, 0.77092403, 5.917166 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8333334,\n\t\t\t\t\t\t\"value\": 6.4794865,\n\t\t\t\t\t\t\"curve\": [ 0.8711326, 6.820064, 1.0833334, 11.365641 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1666667,\n\t\t\t\t\t\t\"value\": 11.365641,\n\t\t\t\t\t\t\"curve\": [ 1.2083334, 11.365641, 1.3166223, 6.1808486 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": 4.5222607 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0, 0, 0.08217778, -2.2354736, 0, 0, 0.08217778, -0.41656494 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"x\": -2.9806519,\n\t\t\t\t\t\t\"y\": -0.5554199,\n\t\t\t\t\t\t\"curve\": [ 0.23188148, -2.2354736, 0.2977778, 0, 0.23188148, -0.41656494, 0.2977778, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.33333334, \"curve\": \"stepped\" },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"curve\": [ 0.8891258, 0, 0.91152465, 0.26154166, 0.8891258, 0, 0.91245764, 0.057972897 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.9333334,\n\t\t\t\t\t\t\"x\": 0.6826019,\n\t\t\t\t\t\t\"y\": 0.22993222,\n\t\t\t\t\t\t\"curve\": [ 1.0156971, 2.223103, 1.0952615, 5.9000072, 1.0227648, 0.966601, 1.0952615, 1.9873968 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1666667,\n\t\t\t\t\t\t\"x\": 6.465088,\n\t\t\t\t\t\t\"y\": 2.1777496,\n\t\t\t\t\t\t\"curve\": [ 1.2295661, 5.746765, 1.2859417, 0, 1.2295661, 1.9357758, 1.2859417, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 4.5222607,\n\t\t\t\t\t\t\"curve\": [ 0.025000002, 4.5222607, 0.075, -6.1718483 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"value\": -6.1718483,\n\t\t\t\t\t\t\"curve\": [ 0.17500001, -6.1718483, 0.3806811, -0.70884365 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"value\": -0.2465725,\n\t\t\t\t\t\t\"curve\": [ 0.44656867, 0.86774325, 0.77500004, 4.844738 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.90000004,\n\t\t\t\t\t\t\"value\": 4.844738,\n\t\t\t\t\t\t\"curve\": [ 1.0083334, 4.844738, 1.225, 4.5222607 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334, \"value\": 4.5222607 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"head-control\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.13816747, -2.4003143, 0.22711481, -10.436157, 0.12329164, 1.045341, 0.22711481, 2.7030587 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"x\": -10.436157,\n\t\t\t\t\t\t\"y\": 2.7030587,\n\t\t\t\t\t\t\"curve\": [ 0.4842861, -10.436157, 0.5852062, -5.6263027, 0.4842861, 2.7030587, 0.6293006, -23.622154 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"x\": -2.2910004,\n\t\t\t\t\t\t\"y\": -26.605564,\n\t\t\t\t\t\t\"curve\": [ 0.8177673, -0.3898468, 0.96165824, 1.2078018, 0.8576197, -30.1698, 0.97151244, -28.751219 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1,\n\t\t\t\t\t\t\"x\": 1.2535706,\n\t\t\t\t\t\t\"y\": -28.74897,\n\t\t\t\t\t\t\"curve\": [ 1.1915685, 1.2838593, 1.2344676, 0.9751358, 1.2243191, -28.746796, 1.2349997, -2.1518683 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-shoulder\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.031111114, -2.2161503, 0.06512593, -3.731018, 0.020125192, -3.2484512, 0.06512593, -14.739197 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"x\": -3.731018,\n\t\t\t\t\t\t\"y\": -14.739197,\n\t\t\t\t\t\t\"curve\": [ 0.21592554, -3.731018, 0.38428962, -0.1688366, 0.21592554, -14.739197, 0.40233436, -12.508928 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"x\": 1.6337395,\n\t\t\t\t\t\t\"y\": -9.514136,\n\t\t\t\t\t\t\"curve\": [ 0.6319762, 3.6897128, 0.9348035, 7.411598, 0.5850231, -6.907011, 0.9085256, 10.862894 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1,\n\t\t\t\t\t\t\"x\": 7.445492,\n\t\t\t\t\t\t\"y\": 10.9888115,\n\t\t\t\t\t\t\"curve\": [ 1.1797043, 7.4618435, 1.2645, 2.857668, 1.1933261, 11.050184, 1.2938455, 3.3825455 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3333334 }\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t\"ik\": {\n\t\t\t\"front-foot-ik\": [\n\t\t\t\t{\n\t\t\t\t\t\"mix\": 0,\n\t\t\t\t\t\"curve\": [ 0.3, 0, 0.90000004, 1, 0.3, 0, 0.90000004, 0 ]\n\t\t\t\t},\n\t\t\t\t{ \"time\": 1.2 }\n\t\t\t],\n\t\t\t\"front-leg-ik\": [\n\t\t\t\t{\n\t\t\t\t\t\"mix\": 0,\n\t\t\t\t\t\"bendPositive\": false,\n\t\t\t\t\t\"curve\": [ 0.3, 0, 0.90000004, 1, 0.3, 0, 0.90000004, 0 ]\n\t\t\t\t},\n\t\t\t\t{ \"time\": 1.2, \"bendPositive\": false }\n\t\t\t],\n\t\t\t\"rear-foot-ik\": [\n\t\t\t\t{ \"mix\": 0 }\n\t\t\t],\n\t\t\t\"rear-leg-ik\": [\n\t\t\t\t{ \"mix\": 0, \"bendPositive\": false }\n\t\t\t]\n\t\t},\n\t\t\"events\": [\n\t\t\t{ \"time\": 1.2, \"name\": \"footstep\" }\n\t\t]\n\t},\n\t\"portal\": {\n\t\t\"slots\": {\n\t\t\t\"clipping\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"clipping\" }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-fist\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"front-fist-open\" }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"mouth\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"time\": 0.90000004, \"name\": \"mouth-grind\" },\n\t\t\t\t\t{ \"time\": 2.266667, \"name\": \"mouth-smile\" }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal-bg\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"portal-bg\" },\n\t\t\t\t\t{ \"time\": 3.0000002 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal-flare1\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"time\": 1.1, \"name\": \"portal-flare1\" },\n\t\t\t\t\t{ \"time\": 1.1333334, \"name\": \"portal-flare2\" },\n\t\t\t\t\t{ \"time\": 1.1666667, \"name\": \"portal-flare3\" },\n\t\t\t\t\t{ \"time\": 1.2, \"name\": \"portal-flare1\" },\n\t\t\t\t\t{ \"time\": 1.2333333, \"name\": \"portal-flare2\" },\n\t\t\t\t\t{ \"time\": 1.2666668, \"name\": \"portal-flare1\" },\n\t\t\t\t\t{ \"time\": 1.3333334 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal-flare2\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"time\": 1.1, \"name\": \"portal-flare2\" },\n\t\t\t\t\t{ \"time\": 1.1333334, \"name\": \"portal-flare3\" },\n\t\t\t\t\t{ \"time\": 1.1666667, \"name\": \"portal-flare1\" },\n\t\t\t\t\t{ \"time\": 1.2, \"name\": \"portal-flare2\" },\n\t\t\t\t\t{ \"time\": 1.2333333, \"name\": \"portal-flare3\" },\n\t\t\t\t\t{ \"time\": 1.2666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal-flare3\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"time\": 1.2, \"name\": \"portal-flare3\" },\n\t\t\t\t\t{ \"time\": 1.2333333, \"name\": \"portal-flare2\" },\n\t\t\t\t\t{ \"time\": 1.2666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal-flare4\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"time\": 1.2, \"name\": \"portal-flare2\" },\n\t\t\t\t\t{ \"time\": 1.2333333, \"name\": \"portal-flare1\" },\n\t\t\t\t\t{ \"time\": 1.2666668, \"name\": \"portal-flare2\" },\n\t\t\t\t\t{ \"time\": 1.3333334 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal-flare5\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"time\": 1.2333333, \"name\": \"portal-flare3\" },\n\t\t\t\t\t{ \"time\": 1.2666668, \"name\": \"portal-flare1\" },\n\t\t\t\t\t{ \"time\": 1.3333334 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal-flare6\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"time\": 1.2666668, \"name\": \"portal-flare3\" },\n\t\t\t\t\t{ \"time\": 1.3333334 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal-flare7\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"time\": 1.1333334, \"name\": \"portal-flare2\" },\n\t\t\t\t\t{ \"time\": 1.1666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal-flare8\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"time\": 1.2, \"name\": \"portal-flare3\" },\n\t\t\t\t\t{ \"time\": 1.2333333, \"name\": \"portal-flare2\" },\n\t\t\t\t\t{ \"time\": 1.2666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal-flare9\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"time\": 1.2, \"name\": \"portal-flare2\" },\n\t\t\t\t\t{ \"time\": 1.2333333, \"name\": \"portal-flare3\" },\n\t\t\t\t\t{ \"time\": 1.2666668, \"name\": \"portal-flare1\" },\n\t\t\t\t\t{ \"time\": 1.3000001 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal-flare10\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"time\": 1.2, \"name\": \"portal-flare2\" },\n\t\t\t\t\t{ \"time\": 1.2333333, \"name\": \"portal-flare1\" },\n\t\t\t\t\t{ \"time\": 1.2666668, \"name\": \"portal-flare3\" },\n\t\t\t\t\t{ \"time\": 1.3000001 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal-shade\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"portal-shade\" },\n\t\t\t\t\t{ \"time\": 3.0000002 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal-streaks1\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"portal-streaks1\" },\n\t\t\t\t\t{ \"time\": 3.0000002 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal-streaks2\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"portal-streaks2\" },\n\t\t\t\t\t{ \"time\": 3.0000002 }\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t\"bones\": {\n\t\t\t\"portal-root\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -458.35168,\n\t\t\t\t\t\t\"y\": 105.19089,\n\t\t\t\t\t\t\"curve\": [ 0.33277133, -458.2243, 0.6693856, -457.8555, 0.9339023, 105.19089, 0.67105263, 105.19087 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1,\n\t\t\t\t\t\t\"x\": -456.01794,\n\t\t\t\t\t\t\"y\": 105.19087,\n\t\t\t\t\t\t\"curve\": [ 1.3385459, -454.13632, 2.208128, -447.27612, 1.35, 105.19087, 2.0500002, 105.19089 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.4,\n\t\t\t\t\t\t\"x\": -439.11795,\n\t\t\t\t\t\t\"y\": 105.19089,\n\t\t\t\t\t\t\"curve\": [ 2.4629574, -436.4411, 2.5015616, -432.91815, 2.487001, 105.19089, 2.5115557, 105.09302 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.6000001,\n\t\t\t\t\t\t\"x\": -432.57776,\n\t\t\t\t\t\t\"y\": 105.09302,\n\t\t\t\t\t\t\"curve\": [ 2.7837849, -431.9423, 2.9776654, -446.5953, 2.771785, 105.09302, 2.9331987, 105.19088 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.0333335, \"x\": -457.41928, \"y\": 105.19087 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 0.002608195,\n\t\t\t\t\t\t\"y\": 0.005767732,\n\t\t\t\t\t\t\"curve\": [ 0.32905644, 0.043797784, 0.34697965, 0.11661968, 0.32905644, 0.09685391, 0.36970246, 0.2491073 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"x\": 0.17486975,\n\t\t\t\t\t\t\"y\": 0.38670492,\n\t\t\t\t\t\t\"curve\": [ 0.629778, 0.61939394, 0.6625376, 0.7229963, 0.60947454, 1.338042, 0.6452585, 1.5238047 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"x\": 0.7239759,\n\t\t\t\t\t\t\"y\": 1.519575,\n\t\t\t\t\t\t\"curve\": [ 0.7982509, 0.72485626, 0.906569, 0.64671034, 0.796669, 1.5166358, 0.8947419, 1.4241506 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1,\n\t\t\t\t\t\t\"x\": 0.6450333,\n\t\t\t\t\t\t\"y\": 1.4264193,\n\t\t\t\t\t\t\"curve\": [ 1.0945557, 0.6433359, 1.1387776, 0.68822306, 1.0885832, 1.4283284, 1.1145829, 1.5133462 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2333333,\n\t\t\t\t\t\t\"x\": 0.685406,\n\t\t\t\t\t\t\"y\": 1.515699,\n\t\t\t\t\t\t\"curve\": [ 1.3248138, 0.68268067, 1.5084356, 0.6355962, 1.342993, 1.5178717, 1.4665207, 1.3996894 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.6000001,\n\t\t\t\t\t\t\"x\": 0.6335794,\n\t\t\t\t\t\t\"y\": 1.4010901,\n\t\t\t\t\t\t\"curve\": [ 1.7278892, 0.63076246, 1.9456958, 0.68683463, 1.7224178, 1.4023747, 1.9238145, 1.5224925 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.0666668,\n\t\t\t\t\t\t\"x\": 0.68847805,\n\t\t\t\t\t\t\"y\": 1.5224925,\n\t\t\t\t\t\t\"curve\": [ 2.1893363, 0.6901446, 2.2887776, 0.6487232, 2.1416662, 1.5224925, 2.2650337, 1.4168181 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.4,\n\t\t\t\t\t\t\"x\": 0.65015763,\n\t\t\t\t\t\t\"y\": 1.4264193,\n\t\t\t\t\t\t\"curve\": [ 2.4938877, 0.6513685, 2.5038357, 0.76620674, 2.5077126, 1.4340816, 2.5430567, 1.5664625 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.6000001,\n\t\t\t\t\t\t\"x\": 0.76588356,\n\t\t\t\t\t\t\"y\": 1.5681682,\n\t\t\t\t\t\t\"curve\": [ 2.7304673, 0.7649903, 3.005668, 0.09835615, 2.7668705, 1.5643423, 2.9972615, 0.09953152 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.0333335, \"x\": 0.006712961, \"y\": 0.014844964 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal-streaks1\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{},\n\t\t\t\t\t{ \"time\": 3.1666667, \"value\": 1200 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 15.154297,\n\t\t\t\t\t\t\"curve\": [ 0.16216217, 15.154297, 0.43243247, 12.6003275, 0.16216217, -3.0517578E-5, 0.43243247, -3.8612854 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"x\": 10.897681,\n\t\t\t\t\t\t\"y\": -6.4354553,\n\t\t\t\t\t\t\"curve\": [ 0.7941177, 9.93388, 0.91176474, 9.211029, 0.7941177, -7.7070704, 0.91176474, -8.660782 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1,\n\t\t\t\t\t\t\"x\": 9.211029,\n\t\t\t\t\t\t\"y\": -8.660782,\n\t\t\t\t\t\t\"curve\": [ 1.0833334, 9.211029, 1.2500001, 21.529388, 1.0833334, -8.660782, 1.2650981, -4.903187 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.3333334,\n\t\t\t\t\t\t\"x\": 21.529388,\n\t\t\t\t\t\t\"y\": -3.1892934,\n\t\t\t\t\t\t\"curve\": [ 1.5000001, 21.529388, 1.9392341, 12.301189, 1.4457349, -0.36605763, 1.9000001, 6.2614594 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.0666668,\n\t\t\t\t\t\t\"x\": 11.259531,\n\t\t\t\t\t\t\"y\": 6.2614594,\n\t\t\t\t\t\t\"curve\": [ 2.2385116, 9.854843, 2.3887858, 9.679258, 2.2083335, 6.2614594, 2.5232892, 0.5087212 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.5666668,\n\t\t\t\t\t\t\"x\": 9.386615,\n\t\t\t\t\t\t\"y\": -0.799057,\n\t\t\t\t\t\t\"curve\": [ 2.6570418, 9.23793, 2.8416667, 9.211029, 2.646262, -3.1987312, 2.8416667, -8.905502 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 2.9333334, \"x\": 9.211029, \"y\": -8.905502 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.16666667, 1, 0.5, 1.0526782, 0.16666667, 1, 0.5, 1.0526782 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6666667,\n\t\t\t\t\t\t\"x\": 1.0526782,\n\t\t\t\t\t\t\"y\": 1.0526782,\n\t\t\t\t\t\t\"curve\": [ 0.8333334, 1.0526782, 1.1666667, 0.98572004, 0.8333334, 1.0526782, 1.1666667, 0.98572004 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.3333334,\n\t\t\t\t\t\t\"x\": 0.98572004,\n\t\t\t\t\t\t\"y\": 0.98572004,\n\t\t\t\t\t\t\"curve\": [ 1.5000001, 0.98572004, 1.8333334, 1.0526782, 1.5000001, 0.98572004, 1.8333334, 1.0526782 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 2, \"x\": 1.0526782, \"y\": 1.0526782 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal-streaks2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{},\n\t\t\t\t\t{ \"time\": 3.1666667, \"value\": 600 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": -2.111206 },\n\t\t\t\t\t{ \"time\": 1, \"x\": -2.111206, \"y\": 6.6306915 },\n\t\t\t\t\t{ \"time\": 1.9333334, \"x\": -2.111206 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 1.0138458,\n\t\t\t\t\t\t\"y\": 1.0138458,\n\t\t\t\t\t\t\"curve\": [ 0.2286043, 0.908657, 0.5014534, 0.75500345, 0.24156241, 0.8915996, 0.5023928, 0.76759005 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"x\": 0.74474704,\n\t\t\t\t\t\t\"y\": 0.74474704,\n\t\t\t\t\t\t\"curve\": [ 1.2819107, 0.73308533, 2.020576, 0.6993992, 1.2702396, 0.71943945, 2.0707247, 0.7086046 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.2,\n\t\t\t\t\t\t\"x\": 0.69985896,\n\t\t\t\t\t\t\"y\": 0.70440763,\n\t\t\t\t\t\t\"curve\": [ 2.3152735, 0.7001541, 2.420857, 0.7939516, 2.3108184, 0.70080996, 2.484517, 0.79691565 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.5666668,\n\t\t\t\t\t\t\"x\": 0.7939414,\n\t\t\t\t\t\t\"y\": 0.7939414,\n\t\t\t\t\t\t\"curve\": [ 2.7342625, 0.7939298, 2.9900746, 0.32295766, 2.7137673, 0.78861564, 3.019252, 0.34115243 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667, \"x\": 0, \"y\": 0 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal-shade\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": -29.679688 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{ \"x\": 0.7142011, \"y\": 0.7142011 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"portal\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{},\n\t\t\t\t\t{ \"time\": 3.1666667, \"value\": 600 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"clipping\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": -476.5466, \"y\": 2.27098 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{ \"x\": 0.9833469, \"y\": 1.1970232 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hip\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"value\": 22.74372,\n\t\t\t\t\t\t\"curve\": [ 1.1629492, 18.836576, 1.7701715, 8.767227 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.9000001,\n\t\t\t\t\t\t\"value\": 7.8165436,\n\t\t\t\t\t\t\"curve\": [ 2.2710757, 5.099299, 2.890143, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": -899.4144, \"y\": 4.4653482, \"curve\": \"stepped\" },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"x\": -694.1554,\n\t\t\t\t\t\t\"y\": 183.27863,\n\t\t\t\t\t\t\"curve\": [ 1.0907409, -602.08105, 1.1377125, -427.59048, 1.11462, 185.6005, 1.1708443, 133.18236 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2333333,\n\t\t\t\t\t\t\"x\": -316.97266,\n\t\t\t\t\t\t\"y\": 55.293976,\n\t\t\t\t\t\t\"curve\": [ 1.3169248, -220.27092, 1.5121045, -123.21176, 1.2707293, 8.68248, 1.461251, -83.18494 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.6000001,\n\t\t\t\t\t\t\"x\": -95.52516,\n\t\t\t\t\t\t\"y\": -112.22975,\n\t\t\t\t\t\t\"curve\": [ 1.7183462, -58.24684, 2.0366814, -22.542114, 1.8576819, -166.17126, 2.1094964, -31.40001 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.1666667,\n\t\t\t\t\t\t\"x\": -14.823854,\n\t\t\t\t\t\t\"y\": -31.12268,\n\t\t\t\t\t\t\"curve\": [ 2.293638, -7.2845535, 2.4421756, -7.199183, 2.27447, -30.599716, 2.3926742, -36.757523 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.6000001,\n\t\t\t\t\t\t\"x\": -7.2000246,\n\t\t\t\t\t\t\"y\": -36.961395,\n\t\t\t\t\t\t\"curve\": [ 2.8543973, -7.2013793, 3.0706227, -11.871496, 2.7856536, -36.266846, 3.0823655, -22.983032 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667, \"x\": -11.972925, \"y\": -23.154968 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-foot-target\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 1.0666667, \"value\": 41.598747, \"curve\": \"stepped\" },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2333333,\n\t\t\t\t\t\t\"value\": 41.598747,\n\t\t\t\t\t\t\"curve\": [ 1.2583334, 41.598747, 1.3790429, 35.45793 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.4000001,\n\t\t\t\t\t\t\"value\": 30.091051,\n\t\t\t\t\t\t\"curve\": [ 1.4119208, 27.03835, 1.4333334, 10.648298 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.4333334, \"value\": -0.28445435 },\n\t\t\t\t\t{ \"time\": 1.6000001, \"value\": 2.4385347 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": -899.4143, \"y\": 4.465324, \"curve\": \"stepped\" },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"x\": -591.1311,\n\t\t\t\t\t\t\"y\": 438.46228,\n\t\t\t\t\t\t\"curve\": [ 1.0760857, -539.7665, 1.2062327, -268.10406, 1.1173495, 418.44333, 1.2102692, 333.1787 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2333333,\n\t\t\t\t\t\t\"x\": -225.28467,\n\t\t\t\t\t\t\"y\": 304.5344,\n\t\t\t\t\t\t\"curve\": [ 1.2650213, -175.21776, 1.3934208, -74.21271, 1.2961504, 226.51976, 1.4014887, 49.61011 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.4333334,\n\t\t\t\t\t\t\"x\": -52.318787,\n\t\t\t\t\t\t\"y\": 0.20110783,\n\t\t\t\t\t\t\"curve\": [ 1.454237, -40.852283, 1.6164666, 40.871223, 1.4661088, 0.16827218, 1.6135046, 0.04275261 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.6666667, \"x\": 45.86647, \"y\": 0.014911354 },\n\t\t\t\t\t{ \"time\": 1.9333334, \"x\": 48.869934 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-foot-target\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"value\": 32.081806,\n\t\t\t\t\t\t\"curve\": [ 1.1083333, 32.081806, 1.1916667, 35.16107 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2333333,\n\t\t\t\t\t\t\"value\": 35.16107,\n\t\t\t\t\t\t\"curve\": [ 1.2583334, 35.16107, 1.3171477, 2.227778 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.3333334,\n\t\t\t\t\t\t\"value\": -4.73641,\n\t\t\t\t\t\t\"curve\": [ 1.3505374, -12.13869, 1.4289122, -34.96195 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.6000001,\n\t\t\t\t\t\t\"value\": -34.76919,\n\t\t\t\t\t\t\"curve\": [ 1.7646466, -34.583694, 1.8974328, -17.251328 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.9333334 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": -899.4143, \"y\": 4.465324, \"curve\": \"stepped\" },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"x\": -533.93445,\n\t\t\t\t\t\t\"y\": 363.75092,\n\t\t\t\t\t\t\"curve\": [ 1.0741596, -480.8532, 1.1800851, -261.30576, 1.0939052, 362.3, 1.195378, 267.76862 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2333333,\n\t\t\t\t\t\t\"x\": -201.23172,\n\t\t\t\t\t\t\"y\": 199.92732,\n\t\t\t\t\t\t\"curve\": [ 1.2686588, -161.37817, 1.2941304, -140.3243, 1.2743199, 126.66837, 1.3083334, 77.123116 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.3333334,\n\t\t\t\t\t\t\"x\": -124.07851,\n\t\t\t\t\t\t\"y\": 0.20110783,\n\t\t\t\t\t\t\"curve\": [ 1.4261837, -85.60126, 1.6333334, -69.06195, 1.4497523, 0.47764254, 1.6333334, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.7333335, \"x\": -69.06195 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"value\": 27.015419,\n\t\t\t\t\t\t\"curve\": [ 1.1870607, 26.857056, 1.2912499, 7.810417 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.3333334,\n\t\t\t\t\t\t\"value\": -2.616394,\n\t\t\t\t\t\t\"curve\": [ 1.4023848, -19.724922, 1.4290673, -48.643036 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.4666667,\n\t\t\t\t\t\t\"value\": -56.31092,\n\t\t\t\t\t\t\"curve\": [ 1.5086124, -64.86512, 1.6203594, -77.14185 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.7333335,\n\t\t\t\t\t\t\"value\": -77.33606,\n\t\t\t\t\t\t\"curve\": [ 1.8370243, -76.88617, 1.8949385, -71.322426 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2,\n\t\t\t\t\t\t\"value\": -57.519188,\n\t\t\t\t\t\t\"curve\": [ 2.104223, -43.826145, 2.1889234, -28.588673 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.3000002,\n\t\t\t\t\t\t\"value\": -29.027878,\n\t\t\t\t\t\t\"curve\": [ 2.4133842, -29.476212, 2.513067, -36.785248 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.6666667,\n\t\t\t\t\t\t\"value\": -36.785248,\n\t\t\t\t\t\t\"curve\": [ 2.8143926, -36.947227, 2.9471097, -22.875427 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667, \"value\": -22.875427 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"neck\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"value\": -3.5730362,\n\t\t\t\t\t\t\"curve\": [ 1.1457309, -3.6556473, 1.1500219, -13.5003395 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2333333,\n\t\t\t\t\t\t\"value\": -13.500336,\n\t\t\t\t\t\t\"curve\": [ 1.4281883, -13.500328, 1.4427328, 11.583879 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.5666667,\n\t\t\t\t\t\t\"value\": 11.418663,\n\t\t\t\t\t\t\"curve\": [ 1.6581892, 11.296658, 1.7750001, 3.7834435 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.8666668,\n\t\t\t\t\t\t\"value\": 3.7834435,\n\t\t\t\t\t\t\"curve\": [ 1.9204302, 3.7834435, 2.036419, 8.012905 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.1000001,\n\t\t\t\t\t\t\"value\": 7.932539,\n\t\t\t\t\t\t\"curve\": [ 2.2657762, 7.7230053, 2.420065, 3.864254 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.5333335,\n\t\t\t\t\t\t\"value\": 3.864254,\n\t\t\t\t\t\t\"curve\": [ 2.7833335, 3.864254, 3.0040057, 3.7834435 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667, \"value\": 3.7834435 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"head\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"value\": 16.39647,\n\t\t\t\t\t\t\"curve\": [ 1.1327258, 9.897343, 1.206983, 1.8651562 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.3333334,\n\t\t\t\t\t\t\"value\": 1.6744652,\n\t\t\t\t\t\t\"curve\": [ 1.4595901, 1.5641422, 1.5468355, 47.54059 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.7333335,\n\t\t\t\t\t\t\"value\": 47.552315,\n\t\t\t\t\t\t\"curve\": [ 1.8973298, 47.562622, 2.0415378, 5.6807213 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.0666668,\n\t\t\t\t\t\t\"value\": 0.85961914,\n\t\t\t\t\t\t\"curve\": [ 2.0743356, -0.61164796, 2.086115, -2.8090944 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.1000001,\n\t\t\t\t\t\t\"value\": -5.3051586,\n\t\t\t\t\t\t\"curve\": [ 2.145329, -13.066894, 2.215558, -23.648317 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.266667,\n\t\t\t\t\t\t\"value\": -23.707361,\n\t\t\t\t\t\t\"curve\": [ 2.3343797, -23.785587, 2.426235, -13.429327 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.4666667,\n\t\t\t\t\t\t\"value\": -9.181419,\n\t\t\t\t\t\t\"curve\": [ 2.4978452, -5.9057274, 2.6044137, 2.527153 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.6666667,\n\t\t\t\t\t\t\"value\": 2.524849,\n\t\t\t\t\t\t\"curve\": [ 2.737512, 2.239935, 2.8498168, -8.761124 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.9333334,\n\t\t\t\t\t\t\"value\": -8.665838,\n\t\t\t\t\t\t\"curve\": [ 3.0360131, -8.548687, 3.089751, -7.0947676 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667, \"value\": -6.7476807 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.3333334,\n\t\t\t\t\t\t\"curve\": [ 1.3916668, 1, 1.5261834, 1.0002627, 1.3916668, 1, 1.5083334, 1.0427603 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.5666667,\n\t\t\t\t\t\t\"x\": 0.9917999,\n\t\t\t\t\t\t\"y\": 1.0427603,\n\t\t\t\t\t\t\"curve\": [ 1.5976762, 0.9853176, 1.6760828, 0.9547964, 1.583889, 1.0427603, 1.6718277, 1.0400101 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.7333335,\n\t\t\t\t\t\t\"x\": 0.95449597,\n\t\t\t\t\t\t\"y\": 1.0289872,\n\t\t\t\t\t\t\"curve\": [ 1.8428999, 0.9539212, 1.9333334, 1, 1.8252529, 1.0125135, 1.9333334, 1 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 2 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.90000004,\n\t\t\t\t\t\t\"value\": 39.23973,\n\t\t\t\t\t\t\"curve\": [ 0.96846473, 39.933254, 1.2674665, 85.31066 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.4666667,\n\t\t\t\t\t\t\"value\": 112.26816,\n\t\t\t\t\t\t\"curve\": [ 1.5551683, 124.24491, 1.5761636, 126.443794 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.6333334,\n\t\t\t\t\t\t\"value\": 126.443726,\n\t\t\t\t\t\t\"curve\": [ 1.7818087, 126.44356, 1.9923621, 94.551315 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.1000001,\n\t\t\t\t\t\t\"value\": 79.962074,\n\t\t\t\t\t\t\"curve\": [ 2.2158399, 64.261215, 2.407094, 34.36 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.5666668,\n\t\t\t\t\t\t\"value\": 33.384064,\n\t\t\t\t\t\t\"curve\": [ 2.8148017, 31.866539, 3.0998373, 39.198883 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667, \"value\": 39.198883 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"back-foot-tip\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"value\": 56.072643,\n\t\t\t\t\t\t\"curve\": [ 1.1376423, 59.208103, 1.1920347, 59.649666 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2333333,\n\t\t\t\t\t\t\"value\": 59.455666,\n\t\t\t\t\t\t\"curve\": [ 1.29476, 59.16713, 1.45, 22.53593 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.4666667, \"value\": -0.8375263 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"value\": 118.03253,\n\t\t\t\t\t\t\"curve\": [ 1.0746608, 93.639275, 1.3576761, -34.03235 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.6666667,\n\t\t\t\t\t\t\"value\": -33.936485,\n\t\t\t\t\t\t\"curve\": [ 1.8077261, -33.89273, 1.8794831, -25.00093 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.9666668,\n\t\t\t\t\t\t\"value\": -25.190529,\n\t\t\t\t\t\t\"curve\": [ 2.0903125, -25.459396, 2.3122675, -34.57672 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.3666668,\n\t\t\t\t\t\t\"value\": -38.36267,\n\t\t\t\t\t\t\"curve\": [ 2.4646735, -45.18345, 2.557451, -60.097862 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.8333335,\n\t\t\t\t\t\t\"value\": -61.09861,\n\t\t\t\t\t\t\"curve\": [ 2.8434134, -61.060944, 3.160012, -60.869286 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667, \"value\": -60.869286 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"value\": 0.6630783,\n\t\t\t\t\t\t\"curve\": [ 1.1083333, 0.6630783, 1.2209586, 44.94986 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2333333,\n\t\t\t\t\t\t\"value\": 49.25002,\n\t\t\t\t\t\t\"curve\": [ 1.2626123, 59.424114, 1.3417951, 68.06456 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.3666668,\n\t\t\t\t\t\t\"value\": 68.33766,\n\t\t\t\t\t\t\"curve\": [ 1.4087822, 68.800125, 1.4755197, 4.9001865 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.5000001,\n\t\t\t\t\t\t\"value\": -2.0490541,\n\t\t\t\t\t\t\"curve\": [ 1.5290676, -10.300376, 1.6946661, -15.950796 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.7333335,\n\t\t\t\t\t\t\"value\": -17.379787,\n\t\t\t\t\t\t\"curve\": [ 1.8070651, -20.104628, 1.8777473, -21.185541 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.9333334,\n\t\t\t\t\t\t\"value\": -21.075684,\n\t\t\t\t\t\t\"curve\": [ 2.0732963, -20.799067, 2.1463492, -7.6346474 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.1666667,\n\t\t\t\t\t\t\"value\": -3.6379185,\n\t\t\t\t\t\t\"curve\": [ 2.1857605, 0.1180954, 2.274763, 15.282471 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.3333335,\n\t\t\t\t\t\t\"value\": 21.777283,\n\t\t\t\t\t\t\"curve\": [ 2.392207, 28.30568, 2.574896, 37.655697 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.7,\n\t\t\t\t\t\t\"value\": 39.43075,\n\t\t\t\t\t\t\"curve\": [ 2.946578, 42.929333, 3.020272, 42.46212 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667, \"value\": 42.46212 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-thigh\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"time\": 1.1, \"x\": -6.409607, \"y\": 18.23201, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 1.1333334, \"x\": -6.409607, \"y\": 18.23201 },\n\t\t\t\t\t{ \"time\": 1.2, \"x\": 1.6055756, \"y\": 3.6637325 },\n\t\t\t\t\t{ \"time\": 1.2333333, \"x\": 4.500519, \"y\": -3.1474762 },\n\t\t\t\t\t{ \"time\": 1.3666668, \"x\": -3.7928925, \"y\": 2.9377594 },\n\t\t\t\t\t{ \"time\": 1.4000001, \"x\": -8.373123, \"y\": 8.724655 },\n\t\t\t\t\t{ \"time\": 1.4333334, \"x\": -11.259293, \"y\": 16.99144 },\n\t\t\t\t\t{ \"time\": 1.4666667, \"x\": -9.890228, \"y\": 24.729523, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 1.8666668, \"x\": -9.890228, \"y\": 24.729523 },\n\t\t\t\t\t{ \"time\": 2.1000001 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-foot-tip\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 1.0666667, \"value\": 42.55, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 1.1333334, \"value\": 42.55 },\n\t\t\t\t\t{ \"time\": 1.2333333, \"value\": 17.713627 },\n\t\t\t\t\t{ \"time\": 1.3666668, \"value\": 3.6307945 },\n\t\t\t\t\t{ \"time\": 1.4333334 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"value\": 108.71448,\n\t\t\t\t\t\t\"curve\": [ 1.0823311, 108.29144, 1.4370247, 50.729336 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.5666667,\n\t\t\t\t\t\t\"value\": 24.8727,\n\t\t\t\t\t\t\"curve\": [ 1.620189, 14.197891, 1.6595292, -11.7419815 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.7333335,\n\t\t\t\t\t\t\"value\": -11.738415,\n\t\t\t\t\t\t\"curve\": [ 1.9605455, -11.727437, 2.1720436, 1.6552868 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.266667,\n\t\t\t\t\t\t\"value\": 7.8765717,\n\t\t\t\t\t\t\"curve\": [ 2.3313801, 12.131332, 2.4386861, 18.653133 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.5333335,\n\t\t\t\t\t\t\"value\": 18.722519,\n\t\t\t\t\t\t\"curve\": [ 2.7882903, 18.909428, 3.14492, -0.29708695 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-fist\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1,\n\t\t\t\t\t\t\"value\": 6.320977,\n\t\t\t\t\t\t\"curve\": [ 1.1101446, 3.3114443, 1.1529944, -5.0706334 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2333333,\n\t\t\t\t\t\t\"value\": -5.1294703,\n\t\t\t\t\t\t\"curve\": [ 1.3112985, -5.1865697, 1.3644075, 34.64959 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.4666667,\n\t\t\t\t\t\t\"value\": 34.53249,\n\t\t\t\t\t\t\"curve\": [ 1.5743638, 34.40917, 1.5473909, -55.780422 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.8666668,\n\t\t\t\t\t\t\"value\": -54.70204,\n\t\t\t\t\t\t\"curve\": [ 1.9466666, -54.70204, 2.029893, -53.937347 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.1333334,\n\t\t\t\t\t\t\"value\": -42.44453,\n\t\t\t\t\t\t\"curve\": [ 2.2145448, -33.42147, 2.3576686, -4.4270964 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.4,\n\t\t\t\t\t\t\"value\": 0.032372475,\n\t\t\t\t\t\t\"curve\": [ 2.443959, 4.6632824, 2.53568, 8.200353 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.6333334,\n\t\t\t\t\t\t\"value\": 8.197518,\n\t\t\t\t\t\t\"curve\": [ 2.7334914, 8.194706, 2.804373, -0.67118263 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.9,\n\t\t\t\t\t\t\"value\": -0.81711864,\n\t\t\t\t\t\t\"curve\": [ 3.127076, -1.1636529, 3.0930977, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"gun\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2666668,\n\t\t\t\t\t\t\"curve\": [ 1.35, 0, 1.5493119, 7.4916267 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.6000001,\n\t\t\t\t\t\t\"value\": 9.502806,\n\t\t\t\t\t\t\"curve\": [ 1.6634296, 12.019538, 1.8455458, 19.577408 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.9333334,\n\t\t\t\t\t\t\"value\": 19.430107,\n\t\t\t\t\t\t\"curve\": [ 1.9850719, 19.403255, 2.057294, 2.9750502 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.2,\n\t\t\t\t\t\t\"value\": 2.9547074,\n\t\t\t\t\t\t\"curve\": [ 2.3040185, 3.5522952, 2.4583335, 10.795151 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.5000002,\n\t\t\t\t\t\t\"value\": 10.795151,\n\t\t\t\t\t\t\"curve\": [ 2.6420834, 10.795151, 2.8727689, -2.5369945 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.9333334,\n\t\t\t\t\t\t\"value\": -2.5451202,\n\t\t\t\t\t\t\"curve\": [ 3.0896354, -2.5660934, 3.0801854, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"value\": 26.193165,\n\t\t\t\t\t\t\"curve\": [ 1.1583334, 26.193165, 1.367548, 25.99863 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.4333334,\n\t\t\t\t\t\t\"value\": 24.43079,\n\t\t\t\t\t\t\"curve\": [ 1.5339298, 22.033314, 1.9999086, -29.144737 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.2,\n\t\t\t\t\t\t\"value\": -29.144737,\n\t\t\t\t\t\t\"curve\": [ 2.2916663, -29.144737, 2.4750001, 6.709568 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.5666668,\n\t\t\t\t\t\t\"value\": 6.709568,\n\t\t\t\t\t\t\"curve\": [ 2.6750002, 6.709568, 2.8138504, -5.0595016 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.9,\n\t\t\t\t\t\t\"value\": -5.0595016,\n\t\t\t\t\t\t\"curve\": [ 2.9728963, -5.0595016, 3.12329, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair4\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.0666667,\n\t\t\t\t\t\t\"value\": 5.2074738,\n\t\t\t\t\t\t\"curve\": [ 1.1083333, 5.2074738, 1.1916667, 26.193161 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2333333,\n\t\t\t\t\t\t\"value\": 26.193161,\n\t\t\t\t\t\t\"curve\": [ 1.3166667, 26.193161, 1.4833335, 10.625244 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.5666667,\n\t\t\t\t\t\t\"value\": 10.625244,\n\t\t\t\t\t\t\"curve\": [ 1.626899, 10.625244, 1.6422284, 17.906029 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.7,\n\t\t\t\t\t\t\"value\": 17.936363,\n\t\t\t\t\t\t\"curve\": [ 1.7612771, 17.968536, 1.7744892, 8.2237015 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.8000001,\n\t\t\t\t\t\t\"value\": 3.3265457,\n\t\t\t\t\t\t\"curve\": [ 1.8392457, -4.207123, 1.95, -22.67175 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2,\n\t\t\t\t\t\t\"value\": -22.67175,\n\t\t\t\t\t\t\"curve\": [ 2.025, -22.67175, 2.1227455, -21.85688 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.1666667,\n\t\t\t\t\t\t\"value\": -18.710205,\n\t\t\t\t\t\t\"curve\": [ 2.228136, -14.306343, 2.293769, -0.2952652 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.3666668,\n\t\t\t\t\t\t\"value\": 6.364601,\n\t\t\t\t\t\t\"curve\": [ 2.433311, 12.453148, 2.4938564, 19.214798 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.6000001,\n\t\t\t\t\t\t\"value\": 19.214798,\n\t\t\t\t\t\t\"curve\": [ 2.7287974, 19.214798, 2.853675, 6.745182 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.9333334,\n\t\t\t\t\t\t\"value\": 4.623581,\n\t\t\t\t\t\t\"curve\": [ 3.0899365, 0.45267487, 3.0616362, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.4333334,\n\t\t\t\t\t\t\"curve\": [ 1.45, 0, 1.4517295, 11.286789 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.5000001,\n\t\t\t\t\t\t\"value\": 11.212746,\n\t\t\t\t\t\t\"curve\": [ 1.5963843, 11.064898, 1.5729342, -14.166687 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.7333335,\n\t\t\t\t\t\t\"value\": -20.39627,\n\t\t\t\t\t\t\"curve\": [ 1.8514245, -24.982697, 1.9430796, -28.45235 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.2,\n\t\t\t\t\t\t\"value\": -28.748043,\n\t\t\t\t\t\t\"curve\": [ 2.3166668, -28.748043, 2.5500002, 7.0427513 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.6666667,\n\t\t\t\t\t\t\"value\": 7.0427513,\n\t\t\t\t\t\t\"curve\": [ 2.7916667, 7.0427513, 2.8853362, -5.194847 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.966667,\n\t\t\t\t\t\t\"value\": -5.194847,\n\t\t\t\t\t\t\"curve\": [ 3.037153, -5.194847, 3.0961804, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair1\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2333333,\n\t\t\t\t\t\t\"curve\": [ 1.2833334, 0, 1.3487784, 3.9884987 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.4333334,\n\t\t\t\t\t\t\"value\": 6.581089,\n\t\t\t\t\t\t\"curve\": [ 1.4972903, 8.542103, 1.6833334, 9.353794 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.7666668,\n\t\t\t\t\t\t\"value\": 9.353794,\n\t\t\t\t\t\t\"curve\": [ 1.825, 9.353794, 1.9451996, -8.706432 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2,\n\t\t\t\t\t\t\"value\": -11.145393,\n\t\t\t\t\t\t\"curve\": [ 2.0576642, -13.711807, 2.2, -14.967854 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.266667,\n\t\t\t\t\t\t\"value\": -14.967854,\n\t\t\t\t\t\t\"curve\": [ 2.3666668, -14.967854, 2.5666668, 18.772907 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.6666667,\n\t\t\t\t\t\t\"value\": 18.772907,\n\t\t\t\t\t\t\"curve\": [ 2.7333336, 18.772907, 2.8169556, 8.285915 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.8666668,\n\t\t\t\t\t\t\"value\": 6.5089645,\n\t\t\t\t\t\t\"curve\": [ 2.9879677, 2.1730347, 3.058226, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"flare1\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 1.1, \"value\": 8.196709 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"time\": 1.1, \"x\": -19.965195, \"y\": 149.67542 },\n\t\t\t\t\t{ \"time\": 1.2, \"x\": 3.8483872, \"y\": 152.43144 },\n\t\t\t\t\t{ \"time\": 1.2333333, \"x\": -15.415985, \"y\": 152.2934 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1,\n\t\t\t\t\t\t\"x\": 0.80524826,\n\t\t\t\t\t\t\"y\": 0.80524826,\n\t\t\t\t\t\t\"curve\": [ 1.1185277, 0.7628392, 1.1604003, 1.1618241, 1.1166667, 0.80524826, 1.1500001, 0.6051311 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.1666667,\n\t\t\t\t\t\t\"x\": 1.2791321,\n\t\t\t\t\t\t\"y\": 0.6051311,\n\t\t\t\t\t\t\"curve\": [ 1.1768612, 1.4699727, 1.1916667, 2.1505902, 1.1750001, 0.6051311, 1.1916667, 0.9112708 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2,\n\t\t\t\t\t\t\"x\": 2.1505902,\n\t\t\t\t\t\t\"y\": 0.9112708,\n\t\t\t\t\t\t\"curve\": [ 1.2083334, 2.1505902, 1.2309632, 1.6683322, 1.2083334, 0.9112708, 1.2267488, 0.84405416 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2333333,\n\t\t\t\t\t\t\"x\": 1.6076452,\n\t\t\t\t\t\t\"y\": 0.80524826,\n\t\t\t\t\t\t\"curve\": [ 1.2490697, 1.2047595, 1.2833334, 0.5473236, 1.2537222, 0.6850893, 1.2833334, 0.4158778 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1.3000001, \"x\": 0.5473236, \"y\": 0.4158778 }\n\t\t\t\t],\n\t\t\t\t\"shear\": [\n\t\t\t\t\t{ \"time\": 1.1, \"y\": 4.625496 },\n\t\t\t\t\t{ \"time\": 1.2333333, \"x\": -5.7423334, \"y\": 4.625496 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"flare2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 1.1, \"value\": 12.288801 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"time\": 1.1, \"x\": -8.631058, \"y\": 132.95828 },\n\t\t\t\t\t{ \"time\": 1.2, \"x\": 4.346024, \"y\": 132.92914 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{ \"time\": 1.1, \"x\": 0.86356777, \"y\": 0.86356777 },\n\t\t\t\t\t{ \"time\": 1.1666667, \"x\": 0.9445455, \"y\": 0.9445455 },\n\t\t\t\t\t{ \"time\": 1.2, \"x\": 1.5108734, \"y\": 1.0808847 }\n\t\t\t\t],\n\t\t\t\t\"shear\": [\n\t\t\t\t\t{ \"time\": 1.1, \"y\": 24.027992 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"flare3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"value\": 2.877605 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"x\": 3.244141, \"y\": 114.80896 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"x\": 0.6682259, \"y\": 0.6682259 }\n\t\t\t\t],\n\t\t\t\t\"shear\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"y\": 38.592064 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"flare4\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"value\": -8.636183 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"x\": -3.8203425, \"y\": 194.06001 },\n\t\t\t\t\t{ \"time\": 1.2666668, \"x\": -1.8212581, \"y\": 198.46616, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 1.3000001, \"x\": -1.9382625, \"y\": 187.80707 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"x\": 0.5447413, \"y\": 0.5447413 },\n\t\t\t\t\t{ \"time\": 1.2666668, \"x\": 0.75667316, \"y\": 0.75667316 }\n\t\t\t\t],\n\t\t\t\t\"shear\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"x\": 7.419382, \"y\": -22.03984 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"flare5\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"time\": 1.2, \"x\": -11.174988, \"y\": 176.42078 },\n\t\t\t\t\t{ \"time\": 1.2333333, \"x\": -8.563156, \"y\": 179.04309, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 1.3000001, \"x\": -14.569656, \"y\": 168.69334 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{ \"time\": 1.2333333, \"x\": 1.146029 },\n\t\t\t\t\t{ \"time\": 1.3000001, \"x\": 0.7027114, \"y\": 0.6102919 }\n\t\t\t\t],\n\t\t\t\t\"shear\": [\n\t\t\t\t\t{ \"time\": 1.2, \"x\": 6.8976173 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"flare6\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 1.2333333, \"value\": -5.36348 },\n\t\t\t\t\t{ \"time\": 1.2666668, \"value\": -0.53577185 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"time\": 1.2333333, \"x\": 14.516861, \"y\": 204.67482 },\n\t\t\t\t\t{ \"time\": 1.2666668, \"x\": 19.158737, \"y\": 212.89761, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 1.3000001, \"x\": 9.226883, \"y\": 202.85278 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{ \"time\": 1.2333333, \"x\": 0.77728415, \"y\": 0.4904852 },\n\t\t\t\t\t{ \"time\": 1.2666668, \"x\": 0.77728415, \"y\": 0.6571744 },\n\t\t\t\t\t{ \"time\": 1.3000001, \"x\": 0.47469667, \"y\": 0.4013442 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"flare7\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 1.1, \"value\": 5.982827 },\n\t\t\t\t\t{ \"time\": 1.1333334, \"value\": 32.82419 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"time\": 1.1, \"x\": -6.3407283, \"y\": 112.97925 },\n\t\t\t\t\t{ \"time\": 1.1333334, \"x\": 2.6628728, \"y\": 111.59957 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{ \"time\": 1.1, \"x\": 0.5884216, \"y\": 0.5884216 }\n\t\t\t\t],\n\t\t\t\t\"shear\": [\n\t\t\t\t\t{ \"time\": 1.1333334, \"x\": -19.934156 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"flare8\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 1.2333333, \"value\": -6.846075 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"x\": 66.67122, \"y\": 125.524506, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 1.2, \"x\": 58.23973, \"y\": 113.53304, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 1.2333333, \"x\": 40.14647, \"y\": 114.69013 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"x\": 1.3134319, \"y\": 1.2030426 },\n\t\t\t\t\t{ \"time\": 1.2333333, \"x\": 1.0375577, \"y\": 0.9503547 }\n\t\t\t\t],\n\t\t\t\t\"shear\": [\n\t\t\t\t\t{ \"time\": 1.2, \"y\": -13.007996 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"flare9\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"value\": 2.9039583 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"x\": 28.448578, \"y\": 151.35078, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 1.2, \"x\": 48.804657, \"y\": 191.08945, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 1.2333333, \"x\": 52.002544, \"y\": 182.52245, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 1.2666668, \"x\": 77.01112, \"y\": 195.95699 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"x\": 0.8714151, \"y\": 1.0726658 },\n\t\t\t\t\t{ \"time\": 1.2, \"x\": 0.92650855, \"y\": 0.94438785 },\n\t\t\t\t\t{ \"time\": 1.2333333, \"x\": 1.1650134, \"y\": 1.3358811 }\n\t\t\t\t],\n\t\t\t\t\"shear\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"x\": 7.9525824, \"y\": 25.483856 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"flare10\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"value\": 2.1832652 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"x\": 55.644714, \"y\": 137.64316, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 1.2, \"x\": 90.491974, \"y\": 151.07211, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 1.2333333, \"x\": 114.0629, \"y\": 153.05193, \"curve\": \"stepped\" },\n\t\t\t\t\t{ \"time\": 1.2666668, \"x\": 90.43509, \"y\": 164.60985 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"x\": 2.6573577, \"y\": 0.8913977 },\n\t\t\t\t\t{ \"time\": 1.2, \"x\": 3.3139129, \"y\": 1.4252579 },\n\t\t\t\t\t{ \"time\": 1.2333333, \"x\": 2.8711078, \"y\": 0.9239545 },\n\t\t\t\t\t{ \"time\": 1.2666668, \"x\": 2.3166406, \"y\": 0.77455306 }\n\t\t\t\t],\n\t\t\t\t\"shear\": [\n\t\t\t\t\t{ \"time\": 1.1666667, \"x\": -1.3484306 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1,\n\t\t\t\t\t\t\"curve\": [ 1.1170001, 0, 1.2550601, 24.936674 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.4000001,\n\t\t\t\t\t\t\"value\": 24.936674,\n\t\t\t\t\t\t\"curve\": [ 1.4766636, 24.936674, 1.5902743, -17.620535 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.6333334,\n\t\t\t\t\t\t\"value\": -19.480213,\n\t\t\t\t\t\t\"curve\": [ 1.7171756, -23.101297, 1.783559, -26.121367 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.9333334,\n\t\t\t\t\t\t\"value\": -26.136948,\n\t\t\t\t\t\t\"curve\": [ 2.067079, -26.150864, 2.1577108, 4.304803 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.3000002,\n\t\t\t\t\t\t\"value\": 4.219494,\n\t\t\t\t\t\t\"curve\": [ 2.450183, 4.129454, 2.5790226, -1.7621814 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.7333336,\n\t\t\t\t\t\t\"value\": -1.8001862,\n\t\t\t\t\t\t\"curve\": [ 2.8159394, -1.8205318, 2.8572183, -2.935091 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.9333334,\n\t\t\t\t\t\t\"value\": -2.9925098,\n\t\t\t\t\t\t\"curve\": [ 3.0557516, -3.0848584, 3.0896554, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.3000001,\n\t\t\t\t\t\t\"curve\": [ 1.3517501, 0, 1.4081577, 6.465316 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.4666667,\n\t\t\t\t\t\t\"value\": 6.43365,\n\t\t\t\t\t\t\"curve\": [ 1.5501257, 6.3898926, 1.7226948, -5.045167 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.7333335,\n\t\t\t\t\t\t\"value\": -5.525421,\n\t\t\t\t\t\t\"curve\": [ 1.7818483, -7.7154818, 1.8426183, -16.944561 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.9666668,\n\t\t\t\t\t\t\"value\": -16.864632,\n\t\t\t\t\t\t\"curve\": [ 2.110735, -16.7757, 2.2587202, -3.9745846 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.4,\n\t\t\t\t\t\t\"value\": -2.426651,\n\t\t\t\t\t\t\"curve\": [ 2.5251248, -1.1212268, 2.6390042, -0.49731347 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.7333336,\n\t\t\t\t\t\t\"value\": -0.48923042,\n\t\t\t\t\t\t\"curve\": [ 2.9313023, -0.4722646, 2.9987574, -2.1479998 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667, \"value\": -2.1479998 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"head-control\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.2333333,\n\t\t\t\t\t\t\"curve\": [ 1.2504523, 0, 1.4742419, 6.891655, 1.2504523, 0, 1.4959949, 0.97906095 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.6666667,\n\t\t\t\t\t\t\"x\": 11.990509,\n\t\t\t\t\t\t\"y\": -6.4160156,\n\t\t\t\t\t\t\"curve\": [ 1.742927, 14.01123, 1.8604802, 14.329775, 1.7850902, -11.547222, 1.8604802, -27.103428 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.9666668,\n\t\t\t\t\t\t\"x\": 13.907715,\n\t\t\t\t\t\t\"y\": -26.877588,\n\t\t\t\t\t\t\"curve\": [ 2.0738876, 13.487291, 2.2436624, 8.129967, 2.0738876, -26.652622, 2.214605, -21.781118 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.3000002,\n\t\t\t\t\t\t\"x\": 6.0683746,\n\t\t\t\t\t\t\"y\": -16.63897,\n\t\t\t\t\t\t\"curve\": [ 2.4156141, 1.8376002, 2.496731, -1.4142227, 2.4174454, -9.5668955, 2.526046, -1.7173506 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.5666668,\n\t\t\t\t\t\t\"x\": -3.780899,\n\t\t\t\t\t\t\"y\": -1.7078501,\n\t\t\t\t\t\t\"curve\": [ 2.6613214, -6.98407, 2.760468, -8.761351, 2.6924124, -1.6784445, 2.821366, -15.747234 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.9,\n\t\t\t\t\t\t\"x\": -8.31778,\n\t\t\t\t\t\t\"y\": -16.697033,\n\t\t\t\t\t\t\"curve\": [ 2.9615943, -8.121979, 3.0824013, -0.04269974, 2.9583626, -17.389893, 3.0887933, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-shoulder\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.3333334,\n\t\t\t\t\t\t\"curve\": [ 1.4878056, 0, 1.7172108, 0.2088337, 1.4878056, 0, 1.6875668, -30.28685 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 1.9000001,\n\t\t\t\t\t\t\"x\": 0.8290405,\n\t\t\t\t\t\t\"y\": -30.28685,\n\t\t\t\t\t\t\"curve\": [ 2.078422, 1.4344292, 2.2743027, 2.8776493, 2.0709288, -30.28685, 2.2891202, 4.477253 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.4333334,\n\t\t\t\t\t\t\"x\": 2.885666,\n\t\t\t\t\t\t\"y\": 4.5938454,\n\t\t\t\t\t\t\"curve\": [ 2.6040773, 2.8942738, 2.6767848, -0.67696, 2.5704894, 4.704731, 2.6941266, -2.4257364 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 2.766667,\n\t\t\t\t\t\t\"x\": -0.6713352,\n\t\t\t\t\t\t\"y\": -2.4671345,\n\t\t\t\t\t\t\"curve\": [ 2.8659508, -0.6651211, 2.985806, -0.07066536, 2.8815293, -2.4677744, 3.0362694, -0.06334877 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 3.1666667 }\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t\"ik\": {\n\t\t\t\"rear-leg-ik\": [\n\t\t\t\t{ \"time\": 3.1666667, \"softness\": 10, \"bendPositive\": false }\n\t\t\t]\n\t\t}\n\t},\n\t\"run\": {\n\t\t\"slots\": {\n\t\t\t\"mouth\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"mouth-grind\" }\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t\"bones\": {\n\t\t\t\"front-thigh\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -5.136677,\n\t\t\t\t\t\t\"y\": 11.130936,\n\t\t\t\t\t\t\"curve\": [ 0.033243228, -7.770458, 0.11222647, -9.025787, 0.034, 11.130936, 0.107572615, 9.737719 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"x\": -9.034784,\n\t\t\t\t\t\t\"y\": 7.9874306,\n\t\t\t\t\t\t\"curve\": [ 0.23010021, -9.045264, 0.31360787, -1.3446236, 0.23623654, 5.9268627, 0.27954492, 3.223648 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"x\": 0.41351128,\n\t\t\t\t\t\t\"y\": 3.1946697,\n\t\t\t\t\t\t\"curve\": [ 0.35211945, 2.0879211, 0.44860503, 11.157831, 0.3841316, 3.1587524, 0.44893667, 4.982999 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"x\": 11.166422,\n\t\t\t\t\t\t\"y\": 6.7602186,\n\t\t\t\t\t\t\"curve\": [ 0.57096416, 10.791886, 0.6208136, -1.8259034, 0.54159856, 8.208021, 0.62500006, 11.130936 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"x\": -5.136677, \"y\": 11.130936 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -37.65892,\n\t\t\t\t\t\t\"curve\": [ 0.034389626, -37.13966, 0.10666875, -36.20868 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": -36.21151,\n\t\t\t\t\t\t\"curve\": [ 0.15832105, -36.214157, 0.20853828, -38.80227 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": -38.791412,\n\t\t\t\t\t\t\"curve\": [ 0.25916564, -38.78009, 0.31348577, -38.026024 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": -37.65892,\n\t\t\t\t\t\t\"curve\": [ 0.3574979, -37.211967, 0.4000104, -36.21151 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"value\": -36.21151,\n\t\t\t\t\t\t\"curve\": [ 0.45833334, -36.21151, 0.53916985, -38.80016 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": -38.80016,\n\t\t\t\t\t\t\"curve\": [ 0.5916668, -38.800156, 0.6445271, -37.999634 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": -37.65892 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-thigh\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -16.413372,\n\t\t\t\t\t\t\"y\": 1.5534182,\n\t\t\t\t\t\t\"curve\": [ 0.01329846, -15.666842, 0.18333334, -8.545134, 0.02999618, 2.3940766, 0.18333334, 6.1697955 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"x\": -8.545134,\n\t\t\t\t\t\t\"y\": 6.1697955,\n\t\t\t\t\t\t\"curve\": [ 0.30833334, -8.545134, 0.4916667, -19.754063, 0.30833334, 6.1697955, 0.4916667, 0.61477757 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"x\": -19.754063,\n\t\t\t\t\t\t\"y\": 0.61477757,\n\t\t\t\t\t\t\"curve\": [ 0.5916667, -19.754063, 0.6411845, -18.062927, 0.5916667, 0.61477757, 0.6317572, 0.7811694 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"x\": -16.413372, \"y\": 1.5534182 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -39.034668,\n\t\t\t\t\t\t\"curve\": [ 0.050558798, -0.10101318, 0.14481726, 88.36118 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": 88.361206,\n\t\t\t\t\t\t\"curve\": [ 0.27979535, 88.765, 0.32359713, 59.5175 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": 51.12628,\n\t\t\t\t\t\t\"curve\": [ 0.35761085, 30.202553, 0.44473714, -74.91441 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": -75.817406,\n\t\t\t\t\t\t\"curve\": [ 0.5989909, -76.056786, 0.6416667, -55.72052 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": -39.034668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 20.536366,\n\t\t\t\t\t\t\"curve\": [ 0.052421574, 11.417128, 0.0888624, 0.12659359 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 0.1466589,\n\t\t\t\t\t\t\"curve\": [ 0.18554536, 0.17022228, 0.22147074, 26.29081 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": 32.368996,\n\t\t\t\t\t\t\"curve\": [ 0.24664007, 39.18712, 0.28609684, 61.44734 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": 61.58177,\n\t\t\t\t\t\t\"curve\": [ 0.3705727, 61.687737, 0.41991156, 55.79158 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.4666667, \"value\": 49.678547 },\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": 20.536366 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-fist\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -36.16143,\n\t\t\t\t\t\t\"curve\": [ 0.014286301, -38.799747, 0.035710063, -43.266056 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": -43.36955,\n\t\t\t\t\t\t\"curve\": [ 0.10190062, -43.48734, 0.18198347, -28.460545 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"value\": -23.042507,\n\t\t\t\t\t\t\"curve\": [ 0.23049557, -13.871703, 0.2638955, 3.857522 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": 3.6990747,\n\t\t\t\t\t\t\"curve\": [ 0.3797016, 3.6420546, 0.53522193, -16.222647 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.5666667, \"value\": -21.28619 },\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": -36.16143 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 40.503326,\n\t\t\t\t\t\t\"curve\": [ 0.028292395, 23.741745, 0.1277068, -79.86003 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": -79.86928,\n\t\t\t\t\t\t\"curve\": [ 0.380056, -79.882126, 0.40309292, 63.25032 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": 64.13361,\n\t\t\t\t\t\t\"curve\": [ 0.6073363, 64.35324, 0.6441763, 53.095383 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": 40.503326 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -3.7868073,\n\t\t\t\t\t\t\"y\": -0.7673931,\n\t\t\t\t\t\t\"curve\": [ 0.043937773, -4.5792294, 0.16938668, -5.4754944, 0.043937773, 0.9303658, 0.16938668, 2.8506088 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"x\": -5.4754944,\n\t\t\t\t\t\t\"y\": 2.8506088,\n\t\t\t\t\t\t\"curve\": [ 0.34626475, -5.4754944, 0.4753447, -2.6843567, 0.34626475, 2.8506088, 0.4753447, -3.129387 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"x\": -2.6843567,\n\t\t\t\t\t\t\"y\": -3.129387,\n\t\t\t\t\t\t\"curve\": [ 0.6106854, -2.6843567, 0.6416667, -3.3359296, 0.6106854, -3.129387, 0.6416667, -1.7333956 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"x\": -3.7868073, \"y\": -0.7673931 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": 28.284866 },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": -11.117936,\n\t\t\t\t\t\t\"curve\": [ 0.2520901, -14.118498, 0.29731876, -19.369314 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": -19.379519,\n\t\t\t\t\t\t\"curve\": [ 0.43469015, -19.408234, 0.5219483, 38.96215 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": 38.86711,\n\t\t\t\t\t\t\"curve\": [ 0.6189629, 38.75598, 0.6442117, 32.01072 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": 28.284866 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"neck\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 11.884876,\n\t\t\t\t\t\t\"curve\": [ 0.02355112, 11.402178, 0.075, 9.74365 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"value\": 9.74365,\n\t\t\t\t\t\t\"curve\": [ 0.125, 9.74365, 0.20833334, 13.35947 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": 13.35947,\n\t\t\t\t\t\t\"curve\": [ 0.25833336, 13.35947, 0.32145488, 12.19964 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": 11.884876,\n\t\t\t\t\t\t\"curve\": [ 0.36456463, 11.057289, 0.40833336, 9.719053 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"value\": 9.719053,\n\t\t\t\t\t\t\"curve\": [ 0.45833334, 9.719053, 0.5416667, 13.35947 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": 13.35947,\n\t\t\t\t\t\t\"curve\": [ 0.5916667, 13.35947, 0.6356395, 12.479662 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": 11.884876 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"head\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 13.143766,\n\t\t\t\t\t\t\"curve\": [ 0.02012745, 11.9917965, 0.038635418, 8.939619 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": 8.926325,\n\t\t\t\t\t\t\"curve\": [ 0.121595174, 8.900274, 0.23190996, 15.799082 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": 15.80678,\n\t\t\t\t\t\t\"curve\": [ 0.32506067, 15.819712, 0.3570971, 8.947962 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"value\": 8.927942,\n\t\t\t\t\t\t\"curve\": [ 0.44371668, 8.907549, 0.5677775, 15.798948 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": 15.767241,\n\t\t\t\t\t\t\"curve\": [ 0.6316619, 15.736086, 0.64897305, 14.051775 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": 13.143766 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.013515494, 0.99573314, 0.068352066, 0.9908737, 0.02733998, 1.005166, 0.083333336, 1.0123813 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"x\": 0.99064595,\n\t\t\t\t\t\t\"y\": 1.0123813,\n\t\t\t\t\t\t\"curve\": [ 0.12777779, 0.99064595, 0.20549612, 1.0184776, 0.12777779, 1.0123813, 0.19723263, 0.98792285 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"x\": 1.0184796,\n\t\t\t\t\t\t\"y\": 0.98803675,\n\t\t\t\t\t\t\"curve\": [ 0.2718352, 1.0184824, 0.3052763, 1.0076591, 0.26181215, 0.9881266, 0.310745, 0.99471015 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"curve\": [ 0.35060236, 0.9952859, 0.4166667, 0.9871599, 0.35857606, 1.0059115, 0.4166667, 1.012646 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"x\": 0.9871599,\n\t\t\t\t\t\t\"y\": 1.012646,\n\t\t\t\t\t\t\"curve\": [ 0.4666667, 0.9871599, 0.53333336, 1.0197783, 0.4666667, 1.012646, 0.53333336, 0.98868805 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"x\": 1.0197783,\n\t\t\t\t\t\t\"y\": 0.98868805,\n\t\t\t\t\t\t\"curve\": [ 0.5916667, 1.0197783, 0.6520706, 1.0043895, 0.5916667, 0.98868805, 0.64363515, 0.99582773 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"gun\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 12.364543,\n\t\t\t\t\t\t\"curve\": [ 0.022356797, 16.282055, 0.087478235, 20.251215 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 20.186804,\n\t\t\t\t\t\t\"curve\": [ 0.16822302, 20.322517, 0.25445935, -8.823136 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": -11.87723,\n\t\t\t\t\t\t\"curve\": [ 0.29076138, -17.905375, 0.34360257, -24.105413 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"value\": -23.880844,\n\t\t\t\t\t\t\"curve\": [ 0.4478289, -23.690395, 0.53275037, -15.4656105 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.5666667, \"value\": -8.6856785 },\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": 12.364543 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hip\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": -8.238106 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -3.595178,\n\t\t\t\t\t\t\"y\": -34.10118,\n\t\t\t\t\t\t\"curve\": [ 0.041666668, -3.8363783, 0.11761589, 7.619495, 0.041666668, -33.74106, 0.11178647, 20.546356 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"x\": 7.6073985,\n\t\t\t\t\t\t\"y\": 20.355682,\n\t\t\t\t\t\t\"curve\": [ 0.1942442, 7.600595, 0.21030916, 5.061855, 0.20362726, 20.651718, 0.21742456, -8.689377 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"x\": 1.6768702,\n\t\t\t\t\t\t\"y\": -18.475845,\n\t\t\t\t\t\t\"curve\": [ 0.27868885, -4.991243, 0.2966124, -5.63762, 0.2538157, -31.075775, 0.29166076, -34.54651 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"x\": -5.7555113,\n\t\t\t\t\t\t\"y\": -35.003036,\n\t\t\t\t\t\t\"curve\": [ 0.37868887, -5.901124, 0.45119137, 6.801747, 0.3841243, -35.559464, 0.42840508, 17.597122 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"x\": 6.613358,\n\t\t\t\t\t\t\"y\": 17.013046,\n\t\t\t\t\t\t\"curve\": [ 0.53639644, 6.472878, 0.54488885, 3.5621903, 0.5326954, 16.746323, 0.54835004, -8.713715 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"x\": 0.3517065,\n\t\t\t\t\t\t\"y\": -18.814713,\n\t\t\t\t\t\t\"curve\": [ 0.5966279, -4.0651555, 0.6416667, -3.4504576, 0.58438176, -28.583954, 0.6416667, -34.317253 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"x\": -3.595178, \"y\": -34.10118 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-foot-target\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -62.538223,\n\t\t\t\t\t\t\"curve\": [ 0.01524261, -74.191124, 0.05595425, -103.19018 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": -111.07755,\n\t\t\t\t\t\t\"curve\": [ 0.09160133, -129.43651, 0.18928674, -146.55014 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": -146.32286,\n\t\t\t\t\t\t\"curve\": [ 0.28452423, -146.05872, 0.319893, -125.09509 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.33333334, \"value\": -117.242096 },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": -35.071888,\n\t\t\t\t\t\t\"curve\": [ 0.5216232, -28.644218, 0.54569703, -24.837662 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": -24.904682,\n\t\t\t\t\t\t\"curve\": [ 0.59532595, -24.996273, 0.62337583, -40.82229 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": -62.538223 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": 16.340538, \"y\": 0.18180084 },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"x\": -101.43166,\n\t\t\t\t\t\t\"y\": 8.04301,\n\t\t\t\t\t\t\"curve\": [ 0.08527877, -131.34503, 0.12872966, -207.6919, 0.07963452, 14.900109, 0.12395439, 113.27577 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"x\": -207.91826,\n\t\t\t\t\t\t\"y\": 145.81108,\n\t\t\t\t\t\t\"curve\": [ 0.1963201, -208.12917, 0.20963398, -202.91498, 0.1856314, 160.25711, 0.206384, 163.47583 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"x\": -189.94456,\n\t\t\t\t\t\t\"y\": 163.85147,\n\t\t\t\t\t\t\"curve\": [ 0.2698811, -169.9423, 0.3099357, -126.192535, 0.269217, 164.35164, 0.31597918, 85.966354 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"x\": -90.56034,\n\t\t\t\t\t\t\"y\": 78.571014,\n\t\t\t\t\t\t\"curve\": [ 0.35471895, -57.992264, 0.37630823, -29.140278, 0.34979862, 71.55447, 0.37555763, 66.39534 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"x\": 2.8686295,\n\t\t\t\t\t\t\"y\": 66.382545,\n\t\t\t\t\t\t\"curve\": [ 0.41211826, 19.24104, 0.46935338, 90.728714, 0.42882884, 66.36746, 0.46877518, 70.660576 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"x\": 117.17877,\n\t\t\t\t\t\t\"y\": 70.4559,\n\t\t\t\t\t\t\"curve\": [ 0.5220906, 136.24426, 0.5417671, 151.32806, 0.5394703, 70.19718, 0.5545734, 38.253704 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"x\": 151.4943,\n\t\t\t\t\t\t\"y\": 25.28957,\n\t\t\t\t\t\t\"curve\": [ 0.5782437, 146.75656, 0.58641464, 133.13281, 0.5718833, 19.697252, 0.58248705, 12.229093 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6, \"x\": 115.016266, \"y\": 0.1018036 },\n\t\t\t\t\t{ \"time\": 0.6666667, \"x\": 16.340538, \"y\": 0.18180084 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-leg-target\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": -13.945175, \"y\": -30.341328 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-foot-target\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": 18.552212 },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": 167.84149,\n\t\t\t\t\t\t\"curve\": [ 0.24598275, 153.66016, 0.25611776, 129.74042 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": 124.32189,\n\t\t\t\t\t\t\"curve\": [ 0.29554114, 124.42799, 0.3131148, 129.92502 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": 129.87119,\n\t\t\t\t\t\t\"curve\": [ 0.42117754, 128.31995, 0.5194363, 0.9795736 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"curve\": [ 0.59955627, 0.26917782, 0.64211744, 4.7290063 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": 18.552212 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -176.39333,\n\t\t\t\t\t\t\"y\": 134.11803,\n\t\t\t\t\t\t\"curve\": [ 0.018171692, -142.26341, 0.05366483, -94.41089, 0.010235078, 120.957504, 0.043573763, 84.08058 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"x\": -73.564575,\n\t\t\t\t\t\t\"y\": 76.684654,\n\t\t\t\t\t\t\"curve\": [ 0.08583902, -42.824852, 0.19448526, 101.19695, 0.097762845, 66.725555, 0.19836502, 60.87754 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.23333335, \"x\": 98.31728, \"y\": 32.16878 },\n\t\t\t\t\t{ \"time\": 0.26666668, \"x\": 49.1316, \"y\": -0.63303185 },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"x\": -147.8964,\n\t\t\t\t\t\t\"y\": 0.31975412,\n\t\t\t\t\t\t\"curve\": [ 0.41444945, -168.78018, 0.47800374, -284.7576, 0.43022326, 30.085405, 0.47836658, 129.13838 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"x\": -283.36942,\n\t\t\t\t\t\t\"y\": 167.1215,\n\t\t\t\t\t\t\"curve\": [ 0.5261606, -285.65964, 0.54818815, -280.53824, 0.5157873, 194.8402, 0.5500097, 216.53189 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"x\": -266.98016,\n\t\t\t\t\t\t\"y\": 216.1206,\n\t\t\t\t\t\t\"curve\": [ 0.5811587, -256.26624, 0.64271414, -206.53629, 0.61030954, 214.81862, 0.6499278, 145.32756 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"x\": -176.39333, \"y\": 134.11803 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-leg-target\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": 85.00298, \"y\": -33.593994 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"back-foot-tip\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -147.03682,\n\t\t\t\t\t\t\"curve\": [ 0.03288975, -113.40238, 0.16121686, 44.340786 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": 43.48135,\n\t\t\t\t\t\t\"curve\": [ 0.23966669, 43.405872, 0.28198713, 35.722797 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.3,\n\t\t\t\t\t\t\"value\": 0.2902248,\n\t\t\t\t\t\t\"curve\": [ 0.34669167, 0.27669734, 0.39574385, 4.266659 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"curve\": [ 0.42376953, -23.799583, 0.52500004, -181.3858 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": -181.3858,\n\t\t\t\t\t\t\"curve\": [ 0.5916667, -181.3858, 0.6416667, -169.08748 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": -147.03682 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-foot-tip\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -0.25173056,\n\t\t\t\t\t\t\"curve\": [ 0.008333334, -0.25173056, 0.056431595, 1.7333364 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": -7.6761312,\n\t\t\t\t\t\t\"curve\": [ 0.0754344, -43.126034, 0.14960335, -130.4444 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"value\": -130.07611,\n\t\t\t\t\t\t\"curve\": [ 0.23899044, -129.79117, 0.2717762, -126.80317 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.3,\n\t\t\t\t\t\t\"value\": -116.242805,\n\t\t\t\t\t\t\"curve\": [ 0.33295068, -103.91382, 0.34845847, -86.10217 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": -71.07932,\n\t\t\t\t\t\t\"curve\": [ 0.38585755, -55.24571, 0.41477472, -32.438164 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"value\": -21.634546,\n\t\t\t\t\t\t\"curve\": [ 0.47047293, -0.014346838, 0.54170907, 33.422134 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": 33.195797,\n\t\t\t\t\t\t\"curve\": [ 0.6215921, 32.69772, 0.5690262, 0.6415044 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": -0.25173056 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair1\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -6.81118,\n\t\t\t\t\t\t\"curve\": [ 0.086661905, -6.81118, 0.1434748, -5.7460938 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"value\": -4.295494,\n\t\t\t\t\t\t\"curve\": [ 0.18288106, -3.2813187, 0.2086277, 2.7883377 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": 2.7795372,\n\t\t\t\t\t\t\"curve\": [ 0.26211023, 2.769287, 0.3045487, -6.631756 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": -6.6432877,\n\t\t\t\t\t\t\"curve\": [ 0.41934446, -6.6777496, 0.48958412, -4.840439 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": -4.3767853,\n\t\t\t\t\t\t\"curve\": [ 0.51827514, -3.5632935, 0.57366735, 2.3180084 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": 2.3286743,\n\t\t\t\t\t\t\"curve\": [ 0.64318144, 2.346161, 0.6333334, -6.81118 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": -6.81118 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -6.81118,\n\t\t\t\t\t\t\"curve\": [ 0.013779227, -3.171306, 0.10898591, 43.93197 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 43.945263,\n\t\t\t\t\t\t\"curve\": [ 0.17706332, 43.96914, 0.1918259, -13.758546 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": -13.831951,\n\t\t\t\t\t\t\"curve\": [ 0.3016804, -13.722519, 0.32213193, -8.864155 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": -6.598316,\n\t\t\t\t\t\t\"curve\": [ 0.3486682, -3.4963722, 0.4357065, 41.100037 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"value\": 41.05412,\n\t\t\t\t\t\t\"curve\": [ 0.51049143, 40.989143, 0.54918325, -14.055373 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": -14.181606,\n\t\t\t\t\t\t\"curve\": [ 0.63049895, -14.257366, 0.6561362, -9.039669 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": -6.81118 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -6.81118,\n\t\t\t\t\t\t\"curve\": [ 0.07915677, -6.834999, 0.10838401, 0.2951547 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 1.9601116,\n\t\t\t\t\t\t\"curve\": [ 0.1772737, 4.8924084, 0.20833737, 6.282898 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": 6.2903996,\n\t\t\t\t\t\t\"curve\": [ 0.31270784, 6.3142223, 0.3833106, 3.4879818 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"value\": 2.5805492,\n\t\t\t\t\t\t\"curve\": [ 0.44239402, 0.27549934, 0.5234759, -6.81118 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6, \"value\": -6.81118 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair4\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -6.81118,\n\t\t\t\t\t\t\"curve\": [ 0.011210872, -4.0627823, 0.10833334, 24.922989 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 24.922989,\n\t\t\t\t\t\t\"curve\": [ 0.15833335, 24.922989, 0.20833334, -10.621323 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": -10.621323,\n\t\t\t\t\t\t\"curve\": [ 0.2541667, -10.621323, 0.31185058, -9.726021 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": -6.3966293,\n\t\t\t\t\t\t\"curve\": [ 0.35559124, -2.9471052, 0.43750003, 24.92807 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"value\": 24.92807,\n\t\t\t\t\t\t\"curve\": [ 0.4916667, 24.92807, 0.57500005, -9.775085 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": -9.775085,\n\t\t\t\t\t\t\"curve\": [ 0.6166667, -9.775085, 0.654536, -8.62735 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": -6.81118 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 3.4965944,\n\t\t\t\t\t\t\"curve\": [ 0.06971177, 3.5069423, 0.07500001, 8.690938 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"value\": 8.690939,\n\t\t\t\t\t\t\"curve\": [ 0.13874887, 8.690939, 0.21436597, 6.899573 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": 6.329458,\n\t\t\t\t\t\t\"curve\": [ 0.2661681, 5.3425198, 0.28525335, 3.4847515 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": 3.4836977,\n\t\t\t\t\t\t\"curve\": [ 0.39762527, 3.4822881, 0.40833336, 8.683571 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"value\": 8.683571,\n\t\t\t\t\t\t\"curve\": [ 0.45833334, 8.683571, 0.5514198, 6.7953053 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": 6.2597165,\n\t\t\t\t\t\t\"curve\": [ 0.5976179, 5.1724644, 0.6416667, 3.4928834 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": 3.4965944 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 4.5222607,\n\t\t\t\t\t\t\"curve\": [ 0.06670141, 4.5386086, 0.075, -7.2679157 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"value\": -7.2679157,\n\t\t\t\t\t\t\"curve\": [ 0.125, -7.2679157, 0.2265727, 0.8404685 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": 1.2426214,\n\t\t\t\t\t\t\"curve\": [ 0.25444564, 2.4984787, 0.300799, 4.5086966 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"value\": 4.5222607,\n\t\t\t\t\t\t\"curve\": [ 0.38591585, 4.5441833, 0.40833336, -7.354608 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.43333337,\n\t\t\t\t\t\t\"value\": -7.354608,\n\t\t\t\t\t\t\"curve\": [ 0.45833334, -7.354608, 0.5494212, -0.13630033 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5666667,\n\t\t\t\t\t\t\"value\": 0.9513377,\n\t\t\t\t\t\t\"curve\": [ 0.5862056, 2.1836119, 0.63187444, 4.5386214 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"value\": 4.5222607 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"aim-constraint-target\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": 30.570744 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-foot\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": -6.4978333 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-foot\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": 4.495182 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"head-control\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"y\": -9.940754,\n\t\t\t\t\t\t\"curve\": [ 0.058333337, 0, 0.17500001, -15.319916, 0.043675702, -4.1921644, 0.17500001, 4.996334 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"x\": -15.319916,\n\t\t\t\t\t\t\"y\": 4.996334,\n\t\t\t\t\t\t\"curve\": [ 0.3166667, -15.319916, 0.42878592, -9.741898, 0.3166667, 4.996334, 0.38218656, -31.713127 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"x\": -7.8084564,\n\t\t\t\t\t\t\"y\": -31.590258,\n\t\t\t\t\t\t\"curve\": [ 0.50683033, -5.7584915, 0.6166667, 0, 0.5488791, -31.47069, 0.6278209, -13.325505 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"y\": -9.940754 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-shoulder\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -0.7361145,\n\t\t\t\t\t\t\"y\": 11.219177,\n\t\t\t\t\t\t\"curve\": [ 0.061111115, -0.7361145, 0.14398998, 1.168911, 0.061111115, 11.219177, 0.14298694, -17.925644 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"x\": 1.1871319,\n\t\t\t\t\t\t\"y\": -17.8975,\n\t\t\t\t\t\t\"curve\": [ 0.5402858, 1.2497454, 0.55833334, -0.7361145, 0.5447008, -17.800505, 0.55833334, 11.219177 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667, \"x\": -0.7361145, \"y\": 11.219177 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"back-shoulder\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.083333336, 0, 0.25, 0, 0.083333336, 0, 0.25, 8.930149 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.33333334,\n\t\t\t\t\t\t\"y\": 8.930149,\n\t\t\t\t\t\t\"curve\": [ 0.4166667, 0, 0.5833334, 0, 0.4166667, 8.930149, 0.5833334, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6666667 }\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t\"ik\": {\n\t\t\t\"front-leg-ik\": [\n\t\t\t\t{ \"softness\": 10, \"bendPositive\": false },\n\t\t\t\t{ \"time\": 0.5666667, \"softness\": 14.8, \"bendPositive\": false },\n\t\t\t\t{ \"time\": 0.6, \"softness\": 48.2, \"bendPositive\": false },\n\t\t\t\t{ \"time\": 0.6666667, \"softness\": 10, \"bendPositive\": false }\n\t\t\t],\n\t\t\t\"rear-leg-ik\": [\n\t\t\t\t{ \"bendPositive\": false },\n\t\t\t\t{ \"time\": 0.16666667, \"softness\": 22.5, \"bendPositive\": false },\n\t\t\t\t{ \"time\": 0.3, \"softness\": 61.4, \"bendPositive\": false },\n\t\t\t\t{ \"time\": 0.6666667, \"bendPositive\": false }\n\t\t\t]\n\t\t},\n\t\t\"events\": [\n\t\t\t{ \"time\": 0.23333335, \"name\": \"footstep\" },\n\t\t\t{ \"time\": 0.5666667, \"name\": \"footstep\" }\n\t\t]\n\t},\n\t\"run-to-idle\": {\n\t\t\"slots\": {\n\t\t\t\"front-fist\": {\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"front-fist-open\" }\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t\"bones\": {\n\t\t\t\"front-foot-target\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -16.498466,\n\t\t\t\t\t\t\"y\": 3.4050598,\n\t\t\t\t\t\t\"curve\": [ 0.033333335, -16.498466, 0.10000001, -69.06195, 0.033333335, 3.4050598, 0.10000001, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.13333334, \"x\": -69.06195 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hip\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -28.77872,\n\t\t\t\t\t\t\"y\": -72.955765,\n\t\t\t\t\t\t\"curve\": [ 0.036270402, -28.634022, 0.20000002, -10.848239, 0.13512377, -62.34809, 0.20000002, -23.154968 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668, \"x\": -11.972925, \"y\": -23.154968 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-foot-target\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 33.151367,\n\t\t\t\t\t\t\"y\": 31.6077,\n\t\t\t\t\t\t\"curve\": [ 0.016666668, 33.151367, 0.050000004, 24.412033, 0.016666668, 31.6077, 0.041164663, 20.72947 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"x\": 24.412033,\n\t\t\t\t\t\t\"y\": 0.18982041,\n\t\t\t\t\t\t\"curve\": [ 0.116666675, 24.412033, 0.21666668, 48.869934, 0.116666675, 0.18982041, 0.21666668, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668, \"x\": 48.869934 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -80.6061,\n\t\t\t\t\t\t\"curve\": [ 0.06666667, -80.6061, 0.20000002, -60.869286 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668, \"value\": -60.869286 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 8.791121,\n\t\t\t\t\t\t\"curve\": [ 0.04088889, 8.791121, 0.11455758, 6.2977123 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"value\": 6.4087296,\n\t\t\t\t\t\t\"curve\": [ 0.20117474, 6.48225, 0.2408889, 42.46212 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668, \"value\": 42.46212 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 55.30082,\n\t\t\t\t\t\t\"curve\": [ 0.06666667, 55.30082, 0.20000002, 39.198883 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668, \"value\": 39.198883 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"head\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.050000004, 0, 0.083333336, 2.6706543 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 2.6706543,\n\t\t\t\t\t\t\"curve\": [ 0.15, 2.6706543, 0.25, -6.7476807 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668, \"value\": -6.7476807 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-fist\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 38.260498,\n\t\t\t\t\t\t\"curve\": [ 0.04088889, 38.260498, 0.1269512, -2.1933112 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"value\": -2.9968967,\n\t\t\t\t\t\t\"curve\": [ 0.208581, -3.8449726, 0.2408889, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 0.8437909,\n\t\t\t\t\t\t\"curve\": [ 0.06666667, 0.8437909, 0.20000002, 1, 0.06666667, 1, 0.20000002, 1 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 57.243587,\n\t\t\t\t\t\t\"curve\": [ 0.06666667, 57.243587, 0.20000002, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"gun\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 2.2769923,\n\t\t\t\t\t\t\"curve\": [ 0.04088889, 2.2769923, 0.10457135, 15.343413 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"value\": 15.31988,\n\t\t\t\t\t\t\"curve\": [ 0.20540346, 15.305198, 0.2408889, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -12.975021,\n\t\t\t\t\t\t\"curve\": [ 0.033333335, -12.975021, 0.102917634, -14.809181 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": -16.633377,\n\t\t\t\t\t\t\"curve\": [ 0.16759779, -18.688416, 0.23333335, -22.875427 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668, \"value\": -22.875427 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 0.96349424,\n\t\t\t\t\t\t\"y\": 1.0741309,\n\t\t\t\t\t\t\"curve\": [ 0.06666667, 0.96349424, 0.13178295, 1, 0.06666667, 1.0741309, 0.13178295, 1 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"neck\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{},\n\t\t\t\t\t{ \"time\": 0.26666668, \"value\": 3.7834435 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.033333335, 0, 0.10000001, 0.8756008 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 0.8756008,\n\t\t\t\t\t\t\"curve\": [ 0.16666667, 0.8756008, 0.23333335, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair4\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.033333335, 0, 0.10000001, 15.97303 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 15.97303,\n\t\t\t\t\t\t\"curve\": [ 0.16666667, 15.97303, 0.23333335, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair1\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.033333335, 0, 0.10000001, 10.7625885 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 10.7625885,\n\t\t\t\t\t\t\"curve\": [ 0.16666667, 10.7625885, 0.23333335, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.013892583, -2.278984, 0.041998185, -7.8367767 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"value\": -7.8186607,\n\t\t\t\t\t\t\"curve\": [ 0.10774197, -7.788499, 0.16569328, 6.5715294 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"value\": 6.6676254,\n\t\t\t\t\t\t\"curve\": [ 0.2215867, 6.7280884, 0.25522697, 1.977066 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.04088889, 0, 0.1068563, 3.0346298 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.16666667,\n\t\t\t\t\t\t\"value\": 3.0346298,\n\t\t\t\t\t\t\"curve\": [ 0.20514372, 3.0346298, 0.2408889, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.04866667, 0, 0.16607822, 0.65653944 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.20000002,\n\t\t\t\t\t\t\"value\": 0.6551018,\n\t\t\t\t\t\t\"curve\": [ 0.23155689, 0.6537645, 0.24866667, -2.1479998 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.26666668, \"value\": -2.1479998 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"head-control\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": -10.115814, \"y\": 8.708984 },\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-shoulder\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": 4.907959, \"y\": 11.540619 },\n\t\t\t\t\t{ \"time\": 0.26666668 }\n\t\t\t\t]\n\t\t\t}\n\t\t}\n\t},\n\t\"shoot\": {\n\t\t\"slots\": {\n\t\t\t\"muzzle\": {\n\t\t\t\t\"rgba\": [\n\t\t\t\t\t{ \"time\": 0.13333334, \"color\": \"ffffffff\" },\n\t\t\t\t\t{ \"time\": 0.20000002, \"color\": \"ffffff62\" }\n\t\t\t\t],\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"time\": 0.033333335, \"name\": \"muzzle01\" },\n\t\t\t\t\t{ \"time\": 0.06666667, \"name\": \"muzzle02\" },\n\t\t\t\t\t{ \"time\": 0.10000001, \"name\": \"muzzle03\" },\n\t\t\t\t\t{ \"time\": 0.13333334, \"name\": \"muzzle04\" },\n\t\t\t\t\t{ \"time\": 0.16666667, \"name\": \"muzzle05\" },\n\t\t\t\t\t{ \"time\": 0.20000002 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"muzzle-glow\": {\n\t\t\t\t\"rgba\": [\n\t\t\t\t\t{ \"color\": \"ff0c0c00\" },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.033333335,\n\t\t\t\t\t\t\"color\": \"ffc9adff\",\n\t\t\t\t\t\t\"curve\": [ 0.2548387, 1, 0.2731183, 1, 0.2548387, 0.76463497, 0.2731183, 0.39813548, 0.2548387, 0.6506967, 0.2731183, 0.2199929, 0.2548387, 1, 0.2731183, 1 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.3, \"color\": \"ff400cff\" },\n\t\t\t\t\t{ \"time\": 0.6333334, \"color\": \"ff0c0c00\" }\n\t\t\t\t],\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"name\": \"muzzle-glow\" }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"muzzle-ring\": {\n\t\t\t\t\"rgba\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.033333335,\n\t\t\t\t\t\t\"color\": \"d8baffff\",\n\t\t\t\t\t\t\"curve\": [ 0.20246169, 0.84704405, 0.21384181, 0.84396595, 0.20246169, 0.7294118, 0.21384181, 0.7294118, 0.20246169, 1, 0.21384181, 1, 0.20246169, 0.99622643, 0.21384181, 0.21132076 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.23333335, \"color\": \"d7baff00\" }\n\t\t\t\t],\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"time\": 0.033333335, \"name\": \"muzzle-ring\" },\n\t\t\t\t\t{ \"time\": 0.23333335 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"muzzle-ring2\": {\n\t\t\t\t\"rgba\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.033333335,\n\t\t\t\t\t\t\"color\": \"d8baffff\",\n\t\t\t\t\t\t\"curve\": [ 0.17427363, 0.84704405, 0.18375707, 0.84396595, 0.17427363, 0.7294118, 0.18375707, 0.7294118, 0.17427363, 1, 0.18375707, 1, 0.17427363, 0.99622643, 0.18375707, 0.21132076 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.20000002, \"color\": \"d7baff00\" }\n\t\t\t\t],\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"time\": 0.033333335, \"name\": \"muzzle-ring\" },\n\t\t\t\t\t{ \"time\": 0.20000002 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"muzzle-ring3\": {\n\t\t\t\t\"rgba\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.033333335,\n\t\t\t\t\t\t\"color\": \"d8baffff\",\n\t\t\t\t\t\t\"curve\": [ 0.17427363, 0.84704405, 0.18375707, 0.84396595, 0.17427363, 0.7294118, 0.18375707, 0.7294118, 0.17427363, 1, 0.18375707, 1, 0.17427363, 0.99622643, 0.18375707, 0.21132076 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.20000002, \"color\": \"d7baff00\" }\n\t\t\t\t],\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"time\": 0.033333335, \"name\": \"muzzle-ring\" },\n\t\t\t\t\t{ \"time\": 0.20000002 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"muzzle-ring4\": {\n\t\t\t\t\"rgba\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.033333335,\n\t\t\t\t\t\t\"color\": \"d8baffff\",\n\t\t\t\t\t\t\"curve\": [ 0.17427363, 0.84704405, 0.18375707, 0.84396595, 0.17427363, 0.7294118, 0.18375707, 0.7294118, 0.17427363, 1, 0.18375707, 1, 0.17427363, 0.99622643, 0.18375707, 0.21132076 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.20000002, \"color\": \"d7baff00\" }\n\t\t\t\t],\n\t\t\t\t\"attachment\": [\n\t\t\t\t\t{ \"time\": 0.033333335, \"name\": \"muzzle-ring\" },\n\t\t\t\t\t{ \"time\": 0.20000002 }\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t\"bones\": {\n\t\t\t\"gun\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"curve\": [ 0.09373267, 25.889433, 0.1123115, 45.26556 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 45.348244,\n\t\t\t\t\t\t\"curve\": [ 0.19222969, 45.27915, 0.17955978, -0.09420347 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.6333334 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"muzzle\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": -11.021545, \"y\": 25.164276 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-upper-arm\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.033333335,\n\t\t\t\t\t\t\"curve\": [ 0.04513854, 0.90854895, 0.083333336, 3.4565647, 0.043651193, 0.86433274, 0.083333336, 3.319756 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"x\": 3.4565647,\n\t\t\t\t\t\t\"y\": 3.319756,\n\t\t\t\t\t\t\"curve\": [ 0.13333334, 3.4565647, 0.17578618, -0.098475456, 0.13333334, 3.319756, 0.16949688, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.23333335 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-bracer\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.033333335,\n\t\t\t\t\t\t\"curve\": [ 0.07454278, -3.7831078, 0.083333336, -4.361145, 0.08020648, -2.7022202, 0.083333336, -2.880127 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"x\": -4.361145,\n\t\t\t\t\t\t\"y\": -2.880127,\n\t\t\t\t\t\t\"curve\": [ 0.13333334, -4.361145, 0.16790564, 0.1801529, 0.13333334, -2.880127, 0.16727631, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.23333335 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"gun-tip\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{},\n\t\t\t\t\t{ \"time\": 0.3, \"x\": 3.1514282, \"y\": 0.3925476 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{ \"x\": 0.36608857, \"y\": 0.36608857 },\n\t\t\t\t\t{ \"time\": 0.033333335, \"x\": 1.4526132, \"y\": 1.4526132 },\n\t\t\t\t\t{ \"time\": 0.3, \"x\": 0.36608857, \"y\": 0.36608857 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"muzzle-ring\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"time\": 0.033333335 },\n\t\t\t\t\t{ \"time\": 0.23333335, \"x\": 64.47002 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{ \"time\": 0.033333335 },\n\t\t\t\t\t{ \"time\": 0.23333335, \"x\": 5.9507008, \"y\": 5.9507008 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"muzzle-ring2\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"time\": 0.033333335 },\n\t\t\t\t\t{ \"time\": 0.20000002, \"x\": 172.5692 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{ \"time\": 0.033333335 },\n\t\t\t\t\t{ \"time\": 0.20000002, \"x\": 4, \"y\": 4 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"muzzle-ring3\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"time\": 0.033333335 },\n\t\t\t\t\t{ \"time\": 0.20000002, \"x\": 277.1667 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{ \"time\": 0.033333335 },\n\t\t\t\t\t{ \"time\": 0.20000002, \"x\": 2, \"y\": 2 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"muzzle-ring4\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"time\": 0.033333335 },\n\t\t\t\t\t{ \"time\": 0.20000002, \"x\": 392.05563 }\n\t\t\t\t]\n\t\t\t}\n\t\t}\n\t},\n\t\"walk\": {\n\t\t\"bones\": {\n\t\t\t\"rear-foot-target\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -32.81834,\n\t\t\t\t\t\t\"curve\": [ 0.035177726, -42.686375, 0.057151146, -70.49265 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"value\": -70.5925,\n\t\t\t\t\t\t\"curve\": [ 0.23636356, -70.77538, 0.33513966, -9.8731165 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": -1.5615536,\n\t\t\t\t\t\t\"curve\": [ 0.39346445, 5.503226, 0.47666666, 13.96162 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": 13.96162,\n\t\t\t\t\t\t\"curve\": [ 0.51903033, 13.96162, 0.5078729, 0.13215438 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.5666667, \"value\": -0.27927172 },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": -0.28445435,\n\t\t\t\t\t\t\"curve\": [ 0.82710564, -0.059506178, 0.95807016, -21.071537 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": -32.81834 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -167.31882,\n\t\t\t\t\t\t\"y\": 0.5788574,\n\t\t\t\t\t\t\"curve\": [ 0.022485092, -180.54509, 0.075, -235.51065, 0.045408167, 0.57885754, 0.075, 30.117172 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"x\": -235.51065,\n\t\t\t\t\t\t\"y\": 39.9159,\n\t\t\t\t\t\t\"curve\": [ 0.14210089, -235.51065, 0.20833334, -201.72728, 0.13834119, 54.94375, 0.17957518, 60.779694 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"x\": -176.32953,\n\t\t\t\t\t\t\"y\": 61.477715,\n\t\t\t\t\t\t\"curve\": [ 0.27243412, -136.60674, 0.32057193, -45.17649, 0.2747891, 62.016003, 0.32139045, 56.60431 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"x\": 8.439873,\n\t\t\t\t\t\t\"y\": 49.669117,\n\t\t\t\t\t\t\"curve\": [ 0.4028122, 51.03225, 0.48574486, 66.86314, 0.40123776, 44.373684, 0.48022777, 23.10683 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.5, \"x\": 66.57163, \"y\": 14.215683 },\n\t\t\t\t\t{ \"time\": 0.53333336, \"x\": 52.583878, \"y\": 0.60189223 },\n\t\t\t\t\t{ \"time\": 1, \"x\": -167.31882, \"y\": 0.5788574 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-foot-target\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 18.190496,\n\t\t\t\t\t\t\"curve\": [ 0.010253842, 11.169012, 0.043031935, 1.3740194 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.10000001, \"value\": 0.47377563 },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": 0.549647,\n\t\t\t\t\t\t\"curve\": [ 0.36355883, 0.30490732, 0.5154175, -80.48494 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": -80.78354,\n\t\t\t\t\t\t\"curve\": [ 0.788268, -80.380875, 0.9209628, 17.418287 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": 18.190496 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 139.21338,\n\t\t\t\t\t\t\"y\": 22.940594,\n\t\t\t\t\t\t\"curve\": [ 0.025000002, 139.21338, 0.06885809, 111.46258, 0.031141926, 3.2452793, 0.075, 0.062235013 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.10000001, \"x\": 96.6911, \"y\": 0.062235013 },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"x\": -94.86861,\n\t\t\t\t\t\t\"y\": -0.026491642,\n\t\t\t\t\t\t\"curve\": [ 0.51828384, -106.82498, 0.57500005, -152.55959, 0.53374636, 5.423584, 0.55704737, 38.45566 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"x\": -152.55959,\n\t\t\t\t\t\t\"y\": 57.052914,\n\t\t\t\t\t\t\"curve\": [ 0.6333334, -152.55959, 0.68800294, -128.047, 0.6428554, 75.60812, 0.70000005, 84.14295 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"x\": -109.42177,\n\t\t\t\t\t\t\"y\": 84.14297,\n\t\t\t\t\t\t\"curve\": [ 0.7710902, -93.908325, 0.83183736, -30.644817, 0.7872632, 84.14297, 0.79855126, 89.6509 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"x\": 16.99742,\n\t\t\t\t\t\t\"y\": 75.25434,\n\t\t\t\t\t\t\"curve\": [ 0.9026194, 66.17633, 0.9666667, 139.21338, 0.9315787, 61.534866, 0.9666667, 44.021843 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": 139.21338, \"y\": 22.940594 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hip\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": -4.354578 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -2.8556595,\n\t\t\t\t\t\t\"y\": -13.8589325,\n\t\t\t\t\t\t\"curve\": [ 0.025000002, -2.8436203, 0.0672245, -2.815521, 0.028263828, -19.13565, 0.054132234, -24.017632 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"x\": -2.6091037,\n\t\t\t\t\t\t\"y\": -24.187546,\n\t\t\t\t\t\t\"curve\": [ 0.14349826, -2.3351557, 0.20189808, -1.788381, 0.1520444, -23.984085, 0.21322109, -14.811488 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"x\": -1.2142082,\n\t\t\t\t\t\t\"y\": -7.1223755,\n\t\t\t\t\t\t\"curve\": [ 0.3076929, -0.85728943, 0.34510875, -0.5055015, 0.30601695, -1.6301517, 0.3410871, 3.1525662 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"x\": -0.33093643,\n\t\t\t\t\t\t\"y\": 3.1525726,\n\t\t\t\t\t\t\"curve\": [ 0.40996072, 0.019638062, 0.45847303, 0.25751495, 0.42667913, 3.304947, 0.48069695, -6.751709 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"x\": 0.25933456,\n\t\t\t\t\t\t\"y\": -10.585007,\n\t\t\t\t\t\t\"curve\": [ 0.5530761, 0.2616577, 0.5589796, 0.19515991, 0.5192444, -14.406647, 0.5481417, -23.88285 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"x\": -0.16674805,\n\t\t\t\t\t\t\"y\": -23.708298,\n\t\t\t\t\t\t\"curve\": [ 0.6630033, -0.72260284, 0.7975605, -2.094368, 0.70228803, -23.363998, 0.8024797, 3.5312805 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"x\": -2.45504,\n\t\t\t\t\t\t\"y\": 3.4846344,\n\t\t\t\t\t\t\"curve\": [ 0.9009761, -2.6341019, 0.9666667, -2.8717117, 0.91301715, 3.4509735, 0.9666667, -7.635769 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": -2.8556595, \"y\": -13.8589325 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-foot-tip\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 28.964193,\n\t\t\t\t\t\t\"curve\": [ 0.056264512, 28.744823, 0.048903983, 19.603014 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.06666667, \"value\": 1.6757278 },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": -10.004452,\n\t\t\t\t\t\t\"curve\": [ 0.52500004, -10.004452, 0.59235626, -54.693066 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": -59.66399,\n\t\t\t\t\t\t\"curve\": [ 0.6228758, -74.54068, 0.674358, -101.77973 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": -101.77972,\n\t\t\t\t\t\t\"curve\": [ 0.8121934, -101.77971, 0.8554095, -84.668015 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"value\": -63.52591,\n\t\t\t\t\t\t\"curve\": [ 0.86940855, -58.376495, 0.975, 28.964195 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": 28.964195 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -20.718887,\n\t\t\t\t\t\t\"curve\": [ 0.02514851, -20.570282, 0.07101186, -20.040886 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": -20.042961,\n\t\t\t\t\t\t\"curve\": [ 0.18665472, -20.044724, 0.28532633, -21.16227 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": -21.163452,\n\t\t\t\t\t\t\"curve\": [ 0.40486357, -21.163992, 0.46991742, -20.901611 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": -20.710213,\n\t\t\t\t\t\t\"curve\": [ 0.5176708, -20.597786, 0.5816134, -20.034668 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": -20.039696,\n\t\t\t\t\t\t\"curve\": [ 0.7092406, -20.047066, 0.81480014, -21.180275 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"value\": -21.177917,\n\t\t\t\t\t\t\"curve\": [ 0.90829295, -21.176025, 0.9711426, -20.930069 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": -20.718887 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"neck\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 17.7811,\n\t\t\t\t\t\t\"curve\": [ 0.02514851, 17.929705, 0.07101186, 18.4591 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 18.457026,\n\t\t\t\t\t\t\"curve\": [ 0.18665472, 18.455263, 0.28532633, 17.33772 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": 17.336538,\n\t\t\t\t\t\t\"curve\": [ 0.40486357, 17.335999, 0.46991742, 17.598377 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": 17.789774,\n\t\t\t\t\t\t\"curve\": [ 0.5176708, 17.9022, 0.5816134, 18.465319 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": 18.46029,\n\t\t\t\t\t\t\"curve\": [ 0.7092406, 18.45292, 0.81480014, 17.319715 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"value\": 17.322073,\n\t\t\t\t\t\t\"curve\": [ 0.90829295, 17.323965, 0.9711426, 17.56992 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": 17.7811 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"head\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -12.234505,\n\t\t\t\t\t\t\"curve\": [ 0.061436713, -12.234504, 0.19128086, -7.4490128 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.26666668,\n\t\t\t\t\t\t\"value\": -7.434601,\n\t\t\t\t\t\t\"curve\": [ 0.34068906, -7.42045, 0.42060423, -12.234505 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": -12.234505,\n\t\t\t\t\t\t\"curve\": [ 0.56714934, -12.26333, 0.6940113, -7.456505 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.7666667,\n\t\t\t\t\t\t\"value\": -7.4722376,\n\t\t\t\t\t\t\"curve\": [ 0.85296947, -7.4909286, 0.942855, -12.234505 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": -12.234505 }\n\t\t\t\t],\n\t\t\t\t\"scale\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.039166667, 1, 0.084469445, 0.99129236, 0.039166667, 1, 0.084469445, 1.0190994 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"x\": 0.99129236,\n\t\t\t\t\t\t\"y\": 1.0190994,\n\t\t\t\t\t\t\"curve\": [ 0.20465556, 0.99129236, 0.31767017, 1.0191765, 0.20465556, 1.0190994, 0.3367928, 0.9916709 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.40000004,\n\t\t\t\t\t\t\"x\": 1.0190022,\n\t\t\t\t\t\t\"y\": 0.9915339,\n\t\t\t\t\t\t\"curve\": [ 0.45603743, 1.0188838, 0.49366122, 1.001296, 0.48314697, 0.9913536, 0.49272755, 0.99871767 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"curve\": [ 0.5076706, 0.9984317, 0.58446944, 0.99129236, 0.5099203, 1.0017492, 0.58446944, 1.0190994 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"x\": 0.99129236,\n\t\t\t\t\t\t\"y\": 1.0190994,\n\t\t\t\t\t\t\"curve\": [ 0.70465565, 0.99129236, 0.8176702, 1.0191765, 0.70465565, 1.0190994, 0.8367928, 0.9916709 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.90000004,\n\t\t\t\t\t\t\"x\": 1.0190022,\n\t\t\t\t\t\t\"y\": 0.9915339,\n\t\t\t\t\t\t\"curve\": [ 0.95603746, 1.0188838, 0.9553667, 1, 0.98314697, 0.9913536, 0.9553667, 1 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"back-foot-tip\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": 4.087345 },\n\t\t\t\t\t{ \"time\": 0.033333335, \"value\": 3.052846 },\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"value\": -59.00914,\n\t\t\t\t\t\t\"curve\": [ 0.1236928, -72.96686, 0.1688272, -100.052795 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": -99.7063,\n\t\t\t\t\t\t\"curve\": [ 0.3260634, -99.208176, 0.34862125, -37.39541 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": -17.847069,\n\t\t\t\t\t\t\"curve\": [ 0.387521, 4.744033, 0.4514076, 32.350677 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": 32.39831,\n\t\t\t\t\t\t\"curve\": [ 0.53745496, 32.43502, 0.56564134, 6.4256186 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 0.5666667, \"value\": 1.9995908 },\n\t\t\t\t\t{ \"time\": 1, \"value\": 4.087345 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-thigh\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 17.154694,\n\t\t\t\t\t\t\"y\": -0.0851593,\n\t\t\t\t\t\t\"curve\": [ 0.17816046, 17.1408, 0.29496992, -4.258095, 0.008833333, -0.0851593, 0.47500002, 0.021209717 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"x\": -4.263954,\n\t\t\t\t\t\t\"y\": 0.021209717,\n\t\t\t\t\t\t\"curve\": [ 0.70503014, -4.2698097, 0.8475542, 17.145517, 0.52500004, 0.021209717, 0.975, -0.0851593 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": 17.154694, \"y\": -0.0851593 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-thigh\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -17.706324,\n\t\t\t\t\t\t\"y\": -4.631317,\n\t\t\t\t\t\t\"curve\": [ 0.036444448, -19.80997, 0.0431217, -20.856989, 0.036444448, -4.631317, 0.049517047, -7.0312443 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"x\": -20.948288,\n\t\t\t\t\t\t\"y\": -7.064457,\n\t\t\t\t\t\t\"curve\": [ 0.16200243, -21.047812, 0.40000004, 7.7919636, 0.20000002, -7.130247, 0.40004575, -1.8982906 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"x\": 7.7919636,\n\t\t\t\t\t\t\"y\": -1.9355936,\n\t\t\t\t\t\t\"curve\": [ 0.61214966, 7.6929226, 0.87500006, -10.4910755, 0.5922905, -1.9700351, 0.9168846, -3.2536535 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": -17.706324, \"y\": -4.631317 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 1.0006008,\n\t\t\t\t\t\t\"curve\": [ 0.0055842493, 1.196775, 0.08436342, 2.8825326 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.13333334,\n\t\t\t\t\t\t\"value\": 2.8825326,\n\t\t\t\t\t\t\"curve\": [ 0.2047951, 2.8825326, 0.28441125, -1.1665286 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": -1.1665286,\n\t\t\t\t\t\t\"curve\": [ 0.41125, -1.1665286, 0.48092797, 0.5709099 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": 1.0006008,\n\t\t\t\t\t\t\"curve\": [ 0.5145122, 1.3275578, 0.59002084, 2.8342242 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6333334,\n\t\t\t\t\t\t\"value\": 2.848558,\n\t\t\t\t\t\t\"curve\": [ 0.68273115, 2.8649056, 0.79590666, -1.2044369 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"value\": -1.2044369,\n\t\t\t\t\t\t\"curve\": [ 0.91603166, -1.2044369, 0.9839159, 0.6246002 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": 1.0006008 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"torso3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"value\": -1.8100241 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -9.513367,\n\t\t\t\t\t\t\"curve\": [ 0.0214478, -13.319992, 0.057899177, -19.404175 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"value\": -19.404175,\n\t\t\t\t\t\t\"curve\": [ 0.23778662, -19.68686, 0.33747208, 7.7796316 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": 16.196716,\n\t\t\t\t\t\t\"curve\": [ 0.39864385, 25.41603, 0.4965658, 60.193893 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": 60.260864,\n\t\t\t\t\t\t\"curve\": [ 0.7185025, 60.129913, 0.8452277, 27.61441 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"value\": 22.449387,\n\t\t\t\t\t\t\"curve\": [ 0.8918752, 16.376228, 0.9789887, -3.2677646 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": -9.513367 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 13.566517,\n\t\t\t\t\t\t\"curve\": [ 0.021665983, 9.707241, 0.14701591, -3.7799788 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": -3.6853304,\n\t\t\t\t\t\t\"curve\": [ 0.4572524, -3.6595783, 0.47930667, 0.83387756 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": 4.0535965,\n\t\t\t\t\t\t\"curve\": [ 0.51304764, 6.0836887, 0.6349345, 30.804699 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.8000001,\n\t\t\t\t\t\t\"value\": 30.91521,\n\t\t\t\t\t\t\"curve\": [ 0.9739747, 31.002415, 0.9800564, 18.353865 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": 13.566517 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-fist\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -28.723831,\n\t\t\t\t\t\t\"curve\": [ 0.02368659, -31.7421, 0.1760204, -43.395367 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": -43.596443,\n\t\t\t\t\t\t\"curve\": [ 0.4030356, -43.64886, 0.47042817, -40.14865 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": -35.63085,\n\t\t\t\t\t\t\"curve\": [ 0.5466348, -28.591816, 0.624065, -4.573129 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": -4.592627,\n\t\t\t\t\t\t\"curve\": [ 0.89064777, -4.620701, 0.9538757, -24.27828 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": -28.480907 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-upper-arm\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 28.27774,\n\t\t\t\t\t\t\"curve\": [ 0.033908315, 30.940815, 0.06758397, 32.047134 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"value\": 31.881027,\n\t\t\t\t\t\t\"curve\": [ 0.19429052, 31.013386, 0.3364573, -0.11212158 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": -7.11055,\n\t\t\t\t\t\t\"curve\": [ 0.42111933, -19.725266, 0.5301549, -46.211 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": -45.74681,\n\t\t\t\t\t\t\"curve\": [ 0.7075172, -45.03224, 0.84383875, -13.561066 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"value\": -6.4833527,\n\t\t\t\t\t\t\"curve\": [ 0.90882653, 6.58815, 0.95758545, 24.209595 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": 28.27774 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -2.7900658,\n\t\t\t\t\t\t\"curve\": [ 0.07374832, -2.8390694, 0.12107788, 25.084599 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": 24.989323,\n\t\t\t\t\t\t\"curve\": [ 0.34985632, 24.890423, 0.427126, -2.8623238 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": -2.799221,\n\t\t\t\t\t\t\"curve\": [ 0.57537323, -2.7339592, 0.6522828, 24.502758 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": 24.54565,\n\t\t\t\t\t\t\"curve\": [ 0.8276177, 24.59554, 0.93160766, -2.6920433 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": -2.7900658 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair4\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -6.013939,\n\t\t\t\t\t\t\"curve\": [ 0.10559352, -5.9694066, 0.15074933, 18.622261 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": 18.723427,\n\t\t\t\t\t\t\"curve\": [ 0.33553022, 18.695812, 0.40528488, -11.373627 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": -11.454224,\n\t\t\t\t\t\t\"curve\": [ 0.62631094, -11.455711, 0.6293055, 18.935051 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": 18.919914,\n\t\t\t\t\t\t\"curve\": [ 0.8333667, 18.919914, 0.9129917, -6.0606966 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": -6.013939 }\n\t\t\t\t],\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{ \"x\": 0.03475952, \"y\": 1.3458252 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"rear-bracer\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": 10.059132,\n\t\t\t\t\t\t\"curve\": [ 0.044048578, 11.161348, 0.06296488, 11.485394 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"value\": 11.489979,\n\t\t\t\t\t\t\"curve\": [ 0.21508875, 11.493621, 0.33587736, 2.9205174 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": 0.8411312,\n\t\t\t\t\t\t\"curve\": [ 0.41645247, -2.521204, 0.49804613, -10.835702 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": -10.82903,\n\t\t\t\t\t\t\"curve\": [ 0.76204896, -10.713412, 0.84460163, -3.0456808 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"value\": -1.3417969,\n\t\t\t\t\t\t\"curve\": [ 0.9168806, 2.5357628, 0.97747505, 8.810119 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": 10.059132 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"gun\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"value\": -14.673275,\n\t\t\t\t\t\t\"curve\": [ 0.08551843, -14.673275, 0.20198415, 8.30991 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": 12.136024,\n\t\t\t\t\t\t\"curve\": [ 0.2789693, 17.705822, 0.39149016, 25.794325 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": 25.770123,\n\t\t\t\t\t\t\"curve\": [ 0.63131285, 25.740833, 0.69434565, 4.530927 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": -0.6513629,\n\t\t\t\t\t\t\"curve\": [ 0.76762193, -5.209029, 0.90234536, -14.40159 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"value\": -14.673275 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-leg-target\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -2.8334198,\n\t\t\t\t\t\t\"y\": -8.48053,\n\t\t\t\t\t\t\"curve\": [ 0.008333334, -2.8334198, 0.058333337, 0.09416199, 0.0010207205, 4.973398, 0.058333337, 6.677925 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.06666667,\n\t\t\t\t\t\t\"x\": 0.09416199,\n\t\t\t\t\t\t\"y\": 6.677925,\n\t\t\t\t\t\t\"curve\": [ 0.3, 0.09416199, 0.7666667, -2.8334198, 0.3, 6.677925, 0.7666667, -8.48053 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": -2.8334198, \"y\": -8.48053 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair1\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.027893249, 1.242131, 0.016340261, 3.4572334 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.10000001,\n\t\t\t\t\t\t\"value\": 3.452011,\n\t\t\t\t\t\t\"curve\": [ 0.15888937, 3.448334, 0.18879353, 0.2309022 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": -2.2886696,\n\t\t\t\t\t\t\"curve\": [ 0.265451, -4.3160477, 0.30461574, -5.9224977 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.36666667,\n\t\t\t\t\t\t\"value\": -5.9397507,\n\t\t\t\t\t\t\"curve\": [ 0.4464736, -5.961938, 0.51965445, 3.4063644 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.6,\n\t\t\t\t\t\t\"value\": 3.4233627,\n\t\t\t\t\t\t\"curve\": [ 0.7168544, 3.417797, 0.77153295, -5.931904 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.86666673,\n\t\t\t\t\t\t\"value\": -5.965309,\n\t\t\t\t\t\t\"curve\": [ 0.9329225, -5.9885674, 0.9819045, -0.9372777 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"hair3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"curve\": [ 0.066672675, 0, 0.1593553, -10.483919 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"value\": -10.492886,\n\t\t\t\t\t\t\"curve\": [ 0.3338235, -10.499889, 0.43866077, -0.08680725 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"value\": -0.08680725,\n\t\t\t\t\t\t\"curve\": [ 0.56881034, -0.08680725, 0.658053, -10.74765 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"value\": -10.697582,\n\t\t\t\t\t\t\"curve\": [ 0.8333085, -10.631088, 0.9472112, 0 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"gun-tip\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 0.23333335, \"value\": 0.108623505 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"muzzle-ring\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 0.23333335, \"value\": 0.108623505 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"muzzle-ring2\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 0.26666668, \"value\": 0.108623505 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"muzzle-ring3\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 0.26666668, \"value\": 0.108623505 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"muzzle-ring4\": {\n\t\t\t\t\"rotate\": [\n\t\t\t\t\t{ \"time\": 0.26666668, \"value\": 0.108623505 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"back-shoulder\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -0.18066406,\n\t\t\t\t\t\t\"y\": -4.490013,\n\t\t\t\t\t\t\"curve\": [ 0.13333334, -0.18066406, 0.33333334, 7.685893, 0.13333334, -4.490013, 0.33333334, 2.7705803 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.4666667,\n\t\t\t\t\t\t\"x\": 7.685893,\n\t\t\t\t\t\t\"y\": 2.7705803,\n\t\t\t\t\t\t\"curve\": [ 0.6, 7.685893, 0.85833335, -0.18066406, 0.6, 2.7705803, 0.85833335, -4.490013 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": -0.18066406, \"y\": -4.490013 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"front-shoulder\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": 1.4606628,\n\t\t\t\t\t\t\"y\": 9.368988,\n\t\t\t\t\t\t\"curve\": [ 0.16249871, 1.4141464, 0.3326475, -1.6649847, 0.16249873, 9.368988, 0.3011157, -7.232888 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"x\": -1.5982952,\n\t\t\t\t\t\t\"y\": -7.2696686,\n\t\t\t\t\t\t\"curve\": [ 0.73537385, -1.5045013, 0.84722304, 1.4606628, 0.72336817, -7.310975, 0.8375013, 9.32247 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": 1.4606628, \"y\": 9.368988 }\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"head-control\": {\n\t\t\t\t\"translate\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"x\": -6.4599304,\n\t\t\t\t\t\t\"y\": -8.401773,\n\t\t\t\t\t\t\"curve\": [ 0.05273333, -5.3143463, 0.1667785, -3.6382523, 0.09303271, -8.401774, 0.1959178, -3.8102424 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.23333335,\n\t\t\t\t\t\t\"x\": -3.6382523,\n\t\t\t\t\t\t\"y\": -1.3223945,\n\t\t\t\t\t\t\"curve\": [ 0.30929983, -3.6382523, 0.43642506, -5.842407, 0.2746537, 1.4250946, 0.3798138, 10.303744 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\t\"x\": -7.0345078,\n\t\t\t\t\t\t\"y\": 10.285416,\n\t\t\t\t\t\t\"curve\": [ 0.53828996, -7.752487, 0.65951717, -10.53656, 0.5982467, 10.270431, 0.6943732, 1.5578316 ]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"time\": 0.73333335,\n\t\t\t\t\t\t\"x\": -10.53656,\n\t\t\t\t\t\t\"y\": -1.2572263,\n\t\t\t\t\t\t\"curve\": [ 0.7973799, -10.53656, 0.9333334, -7.908203, 0.76843554, -3.7935178, 0.8749289, -8.4017725 ]\n\t\t\t\t\t},\n\t\t\t\t\t{ \"time\": 1, \"x\": -6.4599304, \"y\": -8.401773 }\n\t\t\t\t]\n\t\t\t}\n\t\t},\n\t\t\"ik\": {\n\t\t\t\"front-leg-ik\": [\n\t\t\t\t{\n\t\t\t\t\t\"softness\": 25.7,\n\t\t\t\t\t\"bendPositive\": false,\n\t\t\t\t\t\"curve\": [ 0.008333334, 1, 0.025000002, 1, 0.008333334, 25.7, 0.025000002, 9.9 ]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"time\": 0.033333335,\n\t\t\t\t\t\"softness\": 9.9,\n\t\t\t\t\t\"bendPositive\": false,\n\t\t\t\t\t\"curve\": [ 0.15, 1, 0.38333336, 1, 0.15, 9.9, 0.38333336, 43.2 ]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"time\": 0.5,\n\t\t\t\t\t\"softness\": 43.2,\n\t\t\t\t\t\"bendPositive\": false,\n\t\t\t\t\t\"curve\": [ 0.62500006, 1, 0.87500006, 1, 0.62500006, 43.2, 0.8455137, 45.56755 ]\n\t\t\t\t},\n\t\t\t\t{ \"time\": 1, \"softness\": 25.7, \"bendPositive\": false }\n\t\t\t],\n\t\t\t\"rear-leg-ik\": [\n\t\t\t\t{ \"softness\": 5, \"bendPositive\": false },\n\t\t\t\t{ \"time\": 0.43333337, \"softness\": 4.9, \"bendPositive\": false },\n\t\t\t\t{ \"time\": 0.5, \"softness\": 28.81148, \"bendPositive\": false },\n\t\t\t\t{ \"time\": 0.6, \"softness\": 43.8, \"bendPositive\": false },\n\t\t\t\t{ \"time\": 1, \"softness\": 5, \"bendPositive\": false }\n\t\t\t]\n\t\t},\n\t\t\"events\": [\n\t\t\t{ \"name\": \"footstep\" },\n\t\t\t{ \"time\": 0.5, \"name\": \"footstep\" }\n\t\t]\n\t}\n}\n}"
  },
  {
    "path": "examples/assets/spine/spineboy.atlas",
    "content": "spineboy.png\n\tsize: 1024, 256\n\tfilter: Linear, Linear\n\tscale: 0.5\ncrosshair\n\tbounds: 263, 11, 45, 45\neye-indifferent\n\tbounds: 214, 11, 47, 45\neye-surprised\n\tbounds: 965, 33, 47, 45\n\trotate: 90\nfront-bracer\n\tbounds: 2, 5, 29, 40\n\trotate: 90\nfront-fist-closed\n\tbounds: 505, 3, 38, 41\n\trotate: 90\nfront-fist-open\n\tbounds: 790, 9, 43, 44\n\trotate: 90\nfront-foot\n\tbounds: 149, 21, 63, 35\nfront-shin\n\tbounds: 505, 43, 41, 92\n\trotate: 90\nfront-thigh\n\tbounds: 359, 14, 23, 56\n\trotate: 90\nfront-upper-arm\n\tbounds: 955, 8, 23, 49\n\trotate: 90\ngoggles\n\tbounds: 180, 58, 131, 83\ngun\n\tbounds: 313, 39, 105, 102\nhead\n\tbounds: 29, 83, 136, 149\n\trotate: 90\nhoverboard-board\n\tbounds: 180, 143, 246, 76\nhoverboard-thruster\n\tbounds: 790, 57, 30, 32\nhoverglow-small\n\tbounds: 826, 54, 137, 38\nmouth-grind\n\tbounds: 707, 8, 47, 30\nmouth-oooo\n\tbounds: 658, 8, 47, 30\nmouth-smile\n\tbounds: 548, 11, 47, 30\nmuzzle-glow\n\tbounds: 997, 194, 25, 25\nmuzzle-ring\n\tbounds: 2, 114, 25, 105\nmuzzle01\n\tbounds: 965, 82, 67, 40\n\trotate: 90\nmuzzle02\n\tbounds: 953, 151, 68, 42\n\trotate: 90\nmuzzle03\n\tbounds: 420, 31, 83, 53\nmuzzle04\n\tbounds: 2, 36, 75, 45\nmuzzle05\n\tbounds: 79, 43, 68, 38\nneck\n\tbounds: 997, 171, 18, 21\nportal-bg\n\tbounds: 563, 86, 133, 133\nportal-flare1\n\tbounds: 79, 11, 56, 30\nportal-flare2\n\tbounds: 836, 21, 57, 31\nportal-flare3\n\tbounds: 895, 22, 58, 30\nportal-shade\n\tbounds: 428, 86, 133, 133\nportal-streaks1\n\tbounds: 698, 91, 126, 128\nportal-streaks2\n\tbounds: 826, 94, 125, 125\nrear-bracer\n\tbounds: 756, 2, 28, 36\nrear-foot\n\tbounds: 599, 14, 57, 30\nrear-shin\n\tbounds: 599, 46, 38, 89\n\trotate: 90\nrear-thigh\n\tbounds: 310, 9, 28, 47\n\trotate: 90\nrear-upper-arm\n\tbounds: 417, 9, 20, 44\n\trotate: 90\ntorso\n\tbounds: 698, 40, 49, 90\n\trotate: 90\n"
  },
  {
    "path": "examples/assets/tiles/0x72_DungeonTilesetII_v1.4.tsx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<tileset version=\"1.9\" tiledversion=\"1.9.2\" name=\"0x72_DungeonTilesetII_v1.4\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"1024\" columns=\"32\">\n <image source=\"../images/0x72_DungeonTilesetII_v1.4.png\" width=\"512\" height=\"512\"/>\n <tile id=\"36\">\n  <animation>\n   <frame tileid=\"36\" duration=\"180\"/>\n   <frame tileid=\"37\" duration=\"180\"/>\n   <frame tileid=\"38\" duration=\"180\"/>\n  </animation>\n </tile>\n <tile id=\"68\">\n  <animation>\n   <frame tileid=\"68\" duration=\"180\"/>\n   <frame tileid=\"69\" duration=\"180\"/>\n   <frame tileid=\"70\" duration=\"180\"/>\n  </animation>\n </tile>\n <tile id=\"100\">\n  <animation>\n   <frame tileid=\"100\" duration=\"180\"/>\n   <frame tileid=\"101\" duration=\"180\"/>\n   <frame tileid=\"102\" duration=\"180\"/>\n  </animation>\n </tile>\n <tile id=\"132\">\n  <animation>\n   <frame tileid=\"132\" duration=\"180\"/>\n   <frame tileid=\"133\" duration=\"180\"/>\n   <frame tileid=\"134\" duration=\"180\"/>\n  </animation>\n </tile>\n <tile id=\"356\">\n  <animation>\n   <frame tileid=\"353\" duration=\"70\"/>\n   <frame tileid=\"354\" duration=\"70\"/>\n   <frame tileid=\"355\" duration=\"70\"/>\n   <frame tileid=\"356\" duration=\"70\"/>\n   <frame tileid=\"355\" duration=\"70\"/>\n   <frame tileid=\"354\" duration=\"70\"/>\n  </animation>\n </tile>\n <tile id=\"368\">\n  <animation>\n   <frame tileid=\"44\" duration=\"130\"/>\n   <frame tileid=\"45\" duration=\"130\"/>\n   <frame tileid=\"46\" duration=\"130\"/>\n   <frame tileid=\"47\" duration=\"130\"/>\n   <frame tileid=\"48\" duration=\"130\"/>\n  </animation>\n </tile>\n <tile id=\"496\">\n  <animation>\n   <frame tileid=\"488\" duration=\"210\"/>\n   <frame tileid=\"489\" duration=\"210\"/>\n   <frame tileid=\"490\" duration=\"210\"/>\n   <frame tileid=\"491\" duration=\"210\"/>\n  </animation>\n </tile>\n <tile id=\"627\">\n  <animation>\n   <frame tileid=\"627\" duration=\"210\"/>\n   <frame tileid=\"628\" duration=\"210\"/>\n   <frame tileid=\"629\" duration=\"210\"/>\n   <frame tileid=\"628\" duration=\"210\"/>\n  </animation>\n </tile>\n <tile id=\"659\">\n  <animation>\n   <frame tileid=\"659\" duration=\"210\"/>\n   <frame tileid=\"660\" duration=\"210\"/>\n   <frame tileid=\"661\" duration=\"210\"/>\n  </animation>\n </tile>\n</tileset>\n"
  },
  {
    "path": "examples/assets/tiles/dungeon.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.2\" orientation=\"orthogonal\" renderorder=\"right-down\" width=\"10\" height=\"10\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" nextlayerid=\"6\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" source=\"0x72_DungeonTilesetII_v1.4.tsx\"/>\n <layer id=\"3\" name=\"ground\" width=\"10\" height=\"10\" locked=\"1\">\n  <data encoding=\"base64\" compression=\"gzip\">\n   H4sIAAAAAAAACmNgoAw0Y6FHMfkYAK7z31GQAQAA\n  </data>\n </layer>\n <layer id=\"1\" name=\"walls\" width=\"10\" height=\"10\">\n  <data encoding=\"base64\" compression=\"gzip\">\n   H4sIAAAAAAAACp3Q7QmDMBSF4ZuELFIVF9FNdBQXae0i4scegnYNXzDgKfSH9MJDksPh/og3My+i0Dw4s4ddiqTVjI6HTp10krkfPZ3xZu/uvn97Zeo+OV/o8XbfvQzBTgP3GQsmyaP8XYOV944PNuRkFTIcCfCt35ABAAA=\n  </data>\n </layer>\n <layer id=\"4\" name=\"stuff\" width=\"10\" height=\"10\">\n  <data encoding=\"base64\" compression=\"gzip\">\n   H4sIAAAAAAAACmNgoA0oYaKNuamMuOW2IqmBYRgwQ2JfxWHeFKCbw/CYTw8AANEJHwqQAQAA\n  </data>\n </layer>\n</map>\n"
  },
  {
    "path": "examples/assets/yarn/advanced.yarn",
    "content": "<<character Jenny>>\n<<declare $winnings = 0>>\ntitle: gamble\n---\nJenny: Hello {$playerName}. This is a game of chance.\nJenny: You can win or lose up to 10 coins. Do you want to play?\n-> No\n    Jenny: No bids made.\n-> Yes\n    <<set $winnings = dice(21) - 11>> // returns a random value from -10 to 10\n    <<if $winnings == 0 >>\n        Jenny: Too bad, you did not win anything.\n    <<elseif $winnings < 0 >>\n        Jenny: Bad luck. You lost {$winnings} coins.\n        Jenny: Play again to change your fortunes.\n    <<else>>\n        Jenny: Congratulations! You won {$winnings} coins.\n        Jenny: Play again to win even more.\n    <<endif>>\n    <<updateCoins {$winnings}>>\n==="
  },
  {
    "path": "examples/assets/yarn/command_lifecycle.yarn",
    "content": "<<character Jenny>>\n<<declare $exampleVariable = \"foo\">>\ntitle: command_lifecycle\n---\nJenny: The variable starts out as foo, then becomes bar.\n<<exampleCommand bar>>\n==="
  },
  {
    "path": "examples/assets/yarn/simple.yarn",
    "content": "<<character Jenny>>\ntitle: hello_world\n---\nJenny: Hello world. My name is Jenny.\nJenny: Thanks for using Flame!\n==="
  },
  {
    "path": "examples/games/crystal_ball/README.md",
    "content": "# crystal_ball\n\nA game to showcase the shader pipeline API in Flame.\n"
  },
  {
    "path": "examples/games/crystal_ball/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "examples/games/crystal_ball/lib/crystal_ball.dart",
    "content": "import 'dart:ui';\n\nimport 'package:crystal_ball/src/game/game.dart';\n\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass CrystalBallWidget extends StatefulWidget {\n  const CrystalBallWidget({super.key});\n\n  static const String description = '''\nA demonstration of how to leverage the power of Fragment Shaders in Flame.\nThe game is a simple crystal ball that jumps around in a swampy world.\nIt uses custom post process shaders to create fog, fireflies, water reflections\nand the glowing effect of the crystal ball.\n  ''';\n\n  @override\n  State<CrystalBallWidget> createState() => _CrystalBallWidgetState();\n}\n\nclass _CrystalBallWidgetState extends State<CrystalBallWidget> {\n  // PreloadedPrograms is a simple data class that holds the preloaded\n  late final Future<PreloadedPrograms> preloadedPrograms =\n      Future.wait([\n        FragmentProgram.fromAsset('packages/crystal_ball/shaders/ground.frag'),\n        FragmentProgram.fromAsset('packages/crystal_ball/shaders/fog.frag'),\n        FragmentProgram.fromAsset('packages/crystal_ball/shaders/firefly.frag'),\n      ]).then(\n        (l) => (\n          waterFragmentProgram: l[0],\n          fogFragmentProgram: l[1],\n          fireflyFragmentProgram: l[2],\n        ),\n      );\n\n  CrystalBallGame? game;\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: preloadedPrograms,\n      builder: (context, snapshot) {\n        if (snapshot.hasError) {\n          throw snapshot.error!;\n        }\n\n        // Show a loading indicator while the fragment programs are loading\n        if (!snapshot.hasData) {\n          return const Center(\n            child: CircularProgressIndicator(),\n          );\n        }\n\n        return GameWidget(\n          // It is a good idea to save the game instance on state\n          // to avoid issues with hot reload\n          game: game ??= CrystalBallGame(\n            preloadedPrograms: snapshot.data!,\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "examples/games/crystal_ball/lib/main.dart",
    "content": "import 'package:crystal_ball/crystal_ball.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() async {\n  runApp(const CrystalBallWidget());\n}\n"
  },
  {
    "path": "examples/games/crystal_ball/lib/src/game/components/camera_target.dart",
    "content": "import 'package:crystal_ball/src/game/game.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flutter/animation.dart';\n\nclass CameraTarget extends PositionComponent\n    with HasGameReference<CrystalBallGame> {\n  final effectController = CurvedEffectController(\n    0.1,\n    Curves.easeOut,\n  )..setToEnd();\n\n  late final moveEffect = MoveCameraTarget(position, effectController);\n\n  @override\n  Future<void> onLoad() async {\n    await add(moveEffect);\n  }\n\n  void go({\n    required Vector2 to,\n    double duration = 0.25,\n    double scale = 1,\n  }) {\n    effectController.duration = duration * 4;\n    moveEffect.go(to: to);\n  }\n}\n\nclass MoveCameraTarget extends Effect with EffectTarget<CameraTarget> {\n  MoveCameraTarget(this._to, super.controller);\n\n  @override\n  void onMount() {\n    super.onMount();\n    _from = target.position;\n  }\n\n  final Vector2 _to;\n  late final Vector2 _from;\n\n  @override\n  bool get removeOnFinish => false;\n\n  @override\n  void apply(double progress) {\n    target.position.setValues(\n      _from.x + (_to.x - _from.x) * progress,\n      _from.y + (_to.y - _from.y) * progress,\n    );\n  }\n\n  void go({required Vector2 to}) {\n    reset();\n    _to.setFrom(to);\n    _from.setFrom(target.position);\n  }\n}\n"
  },
  {
    "path": "examples/games/crystal_ball/lib/src/game/components/input_handler.dart",
    "content": "import 'package:crystal_ball/src/game/constants.dart';\nimport 'package:crystal_ball/src/game/game.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flutter/services.dart';\n\nclass InputHandler extends PositionComponent\n    with TapCallbacks, HasGameReference<CrystalBallGame> {\n  InputHandler()\n    : super(\n        anchor: Anchor.center,\n        size: kCameraSize,\n      );\n\n  @override\n  Future<void> onLoad() async {\n    await add(\n      KeyboardListenerComponent(\n        keyDown: {\n          LogicalKeyboardKey.arrowLeft: (_) => onLeftStart(),\n          LogicalKeyboardKey.arrowRight: (_) => onRightStart(),\n        },\n        keyUp: {\n          LogicalKeyboardKey.arrowLeft: (_) => onLeftEnd(),\n          LogicalKeyboardKey.arrowRight: (_) => onRightEnd(),\n        },\n      ),\n    );\n\n    return super.onLoad();\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    position = game.world.cameraTarget.position;\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    super.onTapDown(event);\n\n    if (event.localPosition.x < game.size.x * 1 / 3) {\n      onLeftStart();\n    } else if (event.localPosition.x > game.size.x * 2 / 3) {\n      onRightStart();\n    } else {\n      final pos = CameraComponent.currentCamera!.globalToLocal(\n        event.canvasPosition,\n      );\n      game.world.cameraTarget.go(\n        to: Vector2(0, pos.y),\n        duration: 5,\n      );\n    }\n  }\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    super.onTapUp(event);\n\n    if (event.localPosition.x < game.size.x * 1 / 3) {\n      onLeftEnd();\n    }\n    if (event.localPosition.x > game.size.x * 2 / 3) {\n      onRightEnd();\n    }\n  }\n\n  double _directionalCoefficient = 0;\n\n  double get directionalCoefficient => _directionalCoefficient;\n\n  bool onLeftStart() {\n    _directionalCoefficient = -1;\n    return false;\n  }\n\n  bool onRightStart() {\n    _directionalCoefficient = 1;\n    return false;\n  }\n\n  bool onLeftEnd() {\n    if (_directionalCoefficient < 0) {\n      _directionalCoefficient = 0;\n    }\n    return false;\n  }\n\n  bool onRightEnd() {\n    if (_directionalCoefficient > 0) {\n      _directionalCoefficient = 0;\n    }\n    return false;\n  }\n}\n"
  },
  {
    "path": "examples/games/crystal_ball/lib/src/game/constants.dart",
    "content": "import 'package:flame/extensions.dart';\n\nfinal kCameraSize = Vector2(900, 1600);\nconst double kPlayerRadius = 20;\nfinal kPlayerSize = Vector2.all(kPlayerRadius * 2);\n\nconst double kGravity = 100;\nconst double kJumpVelocity = 3000;\n\nconst double kPlatformSpawnDuration = 0.4;\nconst double kPlatformVerticalInterval = 1;\nconst double kStartPlatformHeight = 400;\nconst double kMeanPlatformInterval = 170;\nconst double kPlatformIntervalVariation = 100;\nconst double kPlatformMinWidth = 60;\nconst double kPlatformWidthVariation = 100;\nconst double kPlatformHeight = 10;\nconst double kPlatformPreloadArea = 1600;\n\nconst double kReaperTolerance = 100;\n"
  },
  {
    "path": "examples/games/crystal_ball/lib/src/game/entities/ground.dart",
    "content": "import 'package:crystal_ball/src/game/constants.dart';\nimport 'package:crystal_ball/src/game/game.dart';\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\n\nclass Ground extends Component {\n  Ground() : super(children: [Rectangle(kPlayerSize.y / 2)]) {\n    rectangle = children.first as Rectangle;\n  }\n\n  late final Rectangle rectangle;\n}\n\nclass Rectangle extends PositionComponent\n    with\n        CollisionCallbacks,\n        ParentIsA<Ground>,\n        HasGameReference<CrystalBallGame> {\n  Rectangle(double y)\n    : super(\n        anchor: Anchor.topCenter,\n        position: Vector2(0, y),\n        size: Vector2(\n          kCameraSize.x,\n          kCameraSize.y / 2,\n        ),\n        children: [\n          RectangleHitbox(\n            size: Vector2(\n              kCameraSize.x,\n              kCameraSize.y / 2,\n            ),\n          ),\n          RectangleHitbox(\n            position: Vector2(0, kPlayerRadius),\n            size: Vector2(\n              kCameraSize.x,\n              kCameraSize.y / 2,\n            ),\n          ),\n          for (var i = 2; i < 30; i++)\n            RectangleHitbox(\n              position: Vector2(0, kPlayerRadius * i),\n              size: Vector2(\n                kCameraSize.x,\n                kCameraSize.y / 2,\n              ),\n            ),\n        ],\n      );\n\n  double get topEdge => absolutePositionOfAnchor(Anchor.topCenter).y;\n}\n"
  },
  {
    "path": "examples/games/crystal_ball/lib/src/game/entities/the_ball.dart",
    "content": "// ignore_for_file: unused_field\n\nimport 'dart:math';\nimport 'dart:ui';\n\nimport 'package:crystal_ball/src/game/constants.dart';\nimport 'package:crystal_ball/src/game/entities/ground.dart';\nimport 'package:crystal_ball/src/game/game.dart';\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flutter/animation.dart';\n\nclass TheBall extends PositionComponent\n    with CollisionCallbacks, HasGameReference<CrystalBallGame> {\n  TheBall({\n    required Vector2 super.position,\n  }) : super(\n         anchor: Anchor.center,\n         priority: 100000,\n         children: [\n           CircleHitbox(\n             radius: kPlayerRadius,\n             anchor: Anchor.center,\n           ),\n         ],\n       );\n\n  final Vector2 velocity = Vector2.zero();\n\n  final double _gravity = kGravity;\n\n  late double gamma = 0.6;\n  double get radius => (1.0 - gamma) / 3;\n\n  final effectController = CurvedEffectController(\n    0.4,\n    Curves.easeInOut,\n  )..setToEnd();\n\n  void jump() {\n    velocity.y = -kJumpVelocity;\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    velocity.y += 8500 * dt;\n    final horizontalV = pow(velocity.y.abs(), 1.8) * 0.0015;\n    velocity.x = game.inputHandler.directionalCoefficient * horizontalV;\n\n    final maxH = kCameraSize.x / 2 - kPlayerRadius - 50;\n\n    position += velocity * dt;\n    position.x = clampDouble(x, -maxH, maxH);\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n    if (other is Ground || other is ParentIsA<Ground>) {\n      velocity.y = 0;\n      position.y = 0;\n      jump();\n\n      game.world.cameraTarget.go(\n        to: Vector2(0, -200),\n        duration: 0.5,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "examples/games/crystal_ball/lib/src/game/game.dart",
    "content": "import 'dart:async';\nimport 'dart:ui';\n\nimport 'package:crystal_ball/src/game/components/camera_target.dart';\nimport 'package:crystal_ball/src/game/components/input_handler.dart';\nimport 'package:crystal_ball/src/game/constants.dart';\nimport 'package:crystal_ball/src/game/entities/ground.dart';\nimport 'package:crystal_ball/src/game/entities/the_ball.dart';\nimport 'package:crystal_ball/src/game/post_processes/ball_glow.dart';\nimport 'package:crystal_ball/src/game/post_processes/firefly.dart';\nimport 'package:crystal_ball/src/game/post_processes/foreground_fog.dart';\nimport 'package:crystal_ball/src/game/post_processes/water.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/post_process.dart';\n\nclass CrystalBallGame extends FlameGame<CrystalBallGameWorld>\n    with HasKeyboardHandlerComponents, HasCollisionDetection {\n  CrystalBallGame({\n    required this.preloadedPrograms,\n  }) : super(\n         camera: CameraComponent.withFixedResolution(\n           width: kCameraSize.x,\n           height: kCameraSize.y,\n         ),\n         world: CrystalBallGameWorld(),\n       ) {\n    camera.postProcess = PostProcessGroup(\n      postProcesses: [\n        PostProcessSequentialGroup(\n          postProcesses: [\n            FireflyPostProcess(\n              world: world,\n              fragmentProgram: preloadedPrograms.fireflyFragmentProgram,\n            ),\n            WaterPostProcess(\n              world: world,\n              fragmentProgram: preloadedPrograms.waterFragmentProgram,\n            ),\n          ],\n        ),\n        ForegroundFogPostProcess(\n          world: world,\n          fragmentProgram: preloadedPrograms.fogFragmentProgram,\n        ),\n      ],\n    );\n\n    world.add(\n      inputHandler = InputHandler(),\n    );\n  }\n\n  @override\n  FutureOr<void> onLoad() {\n    camera.follow(world.cameraTarget);\n\n    return super.onLoad();\n  }\n\n  final PreloadedPrograms preloadedPrograms;\n  late final InputHandler inputHandler;\n}\n\nclass CrystalBallGameWorld extends World {\n  CrystalBallGameWorld({\n    super.children,\n    super.key,\n  }) {\n    addAll([\n      BallGlow(),\n      theBall = TheBall(position: Vector2.zero()),\n      ground = Ground(),\n      cameraTarget = CameraTarget(),\n    ]);\n  }\n\n  late final TheBall theBall;\n  late final Ground ground;\n  late final CameraTarget cameraTarget;\n}\n\n// Set of fragment programs to be loaded\n// at the start of the application, on widget level.\ntypedef PreloadedPrograms = ({\n  FragmentProgram waterFragmentProgram,\n  FragmentProgram fogFragmentProgram,\n  FragmentProgram fireflyFragmentProgram,\n});\n"
  },
  {
    "path": "examples/games/crystal_ball/lib/src/game/post_processes/ball_glow.dart",
    "content": "import 'dart:ui';\n\nimport 'package:crystal_ball/src/game/constants.dart';\nimport 'package:crystal_ball/src/game/game.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/post_process.dart';\n\nclass BallGlow extends PostProcessComponent<BallGlowPostProcess>\n    with HasGameReference<CrystalBallGame> {\n  BallGlow({\n    super.position,\n    super.scale,\n    super.angle,\n    super.nativeAngle,\n    super.anchor = Anchor.center,\n    super.children,\n    super.priority,\n    super.key,\n  }) : super(postProcess: BallGlowPostProcess());\n\n  @override\n  Future<void> onLoad() {\n    postProcess.world = game.world;\n    return super.onLoad();\n  }\n\n  @override\n  void update(double dt) {\n    position = game.world.cameraTarget.position;\n    size = game.size;\n    super.update(dt);\n  }\n}\n\n/// A post process that draws a glow effect around the ball component\n///\n/// Differently from the other post processes, this one is not added to the\n/// camera directly, but rather a component.\n///\n/// Also, its shader is not preloaded in the game, but rather loaded\n/// when the post process is loaded.\nclass BallGlowPostProcess extends PostProcess {\n  late CrystalBallGameWorld world;\n\n  late final FragmentProgram fragmentProgram;\n\n  @override\n  Future<void> onLoad() async {\n    // Example of loading a shader from an asset within\n    // the post process. Ideally, you will want to load this\n    // on something like a loading screen.\n    fragmentProgram = await FragmentProgram.fromAsset(\n      'packages/crystal_ball/shaders/the_ball.frag',\n    );\n  }\n\n  @override\n  void postProcess(\n    Vector2 size,\n    Canvas canvas,\n  ) {\n    final origin = world\n        .findGame()!\n        .camera\n        .visibleWorldRect\n        .topLeft\n        .toVector2();\n    final theBall = world.theBall;\n    final ballPosition = theBall.absolutePosition;\n    final uvBall = (ballPosition - origin)..divide(kCameraSize);\n    final velocity = theBall.velocity.clone() / 1600;\n    final shader = fragmentProgram.fragmentShader();\n    shader.setFloatUniforms((value) {\n      value\n        ..setVector(size)\n        ..setVector(uvBall)\n        ..setVector(-velocity)\n        ..setFloat(theBall.gamma)\n        ..setFloat(theBall.radius);\n    });\n\n    canvas\n      ..save()\n      ..drawRect(Offset.zero & size.toSize(), Paint()..shader = shader)\n      ..restore();\n  }\n}\n"
  },
  {
    "path": "examples/games/crystal_ball/lib/src/game/post_processes/firefly.dart",
    "content": "import 'dart:ui';\n\nimport 'package:crystal_ball/src/game/constants.dart';\nimport 'package:crystal_ball/src/game/game.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/post_process.dart';\n\nclass FireflyPostProcess extends PostProcess {\n  FireflyPostProcess({\n    required this.fragmentProgram,\n    required this.world,\n    super.pixelRatio,\n  });\n\n  final FragmentProgram fragmentProgram;\n  final CrystalBallGameWorld world;\n\n  late final FragmentShader shader = fragmentProgram.fragmentShader();\n\n  double time = 0;\n  @override\n  void update(double dt) {\n    super.update(dt);\n    time += dt;\n  }\n\n  @override\n  void postProcess(Vector2 size, Canvas canvas) {\n    final fogShader = shader;\n    fogShader.setFloatUniforms((value) {\n      final camera = CameraComponent.currentCamera!;\n\n      final groundPosition =\n          world.ground.rectangle.absolutePosition + Vector2(0, 1800);\n      final globalGroundPosition = camera.viewfinder.localToGlobal(\n        groundPosition,\n      );\n      final uvGround = globalGroundPosition.y / size.y;\n\n      final cameraVerticalPos = world.cameraTarget.position.clone()\n        ..absolute()\n        ..y *= 1.9;\n\n      final uvCameraVerticalPos = cameraVerticalPos..divide(kCameraSize);\n\n      value\n        ..setVector(size)\n        ..setFloat(uvGround)\n        ..setFloat(uvCameraVerticalPos.y)\n        ..setFloat(3.2)\n        ..setFloat(time * 0.5);\n    });\n\n    canvas.drawRect(\n      Offset.zero & size.toSize(),\n      Paint()..shader = fogShader,\n    );\n\n    renderSubtree(canvas);\n  }\n}\n"
  },
  {
    "path": "examples/games/crystal_ball/lib/src/game/post_processes/foreground_fog.dart",
    "content": "import 'dart:ui';\n\nimport 'package:crystal_ball/src/game/constants.dart';\nimport 'package:crystal_ball/src/game/game.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/post_process.dart';\n\nclass ForegroundFogPostProcess extends PostProcess {\n  ForegroundFogPostProcess({\n    required this.fragmentProgram,\n    required this.world,\n    super.pixelRatio,\n  });\n\n  final FragmentProgram fragmentProgram;\n  final CrystalBallGameWorld world;\n\n  late final FragmentShader shader = fragmentProgram.fragmentShader();\n\n  double time = 0;\n  @override\n  void update(double dt) {\n    super.update(dt);\n    time += dt;\n  }\n\n  @override\n  void postProcess(Vector2 size, Canvas canvas) {\n    final origin = CameraComponent.currentCamera!.visibleWorldRect.topLeft\n        .toVector2();\n\n    shader.setFloatUniforms((value) {\n      value.setVector(size);\n      final groundPosition =\n          world.ground.rectangle.absolutePosition + Vector2(0, 200);\n      final uvGround = (groundPosition - origin)..divide(kCameraSize);\n      final cameraVerticalPos = world.cameraTarget.position.clone()..absolute();\n      final uvCameraVerticalPos = cameraVerticalPos..divide(kCameraSize);\n\n      value\n        ..setFloat(uvGround.y)\n        ..setFloat(uvCameraVerticalPos.y)\n        ..setFloat(0.3)\n        ..setFloat(time);\n    });\n\n    canvas\n      ..save()\n      ..drawRect(\n        Offset.zero & size.toSize(),\n        Paint()..shader = shader,\n      )\n      ..restore();\n  }\n}\n"
  },
  {
    "path": "examples/games/crystal_ball/lib/src/game/post_processes/water.dart",
    "content": "import 'dart:ui';\n\nimport 'package:crystal_ball/src/game/game.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/post_process.dart';\n\nclass WaterPostProcess extends PostProcess {\n  WaterPostProcess({\n    required this.fragmentProgram,\n    required this.world,\n    super.pixelRatio,\n  });\n\n  final FragmentProgram fragmentProgram;\n  final CrystalBallGameWorld world;\n\n  late final FragmentShader shader = fragmentProgram.fragmentShader();\n\n  double time = 0;\n  @override\n  void update(double dt) {\n    super.update(dt);\n    time += dt;\n  }\n\n  @override\n  void postProcess(Vector2 size, Canvas canvas) {\n    final groundPosition = world.ground.rectangle.position;\n    final camera = CameraComponent.currentCamera!;\n    final globalGroundPosition = camera.viewfinder.localToGlobal(\n      groundPosition,\n    );\n    final uvGround = globalGroundPosition.y / size.y;\n\n    final preRenderedSubtree = rasterizeSubtree();\n\n    shader\n      ..setFloatUniforms((value) {\n        value\n          ..setVector(size)\n          ..setFloat(pixelRatio)\n          ..setFloat(uvGround)\n          ..setFloat(time);\n      })\n      ..setImageSampler(0, preRenderedSubtree);\n\n    canvas.drawRect(\n      Offset.zero & size.toSize(),\n      Paint()..shader = shader,\n    );\n  }\n}\n"
  },
  {
    "path": "examples/games/crystal_ball/pubspec.yaml",
    "content": "name: crystal_ball\nresolution: workspace\ndescription: \"A game to showcase the shader pipeline API in Flame.\"\npublish_to: 'none' \nversion: \"0.1.0\"\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n\nflutter:\n  uses-material-design: true\n  shaders:\n    - shaders/firefly.frag\n    - shaders/the_ball.frag\n    - shaders/ground.frag\n    - shaders/fog.frag\n"
  },
  {
    "path": "examples/games/crystal_ball/shaders/firefly.frag",
    "content": "#version 460 core\n\nprecision mediump float;\n\n#include <flutter/runtime_effect.glsl>\n\nuniform vec2 uSize;\nuniform float uGroundPos;\nuniform float uGroundAdd;\nuniform float uFade;\nuniform float uTime;\n\nout vec4 fragColor;\n\nfloat random(vec2 st) {\n    return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);\n}\n\nvec4 circle(vec2 uv, vec2 center, float radius) {\n    vec2 pos = center - uv;\n    pos.y /= uSize.x / uSize.y;\n\n    float dist = 1.0 / length(pos);\n    dist *= radius;\n    dist = dist * 0.9 / 2;\n    float ddist = pow(dist, 1.1);\n\n    vec3 col = ddist * vec3(0.3, 0.9, 0.2);\n    col = 1.0 - exp(-col);\n    return vec4(col, pow(ddist * 0.01, 3));\n}\n\nvoid fragment(vec2 cuv, vec2 pos, inout vec4 color) {\n    vec2 p = pos.xy / uSize.xy;\n    vec2 uv = p * vec2(uSize.x / uSize.y, 1.0);\n\n    float waterline = uGroundPos;\n    float fade = 9.2;\n\n    float tr = step(waterline - fade, uv.y);\n    tr *= smoothstep(waterline - fade, waterline, uv.y);\n    uv.y -= 0.2;\n    uv.y -= uGroundAdd * 0.4;\n\n    vec2 aspect = vec2(uSize.x / uSize.y, 1.0);\n\n    color = vec4(0.0, 0.0, 0.0, 0.0);\n    for(int i = 0; i < 10; i++) {\n        float t = i * 0.5 + uTime * 0.2;\n\n        vec2 base = vec2(random(vec2(float(i), 1.234)) * aspect.x, random(vec2(float(i), 5.678)) * 0.5);\n\n        vec2 pos = base + vec2(sin(uTime + float(i)) * 0.02, cos(uTime * 0.7 + float(i)) * 0.02);\n\n        float size = 0.001 + random(vec2(float(i), 9.012)) * 0.005;\n\n        vec4 firefly = circle(uv, pos, size * 3);\n\n        color += firefly;\n    }\n\n    float f = 1.0;\n    f *= tr;\n    f *= 0.65;\n\n    color.rgb *= f;\n}\n\nvoid main() {\n    vec2 pos = FlutterFragCoord().xy;\n    vec2 uv = pos / uSize;\n    vec4 color;\n\n    fragment(uv, pos, color);\n\n    fragColor = color;\n}\n"
  },
  {
    "path": "examples/games/crystal_ball/shaders/fog.frag",
    "content": "#version 460 core\n\nprecision mediump float;\n\n#include <flutter/runtime_effect.glsl>\n\nuniform vec2 uSize;\nuniform float uGroundPos;\nuniform float uGroundAdd;\nuniform float uFade;\nuniform float uTime;\n\nout vec4 fragColor;\n\n// Thanks iq:\n// https://www.shadertoy.com/view/lsf3WH\n// Copyright © 2013 Inigo Quilez\nfloat hash(vec2 p) {\n    p = 50.0 * fract(p * 0.3183099 + vec2(0.71, 0.113));\n    return -1.0 + 2.0 * fract(p.x * p.y * (p.x + p.y));\n}\n\nfloat noise(in vec2 p) {\n    vec2 i = floor(p);\n    vec2 f = fract(p);\n    vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n\n    return mix(mix(hash(i + vec2(0.0, 0.0)), hash(i + vec2(1.0, 0.0)), u.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y);\n}\n\nfloat fractalNoise(vec2 uv) {\n    float f = 0.0;\n    uv *= 8.0;\n    mat2 m = mat2(1.6, 1.2, -1.2, 1.6);\n    f = 0.5000 * noise(uv);\n    uv = m * uv;\n    f += 0.2500 * noise(uv);\n    uv = m * uv;\n    f += 0.1250 * noise(uv);\n    uv = m * uv;\n    f += 0.0625 * noise(uv);\n    uv = m * uv;\n\n    f = 0.5 + 0.5 * f;\n\n    return f;\n}\n\nvoid fragment(vec2 cuv, vec2 pos, inout vec4 color, float timeMultiplier) {\n    vec2 p = pos.xy / uSize.xy;\n    vec2 uv = p * vec2(uSize.x / uSize.y, 1.0);\n\n    float waterline = uGroundPos;\n    float fade = uFade;\n\n    float tr = step(waterline - fade, uv.y);\n    tr *= smoothstep(waterline - fade, waterline, uv.y);\n    uv.y -= uGroundAdd;\n    uv *= 1.;\n    uv.x += uTime * timeMultiplier;\n\n    float f = fractalNoise(uv);\n    f *= tr;\n    f *= 0.65;\n\n    f = pow(f, 1.8);\n    color = vec4(vec3(0.8, 0.4, 1.0) * f, f);\n}\n\nvoid main() {\n    vec2 pos = FlutterFragCoord().xy;\n    vec2 uv = pos / uSize;\n    vec4 color;\n\n    fragment(uv, pos, color, 0.015);\n\n    vec4 color2;\n    fragment(uv, pos, color2, -0.08);\n\n    fragColor = color + color2;\n}\n"
  },
  {
    "path": "examples/games/crystal_ball/shaders/ground.frag",
    "content": "#version 460 core\n\nprecision highp float;\n\n#include <flutter/runtime_effect.glsl>\n\nuniform vec2 uSize;\nuniform float uPixelSize;\nuniform float uWaterLevel;\nuniform float uTime;\n\nuniform sampler2D tGameCanvas;\n\nout vec4 fragColor;\n\nvec4 fragment(vec2 uv) {\n    vec4 waterColor = vec4(1.0);\n    vec2 reflectedUv = uv.xy;\n\n    float fogIntensity = 0.0;\n    vec4 fogColor = vec4(1, 0.6, 1.0, 0.2);\n\n    if(uv.y >= uWaterLevel) {\n        reflectedUv.y = 2.0 * uWaterLevel - reflectedUv.y;\n        float distFromWater = (reflectedUv.y - uWaterLevel);\n        \n        float angleFactr = 27.31 * exp(-5.11 * 0.5);\n        float magnification = 2.0 + distFromWater;\n\n        reflectedUv.y = uWaterLevel + distFromWater * magnification;\n        reflectedUv.x = reflectedUv.x + (sin((uv.y - uWaterLevel / 1.0) + uTime * 1.0) * 0.01);\n        reflectedUv.y = reflectedUv.y + cos(1.0 / (uv.y - uWaterLevel) + uTime * 1.0) * 0.03;\n\n        if(reflectedUv.y <= 0.0) {\n            return vec4(0.0);\n        }\n\n        waterColor = vec4(1.0);\n        waterColor.rgb *= 1.0 - ((uv.y - uWaterLevel) / (1.0 - uWaterLevel));\n\n        float waterProximity = 1.0 - min(1.0, (uv.y - uWaterLevel) * 23.0);\n        fogIntensity = waterProximity * 0.1;\n    } else {\n        float waterProximity = 1.0 - min(1.0, (uWaterLevel - uv.y) * 5.0);\n        fogIntensity = waterProximity * 0.1;\n    }\n\n    vec4 baseColor = texture(tGameCanvas, reflectedUv / uPixelSize) * waterColor;\n\n    return mix(baseColor, fogColor, fogIntensity);\n}\n\nvoid main() {\n    vec2 pos = FlutterFragCoord().xy;\n    vec2 uv = pos / uSize;\n    fragColor = fragment(uv);\n}\n"
  },
  {
    "path": "examples/games/crystal_ball/shaders/the_ball.frag",
    "content": "#version 460 core\n\nprecision mediump float;\n\n#include <flutter/runtime_effect.glsl>\n\nuniform vec2 uSize;\nuniform vec2 uLightSource;\nuniform vec2 uBallVelocity;\nuniform float uBrightness;\nuniform float uRadius;\n\nout vec4 fragColor;\n\n\nfloat dfLine(vec2 start, vec2 end, vec2 uv) {\n    vec2 line = end - start;\n    float frac = dot(uv - start, line) / dot(line, line);\n    return distance(start + line * clamp(frac, 0.0, 1.0), uv);\n}\n\n\nvec3 trace(vec2 uv) {\n    vec2 v = vec2(uBallVelocity.x*2, pow(uBallVelocity.y, 1)) / 30;\n    float dist = dfLine(uLightSource, uLightSource+v, uv);\n\n    dist = 1.0/dist;\n    dist *= 0.008;\n    float ddist = pow(dist, 2);\n\n    float distanceToLight = length(uLightSource - uv) / 0.1;\n\n    vec3 col = ddist * vec3(1.0, 0.8, 0.6) * pow(distanceToLight, 0.1);\n    col = 1.0 - exp(-col);\n\n    return col;\n}\n\nvoid fragment(vec2 uv, inout vec4 color) {\n    float ballSize = 0.1;\n    vec2 pos = uLightSource - uv;\n    pos.y /= uSize.x/uSize.y;\n\n    float dist = 1.0/length(pos);\n    dist *= uRadius;\n    dist = dist * uBrightness /2;\n    float ddist = pow(dist, 1.1);\n\n    vec3 col = ddist * vec3(0.8, 0.4, 1.0);\n    col = 1.0 - exp(-col);\n    color = vec4(col, pow(ddist* 0.01, 3));\n    color.rgb += trace(uv);\n}\n\nvoid main() {\n    vec2 pos = FlutterFragCoord().xy;\n    vec2 uv = pos / uSize;\n    vec4 color;\n\n    fragment(uv, color);\n\n    fragColor = color;\n}\n"
  },
  {
    "path": "examples/games/padracing/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "examples/games/padracing/README.md",
    "content": "# Padracing\n\nPadracing is a small racing game to showcase the possibility of running\nFlame and Forge2D in DartPad.\n\nIn this game I took on the challenge to build a game completely without\nassets, since you can't have assets in DartPad (yet).\nI could of course have pulled in network assets, but decided that the\nchallenge of not having any assets at all made it more fun.\n\nCreated by: [Lukas Klingsbo](https://twitter.com/spyd0n)\n([spydon](https://github.com/spydon))\n"
  },
  {
    "path": "examples/games/padracing/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "examples/games/padracing/lib/ball.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart' hide Particle, World;\nimport 'package:padracing/car.dart';\nimport 'package:padracing/game_colors.dart';\nimport 'package:padracing/padracing_game.dart';\nimport 'package:padracing/wall.dart';\n\nclass Ball extends BodyComponent<PadRacingGame> with ContactCallbacks {\n  final double radius;\n  final Vector2 initialPosition;\n  final double rotation;\n  final bool isMovable;\n  final rng = Random();\n  late final Paint _shaderPaint;\n\n  Ball({\n    required this.initialPosition,\n    this.radius = 80.0,\n    this.rotation = 1.0,\n    this.isMovable = true,\n  }) : super(priority: 3);\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    renderBody = false;\n    _shaderPaint = GameColors.green.paint\n      ..shader = Gradient.radial(\n        Offset.zero,\n        radius,\n        [\n          GameColors.green.color,\n          BasicPalette.black.color,\n        ],\n        null,\n        TileMode.clamp,\n        null,\n        Offset(radius / 2, radius / 2),\n      );\n  }\n\n  @override\n  Body createBody() {\n    final def = BodyDef()\n      ..userData = this\n      ..type = isMovable ? BodyType.dynamic : BodyType.kinematic\n      ..position = initialPosition;\n    final body = world.createBody(def)..angularVelocity = rotation;\n\n    final shape = CircleShape()..radius = radius;\n    final fixtureDef = FixtureDef(shape)\n      ..restitution = 0.5\n      ..friction = 0.5;\n    return body..createFixture(fixtureDef);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawCircle(Offset.zero, radius, _shaderPaint);\n  }\n\n  @override\n  void beginContact(Object other, Contact contact) {\n    if (isMovable && other is Car) {\n      final carBody = other.body;\n      carBody.applyAngularImpulse(3 * carBody.mass * 100);\n    }\n  }\n\n  late Rect asRect = Rect.fromCircle(\n    center: initialPosition.toOffset(),\n    radius: radius,\n  );\n}\n\nList<Ball> createBalls(Vector2 trackSize, List<Wall> walls, Ball bigBall) {\n  final balls = <Ball>[];\n  final rng = Random();\n  while (balls.length < 20) {\n    final ball = Ball(\n      initialPosition: Vector2.random(rng)..multiply(trackSize),\n      radius: 3.0 + rng.nextInt(5),\n      rotation: (rng.nextBool() ? 1 : -1) * rng.nextInt(5).toDouble(),\n    );\n    final touchesBall =\n        ball.initialPosition.distanceTo(bigBall.initialPosition) <\n        ball.radius + bigBall.radius;\n    if (!touchesBall) {\n      final touchesWall = walls.any(\n        (wall) => wall.asRect.overlaps(ball.asRect),\n      );\n      if (!touchesWall) {\n        balls.add(ball);\n      }\n    }\n  }\n  return balls;\n}\n"
  },
  {
    "path": "examples/games/padracing/lib/car.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart' hide Particle, World;\nimport 'package:flutter/material.dart' hide Image, Gradient;\nimport 'package:padracing/game_colors.dart';\nimport 'package:padracing/lap_line.dart';\nimport 'package:padracing/padracing_game.dart';\nimport 'package:padracing/tire.dart';\n\nclass Car extends BodyComponent<PadRacingGame> {\n  Car({required this.playerNumber, required this.cameraComponent})\n    : super(\n        priority: 3,\n        paint: Paint()..color = colors[playerNumber],\n      );\n\n  static final colors = [\n    GameColors.green.color,\n    GameColors.blue.color,\n  ];\n\n  late final List<Tire> tires;\n  final ValueNotifier<int> lapNotifier = ValueNotifier<int>(1);\n  final int playerNumber;\n  final Set<LapLine> passedStartControl = {};\n  final CameraComponent cameraComponent;\n  late final Image _image;\n  final size = const Size(6, 10);\n  final scale = 10.0;\n  late final _renderPosition = -size.toOffset() / 2;\n  late final _scaledRect = (size * scale).toRect();\n  late final _renderRect = _renderPosition & size;\n\n  final vertices = <Vector2>[\n    Vector2(1.5, -5.0),\n    Vector2(3.0, -2.5),\n    Vector2(2.8, 0.5),\n    Vector2(1.0, 5.0),\n    Vector2(-1.0, 5.0),\n    Vector2(-2.8, 0.5),\n    Vector2(-3.0, -2.5),\n    Vector2(-1.5, -5.0),\n  ];\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    final recorder = PictureRecorder();\n    final canvas = Canvas(recorder, _scaledRect);\n    final path = Path();\n    final bodyPaint = Paint()..color = paint.color;\n    for (var i = 0.0; i < _scaledRect.width / 4; i++) {\n      bodyPaint.color = bodyPaint.color.darken(0.1);\n      path.reset();\n      final offsetVertices = vertices\n          .map(\n            (v) =>\n                v.toOffset() * scale -\n                Offset(i * v.x.sign, i * v.y.sign) +\n                _scaledRect.bottomRight / 2,\n          )\n          .toList();\n      path.addPolygon(offsetVertices, true);\n      canvas.drawPath(path, bodyPaint);\n    }\n    final picture = recorder.endRecording();\n    _image = await picture.toImage(\n      _scaledRect.width.toInt(),\n      _scaledRect.height.toInt(),\n    );\n  }\n\n  @override\n  Body createBody() {\n    final startPosition =\n        Vector2(20, 30) + Vector2(15, 0) * playerNumber.toDouble();\n    final def = BodyDef()\n      ..type = BodyType.dynamic\n      ..position = startPosition;\n    final body = world.createBody(def)\n      ..userData = this\n      ..angularDamping = 3.0;\n\n    final shape = PolygonShape()..set(vertices);\n    final fixtureDef = FixtureDef(shape)\n      ..density = 0.2\n      ..restitution = 2.0;\n    body.createFixture(fixtureDef);\n\n    final jointDef = RevoluteJointDef()\n      ..bodyA = body\n      ..enableLimit = true\n      ..lowerAngle = 0.0\n      ..upperAngle = 0.0\n      ..localAnchorB.setZero();\n\n    tires = List.generate(4, (i) {\n      final isFrontTire = i <= 1;\n      final isLeftTire = i.isEven;\n      return Tire(\n        car: this,\n        pressedKeys: game.pressedKeySets[playerNumber],\n        isFrontTire: isFrontTire,\n        isLeftTire: isLeftTire,\n        jointDef: jointDef,\n        isTurnableTire: isFrontTire,\n      );\n    });\n\n    game.world.addAll(tires);\n    return body;\n  }\n\n  @override\n  void update(double dt) {\n    cameraComponent.viewfinder.position = body.position;\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawImageRect(\n      _image,\n      _scaledRect,\n      _renderRect,\n      paint,\n    );\n  }\n\n  @override\n  void onRemove() {\n    for (final tire in tires) {\n      tire.removeFromParent();\n    }\n  }\n}\n"
  },
  {
    "path": "examples/games/padracing/lib/game_colors.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flutter/material.dart' hide Image, Gradient;\n\nenum GameColors {\n  green,\n  blue,\n}\n\nextension GameColorExtension on GameColors {\n  Color get color {\n    return switch (this) {\n      GameColors.green => ColorExtension.fromRGBHexString('#14F596'),\n      GameColors.blue => ColorExtension.fromRGBHexString('#81DDF9'),\n    };\n  }\n\n  Paint get paint => Paint()..color = color;\n}\n"
  },
  {
    "path": "examples/games/padracing/lib/game_over.dart",
    "content": "import 'package:flutter/material.dart' hide Image, Gradient;\nimport 'package:padracing/menu_card.dart';\nimport 'package:padracing/padracing_game.dart';\n\nclass GameOver extends StatelessWidget {\n  const GameOver(this.game, {super.key});\n\n  final PadRacingGame game;\n\n  @override\n  Widget build(BuildContext context) {\n    final textTheme = Theme.of(context).textTheme;\n    return Material(\n      color: Colors.transparent,\n      child: Center(\n        child: Wrap(\n          children: [\n            MenuCard(\n              children: [\n                Text(\n                  'Player ${game.winner!.playerNumber + 1} wins!',\n                  style: textTheme.displayLarge,\n                ),\n                const SizedBox(height: 10),\n                Text(\n                  'Time: ${game.timePassed}',\n                  style: textTheme.bodyLarge,\n                ),\n                const SizedBox(height: 10),\n                ElevatedButton(\n                  onPressed: game.reset,\n                  child: const Text('Restart'),\n                ),\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/games/padracing/lib/lap_line.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart' hide Particle, World;\nimport 'package:flutter/material.dart' hide Image, Gradient;\n\nimport 'package:padracing/car.dart';\nimport 'package:padracing/game_colors.dart';\n\nclass LapLine extends BodyComponent with ContactCallbacks {\n  LapLine(this.id, this.initialPosition, this.size, {required this.isFinish})\n    : super(priority: 1);\n\n  final int id;\n  final bool isFinish;\n  final Vector2 initialPosition;\n  final Vector2 size;\n  late final Rect rect = size.toRect();\n  Image? _finishOverlay;\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    if (isFinish) {\n      _finishOverlay = await createFinishOverlay();\n    }\n  }\n\n  @override\n  Body createBody() {\n    paint.color = (isFinish ? GameColors.green.color : GameColors.green.color)\n      ..withValues(alpha: 0.5);\n    paint\n      ..style = PaintingStyle.fill\n      ..shader = Gradient.radial(\n        (size / 2).toOffset(),\n        max(size.x, size.y),\n        [\n          paint.color,\n          Colors.black,\n        ],\n      );\n\n    final groundBody = world.createBody(\n      BodyDef(\n        position: initialPosition,\n        userData: this,\n      ),\n    );\n    final shape = PolygonShape()..setAsBoxXY(size.x / 2, size.y / 2);\n    final fixtureDef = FixtureDef(shape, isSensor: true);\n    return groundBody..createFixture(fixtureDef);\n  }\n\n  late final Rect _scaledRect = (size * 10).toRect();\n  late final Rect _drawRect = size.toRect();\n\n  Future<Image> createFinishOverlay() {\n    final recorder = PictureRecorder();\n    final canvas = Canvas(recorder, _scaledRect);\n    final step = _scaledRect.width / 2;\n    final black = BasicPalette.black.paint();\n\n    for (var i = 0; i * step < _scaledRect.height; i++) {\n      canvas.drawRect(\n        Rect.fromLTWH(i.isEven ? 0 : step, i * step, step, step),\n        black,\n      );\n    }\n    final picture = recorder.endRecording();\n    return picture.toImage(\n      _scaledRect.width.toInt(),\n      _scaledRect.height.toInt(),\n    );\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.translate(-size.x / 2, -size.y / 2);\n    canvas.drawRect(rect, paint);\n    if (_finishOverlay != null) {\n      canvas.drawImageRect(_finishOverlay!, _scaledRect, _drawRect, paint);\n    }\n  }\n\n  @override\n  void beginContact(Object other, Contact contact) {\n    if (other is! Car) {\n      return;\n    }\n    if (isFinish && other.passedStartControl.length == 2) {\n      other.lapNotifier.value++;\n      other.passedStartControl.clear();\n    } else if (!isFinish) {\n      other.passedStartControl.removeWhere(\n        (passedControl) => passedControl.id > id,\n      );\n      other.passedStartControl.add(this);\n    }\n  }\n}\n"
  },
  {
    "path": "examples/games/padracing/lib/lap_text.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flutter/material.dart' hide Image, Gradient;\nimport 'package:google_fonts/google_fonts.dart';\n\nimport 'package:padracing/car.dart';\nimport 'package:padracing/padracing_game.dart';\n\nclass LapText extends PositionComponent with HasGameReference<PadRacingGame> {\n  LapText({required this.car, required Vector2 position})\n    : super(position: position);\n\n  final Car car;\n  late final ValueNotifier<int> lapNotifier = car.lapNotifier;\n  late final TextComponent _timePassedComponent;\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    final textStyle = GoogleFonts.vt323(\n      fontSize: 35,\n      color: car.paint.color,\n    );\n    final defaultRenderer = TextPaint(style: textStyle);\n    final lapCountRenderer = TextPaint(\n      style: textStyle.copyWith(fontSize: 55, fontWeight: FontWeight.bold),\n    );\n    add(\n      TextComponent(\n        text: 'Lap',\n        position: Vector2(0, -20),\n        anchor: Anchor.center,\n        textRenderer: defaultRenderer,\n      ),\n    );\n    final lapCounter = TextComponent(\n      position: Vector2(0, 10),\n      anchor: Anchor.center,\n      textRenderer: lapCountRenderer,\n    );\n    add(lapCounter);\n    void updateLapText() {\n      if (lapNotifier.value <= PadRacingGame.numberOfLaps) {\n        final prefix = lapNotifier.value < 10 ? '0' : '';\n        lapCounter.text = '$prefix${lapNotifier.value}';\n      } else {\n        lapCounter.text = 'DONE';\n      }\n    }\n\n    _timePassedComponent = TextComponent(\n      position: Vector2(0, 70),\n      anchor: Anchor.center,\n      textRenderer: defaultRenderer,\n    );\n    add(_timePassedComponent);\n\n    _backgroundPaint = Paint()\n      ..color = car.paint.color\n      ..style = PaintingStyle.stroke\n      ..strokeWidth = 2;\n\n    lapNotifier.addListener(updateLapText);\n    updateLapText();\n  }\n\n  @override\n  void update(double dt) {\n    if (game.isGameOver) {\n      return;\n    }\n    _timePassedComponent.text = game.timePassed;\n  }\n\n  final _backgroundRect = RRect.fromRectAndRadius(\n    Rect.fromCircle(center: Offset.zero, radius: 50),\n    const Radius.circular(10),\n  );\n  late final Paint _backgroundPaint;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(_backgroundRect, _backgroundPaint);\n  }\n}\n"
  },
  {
    "path": "examples/games/padracing/lib/main.dart",
    "content": "import 'package:flutter/material.dart' hide Image, Gradient;\nimport 'package:padracing/padracing_widget.dart';\n\nvoid main() {\n  runApp(\n    const PadracingWidget(),\n  );\n}\n"
  },
  {
    "path": "examples/games/padracing/lib/menu.dart",
    "content": "import 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart' hide Image, Gradient;\nimport 'package:padracing/game_colors.dart';\nimport 'package:padracing/menu_card.dart';\nimport 'package:padracing/padracing_game.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nclass Menu extends StatelessWidget {\n  const Menu(this.game, {super.key});\n\n  final PadRacingGame game;\n\n  @override\n  Widget build(BuildContext context) {\n    final textTheme = Theme.of(context).textTheme;\n    return Material(\n      color: Colors.transparent,\n      child: Center(\n        child: Wrap(\n          children: [\n            Column(\n              children: [\n                MenuCard(\n                  children: [\n                    Text(\n                      'PadRacing',\n                      style: textTheme.displayLarge,\n                    ),\n                    Text(\n                      'First to 3 laps win',\n                      style: textTheme.bodyLarge,\n                    ),\n                    const SizedBox(height: 10),\n                    ElevatedButton(\n                      child: const Text('1 Player'),\n                      onPressed: () {\n                        game.prepareStart(numberOfPlayers: 1);\n                      },\n                    ),\n                    Text(\n                      'Arrow keys',\n                      style: textTheme.bodyMedium,\n                    ),\n                    const SizedBox(height: 10),\n                    ElevatedButton(\n                      child: const Text('2 Players'),\n                      onPressed: () {\n                        game.prepareStart(numberOfPlayers: 2);\n                      },\n                    ),\n                    Text(\n                      'WASD',\n                      style: textTheme.bodyMedium,\n                    ),\n                  ],\n                ),\n                MenuCard(\n                  children: [\n                    RichText(\n                      text: TextSpan(\n                        children: [\n                          TextSpan(\n                            text: 'Made by ',\n                            style: textTheme.bodyMedium,\n                          ),\n                          TextSpan(\n                            text: 'Lukas Klingsbo (spydon)',\n                            style: textTheme.bodyMedium?.copyWith(\n                              color: GameColors.green.color,\n                              decoration: TextDecoration.underline,\n                            ),\n                            recognizer: TapGestureRecognizer()\n                              ..onTap = () {\n                                launchUrl(\n                                  Uri.parse('https://github.com/spydon'),\n                                );\n                              },\n                          ),\n                        ],\n                      ),\n                    ),\n                  ],\n                ),\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/games/padracing/lib/menu_card.dart",
    "content": "import 'package:flutter/material.dart' hide Image, Gradient;\n\nimport 'package:padracing/game_colors.dart';\n\nclass MenuCard extends StatelessWidget {\n  const MenuCard({required this.children, super.key});\n\n  final List<Widget> children;\n\n  @override\n  Widget build(BuildContext context) {\n    return Card(\n      color: Colors.black,\n      shadowColor: GameColors.green.color,\n      elevation: 10,\n      margin: const EdgeInsets.only(bottom: 20),\n      child: Container(\n        margin: const EdgeInsets.all(20),\n        child: Column(\n          children: children,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/games/padracing/lib/padracing_game.dart",
    "content": "import 'dart:math';\n\nimport 'package:collection/collection.dart';\nimport 'package:flame/camera.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart' hide Particle, World;\nimport 'package:flutter/material.dart' hide Image, Gradient;\nimport 'package:flutter/services.dart';\nimport 'package:padracing/ball.dart';\nimport 'package:padracing/car.dart';\nimport 'package:padracing/game_colors.dart';\nimport 'package:padracing/lap_line.dart';\nimport 'package:padracing/lap_text.dart';\nimport 'package:padracing/wall.dart';\n\nfinal List<Map<LogicalKeyboardKey, LogicalKeyboardKey>> playersKeys = [\n  {\n    LogicalKeyboardKey.arrowUp: LogicalKeyboardKey.arrowUp,\n    LogicalKeyboardKey.arrowDown: LogicalKeyboardKey.arrowDown,\n    LogicalKeyboardKey.arrowLeft: LogicalKeyboardKey.arrowLeft,\n    LogicalKeyboardKey.arrowRight: LogicalKeyboardKey.arrowRight,\n  },\n  {\n    LogicalKeyboardKey.keyW: LogicalKeyboardKey.arrowUp,\n    LogicalKeyboardKey.keyS: LogicalKeyboardKey.arrowDown,\n    LogicalKeyboardKey.keyA: LogicalKeyboardKey.arrowLeft,\n    LogicalKeyboardKey.keyD: LogicalKeyboardKey.arrowRight,\n  },\n];\n\nclass PadRacingGame extends Forge2DGame with KeyboardEvents {\n  static const String description = '''\n     This is an example game that uses Forge2D to handle the physics.\n     In this game you should finish 3 laps in as little time as possible, it can\n     be played as single player or with two players (on the same keyboard).\n     Watch out for the balls, they make your car spin.\n  ''';\n\n  PadRacingGame() : super(gravity: Vector2.zero(), zoom: 1);\n\n  @override\n  Color backgroundColor() => Colors.black;\n\n  static final Vector2 trackSize = Vector2.all(500);\n  static const double playZoom = 8.0;\n  static const int numberOfLaps = 3;\n  late CameraComponent startCamera;\n  late List<Map<LogicalKeyboardKey, LogicalKeyboardKey>> activeKeyMaps;\n  late List<Set<LogicalKeyboardKey>> pressedKeySets;\n  final cars = <Car>[];\n  bool isGameOver = true;\n  Car? winner;\n  double _timePassed = 0;\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    camera.removeFromParent();\n    children.register<CameraComponent>();\n\n    final walls = createWalls(trackSize);\n    final bigBall = Ball(initialPosition: Vector2(200, 245), isMovable: false);\n    world.addAll([\n      LapLine(1, Vector2(25, 50), Vector2(50, 5), isFinish: false),\n      LapLine(2, Vector2(25, 70), Vector2(50, 5), isFinish: false),\n      LapLine(3, Vector2(52.5, 25), Vector2(5, 50), isFinish: true),\n      bigBall,\n      ...walls,\n      ...createBalls(trackSize, walls, bigBall),\n    ]);\n\n    openMenu();\n  }\n\n  void openMenu() {\n    overlays.add('menu');\n    final zoomLevel = min(\n      canvasSize.x / trackSize.x,\n      canvasSize.y / trackSize.y,\n    );\n    startCamera = CameraComponent(world: world)\n      ..viewfinder.position = trackSize / 2\n      ..viewfinder.anchor = Anchor.center\n      ..viewfinder.zoom = zoomLevel - 0.2;\n    add(startCamera);\n  }\n\n  void prepareStart({required int numberOfPlayers}) {\n    startCamera.viewfinder\n      ..add(\n        ScaleEffect.to(\n          Vector2.all(playZoom),\n          EffectController(duration: 1.0),\n          onComplete: () => start(numberOfPlayers: numberOfPlayers),\n        ),\n      )\n      ..add(\n        MoveEffect.to(\n          Vector2.all(20),\n          EffectController(duration: 1.0),\n        ),\n      );\n  }\n\n  void start({required int numberOfPlayers}) {\n    isGameOver = false;\n    overlays.remove('menu');\n    startCamera.removeFromParent();\n    final isHorizontal = canvasSize.x > canvasSize.y;\n    Vector2 alignedVector({\n      required double longMultiplier,\n      double shortMultiplier = 1.0,\n    }) {\n      return Vector2(\n        isHorizontal\n            ? canvasSize.x * longMultiplier\n            : canvasSize.x * shortMultiplier,\n        !isHorizontal\n            ? canvasSize.y * longMultiplier\n            : canvasSize.y * shortMultiplier,\n      );\n    }\n\n    final viewportSize = alignedVector(longMultiplier: 1 / numberOfPlayers);\n\n    RectangleComponent viewportRimGenerator() =>\n        RectangleComponent(size: viewportSize, anchor: Anchor.topLeft)\n          ..paint.color = GameColors.blue.color\n          ..paint.strokeWidth = 2.0\n          ..paint.style = PaintingStyle.stroke;\n    final cameras = List.generate(numberOfPlayers, (i) {\n      return CameraComponent(\n          world: world,\n          viewport: FixedSizeViewport(viewportSize.x, viewportSize.y)\n            ..position = alignedVector(\n              longMultiplier: i == 0 ? 0.0 : 1 / (i + 1),\n              shortMultiplier: 0.0,\n            )\n            ..add(viewportRimGenerator()),\n        )\n        ..viewfinder.anchor = Anchor.center\n        ..viewfinder.zoom = playZoom;\n    });\n\n    final mapCameraSize = Vector2.all(500);\n    const mapCameraZoom = 0.5;\n    final mapCameras = List.generate(numberOfPlayers, (i) {\n      return CameraComponent(\n          world: world,\n          viewport: FixedSizeViewport(mapCameraSize.x, mapCameraSize.y)\n            ..position = Vector2(\n              viewportSize.x - mapCameraSize.x * mapCameraZoom - 50,\n              50,\n            ),\n        )\n        ..viewfinder.anchor = Anchor.topLeft\n        ..viewfinder.zoom = mapCameraZoom;\n    });\n    addAll(cameras);\n\n    for (var i = 0; i < numberOfPlayers; i++) {\n      final car = Car(playerNumber: i, cameraComponent: cameras[i]);\n      final lapText = LapText(\n        car: car,\n        position: Vector2.all(100),\n      );\n\n      car.lapNotifier.addListener(() {\n        if (car.lapNotifier.value > numberOfLaps) {\n          isGameOver = true;\n          winner = car;\n          overlays.add('game_over');\n          lapText.addAll([\n            ScaleEffect.by(\n              Vector2.all(1.5),\n              EffectController(duration: 0.2, alternate: true, repeatCount: 3),\n            ),\n            RotateEffect.by(pi * 2, EffectController(duration: 0.5)),\n          ]);\n        } else {\n          lapText.add(\n            ScaleEffect.by(\n              Vector2.all(1.5),\n              EffectController(duration: 0.2, alternate: true),\n            ),\n          );\n        }\n      });\n      cars.add(car);\n      world.add(car);\n      cameras[i].viewport.addAll([lapText, mapCameras[i]]);\n    }\n\n    pressedKeySets = List.generate(numberOfPlayers, (_) => {});\n    activeKeyMaps = List.generate(numberOfPlayers, (i) => playersKeys[i]);\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    if (isGameOver) {\n      return;\n    }\n    _timePassed += dt;\n  }\n\n  @override\n  KeyEventResult onKeyEvent(\n    KeyEvent event,\n    Set<LogicalKeyboardKey> keysPressed,\n  ) {\n    super.onKeyEvent(event, keysPressed);\n    if (!isLoaded || isGameOver) {\n      return KeyEventResult.ignored;\n    }\n\n    _clearPressedKeys();\n    for (final key in keysPressed) {\n      activeKeyMaps.forEachIndexed((i, keyMap) {\n        if (keyMap.containsKey(key)) {\n          pressedKeySets[i].add(keyMap[key]!);\n        }\n      });\n    }\n    return KeyEventResult.handled;\n  }\n\n  void _clearPressedKeys() {\n    for (final pressedKeySet in pressedKeySets) {\n      pressedKeySet.clear();\n    }\n  }\n\n  void reset() {\n    _clearPressedKeys();\n    activeKeyMaps.clear();\n    _timePassed = 0;\n    overlays.remove('game_over');\n    openMenu();\n    for (final car in cars) {\n      car.removeFromParent();\n    }\n    for (final camera in children.query<CameraComponent>()) {\n      camera.removeFromParent();\n    }\n  }\n\n  String _maybePrefixZero(int number) {\n    if (number < 10) {\n      return '0$number';\n    }\n    return number.toString();\n  }\n\n  String get timePassed {\n    final minutes = _maybePrefixZero((_timePassed / 60).floor());\n    final seconds = _maybePrefixZero((_timePassed % 60).floor());\n    final ms = _maybePrefixZero(((_timePassed % 1) * 100).floor());\n    return [minutes, seconds, ms].join(':');\n  }\n}\n"
  },
  {
    "path": "examples/games/padracing/lib/padracing_widget.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flutter/material.dart' hide Image, Gradient;\nimport 'package:google_fonts/google_fonts.dart';\n\nimport 'package:padracing/game_over.dart';\nimport 'package:padracing/menu.dart';\nimport 'package:padracing/padracing_game.dart';\n\nclass PadracingWidget extends StatelessWidget {\n  const PadracingWidget({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = ThemeData(\n      textTheme: TextTheme(\n        displayLarge: GoogleFonts.vt323(\n          fontSize: 35,\n          color: Colors.white,\n        ),\n        labelLarge: GoogleFonts.vt323(\n          fontSize: 30,\n          fontWeight: FontWeight.w500,\n        ),\n        bodyLarge: GoogleFonts.vt323(\n          fontSize: 28,\n          color: Colors.grey,\n        ),\n        bodyMedium: GoogleFonts.vt323(\n          fontSize: 18,\n          color: Colors.grey,\n        ),\n      ),\n      elevatedButtonTheme: ElevatedButtonThemeData(\n        style: ElevatedButton.styleFrom(\n          backgroundColor: Colors.black,\n          minimumSize: const Size(150, 50),\n        ),\n      ),\n      inputDecorationTheme: InputDecorationTheme(\n        hoverColor: Colors.red.shade700,\n        focusedBorder: const UnderlineInputBorder(\n          borderSide: BorderSide(color: Colors.white),\n        ),\n        border: const UnderlineInputBorder(\n          borderSide: BorderSide(color: Colors.white),\n        ),\n        errorBorder: UnderlineInputBorder(\n          borderSide: BorderSide(\n            color: Colors.red.shade700,\n          ),\n        ),\n      ),\n    );\n\n    return MaterialApp(\n      title: 'PadRacing',\n      home: GameWidget<PadRacingGame>(\n        game: PadRacingGame(),\n        loadingBuilder: (context) => Center(\n          child: Text(\n            'Loading...',\n            style: Theme.of(context).textTheme.displayLarge,\n          ),\n        ),\n        overlayBuilderMap: {\n          'menu': (_, game) => Menu(game),\n          'game_over': (_, game) => GameOver(game),\n        },\n        initialActiveOverlays: const ['menu'],\n      ),\n      theme: theme,\n    );\n  }\n}\n"
  },
  {
    "path": "examples/games/padracing/lib/tire.dart",
    "content": "import 'package:flame/palette.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart' hide Particle, World;\nimport 'package:flutter/material.dart' hide Image, Gradient;\nimport 'package:flutter/services.dart';\nimport 'package:padracing/car.dart';\nimport 'package:padracing/padracing_game.dart';\nimport 'package:padracing/trail.dart';\n\nclass Tire extends BodyComponent<PadRacingGame> {\n  Tire({\n    required this.car,\n    required this.pressedKeys,\n    required this.isFrontTire,\n    required this.isLeftTire,\n    required this.jointDef,\n    this.isTurnableTire = false,\n  }) : super(\n         paint: Paint()\n           ..color = car.paint.color\n           ..strokeWidth = 0.2\n           ..style = PaintingStyle.stroke,\n         priority: 2,\n       );\n\n  static const double _backTireMaxDriveForce = 300.0;\n  static const double _frontTireMaxDriveForce = 600.0;\n  static const double _backTireMaxLateralImpulse = 8.5;\n  static const double _frontTireMaxLateralImpulse = 7.5;\n\n  final Car car;\n  final size = Vector2(0.5, 1.25);\n  late final RRect _renderRect = RRect.fromLTRBR(\n    -size.x,\n    -size.y,\n    size.x,\n    size.y,\n    const Radius.circular(0.3),\n  );\n\n  final Set<LogicalKeyboardKey> pressedKeys;\n\n  late final double _maxDriveForce = isFrontTire\n      ? _frontTireMaxDriveForce\n      : _backTireMaxDriveForce;\n  late final double _maxLateralImpulse = isFrontTire\n      ? _frontTireMaxLateralImpulse\n      : _backTireMaxLateralImpulse;\n\n  // Make mutable if ice or something should be implemented\n  final double _currentTraction = 1.0;\n\n  final double _maxForwardSpeed = 250.0;\n  final double _maxBackwardSpeed = -40.0;\n\n  final RevoluteJointDef jointDef;\n  late final RevoluteJoint joint;\n  final bool isTurnableTire;\n  final bool isFrontTire;\n  final bool isLeftTire;\n\n  final double _lockAngle = 0.6;\n  final double _turnSpeedPerSecond = 4;\n\n  final Paint _black = BasicPalette.black.paint();\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    game.world.add(Trail(car: car, tire: this));\n  }\n\n  @override\n  Body createBody() {\n    final jointAnchor = Vector2(\n      isLeftTire ? -3.0 : 3.0,\n      isFrontTire ? 3.5 : -4.25,\n    );\n\n    final def = BodyDef()\n      ..type = BodyType.dynamic\n      ..position = car.body.position + jointAnchor;\n    final body = world.createBody(def)..userData = this;\n\n    final polygonShape = PolygonShape()..setAsBoxXY(0.5, 1.25);\n    body.createFixtureFromShape(polygonShape).userData = this;\n\n    jointDef.bodyB = body;\n    jointDef.localAnchorA.setFrom(jointAnchor);\n    world.createJoint(joint = RevoluteJoint(jointDef));\n    joint.setLimits(0, 0);\n    return body;\n  }\n\n  @override\n  void update(double dt) {\n    if (body.isAwake || pressedKeys.isNotEmpty) {\n      _updateTurn(dt);\n      _updateFriction();\n      if (!game.isGameOver) {\n        _updateDrive();\n      }\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(_renderRect, _black);\n    canvas.drawRRect(_renderRect, paint);\n  }\n\n  void _updateFriction() {\n    final impulse = _lateralVelocity\n      ..scale(-body.mass)\n      ..clampScalar(-_maxLateralImpulse, _maxLateralImpulse)\n      ..scale(_currentTraction);\n    body.applyLinearImpulse(impulse);\n    body.applyAngularImpulse(\n      0.1 * _currentTraction * body.getInertia() * -body.angularVelocity,\n    );\n\n    final currentForwardNormal = _forwardVelocity;\n    final currentForwardSpeed = currentForwardNormal.length;\n    currentForwardNormal.normalize();\n    final dragForceMagnitude = -2 * currentForwardSpeed;\n    body.applyForce(\n      currentForwardNormal..scale(_currentTraction * dragForceMagnitude),\n    );\n  }\n\n  void _updateDrive() {\n    var desiredSpeed = 0.0;\n    if (pressedKeys.contains(LogicalKeyboardKey.arrowUp)) {\n      desiredSpeed = _maxForwardSpeed;\n    }\n    if (pressedKeys.contains(LogicalKeyboardKey.arrowDown)) {\n      desiredSpeed += _maxBackwardSpeed;\n    }\n\n    final currentForwardNormal = body.worldVector(Vector2(0.0, 1.0));\n    final currentSpeed = _forwardVelocity.dot(currentForwardNormal);\n    var force = 0.0;\n    if (desiredSpeed < currentSpeed) {\n      force = -_maxDriveForce;\n    } else if (desiredSpeed > currentSpeed) {\n      force = _maxDriveForce;\n    }\n\n    if (force.abs() > 0) {\n      body.applyForce(currentForwardNormal..scale(_currentTraction * force));\n    }\n  }\n\n  void _updateTurn(double dt) {\n    var desiredAngle = 0.0;\n    var desiredTorque = 0.0;\n    var isTurning = false;\n    if (pressedKeys.contains(LogicalKeyboardKey.arrowLeft)) {\n      desiredTorque = -15.0;\n      desiredAngle = -_lockAngle;\n      isTurning = true;\n    }\n    if (pressedKeys.contains(LogicalKeyboardKey.arrowRight)) {\n      desiredTorque += 15.0;\n      desiredAngle += _lockAngle;\n      isTurning = true;\n    }\n    if (isTurnableTire && isTurning) {\n      final turnPerTimeStep = _turnSpeedPerSecond * dt;\n      final angleNow = joint.jointAngle();\n      final angleToTurn = (desiredAngle - angleNow).clamp(\n        -turnPerTimeStep,\n        turnPerTimeStep,\n      );\n      final angle = angleNow + angleToTurn;\n      joint.setLimits(angle, angle);\n    } else {\n      joint.setLimits(0, 0);\n    }\n    body.applyTorque(desiredTorque);\n  }\n\n  // Cached Vectors to reduce unnecessary object creation.\n  final Vector2 _worldLeft = Vector2(1.0, 0.0);\n  final Vector2 _worldUp = Vector2(0.0, -1.0);\n\n  Vector2 get _lateralVelocity {\n    final currentRightNormal = body.worldVector(_worldLeft);\n    return currentRightNormal\n      ..scale(currentRightNormal.dot(body.linearVelocity));\n  }\n\n  Vector2 get _forwardVelocity {\n    final currentForwardNormal = body.worldVector(_worldUp);\n    return currentForwardNormal\n      ..scale(currentForwardNormal.dot(body.linearVelocity));\n  }\n}\n"
  },
  {
    "path": "examples/games/padracing/lib/trail.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\n\nimport 'package:padracing/car.dart';\nimport 'package:padracing/tire.dart';\n\nclass Trail extends Component with HasPaint {\n  Trail({\n    required this.car,\n    required this.tire,\n  }) : super(priority: 1);\n\n  final Car car;\n  final Tire tire;\n\n  final trail = <Offset>[];\n  final _trailLength = 30;\n\n  @override\n  Future<void> onLoad() async {\n    paint\n      ..color = (tire.paint.color.withValues(alpha: 0.9))\n      ..strokeWidth = 1.0;\n  }\n\n  @override\n  void update(double dt) {\n    if (tire.body.linearVelocity.length2 > 100) {\n      if (trail.length > _trailLength) {\n        trail.removeAt(0);\n      }\n      final trailPoint = tire.body.position.toOffset();\n      trail.add(trailPoint);\n    } else if (trail.isNotEmpty) {\n      trail.removeAt(0);\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawPoints(PointMode.polygon, trail, paint);\n  }\n}\n"
  },
  {
    "path": "examples/games/padracing/lib/wall.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart' hide Particle, World;\n\nimport 'package:padracing/padracing_game.dart';\n\nList<Wall> createWalls(Vector2 size) {\n  final topCenter = Vector2(size.x / 2, 0);\n  final bottomCenter = Vector2(size.x / 2, size.y);\n  final leftCenter = Vector2(0, size.y / 2);\n  final rightCenter = Vector2(size.x, size.y / 2);\n\n  final filledSize = size.clone() + Vector2.all(5);\n  return [\n    Wall(topCenter, Vector2(filledSize.x, 5)),\n    Wall(leftCenter, Vector2(5, filledSize.y)),\n    Wall(Vector2(52.5, 240), Vector2(5, 380)),\n    Wall(Vector2(200, 50), Vector2(300, 5)),\n    Wall(Vector2(72.5, 300), Vector2(5, 400)),\n    Wall(Vector2(180, 100), Vector2(220, 5)),\n    Wall(Vector2(350, 105), Vector2(5, 115)),\n    Wall(Vector2(310, 160), Vector2(240, 5)),\n    Wall(Vector2(211.5, 400), Vector2(283, 5)),\n    Wall(Vector2(351, 312.5), Vector2(5, 180)),\n    Wall(Vector2(430, 302.5), Vector2(5, 290)),\n    Wall(Vector2(292.5, 450), Vector2(280, 5)),\n    Wall(bottomCenter, Vector2(filledSize.y, 5)),\n    Wall(rightCenter, Vector2(5, filledSize.y)),\n  ];\n}\n\nclass Wall extends BodyComponent<PadRacingGame> {\n  Wall(this._position, this.size) : super(priority: 3);\n\n  final Vector2 _position;\n  final Vector2 size;\n\n  final Random rng = Random();\n  late final Image _image;\n\n  final scale = 10.0;\n  late final _renderPosition = -size.toOffset() / 2;\n  late final _scaledRect = (size * scale).toRect();\n  late final _renderRect = _renderPosition & size.toSize();\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    paint.color = ColorExtension.fromRGBHexString('#14F596');\n\n    final recorder = PictureRecorder();\n    final canvas = Canvas(recorder, _scaledRect);\n    final drawSize = _scaledRect.size.toVector2();\n    final center = (drawSize / 2).toOffset();\n    const step = 1.0;\n\n    canvas.drawRect(\n      Rect.fromCenter(center: center, width: drawSize.x, height: drawSize.y),\n      BasicPalette.black.paint(),\n    );\n    paint.style = PaintingStyle.stroke;\n    paint.strokeWidth = step;\n    for (var x = 0; x < 30; x++) {\n      canvas.drawRect(\n        Rect.fromCenter(center: center, width: drawSize.x, height: drawSize.y),\n        paint,\n      );\n      paint.color = paint.color.darken(0.07);\n      drawSize.x -= step;\n      drawSize.y -= step;\n    }\n    final picture = recorder.endRecording();\n    _image = await picture.toImage(\n      _scaledRect.width.toInt(),\n      _scaledRect.height.toInt(),\n    );\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawImageRect(\n      _image,\n      _scaledRect,\n      _renderRect,\n      paint,\n    );\n  }\n\n  @override\n  Body createBody() {\n    final def = BodyDef()\n      ..type = BodyType.static\n      ..position = _position;\n    final body = world.createBody(def)\n      ..userData = this\n      ..angularDamping = 3.0;\n\n    final shape = PolygonShape()..setAsBoxXY(size.x / 2, size.y / 2);\n    final fixtureDef = FixtureDef(shape)..restitution = 0.5;\n    return body..createFixture(fixtureDef);\n  }\n\n  late Rect asRect = Rect.fromCenter(\n    center: _position.toOffset(),\n    width: size.x,\n    height: size.y,\n  );\n}\n"
  },
  {
    "path": "examples/games/padracing/pubspec.yaml",
    "content": "name: padracing\nresolution: workspace\ndescription: A sample game featuring Flame and Forge2D for DartPad\npublish_to: 'none'\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  collection: ^1.17.1\n  flame: ^1.36.0\n  flame_forge2d: ^0.19.2+5\n  flutter:\n    sdk: flutter\n  google_fonts: ^8.0.2\n  url_launcher: ^6.1.11\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n\nflutter:\n  uses-material-design: true\n"
  },
  {
    "path": "examples/games/padracing/scripts/merge_files.sh",
    "content": "#!/usr/bin/env bash\n# This script is used to create a dart file that can run in Dartpad.\n# If you are on linux it will copy the output to the clipboard, otherwise\n# you can find the resulting file in scripts/main.dart (which also is generated\n# on linux).\nPROJECT_ROOT=$(dirname $(realpath -s $0))/..\nLIB=$PROJECT_ROOT/lib\nOUTPUT=$PROJECT_ROOT/scripts\n\ncat $LIB/main.dart > $OUTPUT/main.dart\ncat $LIB/padracing_game.dart >> $OUTPUT/main.dart\ncd $LIB\ncat $(ls | grep -v 'main.dart' | grep -v 'padracing_game.dart') >> $OUTPUT/main.dart\ncd $OUTPUT\ngrep import < main.dart > imports.dart\ngrep -v import < main.dart > tmp.dart\nLC_COLLATE=c sort -u imports.dart | grep -v padracing > imports_tmp.dart\ncat imports_tmp.dart tmp.dart > main.dart\nrm tmp.dart imports_tmp.dart imports.dart\necho '// ignore_for_file: avoid_web_libraries_in_flutter' >> main.dart\ndart format main.dart\n\nif command -v xclip &> /dev/null\nthen\n    echo \"Copied with xclip\"\n    xclip -selection clipboard < main.dart\nelif command -v wl-copy &> /dev/null\nthen\n    echo \"Copied with wl-copy\"\n    wl-copy < main.dart\nfi\n"
  },
  {
    "path": "examples/games/rogue_shooter/README.md",
    "content": "# Flame Performance Test Game\n\nThis is a simple scrolling shooter game which we use for testing the performance of Flame,\nsince it uses a lot of components and hitboxes. When it reaches a certain amount of\ncomponents (counted in the lower right corner) you can expect it to drop a bit in FPS,\ndepending on what platform you are on.\n"
  },
  {
    "path": "examples/games/rogue_shooter/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "examples/games/rogue_shooter/lib/components/bullet_component.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:rogue_shooter/components/enemy_component.dart';\n\nclass BulletComponent extends SpriteAnimationComponent\n    with HasGameReference, CollisionCallbacks {\n  static const speed = 500.0;\n  late final Vector2 velocity;\n  final Vector2 deltaPosition = Vector2.zero();\n\n  BulletComponent({required super.position, super.angle})\n    : super(size: Vector2(10, 20), anchor: Anchor.center);\n\n  @override\n  Future<void> onLoad() async {\n    add(CircleHitbox());\n    animation = await game.loadSpriteAnimation(\n      'rogue_shooter/bullet.png',\n      SpriteAnimationData.sequenced(\n        stepTime: 0.2,\n        amount: 4,\n        textureSize: Vector2(8, 16),\n      ),\n    );\n    velocity = Vector2(0, -1)\n      ..rotate(angle)\n      ..scale(speed);\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n    if (other is EnemyComponent) {\n      other.takeHit();\n      removeFromParent();\n    }\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    deltaPosition\n      ..setFrom(velocity)\n      ..scale(dt);\n    position += deltaPosition;\n\n    if (position.y < 0 || position.x > game.size.x || position.x + size.x < 0) {\n      removeFromParent();\n    }\n  }\n}\n"
  },
  {
    "path": "examples/games/rogue_shooter/lib/components/enemy_component.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:rogue_shooter/components/explosion_component.dart';\nimport 'package:rogue_shooter/rogue_shooter_game.dart';\n\nclass EnemyComponent extends SpriteAnimationComponent\n    with HasGameReference<RogueShooterGame>, CollisionCallbacks {\n  static const speed = 150;\n  static final Vector2 initialSize = Vector2.all(25);\n\n  EnemyComponent({required super.position})\n    : super(size: initialSize, anchor: Anchor.center);\n\n  @override\n  Future<void> onLoad() async {\n    animation = await game.loadSpriteAnimation(\n      'rogue_shooter/enemy.png',\n      SpriteAnimationData.sequenced(\n        stepTime: 0.2,\n        amount: 4,\n        textureSize: Vector2.all(16),\n      ),\n    );\n    add(CircleHitbox(collisionType: CollisionType.passive));\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    y += speed * dt;\n    if (y >= game.size.y) {\n      removeFromParent();\n    }\n  }\n\n  void takeHit() {\n    removeFromParent();\n\n    game.explosionGroup.add(ExplosionComponent(position: position));\n    game.increaseScore();\n  }\n}\n"
  },
  {
    "path": "examples/games/rogue_shooter/lib/components/enemy_creator.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:rogue_shooter/components/enemy_component.dart';\nimport 'package:rogue_shooter/rogue_shooter_game.dart';\n\nclass EnemyCreator extends TimerComponent\n    with HasGameReference<RogueShooterGame> {\n  final Random random = Random();\n  final _halfWidth = EnemyComponent.initialSize.x / 2;\n\n  EnemyCreator() : super(period: 0.05, repeat: true);\n\n  @override\n  void onTick() {\n    game.enemyGroup.addAll(\n      List.generate(\n        5,\n        (index) => EnemyComponent(\n          position: Vector2(\n            _halfWidth + (game.size.x - _halfWidth) * random.nextDouble(),\n            0,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/games/rogue_shooter/lib/components/explosion_component.dart",
    "content": "import 'package:flame/components.dart';\n\nclass ExplosionComponent extends SpriteAnimationComponent\n    with HasGameReference {\n  ExplosionComponent({super.position})\n    : super(\n        size: Vector2.all(50),\n        anchor: Anchor.center,\n        removeOnFinish: true,\n      );\n\n  @override\n  Future<void> onLoad() async {\n    animation = await game.loadSpriteAnimation(\n      'rogue_shooter/explosion.png',\n      SpriteAnimationData.sequenced(\n        stepTime: 0.1,\n        amount: 6,\n        textureSize: Vector2.all(32),\n        loop: false,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/games/rogue_shooter/lib/components/player_component.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:rogue_shooter/components/bullet_component.dart';\nimport 'package:rogue_shooter/components/enemy_component.dart';\nimport 'package:rogue_shooter/components/explosion_component.dart';\nimport 'package:rogue_shooter/rogue_shooter_game.dart';\n\nclass PlayerComponent extends SpriteAnimationComponent\n    with HasGameReference<RogueShooterGame>, CollisionCallbacks {\n  late TimerComponent bulletCreator;\n\n  PlayerComponent() : super(size: Vector2(50, 75), anchor: Anchor.center);\n\n  @override\n  Future<void> onLoad() async {\n    position = game.size / 2;\n    add(CircleHitbox());\n    add(\n      bulletCreator = TimerComponent(\n        period: 0.05,\n        repeat: true,\n        autoStart: false,\n        onTick: _createBullet,\n      ),\n    );\n    animation = await game.loadSpriteAnimation(\n      'rogue_shooter/player.png',\n      SpriteAnimationData.sequenced(\n        stepTime: 0.2,\n        amount: 4,\n        textureSize: Vector2(32, 39),\n      ),\n    );\n  }\n\n  final _bulletAngles = [0.5, 0.3, 0.0, -0.5, -0.3];\n  void _createBullet() {\n    game.bulletGroup.addAll(\n      _bulletAngles.map(\n        (angle) => BulletComponent(\n          position: position + Vector2(0, -size.y / 2),\n          angle: angle,\n        ),\n      ),\n    );\n  }\n\n  void beginFire() {\n    bulletCreator.timer.start();\n  }\n\n  void stopFire() {\n    bulletCreator.timer.pause();\n  }\n\n  void takeHit() {\n    game.explosionGroup.add(ExplosionComponent(position: position));\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n    if (other is EnemyComponent) {\n      other.takeHit();\n    }\n  }\n}\n"
  },
  {
    "path": "examples/games/rogue_shooter/lib/components/star_background_creator.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/sprite.dart';\nimport 'package:rogue_shooter/components/star_component.dart';\nimport 'package:rogue_shooter/rogue_shooter_game.dart';\n\nclass StarBackGroundCreator extends Component\n    with HasGameReference<RogueShooterGame> {\n  final gapSize = 12;\n\n  late final SpriteSheet spriteSheet;\n  Random random = Random();\n\n  StarBackGroundCreator();\n\n  @override\n  Future<void> onLoad() async {\n    spriteSheet = SpriteSheet.fromColumnsAndRows(\n      image: await game.images.load('rogue_shooter/stars.png'),\n      rows: 4,\n      columns: 4,\n    );\n\n    final starGapTime = (game.size.y / gapSize) / StarComponent.speed;\n\n    add(\n      TimerComponent(\n        period: starGapTime,\n        repeat: true,\n        onTick: () => _createRowOfStars(0),\n      ),\n    );\n\n    _createInitialStars();\n  }\n\n  void _createStarAt(double x, double y) {\n    final animation = spriteSheet.createAnimation(\n      row: random.nextInt(3),\n      to: 4,\n      stepTime: 0.1,\n    )..variableStepTimes = [max(20, 100 * random.nextDouble()), 0.1, 0.1, 0.1];\n\n    game.starGroup.add(\n      StarComponent(animation: animation, position: Vector2(x, y)),\n    );\n  }\n\n  void _createRowOfStars(double y) {\n    const gapSize = 6;\n    final starGap = game.size.x / gapSize;\n\n    for (var i = 0; i < gapSize; i++) {\n      _createStarAt(\n        starGap * i + (random.nextDouble() * starGap),\n        y + (random.nextDouble() * 20),\n      );\n    }\n  }\n\n  void _createInitialStars() {\n    final rows = game.size.y / gapSize;\n\n    for (var i = 0; i < gapSize; i++) {\n      _createRowOfStars(i * rows);\n    }\n  }\n}\n"
  },
  {
    "path": "examples/games/rogue_shooter/lib/components/star_component.dart",
    "content": "import 'package:flame/components.dart';\n\nclass StarComponent extends SpriteAnimationComponent with HasGameReference {\n  static const speed = 10;\n\n  StarComponent({super.animation, super.position})\n    : super(size: Vector2.all(20));\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    y += dt * speed;\n    if (y >= game.size.y) {\n      removeFromParent();\n    }\n  }\n}\n"
  },
  {
    "path": "examples/games/rogue_shooter/lib/main.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:rogue_shooter/rogue_shooter_game.dart';\n\nvoid main() {\n  runApp(GameWidget(game: RogueShooterGame()));\n}\n"
  },
  {
    "path": "examples/games/rogue_shooter/lib/rogue_shooter_game.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/services.dart';\nimport 'package:rogue_shooter/components/enemy_creator.dart';\nimport 'package:rogue_shooter/components/player_component.dart';\nimport 'package:rogue_shooter/components/star_background_creator.dart';\n\nclass RogueShooterGame extends FlameGame\n    with\n        DragCallbacks,\n        HasCollisionDetection,\n        HasPerformanceTracker,\n        HasKeyboardHandlerComponents {\n  static const String description = '''\n    A simple space shooter game used for testing performance of the collision\n    detection system in Flame.\n  ''';\n\n  late final PlayerComponent _player;\n  late final TextComponent _componentCounter;\n  late final TextComponent _scoreText;\n\n  // Batch groups — one per sprite type for isolated draw-call batching.\n  // Each is a plain PositionComponent with HasAutoBatchedChildren mixed in.\n  late final BatchGroup bulletGroup;\n  late final BatchGroup enemyGroup;\n  late final BatchGroup starGroup;\n  late final BatchGroup explosionGroup;\n\n  static final _textStyleRed = TextPaint(\n    style: TextPaint.defaultTextStyle.copyWith(color: const Color(0xFFFF0000)),\n  );\n\n  static final _textStyleGreen = TextPaint(\n    style: TextPaint.defaultTextStyle.copyWith(color: const Color(0xFF00FF00)),\n  );\n\n  final _updateTime = TextComponent(\n    text: 'Update time: 0ms',\n    position: Vector2(0, 0),\n    priority: 1,\n  );\n\n  final TextComponent _renderTime = TextComponent(\n    text: 'Render time: 0ms',\n    position: Vector2(0, 25),\n    priority: 1,\n  );\n\n  final TextComponent _batchingText = TextComponent(\n    position: Vector2(0, 50),\n    priority: 1,\n    textRenderer: _textStyleRed,\n  );\n\n  int _score = 0;\n\n  @override\n  Future<void> onLoad() async {\n    // Add batch groups first so component creators can reference them.\n    addAll([\n      starGroup = BatchGroup(priority: -1),\n      bulletGroup = BatchGroup(priority: 0),\n      enemyGroup = BatchGroup(priority: 0),\n      explosionGroup = BatchGroup(priority: 0),\n    ]);\n\n    add(_player = PlayerComponent());\n    addAll([\n      FpsTextComponent(\n        position: size - Vector2(0, 50),\n        anchor: Anchor.bottomRight,\n      ),\n      _scoreText = TextComponent(\n        position: size - Vector2(0, 25),\n        anchor: Anchor.bottomRight,\n        priority: 1,\n      ),\n      _componentCounter = TextComponent(\n        position: size,\n        anchor: Anchor.bottomRight,\n        priority: 1,\n      ),\n    ]);\n\n    add(EnemyCreator());\n    add(StarBackGroundCreator());\n\n    addAll([_updateTime, _renderTime, _batchingText]);\n    _updateBatchingLabel();\n\n    add(\n      KeyboardListenerComponent(\n        keyDown: {\n          LogicalKeyboardKey.keyB: (_) {\n            final enabled = !bulletGroup.batchingEnabled;\n            bulletGroup.batchingEnabled = enabled;\n            enemyGroup.batchingEnabled = enabled;\n            starGroup.batchingEnabled = enabled;\n            explosionGroup.batchingEnabled = enabled;\n            _updateBatchingLabel();\n            return true;\n          },\n        },\n      ),\n    );\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    _scoreText.text = 'Score: $_score';\n    _componentCounter.text = 'Components: ${descendants().length}';\n    _updateTime.text = 'Update time: $updateTime ms';\n    _renderTime.text = 'Render time: $renderTime ms';\n  }\n\n  /// Whether all batch groups are currently enabled.\n  bool get batchingEnabled => bulletGroup.batchingEnabled;\n\n  void _updateBatchingLabel() {\n    _batchingText.text =\n        'Batching: ${batchingEnabled ? \"ON\" : \"OFF\"}  [press B to toggle]';\n\n    _batchingText.textRenderer = TextPaint(\n      style: batchingEnabled ? _textStyleGreen.style : _textStyleRed.style,\n    );\n  }\n\n  @override\n  void onDragStart(DragStartEvent event) {\n    _player.beginFire();\n    super.onDragStart(event);\n  }\n\n  @override\n  void onDragEnd(DragEndEvent event) {\n    _player.stopFire();\n    super.onDragEnd(event);\n  }\n\n  @override\n  void onDragCancel(DragCancelEvent event) {\n    _player.stopFire();\n    super.onDragCancel(event);\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    _player.position += event.canvasDelta;\n    super.onDragUpdate(event);\n  }\n\n  void increaseScore() {\n    _score++;\n  }\n}\n\nclass BatchGroup extends PositionComponent with HasAutoBatchedChildren {\n  BatchGroup({super.priority, bool batchingEnabled = false}) {\n    this.batchingEnabled = batchingEnabled;\n  }\n}\n"
  },
  {
    "path": "examples/games/rogue_shooter/lib/rogue_shooter_widget.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:rogue_shooter/rogue_shooter_game.dart';\n\nclass RogueShooterWidget extends StatelessWidget {\n  const RogueShooterWidget({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return GameWidget(\n      game: RogueShooterGame(),\n      loadingBuilder: (_) => const Center(\n        child: Text('Loading'),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/games/rogue_shooter/pubspec.yaml",
    "content": "name: rogue_shooter\nresolution: workspace\ndescription: A simple game benchmarking the collision detection performance.\nhomepage: https://github.com/flame-engine/flame/tree/main/examples/games/rogue_shooter\npublish_to: 'none'\n\nversion: 0.1.0\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n\nflutter:\n  assets:\n    - assets/images/rogue_shooter/\n"
  },
  {
    "path": "examples/games/trex/README.md",
    "content": "# T-rex\n\n![https://cdn-images-1.medium.com/max/1600/1*BadLUm5ZzpcS34eTVQAz5g.gif](https://cdn-images-1.medium.com/max/1600/1*BadLUm5ZzpcS34eTVQAz5g.gif)\n\nThe joy of our offline hours recreated with [Flutter](https://github.com/flutter/flutter) and [Flame](https://github.com/flame-engine/flame)\n\n\n## Article\n\nThis was the original article written when the game initially was ported.\n(It is outdated now since it uses a very old version of Flame)\n\n<https://medium.com/@renancaraujo/creating-the-t-rex-game-with-flutter-and-flame-6d01add1ad5b>\n"
  },
  {
    "path": "examples/games/trex/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "examples/games/trex/lib/background/cloud.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:trex_game/background/cloud_manager.dart';\nimport 'package:trex_game/trex_game.dart';\n\nclass Cloud extends SpriteComponent\n    with ParentIsA<CloudManager>, HasGameReference<TRexGame> {\n  Cloud({required Vector2 position})\n    : super(\n        position: position,\n        size: initialSize,\n      );\n\n  static final Vector2 initialSize = Vector2(92.0, 28.0);\n\n  static const double maxCloudGap = 400.0;\n  static const double minCloudGap = 100.0;\n\n  static const double maxSkyLevel = 71.0;\n  static const double minSkyLevel = 30.0;\n\n  late final double cloudGap = game.random.nextDoubleBetween(\n    minCloudGap,\n    maxCloudGap,\n  );\n\n  @override\n  Future<void> onLoad() async {\n    sprite = Sprite(\n      game.spriteImage,\n      srcPosition: Vector2(166.0, 2.0),\n      srcSize: initialSize,\n    );\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    if (isRemoving) {\n      return;\n    }\n    x -= parent.cloudSpeed.ceil() * 50 * dt;\n\n    if (!isVisible) {\n      removeFromParent();\n    }\n  }\n\n  bool get isVisible {\n    return x + width > 0;\n  }\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    y =\n        ((absolutePosition.y / 2 - (maxSkyLevel - minSkyLevel)) +\n            game.random.nextDoubleBetween(minSkyLevel, maxSkyLevel)) -\n        absolutePositionOf(absoluteTopLeftPosition).y;\n  }\n}\n"
  },
  {
    "path": "examples/games/trex/lib/background/cloud_manager.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:trex_game/background/cloud.dart';\nimport 'package:trex_game/trex_game.dart';\n\nclass CloudManager extends PositionComponent with HasGameReference<TRexGame> {\n  final double cloudFrequency = 0.5;\n  final int maxClouds = 20;\n  final double bgCloudSpeed = 0.2;\n\n  void addCloud() {\n    final cloudPosition = Vector2(\n      game.size.x + Cloud.initialSize.x + 10,\n      (absolutePosition.y / 2 - (Cloud.maxSkyLevel - Cloud.minSkyLevel)) +\n          game.random.nextDoubleBetween(Cloud.minSkyLevel, Cloud.maxSkyLevel) -\n          absolutePosition.y,\n    );\n    add(Cloud(position: cloudPosition));\n  }\n\n  double get cloudSpeed => bgCloudSpeed / 1000 * game.currentSpeed;\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    final numClouds = children.length;\n    if (numClouds > 0) {\n      final lastCloud = children.last as Cloud;\n      if (numClouds < maxClouds &&\n          (game.size.x / 2 - lastCloud.x) > lastCloud.cloudGap) {\n        addCloud();\n      }\n    } else {\n      addCloud();\n    }\n  }\n\n  void reset() {\n    removeAll(children);\n  }\n}\n"
  },
  {
    "path": "examples/games/trex/lib/background/horizon.dart",
    "content": "import 'dart:collection';\nimport 'dart:math';\n\nimport 'package:collection/collection.dart';\nimport 'package:flame/components.dart';\nimport 'package:trex_game/background/cloud_manager.dart';\nimport 'package:trex_game/obstacle/obstacle_manager.dart';\nimport 'package:trex_game/trex_game.dart';\n\nclass Horizon extends PositionComponent with HasGameReference<TRexGame> {\n  Horizon() : super();\n\n  static final Vector2 lineSize = Vector2(1200, 24);\n  final Queue<SpriteComponent> groundLayers = Queue();\n  late final CloudManager cloudManager = CloudManager();\n  late final ObstacleManager obstacleManager = ObstacleManager();\n\n  late final _softSprite = Sprite(\n    game.spriteImage,\n    srcPosition: Vector2(2.0, 104.0),\n    srcSize: lineSize,\n  );\n\n  late final _bumpySprite = Sprite(\n    game.spriteImage,\n    srcPosition: Vector2(game.spriteImage.width / 2, 104.0),\n    srcSize: lineSize,\n  );\n\n  @override\n  Future<void> onLoad() async {\n    add(cloudManager);\n    add(obstacleManager);\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    final increment = game.currentSpeed * dt;\n    for (final line in groundLayers) {\n      line.x -= increment;\n    }\n\n    final firstLine = groundLayers.first;\n    if (firstLine.x <= -firstLine.width) {\n      firstLine.x = groundLayers.last.x + groundLayers.last.width;\n      groundLayers.remove(firstLine);\n      groundLayers.add(firstLine);\n    }\n  }\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    final newLines = _generateLines();\n    groundLayers.addAll(newLines);\n    addAll(newLines);\n    y = (size.y / 2) + 21.0;\n  }\n\n  void reset() {\n    cloudManager.reset();\n    obstacleManager.reset();\n    groundLayers.forEachIndexed((i, line) => line.x = i * lineSize.x);\n  }\n\n  List<SpriteComponent> _generateLines() {\n    final number = 1 + (game.size.x / lineSize.x).ceil() - groundLayers.length;\n    final lastX =\n        (groundLayers.lastOrNull?.x ?? 0) +\n        (groundLayers.lastOrNull?.width ?? 0);\n    return List.generate(\n      max(number, 0),\n      (i) => SpriteComponent(\n        sprite: (i + groundLayers.length).isEven ? _softSprite : _bumpySprite,\n        size: lineSize,\n      )..x = lastX + lineSize.x * i,\n      growable: false,\n    );\n  }\n}\n"
  },
  {
    "path": "examples/games/trex/lib/game_over.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:trex_game/trex_game.dart';\n\nclass GameOverPanel extends Component {\n  bool visible = false;\n\n  @override\n  Future<void> onLoad() async {\n    add(GameOverText());\n    add(GameOverRestart());\n  }\n\n  @override\n  void renderTree(Canvas canvas) {\n    if (visible) {\n      super.renderTree(canvas);\n    }\n  }\n}\n\nclass GameOverText extends SpriteComponent with HasGameReference<TRexGame> {\n  GameOverText() : super(size: Vector2(382, 25), anchor: Anchor.center);\n\n  @override\n  Future<void> onLoad() async {\n    sprite = Sprite(\n      game.spriteImage,\n      srcPosition: Vector2(955.0, 26.0),\n      srcSize: size,\n    );\n  }\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    x = size.x / 2;\n    y = size.y * 0.25;\n  }\n}\n\nclass GameOverRestart extends SpriteComponent with HasGameReference<TRexGame> {\n  GameOverRestart() : super(size: Vector2(72, 64), anchor: Anchor.center);\n\n  @override\n  Future<void> onLoad() async {\n    sprite = Sprite(\n      game.spriteImage,\n      srcPosition: Vector2.all(2.0),\n      srcSize: size,\n    );\n  }\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    x = size.x / 2;\n    y = size.y * 0.75;\n  }\n}\n"
  },
  {
    "path": "examples/games/trex/lib/main.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:trex_game/trex_game.dart';\n\nvoid main() {\n  runApp(\n    GameWidget(game: TRexGame()),\n  );\n}\n"
  },
  {
    "path": "examples/games/trex/lib/obstacle/obstacle.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:trex_game/obstacle/obstacle_type.dart';\nimport 'package:trex_game/trex_game.dart';\n\nclass Obstacle extends SpriteComponent with HasGameReference<TRexGame> {\n  Obstacle({\n    required this.settings,\n    required this.groupIndex,\n  }) : super(size: settings.size);\n\n  final double _gapCoefficient = 0.6;\n  final double _maxGapCoefficient = 1.5;\n\n  bool followingObstacleCreated = false;\n  late double gap;\n  final ObstacleTypeSettings settings;\n  final int groupIndex;\n\n  bool get isVisible => (x + width) > 0;\n\n  @override\n  Future<void> onLoad() async {\n    sprite = settings.sprite(game.spriteImage);\n    x = game.size.x + width * groupIndex;\n    y = settings.y;\n    gap = computeGap(_gapCoefficient, game.currentSpeed);\n    addAll(settings.generateHitboxes());\n  }\n\n  double computeGap(double gapCoefficient, double speed) {\n    final minGap = (width * speed * settings.minGap * gapCoefficient)\n        .roundToDouble();\n    final maxGap = (minGap * _maxGapCoefficient).roundToDouble();\n    return game.random.nextDoubleBetween(minGap, maxGap);\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    x -= game.currentSpeed * dt;\n\n    if (!isVisible) {\n      removeFromParent();\n    }\n  }\n}\n"
  },
  {
    "path": "examples/games/trex/lib/obstacle/obstacle_manager.dart",
    "content": "import 'dart:collection';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:trex_game/obstacle/obstacle.dart';\nimport 'package:trex_game/obstacle/obstacle_type.dart';\nimport 'package:trex_game/trex_game.dart';\n\nclass ObstacleManager extends Component with HasGameReference<TRexGame> {\n  ObstacleManager();\n\n  ListQueue<ObstacleType> history = ListQueue();\n  static const int maxObstacleDuplication = 2;\n\n  @override\n  void update(double dt) {\n    final obstacles = children.query<Obstacle>();\n\n    if (obstacles.isNotEmpty) {\n      final lastObstacle = children.last as Obstacle?;\n\n      if (lastObstacle != null &&\n          !lastObstacle.followingObstacleCreated &&\n          lastObstacle.isVisible &&\n          (lastObstacle.x + lastObstacle.width + lastObstacle.gap) <\n              game.size.x) {\n        addNewObstacle();\n        lastObstacle.followingObstacleCreated = true;\n      }\n    } else {\n      addNewObstacle();\n    }\n  }\n\n  void addNewObstacle() {\n    final speed = game.currentSpeed;\n    if (speed == 0) {\n      return;\n    }\n    var settings = game.random.nextBool()\n        ? ObstacleTypeSettings.cactusSmall\n        : ObstacleTypeSettings.cactusLarge;\n    if (duplicateObstacleCheck(settings.type) || speed < settings.allowedAt) {\n      settings = ObstacleTypeSettings.cactusSmall;\n    }\n\n    final groupSize = _groupSize(settings);\n    for (var i = 0; i < groupSize; i++) {\n      add(Obstacle(settings: settings, groupIndex: i));\n      game.score++;\n    }\n\n    history.addFirst(settings.type);\n    while (history.length > maxObstacleDuplication) {\n      history.removeLast();\n    }\n  }\n\n  bool duplicateObstacleCheck(ObstacleType nextType) {\n    var duplicateCount = 0;\n\n    for (final type in history) {\n      duplicateCount += type == nextType ? 1 : 0;\n    }\n    return duplicateCount >= maxObstacleDuplication;\n  }\n\n  void reset() {\n    removeAll(children);\n    history.clear();\n  }\n\n  int _groupSize(ObstacleTypeSettings settings) {\n    if (game.currentSpeed > settings.multipleAt) {\n      return game.random\n          .nextDoubleBetween(1.0, ObstacleTypeSettings.maxGroupSize)\n          .floor();\n    } else {\n      return 1;\n    }\n  }\n}\n"
  },
  {
    "path": "examples/games/trex/lib/obstacle/obstacle_type.dart",
    "content": "// ignore_for_file: unused_element, unused_element_parameter\n\nimport 'dart:ui';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\n\nenum ObstacleType {\n  cactusSmall,\n  cactusLarge,\n}\n\nclass ObstacleTypeSettings {\n  const ObstacleTypeSettings._internal(\n    this.type, {\n    required this.size,\n    required this.y,\n    required this.allowedAt,\n    required this.multipleAt,\n    required this.minGap,\n    required this.minSpeed,\n    required this.generateHitboxes,\n    this.numFrames,\n    this.frameRate,\n    this.speedOffset,\n  });\n\n  final ObstacleType type;\n  final Vector2 size;\n  final double y;\n  final int allowedAt;\n  final int multipleAt;\n  final double minGap;\n  final double minSpeed;\n  final int? numFrames;\n  final double? frameRate;\n  final double? speedOffset;\n\n  static const maxGroupSize = 3.0;\n\n  final List<ShapeHitbox> Function() generateHitboxes;\n\n  static final cactusSmall = ObstacleTypeSettings._internal(\n    ObstacleType.cactusSmall,\n    size: Vector2(34.0, 70.0),\n    y: -55.0,\n    allowedAt: 0,\n    multipleAt: 1000,\n    minGap: 120.0,\n    minSpeed: 0.0,\n    generateHitboxes: () => <ShapeHitbox>[\n      RectangleHitbox(\n        position: Vector2(5.0, 7.0),\n        size: Vector2(10.0, 54.0),\n      ),\n      RectangleHitbox(\n        position: Vector2(5.0, 7.0),\n        size: Vector2(12.0, 68.0),\n      ),\n      RectangleHitbox(\n        position: Vector2(15.0, 4.0),\n        size: Vector2(14.0, 28.0),\n      ),\n    ],\n  );\n\n  static final cactusLarge = ObstacleTypeSettings._internal(\n    ObstacleType.cactusLarge,\n    size: Vector2(50.0, 100.0),\n    y: -74.0,\n    allowedAt: 800,\n    multipleAt: 1500,\n    minGap: 120.0,\n    minSpeed: 0.0,\n    generateHitboxes: () => <ShapeHitbox>[\n      RectangleHitbox(\n        position: Vector2(0.0, 26.0),\n        size: Vector2(14.0, 40.0),\n      ),\n      RectangleHitbox(\n        position: Vector2(16.0, 0.0),\n        size: Vector2(14.0, 98.0),\n      ),\n      RectangleHitbox(\n        position: Vector2(28.0, 22.0),\n        size: Vector2(20.0, 40.0),\n      ),\n    ],\n  );\n\n  Sprite sprite(Image spriteImage) {\n    return switch (type) {\n      ObstacleType.cactusSmall => Sprite(\n        spriteImage,\n        srcPosition: Vector2(446.0, 2.0),\n        srcSize: size,\n      ),\n      ObstacleType.cactusLarge => Sprite(\n        spriteImage,\n        srcPosition: Vector2(652.0, 2.0),\n        srcSize: size,\n      ),\n    };\n  }\n}\n"
  },
  {
    "path": "examples/games/trex/lib/player.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:trex_game/trex_game.dart';\n\nenum PlayerState { crashed, jumping, running, waiting }\n\nclass Player extends SpriteAnimationGroupComponent<PlayerState>\n    with HasGameReference<TRexGame>, CollisionCallbacks {\n  Player() : super(size: Vector2(90, 88));\n\n  final double gravity = 0.85;\n\n  final double initialJumpVelocity = -16;\n  final double introDuration = 1500.0;\n  final double startXPosition = 50;\n\n  double _jumpVelocity = 0.0;\n\n  double get groundYPos {\n    return (game.size.y / 2) - height / 2;\n  }\n\n  @override\n  Future<void> onLoad() async {\n    // Body hitbox\n    add(\n      RectangleHitbox.relative(\n        Vector2(0.7, 0.6),\n        position: Vector2(0, height / 3),\n        parentSize: size,\n      ),\n    );\n    // Head hitbox\n    add(\n      RectangleHitbox.relative(\n        Vector2(0.45, 0.35),\n        position: Vector2(width / 2, 0),\n        parentSize: size,\n      ),\n    );\n    animations = {\n      PlayerState.running: _getAnimation(\n        size: Vector2(88.0, 90.0),\n        frames: [Vector2(1514.0, 4.0), Vector2(1602.0, 4.0)],\n        stepTime: 0.2,\n      ),\n      PlayerState.waiting: _getAnimation(\n        size: Vector2(88.0, 90.0),\n        frames: [Vector2(76.0, 6.0)],\n      ),\n      PlayerState.jumping: _getAnimation(\n        size: Vector2(88.0, 90.0),\n        frames: [Vector2(1339.0, 6.0)],\n      ),\n      PlayerState.crashed: _getAnimation(\n        size: Vector2(88.0, 90.0),\n        frames: [Vector2(1782.0, 6.0)],\n      ),\n    };\n    current = PlayerState.waiting;\n  }\n\n  void jump(double speed) {\n    if (current == PlayerState.jumping) {\n      return;\n    }\n\n    current = PlayerState.jumping;\n    _jumpVelocity = initialJumpVelocity - (speed / 500);\n  }\n\n  void reset() {\n    y = groundYPos;\n    _jumpVelocity = 0.0;\n    current = PlayerState.running;\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    if (current == PlayerState.jumping) {\n      y += _jumpVelocity;\n      _jumpVelocity += gravity;\n      if (y > groundYPos) {\n        reset();\n      }\n    } else {\n      y = groundYPos;\n    }\n\n    if (game.isIntro && x < startXPosition) {\n      x += (startXPosition / introDuration) * dt * 5000;\n    }\n  }\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    y = groundYPos;\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n    game.gameOver();\n  }\n\n  SpriteAnimation _getAnimation({\n    required Vector2 size,\n    required List<Vector2> frames,\n    double stepTime = double.infinity,\n  }) {\n    return SpriteAnimation.spriteList(\n      frames\n          .map(\n            (vector) => Sprite(\n              game.spriteImage,\n              srcSize: size,\n              srcPosition: vector,\n            ),\n          )\n          .toList(),\n      stepTime: stepTime,\n    );\n  }\n}\n"
  },
  {
    "path": "examples/games/trex/lib/trex_game.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/text.dart';\nimport 'package:flutter/material.dart' hide Image;\nimport 'package:flutter/services.dart';\nimport 'package:trex_game/background/horizon.dart';\nimport 'package:trex_game/game_over.dart';\nimport 'package:trex_game/player.dart';\n\nenum GameState { playing, intro, gameOver }\n\nclass TRexGame extends FlameGame\n    with KeyboardEvents, TapCallbacks, HasCollisionDetection {\n  static const String description = '''\n    A game similar to the game in chrome that you get to play while offline.\n    Press space or tap/click the screen to jump, the more obstacles you manage\n    to survive, the more points you get.\n  ''';\n\n  final Random random = Random();\n  late final Image spriteImage;\n\n  @override\n  Color backgroundColor() => const Color(0xFFFFFFFF);\n\n  late final player = Player();\n  late final horizon = Horizon();\n  late final gameOverPanel = GameOverPanel();\n  late final TextComponent scoreText;\n\n  int _score = 0;\n  int _highScore = 0;\n  int get score => _score;\n  set score(int newScore) {\n    _score = newScore;\n    scoreText.text = '${scoreString(_score)}  HI ${scoreString(_highScore)}';\n  }\n\n  String scoreString(int score) => score.toString().padLeft(5, '0');\n\n  /// Used for score calculation\n  double _distanceTraveled = 0;\n\n  @override\n  Future<void> onLoad() async {\n    spriteImage = await Flame.images.load('trex.png');\n    add(horizon);\n    add(player);\n    add(gameOverPanel);\n\n    const chars = '0123456789HI ';\n    final renderer = SpriteFontRenderer.fromFont(\n      SpriteFont(\n        source: spriteImage,\n        size: 23,\n        ascent: 23,\n        glyphs: [\n          for (var i = 0; i < chars.length; i++)\n            Glyph(chars[i], left: 954.0 + 20 * i, top: 0, width: 20),\n        ],\n      ),\n      letterSpacing: 2,\n    );\n    add(\n      scoreText = TextComponent(\n        position: Vector2(20, 20),\n        textRenderer: renderer,\n      ),\n    );\n    score = 0;\n  }\n\n  GameState state = GameState.intro;\n  double currentSpeed = 0.0;\n  double timePlaying = 0.0;\n\n  final double acceleration = 10;\n  final double maxSpeed = 2500.0;\n  final double startSpeed = 600;\n\n  bool get isPlaying => state == GameState.playing;\n  bool get isGameOver => state == GameState.gameOver;\n  bool get isIntro => state == GameState.intro;\n\n  @override\n  KeyEventResult onKeyEvent(\n    KeyEvent event,\n    Set<LogicalKeyboardKey> keysPressed,\n  ) {\n    if (keysPressed.contains(LogicalKeyboardKey.enter) ||\n        keysPressed.contains(LogicalKeyboardKey.space)) {\n      onAction();\n    }\n    return KeyEventResult.handled;\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    onAction();\n  }\n\n  void onAction() {\n    if (isGameOver || isIntro) {\n      restart();\n      return;\n    }\n    player.jump(currentSpeed);\n  }\n\n  void gameOver() {\n    gameOverPanel.visible = true;\n    state = GameState.gameOver;\n    player.current = PlayerState.crashed;\n    currentSpeed = 0.0;\n  }\n\n  void restart() {\n    state = GameState.playing;\n    player.reset();\n    horizon.reset();\n    currentSpeed = startSpeed;\n    gameOverPanel.visible = false;\n    timePlaying = 0.0;\n    if (score > _highScore) {\n      _highScore = score;\n    }\n    score = 0;\n    _distanceTraveled = 0;\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    if (isGameOver) {\n      return;\n    }\n\n    if (isPlaying) {\n      timePlaying += dt;\n      _distanceTraveled += dt * currentSpeed;\n      score = _distanceTraveled ~/ 50;\n\n      if (currentSpeed < maxSpeed) {\n        currentSpeed += acceleration * dt;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "examples/games/trex/lib/trex_widget.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flutter/material.dart' hide Image, Gradient;\nimport 'package:trex_game/trex_game.dart';\n\nclass TRexWidget extends StatelessWidget {\n  const TRexWidget({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      title: 'T-Rex',\n      home: Container(\n        color: Colors.black,\n        margin: const EdgeInsets.all(45),\n        child: ClipRect(\n          child: GameWidget(\n            game: TRexGame(),\n            loadingBuilder: (_) => const Center(\n              child: Text('Loading'),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/games/trex/pubspec.yaml",
    "content": "name: trex_game\nresolution: workspace\ndescription: A clone of the classic browser T-Rex game.\nhomepage: https://github.com/flame-engine/flame/tree/main/examples/games/trex/\npublish_to: 'none'\n\nversion: 0.1.0\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  collection: ^1.16.0\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\nflutter:\n  uses-material-design: true\n  assets:\n    - assets/images/\n"
  },
  {
    "path": "examples/lib/commons/commons.dart",
    "content": "String baseLink(String path) {\n  const basePath =\n      'https://github.com/flame-engine/flame/blob/main/examples/lib/stories/';\n\n  return '$basePath$path';\n}\n"
  },
  {
    "path": "examples/lib/commons/ember.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:meta/meta.dart';\n\nclass Ember<T extends FlameGame> extends SpriteAnimationComponent\n    with HasGameReference<T> {\n  Ember({super.position, Vector2? size, super.priority, super.key})\n    : super(\n        size: size ?? Vector2.all(50),\n        anchor: Anchor.center,\n      );\n\n  @mustCallSuper\n  @override\n  Future<void> onLoad() async {\n    animation = await game.loadSpriteAnimation(\n      'animations/ember.png',\n      SpriteAnimationData.sequenced(\n        amount: 3,\n        textureSize: Vector2.all(16),\n        stepTime: 0.15,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/main.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/platform/stub_provider.dart'\n    if (dart.library.html) 'platform/web_provider.dart';\nimport 'package:examples/stories/animations/animations.dart';\nimport 'package:examples/stories/bridge_libraries/audio/audio.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/flame_forge2d.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/constant_volume_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/distance_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/friction_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/gear_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/motor_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/prismatic_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/pulley_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/revolute_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/rope_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/weld_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_isolate/isolate.dart';\nimport 'package:examples/stories/bridge_libraries/flame_jenny/jenny.dart';\nimport 'package:examples/stories/bridge_libraries/flame_lottie/lottie.dart';\nimport 'package:examples/stories/bridge_libraries/flame_spine/flame_spine.dart';\nimport 'package:examples/stories/camera_and_viewport/camera_and_viewport.dart';\nimport 'package:examples/stories/collision_detection/collision_detection.dart';\nimport 'package:examples/stories/components/components.dart';\nimport 'package:examples/stories/effects/effects.dart';\nimport 'package:examples/stories/experimental/experimental.dart';\nimport 'package:examples/stories/games/games.dart';\nimport 'package:examples/stories/image/image.dart';\nimport 'package:examples/stories/input/input.dart';\nimport 'package:examples/stories/layout/layout.dart';\nimport 'package:examples/stories/parallax/parallax.dart';\nimport 'package:examples/stories/rendering/decorators.dart';\nimport 'package:examples/stories/rendering/rendering.dart';\nimport 'package:examples/stories/router/router.dart';\nimport 'package:examples/stories/sprites/sprites.dart';\nimport 'package:examples/stories/structure/structure.dart';\nimport 'package:examples/stories/svg/svg.dart';\nimport 'package:examples/stories/system/system.dart';\nimport 'package:examples/stories/tiled/tiled.dart';\nimport 'package:examples/stories/utils/utils.dart';\nimport 'package:examples/stories/widgets/widgets.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  final page = PageProviderImpl().getPage();\n\n  final routes = <String, FlameGame Function()>{\n    'constant_volume_joint': ConstantVolumeJointExample.new,\n    'distance_joint': DistanceJointExample.new,\n    'friction_joint': FrictionJointExample.new,\n    'gear_joint': GearJointExample.new,\n    'motor_joint': MotorJointExample.new,\n    'mouse_joint': MouseJointExample.new,\n    'pulley_joint': PulleyJointExample.new,\n    'prismatic_joint': PrismaticJointExample.new,\n    'revolute_joint': RevoluteJointExample.new,\n    'rope_joint': RopeJointExample.new,\n    'weld_joint': WeldJointExample.new,\n  };\n  final game = routes[page]?.call();\n  if (game != null) {\n    runApp(GameWidget(game: game));\n  } else {\n    runAsDashbook();\n  }\n}\n\nvoid runAsDashbook() {\n  final dashbook = Dashbook(\n    title: 'Flame Examples',\n    theme: ThemeData.dark(),\n  );\n\n  // Some small sample games\n  addGameStories(dashbook);\n\n  // Show some different ways of structuring games\n  addStructureStories(dashbook);\n\n  // Feature examples\n  addAudioStories(dashbook);\n  addAnimationStories(dashbook);\n  addCameraAndViewportStories(dashbook);\n  addCollisionDetectionStories(dashbook);\n  addComponentsStories(dashbook);\n  addDecoratorStories(dashbook);\n  addEffectsStories(dashbook);\n  addExperimentalStories(dashbook);\n  addInputStories(dashbook);\n  addLayoutStories(dashbook);\n  addParallaxStories(dashbook);\n  addRenderingStories(dashbook);\n  addRouterStories(dashbook);\n  addTiledStories(dashbook);\n  addSpritesStories(dashbook);\n  addSvgStories(dashbook);\n  addSystemStories(dashbook);\n  addUtilsStories(dashbook);\n  addWidgetsStories(dashbook);\n  addImageStories(dashbook);\n\n  // Bridge package examples\n  addForge2DStories(dashbook);\n  addFlameIsolateExample(dashbook);\n  addFlameJennyExample(dashbook);\n  addFlameLottieExample(dashbook);\n  addFlameSpineExamples(dashbook);\n\n  runApp(dashbook);\n}\n"
  },
  {
    "path": "examples/lib/platform/page_provider.dart",
    "content": "abstract class PageProvider {\n  String? getPage();\n}\n"
  },
  {
    "path": "examples/lib/platform/stub_provider.dart",
    "content": "import 'package:examples/platform/page_provider.dart';\n\nclass PageProviderImpl extends PageProvider {\n  @override\n  String? getPage() {\n    return null;\n  }\n}\n"
  },
  {
    "path": "examples/lib/platform/web_provider.dart",
    "content": "import 'package:examples/platform/page_provider.dart';\nimport 'package:web/web.dart';\n\nclass PageProviderImpl extends PageProvider {\n  @override\n  String? getPage() {\n    var page = window.location.search;\n    if (page.startsWith('?')) {\n      page = page.substring(1);\n    }\n    return page;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/animations/animation_group_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\n\nenum RobotState {\n  idle,\n  running,\n}\n\nclass AnimationGroupExample extends FlameGame with TapCallbacks {\n  static const description = '''\n    This example shows how to create a component that can be switched between\n    different states to change the animation that is playing.\\n\\n\n    \n    Usage: Click/tap and hold the screen to change state and then let go to go\n    back to the original animation.\n  ''';\n\n  late SpriteAnimationGroupComponent<RobotState> robot;\n\n  @override\n  Future<void> onLoad() async {\n    final running = await loadSpriteAnimation(\n      'animations/robot.png',\n      SpriteAnimationData.sequenced(\n        amount: 8,\n        stepTime: 0.2,\n        textureSize: Vector2(16, 18),\n      ),\n    );\n    final idle = await loadSpriteAnimation(\n      'animations/robot-idle.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        stepTime: 0.4,\n        textureSize: Vector2(16, 18),\n      ),\n    );\n\n    final robotSize = Vector2(64, 72);\n    robot = SpriteAnimationGroupComponent<RobotState>(\n      animations: {\n        RobotState.running: running,\n        RobotState.idle: idle,\n      },\n      current: RobotState.idle,\n      position: size / 2 - robotSize / 2,\n      size: robotSize,\n    );\n\n    add(robot);\n  }\n\n  @override\n  void onTapDown(_) {\n    robot.current = RobotState.running;\n  }\n\n  @override\n  void onTapCancel(_) {\n    robot.current = RobotState.idle;\n  }\n\n  @override\n  void onTapUp(_) {\n    robot.current = RobotState.idle;\n  }\n\n  @override\n  Color backgroundColor() => const Color(0xFF222222);\n}\n"
  },
  {
    "path": "examples/lib/stories/animations/animations.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/animations/animation_group_example.dart';\nimport 'package:examples/stories/animations/aseprite_example.dart';\nimport 'package:examples/stories/animations/basic_animation_example.dart';\nimport 'package:examples/stories/animations/benchmark_example.dart';\nimport 'package:flame/game.dart';\n\nvoid addAnimationStories(Dashbook dashbook) {\n  dashbook.storiesOf('Animations')\n    ..add(\n      'Basic Animations',\n      (_) => GameWidget(game: BasicAnimationsExample()),\n      codeLink: baseLink('animations/basic_animation_example.dart'),\n      info: BasicAnimationsExample.description,\n    )\n    ..add(\n      'Group animation',\n      (_) => GameWidget(game: AnimationGroupExample()),\n      codeLink: baseLink('animations/animation_group_example.dart'),\n      info: AnimationGroupExample.description,\n    )\n    ..add(\n      'Aseprite',\n      (_) => GameWidget(game: AsepriteExample()),\n      codeLink: baseLink('animations/aseprite_example.dart'),\n      info: AsepriteExample.description,\n    )\n    ..add(\n      'Benchmark',\n      (_) => GameWidget(game: BenchmarkExample()),\n      codeLink: baseLink('animations/benchmark_example.dart'),\n      info: BenchmarkExample.description,\n    );\n}\n"
  },
  {
    "path": "examples/lib/stories/animations/aseprite_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\n\nclass AsepriteExample extends FlameGame {\n  static const String description = '''\n    This example shows how to load animations from an Aseprite json file and a\n    sprite sheet. There is no interaction on this example.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final image = await images.load('animations/chopper.png');\n    final jsonData = await assets.readJson('images/animations/chopper.json');\n    final animation = SpriteAnimation.fromAsepriteData(image, jsonData);\n    final spriteSize = Vector2.all(200);\n    final animationComponent = SpriteAnimationComponent(\n      animation: animation,\n      position: (size - spriteSize) / 2,\n      size: spriteSize,\n    );\n    add(animationComponent);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/animations/basic_animation_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:examples/commons/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\n\nclass BasicAnimationsExample extends FlameGame {\n  static const description = '''\n    Basic example of how to use `SpriteAnimation`s in Flame's.\n\n    In this example, click or touch anywhere on the screen to dynamically add\n    animations.\n  ''';\n\n  BasicAnimationsExample() : super(world: BasicAnimationsWorld());\n}\n\nclass BasicAnimationsWorld extends World with TapCallbacks, HasGameReference {\n  late Image creature;\n\n  @override\n  Future<void> onLoad() async {\n    creature = await game.images.load('animations/creature.png');\n\n    final animation = await game.loadSpriteAnimation(\n      'animations/chopper.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        textureSize: Vector2.all(48),\n        stepTime: 0.15,\n      ),\n    );\n\n    final spriteSize = Vector2.all(100.0);\n    final animationComponent = SpriteAnimationComponent(\n      animation: animation,\n      position: Vector2(-spriteSize.x, 0),\n      size: spriteSize,\n      anchor: Anchor.center,\n    );\n\n    final reversedAnimationComponent = SpriteAnimationComponent(\n      animation: animation.reversed(),\n      position: Vector2(spriteSize.x, 0),\n      size: spriteSize,\n      anchor: Anchor.center,\n    );\n\n    add(animationComponent);\n    add(reversedAnimationComponent);\n    add(Ember());\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    final size = Vector2(291, 178);\n\n    final animationComponent = SpriteAnimationComponent.fromFrameData(\n      creature,\n      SpriteAnimationData.sequenced(\n        amount: 18,\n        amountPerRow: 10,\n        textureSize: size,\n        stepTime: 0.15,\n        loop: false,\n      ),\n      position: event.localPosition,\n      anchor: Anchor.center,\n      size: size,\n      removeOnFinish: true,\n    );\n\n    add(animationComponent);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/animations/benchmark_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:examples/commons/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\n\nclass BenchmarkExample extends FlameGame {\n  static const description = '''\nSee how many SpriteAnimationComponent's your platform can handle before it\nstarts to drop in FPS, this is without any sprite batching and such.\n100 animation components are added per tap.\n  ''';\n\n  BenchmarkExample() : super(world: BenchmarkWorld());\n\n  final emberSize = Vector2.all(20);\n  late final TextComponent emberCounter;\n  final counterPrefix = 'Animations: ';\n\n  @override\n  Future<void> onLoad() async {\n    await camera.viewport.addAll([\n      FpsTextComponent(\n        position: size - Vector2(10, 50),\n        anchor: Anchor.bottomRight,\n      ),\n      emberCounter = TextComponent(\n        position: size - Vector2(10, 25),\n        anchor: Anchor.bottomRight,\n        priority: 1,\n      ),\n    ]);\n    world.add(Ember(size: emberSize));\n    children.register<Ember>();\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    emberCounter.text =\n        '$counterPrefix ${world.children.query<Ember>().length}';\n  }\n}\n\nclass BenchmarkWorld extends World\n    with TapCallbacks, HasGameReference<BenchmarkExample> {\n  final Random random = Random();\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    addAll(\n      List.generate(\n        100,\n        (_) => Ember(\n          size: game.emberSize,\n          position: Vector2(\n            (game.size.x / 2) *\n                random.nextDouble() *\n                (random.nextBool() ? 1 : -1),\n            (game.size.y / 2) *\n                random.nextDouble() *\n                (random.nextBool() ? 1 : -1),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/audio/audio.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/bridge_libraries/audio/basic_audio_example.dart';\nimport 'package:flame/game.dart';\n\nvoid addAudioStories(Dashbook dashbook) {\n  dashbook\n      .storiesOf('Audio')\n      .add(\n        'Basic Audio',\n        (_) => GameWidget(game: BasicAudioExample()),\n        codeLink: baseLink('bridge_libraries/audio/basic_audio_example.dart'),\n        info: BasicAudioExample.description,\n      );\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/audio/basic_audio_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame_audio/flame_audio.dart';\nimport 'package:flutter/painting.dart';\n\nclass BasicAudioExample extends FlameGame {\n  static const String description = '''\n    This example showcases the most basic Flame Audio functionalities.\n\n    1. Use the static FlameAudio class to easily fire a sfx using the default\n    configs for the button tap.\n    2. Uses a custom AudioPool for extremely efficient audio loading and pooling\n    for tapping elsewhere.\n    3. Uses the Bgm utility for background music.\n  ''';\n\n  static final Paint black = BasicPalette.black.paint();\n  static final Paint gray = const PaletteEntry(Color(0xFFCCCCCC)).paint();\n  static final TextPaint topTextPaint = TextPaint(\n    style: TextStyle(color: BasicPalette.lightBlue.color),\n  );\n  static final TextPaint bottomTextPaint = TextPaint(\n    style: TextStyle(color: BasicPalette.black.color),\n  );\n\n  late AudioPool pool;\n\n  @override\n  Future<void> onLoad() async {\n    pool = await FlameAudio.createPool(\n      'sfx/fire_2.mp3',\n      minPlayers: 3,\n      maxPlayers: 4,\n    );\n    startBgmMusic();\n    final firstButtonSize = Vector2(size.x - 40, size.y * (4 / 5));\n    final secondButtonSize = Vector2(size.x - 40, size.y / 5);\n    addAll(\n      [\n        ButtonComponent(\n          position: Vector2(20, 20),\n          size: firstButtonSize,\n          button: RectangleComponent(paint: black, size: firstButtonSize),\n          onPressed: fireOne,\n          children: [\n            TextComponent(\n              text: 'Click here for 1',\n              textRenderer: topTextPaint,\n              position: firstButtonSize / 2,\n              anchor: Anchor.center,\n              priority: 1,\n            ),\n          ],\n        ),\n        ButtonComponent(\n          position: Vector2(20, size.y - size.y / 5),\n          size: secondButtonSize,\n          button: RectangleComponent(paint: gray, size: secondButtonSize),\n          onPressed: fireTwo,\n          children: [\n            TextComponent(\n              text: 'Click here for 2',\n              textRenderer: bottomTextPaint,\n              position: secondButtonSize / 2,\n              anchor: Anchor.center,\n              priority: 1,\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n\n  void startBgmMusic() {\n    FlameAudio.bgm.initialize();\n    FlameAudio.bgm.play('music/bg_music.ogg');\n  }\n\n  void fireOne() {\n    FlameAudio.play('sfx/fire_1.mp3');\n  }\n\n  void fireTwo() {\n    pool.start();\n  }\n\n  @override\n  void onRemove() {\n    FlameAudio.bgm.dispose();\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/animated_body_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass AnimatedBodyExample extends Forge2DGame {\n  static const String description = '''\n    In this example we show how to add an animated chopper, which is created\n    with a SpriteAnimationComponent, on top of a BodyComponent.\n    \n    Tap the screen to add more choppers.\n  ''';\n\n  AnimatedBodyExample()\n    : super(\n        gravity: Vector2.zero(),\n        world: AnimatedBodyWorld(),\n      );\n}\n\nclass AnimatedBodyWorld extends Forge2DWorld\n    with TapCallbacks, HasGameReference<Forge2DGame> {\n  late Image chopper;\n  late SpriteAnimation animation;\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    chopper = await Flame.images.load('animations/chopper.png');\n\n    animation = SpriteAnimation.fromFrameData(\n      chopper,\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        textureSize: Vector2.all(48),\n        stepTime: 0.15,\n      ),\n    );\n\n    final boundaries = createBoundaries(game);\n    addAll(boundaries);\n  }\n\n  @override\n  void onTapDown(TapDownEvent info) {\n    super.onTapDown(info);\n    final position = info.localPosition;\n    final spriteSize = Vector2.all(10);\n    final animationComponent = SpriteAnimationComponent(\n      animation: animation,\n      size: spriteSize,\n      anchor: Anchor.center,\n    );\n    add(ChopperBody(position, animationComponent));\n  }\n}\n\nclass ChopperBody extends BodyComponent {\n  final Vector2 _position;\n  final Vector2 size;\n\n  ChopperBody(\n    this._position,\n    PositionComponent component,\n  ) : size = component.size {\n    renderBody = false;\n    add(component);\n  }\n\n  @override\n  Body createBody() {\n    final shape = CircleShape()..radius = size.x / 4;\n    final fixtureDef = FixtureDef(\n      shape,\n      userData: this, // To be able to determine object in collision\n      restitution: 0.8,\n      friction: 0.2,\n    );\n\n    final velocity = (Vector2.random() - Vector2.random()) * 200;\n    final bodyDef = BodyDef(\n      position: _position,\n      angle: velocity.angleTo(Vector2(1, 0)),\n      linearVelocity: velocity,\n      type: BodyType.dynamic,\n    );\n    return world.createBody(bodyDef)..createFixture(fixtureDef);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/blob_example.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass BlobExample extends Forge2DGame {\n  static const String description = '''\n    In this example we show the power of joints by showing interactions between\n    bodies tied together.\n    \n    Tap the screen to add boxes that will bounce on the \"blob\" in the center.\n  ''';\n  BlobExample() : super(world: BlobWorld());\n}\n\nclass BlobWorld extends Forge2DWorld\n    with TapCallbacks, HasGameReference<Forge2DGame> {\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    final blobCenter = Vector2(0, -30);\n    final blobRadius = Vector2.all(6.0);\n    addAll(createBoundaries(game));\n    add(Ground(Vector2.zero()));\n    final jointDef = ConstantVolumeJointDef()\n      ..frequencyHz = 20.0\n      ..dampingRatio = 1.0\n      ..collideConnected = false;\n\n    await addAll([\n      for (var i = 0; i < 20; i++)\n        BlobPart(i, jointDef, blobRadius, blobCenter),\n    ]);\n    createJoint(ConstantVolumeJoint(physicsWorld, jointDef));\n  }\n\n  @override\n  void onTapDown(TapDownEvent info) {\n    super.onTapDown(info);\n    add(FallingBox(info.localPosition));\n  }\n}\n\nclass Ground extends BodyComponent {\n  final Vector2 worldCenter;\n\n  Ground(this.worldCenter);\n\n  @override\n  Body createBody() {\n    final shape = PolygonShape();\n    shape.setAsBoxXY(20.0, 0.4);\n    final fixtureDef = FixtureDef(shape, friction: 0.2);\n\n    final bodyDef = BodyDef(position: worldCenter.clone());\n    final ground = world.createBody(bodyDef);\n    ground.createFixture(fixtureDef);\n\n    shape.setAsBox(0.4, 20.0, Vector2(-10.0, 0.0), 0.0);\n    ground.createFixture(fixtureDef);\n    shape.setAsBox(0.4, 20.0, Vector2(10.0, 0.0), 0.0);\n    ground.createFixture(fixtureDef);\n    return ground;\n  }\n}\n\nclass BlobPart extends BodyComponent {\n  final ConstantVolumeJointDef jointDef;\n  final int bodyNumber;\n  final Vector2 blobRadius;\n  final Vector2 blobCenter;\n\n  BlobPart(\n    this.bodyNumber,\n    this.jointDef,\n    this.blobRadius,\n    this.blobCenter,\n  );\n\n  @override\n  Body createBody() {\n    const nBodies = 20.0;\n    const bodyRadius = 0.5;\n    final angle = (bodyNumber / nBodies) * math.pi * 2;\n    final x = blobCenter.x + blobRadius.x * math.sin(angle);\n    final y = blobCenter.y + blobRadius.y * math.cos(angle);\n\n    final bodyDef = BodyDef(\n      fixedRotation: true,\n      position: Vector2(x, y),\n      type: BodyType.dynamic,\n    );\n    final body = world.createBody(bodyDef);\n\n    final shape = CircleShape()..radius = bodyRadius;\n    final fixtureDef = FixtureDef(\n      shape,\n      friction: 0.2,\n    );\n    body.createFixture(fixtureDef);\n    jointDef.addBody(body);\n    return body;\n  }\n}\n\nclass FallingBox extends BodyComponent {\n  final Vector2 _position;\n\n  FallingBox(this._position);\n\n  @override\n  Body createBody() {\n    final bodyDef = BodyDef(\n      type: BodyType.dynamic,\n      position: _position,\n    );\n    final shape = PolygonShape()..setAsBoxXY(2, 4);\n    final body = world.createBody(bodyDef);\n    body.createFixtureFromShape(shape);\n    return body;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/camera_example.dart",
    "content": "import 'package:examples/stories/bridge_libraries/flame_forge2d/domino_example.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/sprite_body_example.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass CameraExample extends Forge2DGame {\n  static const String description = '''\n    This example showcases the possibility to follow BodyComponents with the\n    camera. When the screen is tapped a pizza is added, which the camera will\n    follow. Other than that it is the same as the domino example.\n  ''';\n  CameraExample() : super(world: CameraExampleWorld());\n}\n\nclass CameraExampleWorld extends DominoExampleWorld {\n  @override\n  void onTapDown(TapDownEvent info) {\n    final position = info.localPosition;\n    final pizza = Pizza(position);\n    add(pizza);\n    pizza.mounted.whenComplete(() => game.camera.follow(pizza));\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/composition_example.dart",
    "content": "import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\nimport 'package:flutter/material.dart';\n\nconst TextStyle _textStyle = TextStyle(color: Colors.white, fontSize: 2);\n\nclass CompositionExample extends Forge2DGame {\n  static const description = '''\n    This example shows how to compose a `BodyComponent` together with a normal\n    Flame component. Click the ball to see the number increment.\n  ''';\n\n  CompositionExample() : super(zoom: 20, gravity: Vector2(0, 10.0));\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    final boundaries = createBoundaries(this);\n    world.addAll(boundaries);\n    world.add(TappableText(Vector2(0, 5)));\n    world.add(TappableBall(Vector2.zero()));\n  }\n}\n\nclass TappableText extends TextComponent with TapCallbacks {\n  TappableText(Vector2 position)\n    : super(\n        text: 'A normal tappable Flame component',\n        textRenderer: TextPaint(style: _textStyle),\n        position: position,\n        anchor: Anchor.center,\n      );\n\n  @override\n  Future<void> onLoad() async {\n    final scaleEffect = ScaleEffect.by(\n      Vector2.all(1.1),\n      EffectController(\n        duration: 0.7,\n        alternate: true,\n        infinite: true,\n      ),\n    );\n    add(scaleEffect);\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    add(\n      MoveEffect.by(\n        Vector2.all(5),\n        EffectController(\n          speed: 5,\n          alternate: true,\n        ),\n      ),\n    );\n  }\n}\n\nclass TappableBall extends Ball with TapCallbacks {\n  late final TextComponent textComponent;\n  int counter = 0;\n  late final TextPaint _textPaint;\n\n  TappableBall(super.position) {\n    originalPaint = Paint()..color = Colors.amber;\n    paint = originalPaint;\n  }\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    _textPaint = TextPaint(style: _textStyle);\n    textComponent = TextComponent(\n      text: counter.toString(),\n      textRenderer: _textPaint,\n    );\n    add(textComponent);\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    textComponent.text = counter.toString();\n  }\n\n  @override\n  bool onTapDown(_) {\n    counter++;\n    body.applyLinearImpulse(Vector2.random() * 1000);\n    paint = randomPaint();\n    return false;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/contact_callbacks_example.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass ContactCallbacksExample extends Forge2DGame {\n  static const description = '''\n    This example shows how `BodyComponent`s can react to collisions with other\n    bodies.\n    Tap the screen to add balls, the white balls will give an impulse to the\n    balls that it collides with.\n  ''';\n\n  ContactCallbacksExample()\n    : super(gravity: Vector2(0, 10.0), world: ContactCallbackWorld());\n}\n\nclass ContactCallbackWorld extends Forge2DWorld\n    with TapCallbacks, HasGameReference<Forge2DGame> {\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    final boundaries = createBoundaries(game);\n    addAll(boundaries);\n  }\n\n  @override\n  void onTapDown(TapDownEvent info) {\n    super.onTapDown(info);\n    final position = info.localPosition;\n    if (math.Random().nextInt(10) < 2) {\n      add(WhiteBall(position));\n    } else {\n      add(Ball(position));\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/domino_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/sprite_body_example.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass DominoExample extends Forge2DGame {\n  static const description = '''\n    In this example we can see some domino tiles lined up.\n    If you tap on the screen a pizza is added which can tip the tiles over and\n    cause a chain reaction. \n  ''';\n\n  DominoExample()\n    : super(gravity: Vector2(0, 10.0), world: DominoExampleWorld());\n}\n\nclass DominoExampleWorld extends Forge2DWorld\n    with TapCallbacks, HasGameReference<Forge2DGame> {\n  late Image pizzaImage;\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    final boundaries = createBoundaries(game);\n    addAll(boundaries);\n\n    const numberOfRows = 7;\n    for (var i = 0; i < numberOfRows - 2; i++) {\n      add(Platform(Vector2(0.0, 5.0 * i)));\n    }\n\n    const numberPerRow = 25;\n    for (var i = 0; i < numberOfRows; ++i) {\n      for (var j = 0; j < numberPerRow; j++) {\n        final position = Vector2(\n          -14.75 + j * (29.5 / (numberPerRow - 1)),\n          -12.7 + 5 * i,\n        );\n        add(DominoBrick(position));\n      }\n    }\n  }\n\n  @override\n  void onTapDown(TapDownEvent info) {\n    final position = info.localPosition;\n    add(Pizza(position));\n  }\n}\n\nclass Platform extends BodyComponent {\n  final Vector2 _position;\n\n  Platform(this._position);\n\n  @override\n  Body createBody() {\n    final shape = PolygonShape()..setAsBoxXY(14.8, 0.125);\n    final fixtureDef = FixtureDef(shape);\n\n    final bodyDef = BodyDef(position: _position);\n    final body = world.createBody(bodyDef);\n    return body..createFixture(fixtureDef);\n  }\n}\n\nclass DominoBrick extends BodyComponent {\n  final Vector2 _position;\n\n  DominoBrick(this._position);\n\n  @override\n  Body createBody() {\n    final shape = PolygonShape()..setAsBoxXY(0.125, 2.0);\n    final fixtureDef = FixtureDef(\n      shape,\n      density: 25.0,\n      restitution: 0.4,\n      friction: 0.5,\n    );\n\n    final bodyDef = BodyDef(type: BodyType.dynamic, position: _position);\n    return world.createBody(bodyDef)..createFixture(fixtureDef);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/drag_callbacks_example.dart",
    "content": "import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\nimport 'package:flutter/material.dart' hide Draggable;\n\nclass DragCallbacksExample extends Forge2DGame {\n  static const description = '''\n    In this example we use Flame's normal `DragCallbacks` mixin to give impulses\n    to a ball when we are dragging it around. If you are interested in dragging\n    bodies around, also have a look at the MouseJointExample.\n  ''';\n\n  DragCallbacksExample() : super(gravity: Vector2.all(0.0));\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    final boundaries = createBoundaries(this);\n    world.addAll(boundaries);\n    world.add(DraggableBall(Vector2.zero()));\n  }\n}\n\nclass DraggableBall extends Ball with DragCallbacks {\n  DraggableBall(super.position) : super(radius: 5) {\n    originalPaint = Paint()..color = Colors.amber;\n    paint = originalPaint;\n  }\n\n  @override\n  void onDragStart(DragStartEvent event) {\n    super.onDragStart(event);\n    paint = randomPaint();\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    body.applyLinearImpulse(event.localDelta * 1000);\n  }\n\n  @override\n  void onDragEnd(DragEndEvent event) {\n    super.onDragEnd(event);\n    paint = originalPaint;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/flame_forge2d.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/animated_body_example.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/blob_example.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/camera_example.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/composition_example.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/contact_callbacks_example.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/domino_example.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/drag_callbacks_example.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/constant_volume_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/distance_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/friction_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/gear_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/motor_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/prismatic_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/pulley_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/revolute_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/rope_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/joints/weld_joint.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/raycast_example.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/revolute_joint_with_motor_example.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/sprite_body_example.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/tap_callbacks_example.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/widget_example.dart';\nimport 'package:flame/game.dart';\n\nString link(String example) =>\n    baseLink('bridge_libraries/flame_forge2d/$example');\n\nvoid addForge2DStories(Dashbook dashbook) {\n  dashbook.storiesOf('flame_forge2d')\n    ..add(\n      'Blob example',\n      (DashbookContext ctx) => GameWidget(game: BlobExample()),\n      codeLink: link('blob_example.dart'),\n      info: BlobExample.description,\n    )\n    ..add(\n      'Composition example',\n      (DashbookContext ctx) => GameWidget(game: CompositionExample()),\n      codeLink: link('composition_example.dart'),\n      info: CompositionExample.description,\n    )\n    ..add(\n      'Domino example',\n      (DashbookContext ctx) => GameWidget(game: DominoExample()),\n      codeLink: link('domino_example.dart'),\n      info: DominoExample.description,\n    )\n    ..add(\n      'Contact Callbacks',\n      (DashbookContext ctx) => GameWidget(game: ContactCallbacksExample()),\n      codeLink: link('contact_callbacks_example.dart'),\n      info: ContactCallbacksExample.description,\n    )\n    ..add(\n      'RevoluteJoint with Motor',\n      (DashbookContext ctx) =>\n          GameWidget(game: RevoluteJointWithMotorExample()),\n      codeLink: link('revolute_joint_with_motor_example.dart'),\n      info: RevoluteJointExample.description,\n    )\n    ..add(\n      'Sprite Bodies',\n      (DashbookContext ctx) => GameWidget(game: SpriteBodyExample()),\n      codeLink: link('sprite_body_example.dart'),\n      info: SpriteBodyExample.description,\n    )\n    ..add(\n      'Animated Bodies',\n      (DashbookContext ctx) => GameWidget(game: AnimatedBodyExample()),\n      codeLink: link('animated_body_example.dart'),\n      info: AnimatedBodyExample.description,\n    )\n    ..add(\n      'Tappable Body',\n      (DashbookContext ctx) => GameWidget(game: TapCallbacksExample()),\n      codeLink: link('tap_callbacks_example.dart'),\n      info: TapCallbacksExample.description,\n    )\n    ..add(\n      'Draggable Body',\n      (DashbookContext ctx) => GameWidget(game: DragCallbacksExample()),\n      codeLink: link('drag_callbacks_example.dart'),\n      info: DragCallbacksExample.description,\n    )\n    ..add(\n      'Camera',\n      (DashbookContext ctx) => GameWidget(game: CameraExample()),\n      codeLink: link('camera_example.dart'),\n      info: CameraExample.description,\n    )\n    ..add(\n      'Raycasting',\n      (DashbookContext ctx) => GameWidget(game: RaycastExample()),\n      codeLink: link('raycast_example.dart'),\n      info: RaycastExample.description,\n    )\n    ..add(\n      'Widgets',\n      (DashbookContext ctx) => const BodyWidgetExample(),\n      codeLink: link('widget_example.dart'),\n      info: WidgetExample.description,\n    );\n  addJointsStories(dashbook);\n}\n\nvoid addJointsStories(Dashbook dashbook) {\n  dashbook\n      .storiesOf('flame_forge2d/joints')\n      .add(\n        'ConstantVolumeJoint',\n        (DashbookContext ctx) => GameWidget(game: ConstantVolumeJointExample()),\n        codeLink: link('joints/constant_volume_joint.dart'),\n        info: ConstantVolumeJointExample.description,\n      )\n      .add(\n        'DistanceJoint',\n        (DashbookContext ctx) => GameWidget(game: DistanceJointExample()),\n        codeLink: link('joints/distance_joint.dart'),\n        info: DistanceJointExample.description,\n      )\n      .add(\n        'FrictionJoint',\n        (DashbookContext ctx) => GameWidget(game: FrictionJointExample()),\n        codeLink: link('joints/friction_joint.dart'),\n        info: FrictionJointExample.description,\n      )\n      .add(\n        'GearJoint',\n        (DashbookContext ctx) => GameWidget(game: GearJointExample()),\n        codeLink: link('joints/gear_joint.dart'),\n        info: GearJointExample.description,\n      )\n      .add(\n        'MotorJoint',\n        (DashbookContext ctx) => GameWidget(game: MotorJointExample()),\n        codeLink: link('joints/motor_joint.dart'),\n        info: MotorJointExample.description,\n      )\n      .add(\n        'MouseJoint',\n        (DashbookContext ctx) => GameWidget(game: MouseJointExample()),\n        codeLink: link('joints/mouse_joint.dart'),\n        info: MouseJointExample.description,\n      )\n      .add(\n        'PrismaticJoint',\n        (DashbookContext ctx) => GameWidget(game: PrismaticJointExample()),\n        codeLink: link('joints/prismatic_joint.dart'),\n        info: PrismaticJointExample.description,\n      )\n      .add(\n        'PulleyJoint',\n        (DashbookContext ctx) => GameWidget(game: PulleyJointExample()),\n        codeLink: link('joints/pulley_joint.dart'),\n        info: PulleyJointExample.description,\n      )\n      .add(\n        'RevoluteJoint',\n        (DashbookContext ctx) => GameWidget(game: RevoluteJointExample()),\n        codeLink: link('joints/revolute_joint.dart'),\n        info: RevoluteJointExample.description,\n      )\n      .add(\n        'RopeJoint',\n        (DashbookContext ctx) => GameWidget(game: RopeJointExample()),\n        codeLink: link('joints/rope_joint.dart'),\n        info: RopeJointExample.description,\n      )\n      .add(\n        'WeldJoint',\n        (DashbookContext ctx) => GameWidget(game: WeldJointExample()),\n        codeLink: link('joints/weld_joint.dart'),\n        info: WeldJointExample.description,\n      );\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/joints/constant_volume_joint.dart",
    "content": "import 'dart:math';\n\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass ConstantVolumeJointExample extends Forge2DGame {\n  static const description = '''\n    This example shows how to use a `ConstantVolumeJoint`. Tap the screen to add \n    a bunch off balls, that maintain a constant volume within them.\n  ''';\n\n  ConstantVolumeJointExample() : super(world: SpriteBodyWorld());\n}\n\nclass SpriteBodyWorld extends Forge2DWorld\n    with TapCallbacks, HasGameReference<Forge2DGame> {\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    addAll(createBoundaries(game));\n  }\n\n  @override\n  Future<void> onTapDown(TapDownEvent info) async {\n    super.onTapDown(info);\n    final center = info.localPosition;\n\n    const numPieces = 20;\n    const radius = 5.0;\n    final balls = <Ball>[];\n\n    for (var i = 0; i < numPieces; i++) {\n      final x = radius * cos(2 * pi * (i / numPieces));\n      final y = radius * sin(2 * pi * (i / numPieces));\n\n      final ball = Ball(Vector2(x + center.x, y + center.y), radius: 0.5);\n\n      add(ball);\n      balls.add(ball);\n    }\n\n    await Future.wait(balls.map((e) => e.loaded));\n\n    final constantVolumeJoint = ConstantVolumeJointDef()\n      ..frequencyHz = 10\n      ..dampingRatio = 0.8;\n\n    balls.forEach((ball) {\n      constantVolumeJoint.addBody(ball.body);\n    });\n\n    createJoint(\n      ConstantVolumeJoint(\n        physicsWorld,\n        constantVolumeJoint,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/joints/distance_joint.dart",
    "content": "import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass DistanceJointExample extends Forge2DGame {\n  static const description = '''\n    This example shows how to use a `DistanceJoint`. Tap the screen to add a \n    pair of balls joined with a `DistanceJoint`.\n  ''';\n\n  DistanceJointExample() : super(world: DistanceJointWorld());\n}\n\nclass DistanceJointWorld extends Forge2DWorld\n    with TapCallbacks, HasGameReference<Forge2DGame> {\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    addAll(createBoundaries(game));\n  }\n\n  @override\n  Future<void> onTapDown(TapDownEvent info) async {\n    super.onTapDown(info);\n    final tap = info.localPosition;\n\n    final first = Ball(tap);\n    final second = Ball(Vector2(tap.x + 3, tap.y + 3));\n    addAll([first, second]);\n\n    await Future.wait([first.loaded, second.loaded]);\n\n    final distanceJointDef = DistanceJointDef()\n      ..initialize(\n        first.body,\n        second.body,\n        first.body.worldCenter,\n        second.center,\n      )\n      ..length = 10\n      ..frequencyHz = 3\n      ..dampingRatio = 0.2;\n\n    createJoint(DistanceJoint(distanceJointDef));\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/joints/friction_joint.dart",
    "content": "import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass FrictionJointExample extends Forge2DGame {\n  static const description = '''\n    This example shows how to use a `FrictionJoint`. Tap the screen to move the \n    ball around and observe it slows down due to the friction force.\n  ''';\n\n  FrictionJointExample()\n    : super(gravity: Vector2.all(0), world: FrictionJointWorld());\n}\n\nclass FrictionJointWorld extends Forge2DWorld\n    with TapCallbacks, HasGameReference<Forge2DGame> {\n  late Wall border;\n  late Ball ball;\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    final boundaries = createBoundaries(game);\n    border = boundaries.first;\n    addAll(boundaries);\n\n    ball = Ball(Vector2.zero(), radius: 3);\n    add(ball);\n\n    await Future.wait([ball.loaded, border.loaded]);\n\n    createFrictionJoint(ball.body, border.body);\n  }\n\n  @override\n  Future<void> onTapDown(TapDownEvent info) async {\n    super.onTapDown(info);\n    ball.body.applyLinearImpulse(Vector2.random() * 5000);\n  }\n\n  void createFrictionJoint(Body first, Body second) {\n    final frictionJointDef = FrictionJointDef()\n      ..initialize(first, second, first.worldCenter)\n      ..collideConnected = true\n      ..maxForce = 500\n      ..maxTorque = 500;\n\n    createJoint(FrictionJoint(frictionJointDef));\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/joints/gear_joint.dart",
    "content": "import 'dart:ui';\n\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass GearJointExample extends Forge2DGame {\n  static const description = '''\n    This example shows how to use a `GearJoint`. \n        \n    Drag the box along the specified axis and observe gears respond to the \n    translation.\n  ''';\n\n  GearJointExample() : super(world: GearJointWorld());\n}\n\nclass GearJointWorld extends Forge2DWorld with HasGameReference<Forge2DGame> {\n  late PrismaticJoint prismaticJoint;\n  Vector2 boxAnchor = Vector2.zero();\n\n  double boxWidth = 2;\n  double ball1Radius = 4;\n  double ball2Radius = 2;\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n\n    final box = DraggableBox(\n      startPosition: boxAnchor,\n      width: boxWidth,\n      height: 20,\n    );\n    add(box);\n\n    final ball1Anchor = boxAnchor - Vector2(boxWidth / 2 + ball1Radius, 0);\n    final ball1 = Ball(ball1Anchor, radius: ball1Radius);\n    add(ball1);\n\n    final ball2Anchor = ball1Anchor - Vector2(ball1Radius + ball2Radius, 0);\n    final ball2 = Ball(ball2Anchor, radius: ball2Radius);\n    add(ball2);\n\n    await Future.wait([box.loaded, ball1.loaded, ball2.loaded]);\n\n    prismaticJoint = createPrismaticJoint(box.body, boxAnchor);\n    final revoluteJoint1 = createRevoluteJoint(ball1.body, ball1Anchor);\n    final revoluteJoint2 = createRevoluteJoint(ball2.body, ball2Anchor);\n\n    createGearJoint(prismaticJoint, revoluteJoint1, 1);\n    createGearJoint(revoluteJoint1, revoluteJoint2, 0.5);\n    add(JointRenderer(joint: prismaticJoint, anchor: boxAnchor));\n  }\n\n  PrismaticJoint createPrismaticJoint(Body box, Vector2 anchor) {\n    final groundBody = createBody(BodyDef());\n\n    final prismaticJointDef = PrismaticJointDef()\n      ..initialize(\n        groundBody,\n        box,\n        anchor,\n        Vector2(0, 1),\n      )\n      ..enableLimit = true\n      ..lowerTranslation = -10\n      ..upperTranslation = 10;\n\n    final joint = PrismaticJoint(prismaticJointDef);\n    createJoint(joint);\n    return joint;\n  }\n\n  RevoluteJoint createRevoluteJoint(Body ball, Vector2 anchor) {\n    final groundBody = createBody(BodyDef());\n\n    final revoluteJointDef = RevoluteJointDef()\n      ..initialize(\n        groundBody,\n        ball,\n        anchor,\n      );\n\n    final joint = RevoluteJoint(revoluteJointDef);\n    createJoint(joint);\n    return joint;\n  }\n\n  void createGearJoint(Joint first, Joint second, double gearRatio) {\n    final gearJointDef = GearJointDef()\n      ..bodyA = first.bodyA\n      ..bodyB = second.bodyA\n      ..joint1 = first\n      ..joint2 = second\n      ..ratio = gearRatio;\n\n    final joint = GearJoint(gearJointDef);\n    createJoint(joint);\n  }\n}\n\nclass JointRenderer extends Component {\n  JointRenderer({required this.joint, required this.anchor});\n\n  final PrismaticJoint joint;\n  final Vector2 anchor;\n  final Vector2 p1 = Vector2.zero();\n  final Vector2 p2 = Vector2.zero();\n\n  @override\n  void render(Canvas canvas) {\n    p1\n      ..setFrom(joint.getLocalAxisA())\n      ..scale(joint.getLowerLimit())\n      ..add(anchor);\n    p2\n      ..setFrom(joint.getLocalAxisA())\n      ..scale(joint.getUpperLimit())\n      ..add(anchor);\n\n    canvas.drawLine(p1.toOffset(), p2.toOffset(), debugPaint);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/joints/motor_joint.dart",
    "content": "import 'dart:ui';\n\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass MotorJointExample extends Forge2DGame {\n  static const description = '''\n    This example shows how to use a `MotorJoint`. The ball spins around the \n    center point. Tap the screen to change the direction.\n  ''';\n\n  MotorJointExample()\n    : super(gravity: Vector2.zero(), world: MotorJointWorld());\n}\n\nclass MotorJointWorld extends Forge2DWorld with TapCallbacks {\n  late Ball ball;\n  late MotorJoint joint;\n  final motorSpeed = 1;\n\n  bool clockWise = true;\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n\n    final box = Box(\n      startPosition: Vector2.zero(),\n      width: 2,\n      height: 1,\n      bodyType: BodyType.static,\n    );\n    add(box);\n\n    ball = Ball(Vector2(0, -5));\n    add(ball);\n\n    await Future.wait([ball.loaded, box.loaded]);\n\n    joint = createMotorJoint(ball.body, box.body);\n    add(JointRenderer(joint: joint));\n  }\n\n  @override\n  void onTapDown(TapDownEvent info) {\n    super.onTapDown(info);\n    clockWise = !clockWise;\n  }\n\n  MotorJoint createMotorJoint(Body first, Body second) {\n    final motorJointDef = MotorJointDef()\n      ..initialize(first, second)\n      ..maxForce = 1000\n      ..maxTorque = 1000\n      ..correctionFactor = 0.1;\n\n    final joint = MotorJoint(motorJointDef);\n    createJoint(joint);\n    return joint;\n  }\n\n  final linearOffset = Vector2.zero();\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    var deltaOffset = motorSpeed * dt;\n    if (clockWise) {\n      deltaOffset = -deltaOffset;\n    }\n\n    final linearOffsetX = joint.getLinearOffset().x + deltaOffset;\n    final linearOffsetY = joint.getLinearOffset().y + deltaOffset;\n    linearOffset.setValues(linearOffsetX, linearOffsetY);\n    final angularOffset = joint.getAngularOffset() + deltaOffset;\n\n    joint.setLinearOffset(linearOffset);\n    joint.setAngularOffset(angularOffset);\n  }\n}\n\nclass JointRenderer extends Component {\n  JointRenderer({required this.joint});\n\n  final MotorJoint joint;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawLine(\n      joint.anchorA.toOffset(),\n      joint.anchorB.toOffset(),\n      debugPaint,\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart",
    "content": "import 'package:examples/stories/bridge_libraries/flame_forge2d/revolute_joint_with_motor_example.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass MouseJointExample extends Forge2DGame {\n  static const description = '''\n    In this example we use a `MouseJoint` to make the ball follow the mouse\n    when you drag it around.\n  ''';\n\n  MouseJointExample()\n    : super(gravity: Vector2(0, 10.0), world: MouseJointWorld());\n}\n\nclass MouseJointWorld extends Forge2DWorld\n    with DragCallbacks, HasGameReference<Forge2DGame> {\n  late Ball ball;\n  late Body groundBody;\n  MouseJoint? mouseJoint;\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    final boundaries = createBoundaries(game);\n    addAll(boundaries);\n\n    final center = Vector2.zero();\n    groundBody = createBody(BodyDef());\n    ball = Ball(center, radius: 5);\n    add(ball);\n    add(CornerRamp(center));\n    add(CornerRamp(center, isMirrored: true));\n  }\n\n  @override\n  void onDragStart(DragStartEvent info) {\n    super.onDragStart(info);\n    if (mouseJoint != null) {\n      return;\n    }\n    final mouseJointDef = MouseJointDef()\n      ..maxForce = 3000 * ball.body.mass * 10\n      ..dampingRatio = 0.1\n      ..frequencyHz = 5\n      ..target.setFrom(ball.body.position)\n      ..collideConnected = false\n      ..bodyA = groundBody\n      ..bodyB = ball.body;\n\n    mouseJoint = MouseJoint(mouseJointDef);\n    createJoint(mouseJoint!);\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent info) {\n    mouseJoint?.setTarget(info.localEndPosition);\n  }\n\n  @override\n  void onDragEnd(DragEndEvent info) {\n    super.onDragEnd(info);\n    destroyJoint(mouseJoint!);\n    mouseJoint = null;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/joints/prismatic_joint.dart",
    "content": "import 'dart:ui';\n\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass PrismaticJointExample extends Forge2DGame {\n  static const description = '''\n    This example shows how to use a `PrismaticJoint`. \n    \n    Drag the box along the specified axis, bound between lower and upper limits.\n    Also, there's a motor enabled that's pulling the box to the lower limit.\n  ''';\n\n  final Vector2 anchor = Vector2.zero();\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n\n    final box = DraggableBox(startPosition: anchor, width: 6, height: 6);\n    world.add(box);\n    await Future.wait([box.loaded]);\n\n    final joint = createJoint(box.body, anchor);\n    world.add(JointRenderer(joint: joint, anchor: anchor));\n  }\n\n  PrismaticJoint createJoint(Body box, Vector2 anchor) {\n    final groundBody = world.createBody(BodyDef());\n\n    final prismaticJointDef = PrismaticJointDef()\n      ..initialize(\n        box,\n        groundBody,\n        anchor,\n        Vector2(1, 0),\n      )\n      ..enableLimit = true\n      ..lowerTranslation = -20\n      ..upperTranslation = 20\n      ..enableMotor = true\n      ..motorSpeed = 1\n      ..maxMotorForce = 100;\n\n    final joint = PrismaticJoint(prismaticJointDef);\n    world.createJoint(joint);\n    return joint;\n  }\n}\n\nclass JointRenderer extends Component {\n  JointRenderer({required this.joint, required this.anchor});\n\n  final PrismaticJoint joint;\n  final Vector2 anchor;\n  final Vector2 p1 = Vector2.zero();\n  final Vector2 p2 = Vector2.zero();\n\n  @override\n  void render(Canvas canvas) {\n    p1\n      ..setFrom(joint.getLocalAxisA())\n      ..scale(joint.getLowerLimit())\n      ..add(anchor);\n    p2\n      ..setFrom(joint.getLocalAxisA())\n      ..scale(joint.getUpperLimit())\n      ..add(anchor);\n\n    canvas.drawLine(p1.toOffset(), p2.toOffset(), debugPaint);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/joints/pulley_joint.dart",
    "content": "import 'dart:ui';\n\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass PulleyJointExample extends Forge2DGame {\n  static const description = '''\n    This example shows how to use a `PulleyJoint`. Drag one of the boxes and see \n    how the other one gets moved by the pulley\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    final distanceFromCenter = camera.visibleWorldRect.width / 5;\n\n    final firstPulley = Ball(\n      Vector2(-distanceFromCenter, -10),\n      bodyType: BodyType.static,\n    );\n    final secondPulley = Ball(\n      Vector2(distanceFromCenter, -10),\n      bodyType: BodyType.static,\n    );\n\n    final firstBox = DraggableBox(\n      startPosition: Vector2(-distanceFromCenter, 20),\n      width: 5,\n      height: 10,\n    );\n    final secondBox = DraggableBox(\n      startPosition: Vector2(distanceFromCenter, 20),\n      width: 7,\n      height: 10,\n    );\n    world.addAll([firstBox, secondBox, firstPulley, secondPulley]);\n\n    await Future.wait([\n      firstBox.loaded,\n      secondBox.loaded,\n      firstPulley.loaded,\n      secondPulley.loaded,\n    ]);\n\n    final joint = createJoint(firstBox, secondBox, firstPulley, secondPulley);\n    world.add(PulleyRenderer(joint: joint));\n  }\n\n  PulleyJoint createJoint(\n    Box firstBox,\n    Box secondBox,\n    Ball firstPulley,\n    Ball secondPulley,\n  ) {\n    final pulleyJointDef = PulleyJointDef()\n      ..initialize(\n        firstBox.body,\n        secondBox.body,\n        firstPulley.center,\n        secondPulley.center,\n        firstBox.body.worldPoint(Vector2(0, -firstBox.height / 2)),\n        secondBox.body.worldPoint(Vector2(0, -secondBox.height / 2)),\n        1,\n      );\n    final joint = PulleyJoint(pulleyJointDef);\n    world.createJoint(joint);\n    return joint;\n  }\n}\n\nclass PulleyRenderer extends Component {\n  PulleyRenderer({required this.joint});\n\n  final PulleyJoint joint;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawLine(\n      joint.anchorA.toOffset(),\n      joint.getGroundAnchorA().toOffset(),\n      debugPaint,\n    );\n\n    canvas.drawLine(\n      joint.anchorB.toOffset(),\n      joint.getGroundAnchorB().toOffset(),\n      debugPaint,\n    );\n\n    canvas.drawLine(\n      joint.getGroundAnchorA().toOffset(),\n      joint.getGroundAnchorB().toOffset(),\n      debugPaint,\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/joints/revolute_joint.dart",
    "content": "import 'dart:math';\n\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass RevoluteJointExample extends Forge2DGame {\n  static const description = '''\n    In this example we use a joint to keep a body with several fixtures stuck\n    to another body.\n\n    Tap the screen to add more of these combined bodies.\n  ''';\n\n  RevoluteJointExample()\n    : super(gravity: Vector2(0, 10.0), world: RevoluteJointWorld());\n}\n\nclass RevoluteJointWorld extends Forge2DWorld\n    with TapCallbacks, HasGameReference<Forge2DGame> {\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    addAll(createBoundaries(game));\n  }\n\n  @override\n  void onTapDown(TapDownEvent info) {\n    super.onTapDown(info);\n    final ball = Ball(info.localPosition);\n    add(ball);\n    add(CircleShuffler(ball));\n  }\n}\n\nclass CircleShuffler extends BodyComponent {\n  final Ball ball;\n\n  CircleShuffler(this.ball);\n\n  @override\n  Body createBody() {\n    final bodyDef = BodyDef(\n      type: BodyType.dynamic,\n      position: ball.body.position.clone(),\n    );\n    const numPieces = 5;\n    const radius = 6.0;\n    final body = world.createBody(bodyDef);\n\n    for (var i = 0; i < numPieces; i++) {\n      final xPos = radius * cos(2 * pi * (i / numPieces));\n      final yPos = radius * sin(2 * pi * (i / numPieces));\n\n      final shape = CircleShape()\n        ..radius = 1.2\n        ..position.setValues(xPos, yPos);\n\n      final fixtureDef = FixtureDef(\n        shape,\n        density: 50.0,\n        friction: 0.1,\n        restitution: 0.9,\n      );\n\n      body.createFixture(fixtureDef);\n    }\n\n    final jointDef = RevoluteJointDef()\n      ..initialize(body, ball.body, body.position);\n    world.createJoint(RevoluteJoint(jointDef));\n\n    return body;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/joints/rope_joint.dart",
    "content": "import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\nimport 'package:flutter/material.dart';\n\nclass RopeJointExample extends Forge2DGame {\n  static const description = '''\n    This example shows how to use a `RopeJoint`. \n    \n    Drag the box handle along the axis and observe the rope respond to the \n    movement.\n  ''';\n\n  RopeJointExample() : super(world: RopeJointWorld());\n}\n\nclass RopeJointWorld extends Forge2DWorld\n    with DragCallbacks, HasGameReference<Forge2DGame> {\n  double handleWidth = 6;\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n\n    final handleBody = await createHandle();\n    createRope(handleBody);\n  }\n\n  Future<Body> createHandle() async {\n    final anchor = game.screenToWorld(Vector2(0, 100))..x = 0;\n\n    final box = DraggableBox(\n      startPosition: anchor,\n      width: handleWidth,\n      height: 3,\n    );\n    await add(box);\n\n    createPrismaticJoint(box.body, anchor);\n    return box.body;\n  }\n\n  Future<void> createRope(Body handle) async {\n    const length = 50;\n    var prevBody = handle;\n\n    for (var i = 0; i < length; i++) {\n      final newPosition = prevBody.worldCenter + Vector2(0, 1);\n      final ball = Ball(newPosition, radius: 0.5, color: Colors.white);\n      await add(ball);\n\n      createRopeJoint(ball.body, prevBody);\n      prevBody = ball.body;\n    }\n  }\n\n  void createPrismaticJoint(Body box, Vector2 anchor) {\n    final groundBody = createBody(BodyDef());\n    final halfWidth = game.screenToWorld(Vector2.zero()).x.abs();\n\n    final prismaticJointDef = PrismaticJointDef()\n      ..initialize(\n        box,\n        groundBody,\n        anchor,\n        Vector2(1, 0),\n      )\n      ..enableLimit = true\n      ..lowerTranslation = -halfWidth + handleWidth / 2\n      ..upperTranslation = halfWidth - handleWidth / 2;\n\n    final joint = PrismaticJoint(prismaticJointDef);\n    createJoint(joint);\n  }\n\n  void createRopeJoint(Body first, Body second) {\n    final ropeJointDef = RopeJointDef()\n      ..bodyA = first\n      ..localAnchorA.setFrom(first.getLocalCenter())\n      ..bodyB = second\n      ..localAnchorB.setFrom(second.getLocalCenter())\n      ..maxLength = (second.worldCenter - first.worldCenter).length;\n\n    createJoint(RopeJoint(ropeJointDef));\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/joints/weld_joint.dart",
    "content": "import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\nimport 'package:flutter/material.dart';\n\nclass WeldJointExample extends Forge2DGame {\n  static const description = '''\n    This example shows how to use a `WeldJoint`. Tap the screen to add a \n    ball to test the bridge built using a `WeldJoint`\n  ''';\n\n  WeldJointExample() : super(world: WeldJointWorld());\n}\n\nclass WeldJointWorld extends Forge2DWorld\n    with TapCallbacks, HasGameReference<Forge2DGame> {\n  final pillarHeight = 20.0;\n  final pillarWidth = 5.0;\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n\n    final leftPillar = Box(\n      startPosition: game.screenToWorld(Vector2(50, game.size.y))\n        ..y -= pillarHeight / 2,\n      width: pillarWidth,\n      height: pillarHeight,\n      bodyType: BodyType.static,\n      color: Colors.white,\n    );\n    final rightPillar = Box(\n      startPosition: game.screenToWorld(Vector2(game.size.x - 50, game.size.y))\n        ..y -= pillarHeight / 2,\n      width: pillarWidth,\n      height: pillarHeight,\n      bodyType: BodyType.static,\n      color: Colors.white,\n    );\n\n    await addAll([leftPillar, rightPillar]);\n\n    createBridge(leftPillar, rightPillar);\n  }\n\n  Future<void> createBridge(\n    Box leftPillar,\n    Box rightPillar,\n  ) async {\n    const sectionsCount = 10;\n    // Vector2.zero is used here since 0,0 is in the middle and 0,0 in the\n    // screen space then gives us the coordinates of the upper left corner in\n    // world space.\n    final halfSize = game.screenToWorld(Vector2.zero())..absolute();\n    final sectionWidth =\n        ((leftPillar.center.x.abs() +\n                    rightPillar.center.x.abs() +\n                    pillarWidth) /\n                sectionsCount)\n            .ceilToDouble();\n    Body? prevSection;\n\n    for (var i = 0; i < sectionsCount; i++) {\n      final section = Box(\n        startPosition: Vector2(\n          sectionWidth * i - halfSize.x + sectionWidth / 2,\n          halfSize.y - pillarHeight,\n        ),\n        width: sectionWidth,\n        height: 1,\n      );\n      await add(section);\n\n      if (prevSection != null) {\n        createWeldJoint(\n          prevSection,\n          section.body,\n          Vector2(\n            sectionWidth * i - halfSize.x + sectionWidth,\n            halfSize.y - pillarHeight,\n          ),\n        );\n      }\n\n      prevSection = section.body;\n    }\n  }\n\n  void createWeldJoint(Body first, Body second, Vector2 anchor) {\n    final weldJointDef = WeldJointDef()..initialize(first, second, anchor);\n\n    createJoint(WeldJoint(weldJointDef));\n  }\n\n  @override\n  Future<void> onTapDown(TapDownEvent info) async {\n    super.onTapDown(info);\n    final ball = Ball(info.localPosition, radius: 5);\n    add(ball);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/raycast_example.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\nimport 'package:flutter/material.dart' show Colors, Paint, Canvas;\n\nclass RaycastExample extends Forge2DGame with MouseMovementDetector {\n  static const String description = '''\n    This example shows how raycasts can be used to find nearest and farthest\n    fixtures.\n    Red ray finds the nearest fixture and blue ray finds the farthest fixture.\n  ''';\n\n  final random = Random();\n\n  final redPoints = <Vector2>[];\n  final bluePoints = <Vector2>[];\n\n  Box? nearestBox;\n  Box? farthestBox;\n\n  RaycastExample() : super(gravity: Vector2.zero());\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    world.addAll(createBoundaries(this));\n\n    const numberOfRows = 3;\n    const numberOfBoxes = 4;\n    for (var i = 0; i < numberOfBoxes; ++i) {\n      for (var j = 0; j < numberOfRows; ++j) {\n        world.add(Box(Vector2(i * 10, j * 20 - 20)));\n      }\n    }\n    world.add(\n      LineComponent(\n        redPoints,\n        Paint()\n          ..color = Colors.red\n          ..strokeWidth = 1,\n      ),\n    );\n    world.add(\n      LineComponent(\n        bluePoints,\n        Paint()\n          ..color = Colors.blue\n          ..strokeWidth = 1,\n      ),\n    );\n  }\n\n  @override\n  void onMouseMove(PointerHoverInfo info) {\n    final rayStart = screenToWorld(\n      Vector2(\n        camera.viewport.size.x / 4,\n        camera.viewport.size.y / 2,\n      ),\n    );\n\n    final worldPosition = screenToWorld(info.eventPosition.widget);\n    final redRayTarget = worldPosition + Vector2(0, 2);\n    fireRedRay(rayStart, redRayTarget);\n\n    final blueRayTarget = worldPosition - Vector2(0, 2);\n    fireBlueRay(rayStart, blueRayTarget);\n\n    super.onMouseMove(info);\n  }\n\n  void fireBlueRay(Vector2 rayStart, Vector2 rayTarget) {\n    bluePoints.clear();\n    bluePoints.add(rayStart);\n\n    final farthestCallback = FarthestBoxRayCastCallback();\n    world.raycast(farthestCallback, rayStart, rayTarget);\n\n    if (farthestCallback.farthestPoint != null) {\n      bluePoints.add(farthestCallback.farthestPoint!);\n    } else {\n      bluePoints.add(rayTarget);\n    }\n    farthestBox = farthestCallback.box;\n  }\n\n  void fireRedRay(Vector2 rayStart, Vector2 rayTarget) {\n    redPoints.clear();\n    redPoints.add(rayStart);\n\n    final nearestCallback = NearestBoxRayCastCallback();\n    world.raycast(nearestCallback, rayStart, rayTarget);\n\n    if (nearestCallback.nearestPoint != null) {\n      redPoints.add(nearestCallback.nearestPoint!);\n    } else {\n      redPoints.add(rayTarget);\n    }\n    nearestBox = nearestCallback.box;\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    children.whereType<Box>().forEach((component) {\n      if ((component == nearestBox) && (component == farthestBox)) {\n        component.paint.color = Colors.yellow;\n      } else if (component == nearestBox) {\n        component.paint.color = Colors.red;\n      } else if (component == farthestBox) {\n        component.paint.color = Colors.blue;\n      } else {\n        component.paint.color = Colors.white;\n      }\n    });\n  }\n}\n\nclass LineComponent extends Component {\n  LineComponent(this.points, this.paint);\n\n  final List<Vector2> points;\n  final Paint paint;\n  final Path path = Path();\n\n  @override\n  void update(double dt) {\n    path\n      ..reset()\n      ..addPolygon(\n        points.map((p) => p.toOffset()).toList(growable: false),\n        false,\n      );\n  }\n\n  @override\n  void render(Canvas canvas) {\n    for (var i = 0; i < points.length - 1; ++i) {\n      canvas.drawLine(\n        points[i].toOffset(),\n        points[i + 1].toOffset(),\n        paint,\n      );\n    }\n  }\n}\n\nclass Box extends BodyComponent {\n  Box(this.initialPosition);\n\n  final Vector2 initialPosition;\n\n  @override\n  Body createBody() {\n    final shape = PolygonShape()..setAsBoxXY(2.0, 4.0);\n    final fixtureDef = FixtureDef(shape, userData: this);\n    final bodyDef = BodyDef(position: initialPosition);\n    return world.createBody(bodyDef)..createFixture(fixtureDef);\n  }\n}\n\nclass NearestBoxRayCastCallback extends RayCastCallback {\n  Box? box;\n  Vector2? nearestPoint;\n  Vector2? normalAtInter;\n\n  @override\n  double reportFixture(\n    Fixture fixture,\n    Vector2 point,\n    Vector2 normal,\n    double fraction,\n  ) {\n    nearestPoint = point.clone();\n    normalAtInter = normal.clone();\n    box = fixture.userData as Box?;\n\n    // Returning fraction implies that we care only about\n    // fixtures that are closer to ray start point than\n    // the current fixture\n    return fraction;\n  }\n}\n\nclass FarthestBoxRayCastCallback extends RayCastCallback {\n  Box? box;\n  Vector2? farthestPoint;\n  Vector2? normalAtInter;\n  double previousFraction = 0.0;\n\n  @override\n  double reportFixture(\n    Fixture fixture,\n    Vector2 point,\n    Vector2 normal,\n    double fraction,\n  ) {\n    // Value of fraction is directly proportional to\n    // the distance of fixture from ray start point.\n    // So we are interested in the current fixture only if\n    // it has a higher fraction value than previousFraction.\n    if (previousFraction < fraction) {\n      farthestPoint = point.clone();\n      normalAtInter = normal.clone();\n      box = fixture.userData as Box?;\n      previousFraction = fraction;\n    }\n\n    return 1;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/revolute_joint_with_motor_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass RevoluteJointWithMotorExample extends Forge2DGame {\n  static const String description = '''\n    This example showcases a revolute joint, which is the spinning balls in the\n    center.\n    \n    If you tap the screen some colorful balls are added and will\n    interact with the bodies tied to the revolute joint once they have fallen\n    down the funnel.\n  ''';\n\n  RevoluteJointWithMotorExample() : super(world: RevoluteJointWithMotorWorld());\n}\n\nclass RevoluteJointWithMotorWorld extends Forge2DWorld\n    with TapCallbacks, HasGameReference<Forge2DGame> {\n  final random = Random();\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    final boundaries = createBoundaries(game);\n    addAll(boundaries);\n    final center = Vector2.zero();\n    add(CircleShuffler(center));\n    add(CornerRamp(center, isMirrored: true));\n    add(CornerRamp(center));\n  }\n\n  @override\n  void onTapDown(TapDownEvent info) {\n    super.onTapDown(info);\n    final tapPosition = info.localPosition;\n    List.generate(15, (i) {\n      final randomVector = (Vector2.random() - Vector2.all(-0.5)).normalized();\n      add(Ball(tapPosition + randomVector, radius: random.nextDouble()));\n    });\n  }\n}\n\nclass CircleShuffler extends BodyComponent {\n  CircleShuffler(this._center);\n\n  final Vector2 _center;\n\n  @override\n  Body createBody() {\n    final bodyDef = BodyDef(\n      type: BodyType.dynamic,\n      position: _center + Vector2(0.0, 25.0),\n    );\n    const numPieces = 5;\n    const radius = 6.0;\n    final body = world.createBody(bodyDef);\n\n    for (var i = 0; i < numPieces; i++) {\n      final xPos = radius * cos(2 * pi * (i / numPieces));\n      final yPos = radius * sin(2 * pi * (i / numPieces));\n\n      final shape = CircleShape()\n        ..radius = 1.2\n        ..position.setValues(xPos, yPos);\n\n      final fixtureDef = FixtureDef(\n        shape,\n        density: 50.0,\n        friction: 0.1,\n        restitution: 0.9,\n      );\n\n      body.createFixture(fixtureDef);\n    }\n    // Create an empty ground body.\n    final groundBody = world.createBody(BodyDef());\n\n    final revoluteJointDef = RevoluteJointDef()\n      ..initialize(body, groundBody, body.position)\n      ..motorSpeed = pi\n      ..maxMotorTorque = 1000000.0\n      ..enableMotor = true;\n\n    world.createJoint(RevoluteJoint(revoluteJointDef));\n    return body;\n  }\n}\n\nclass CornerRamp extends BodyComponent {\n  CornerRamp(this._center, {this.isMirrored = false});\n\n  final bool isMirrored;\n  final Vector2 _center;\n\n  @override\n  Body createBody() {\n    final shape = ChainShape();\n    final mirrorFactor = isMirrored ? -1 : 1;\n    final diff = 2.0 * mirrorFactor;\n    final vertices = [\n      Vector2(diff, 0),\n      Vector2(diff + 20.0 * mirrorFactor, -20.0),\n      Vector2(diff + 35.0 * mirrorFactor, -30.0),\n    ];\n    shape.createLoop(vertices);\n\n    final fixtureDef = FixtureDef(shape, friction: 0.1);\n    final bodyDef = BodyDef()\n      ..position = _center\n      ..type = BodyType.static;\n\n    return world.createBody(bodyDef)..createFixture(fixtureDef);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/sprite_body_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass SpriteBodyExample extends Forge2DGame {\n  static const String description = '''\n    In this example we show how to add a sprite on top of a `BodyComponent`.\n    Tap the screen to add more pizzas.\n  ''';\n\n  SpriteBodyExample()\n    : super(\n        gravity: Vector2(0, 10.0),\n        world: SpriteBodyWorld(),\n      );\n}\n\nclass SpriteBodyWorld extends Forge2DWorld\n    with TapCallbacks, HasGameReference<Forge2DGame> {\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    addAll(createBoundaries(game));\n  }\n\n  @override\n  void onTapDown(TapDownEvent info) {\n    super.onTapDown(info);\n    final position = info.localPosition;\n    add(Pizza(position, size: Vector2(10, 15)));\n  }\n}\n\nclass Pizza extends BodyComponent {\n  final Vector2 initialPosition;\n  final Vector2 size;\n\n  Pizza(\n    this.initialPosition, {\n    Vector2? size,\n  }) : size = size ?? Vector2(2, 3);\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    final sprite = await game.loadSprite('pizza.png');\n    renderBody = false;\n    add(\n      SpriteComponent(\n        sprite: sprite,\n        size: size,\n        anchor: Anchor.center,\n      ),\n    );\n  }\n\n  @override\n  Body createBody() {\n    final shape = PolygonShape();\n\n    final vertices = [\n      Vector2(-size.x / 2, size.y / 2),\n      Vector2(size.x / 2, size.y / 2),\n      Vector2(0, -size.y / 2),\n    ];\n    shape.set(vertices);\n\n    final fixtureDef = FixtureDef(\n      shape,\n      userData: this, // To be able to determine object in collision\n      restitution: 0.4,\n      friction: 0.5,\n    );\n\n    final bodyDef = BodyDef(\n      position: initialPosition,\n      angle: (initialPosition.x + initialPosition.y) / 2 * pi,\n      type: BodyType.dynamic,\n    );\n    return world.createBody(bodyDef)..createFixture(fixtureDef);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/tap_callbacks_example.dart",
    "content": "import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';\nimport 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass TapCallbacksExample extends Forge2DGame {\n  static const String description = '''\n    In this example we show how to use Flame's TapCallbacks mixin to react to\n    taps on `BodyComponent`s.\n    Tap the ball to give it a random impulse, or the text to add an effect to\n    it.\n  ''';\n  TapCallbacksExample() : super(zoom: 20, gravity: Vector2(0, 10.0));\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    final boundaries = createBoundaries(this);\n    world.addAll(boundaries);\n    world.add(TappableBall(Vector2.zero()));\n  }\n}\n\nclass TappableBall extends Ball with TapCallbacks {\n  TappableBall(super.position) {\n    originalPaint = BasicPalette.white.paint();\n    paint = originalPaint;\n  }\n\n  @override\n  void onTapDown(_) {\n    body.applyLinearImpulse(Vector2.random() * 1000);\n    paint = randomPaint();\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/utils/balls.dart",
    "content": "import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\nimport 'package:flutter/material.dart';\n\nclass Ball extends BodyComponent with ContactCallbacks {\n  late Paint originalPaint;\n  bool giveNudge = false;\n  final double radius;\n  final BodyType bodyType;\n  final Vector2 _position;\n  double _timeSinceNudge = 0.0;\n  static const double _minNudgeRest = 2.0;\n\n  final Paint _blue = BasicPalette.blue.paint();\n\n  Ball(\n    this._position, {\n    this.radius = 2,\n    this.bodyType = BodyType.dynamic,\n    Color? color,\n  }) {\n    if (color != null) {\n      originalPaint = PaletteEntry(color).paint();\n    } else {\n      originalPaint = randomPaint();\n    }\n    paint = originalPaint;\n  }\n\n  Paint randomPaint() => PaintExtension.random(withAlpha: 0.9, base: 100);\n\n  @override\n  Body createBody() {\n    final shape = CircleShape();\n    shape.radius = radius;\n\n    final fixtureDef = FixtureDef(\n      shape,\n      restitution: 0.8,\n      friction: 0.4,\n    );\n\n    final bodyDef = BodyDef(\n      userData: this,\n      angularDamping: 0.8,\n      position: _position,\n      type: bodyType,\n    );\n\n    return world.createBody(bodyDef)..createFixture(fixtureDef);\n  }\n\n  @override\n  void renderCircle(Canvas canvas, Offset center, double radius) {\n    super.renderCircle(canvas, center, radius);\n    final lineRotation = Offset(0, radius);\n    canvas.drawLine(center, center + lineRotation, _blue);\n  }\n\n  final _impulseForce = Vector2(0, 1000);\n\n  @override\n  @mustCallSuper\n  void update(double dt) {\n    _timeSinceNudge += dt;\n    if (giveNudge) {\n      giveNudge = false;\n      if (_timeSinceNudge > _minNudgeRest) {\n        body.applyLinearImpulse(_impulseForce);\n        _timeSinceNudge = 0.0;\n      }\n    }\n  }\n\n  @override\n  void beginContact(Object other, Contact contact) {\n    if (other is Wall) {\n      other.paint = paint;\n    }\n\n    if (other is WhiteBall) {\n      return;\n    }\n\n    if (other is Ball) {\n      if (paint != originalPaint) {\n        paint = other.paint;\n      } else {\n        other.paint = paint;\n      }\n    }\n  }\n}\n\nclass WhiteBall extends Ball with ContactCallbacks {\n  WhiteBall(super.position) {\n    originalPaint = BasicPalette.white.paint();\n    paint = originalPaint;\n  }\n\n  @override\n  void beginContact(Object other, Contact contact) {\n    if (other is Ball) {\n      other.giveNudge = true;\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nList<Wall> createBoundaries(Forge2DGame game, {double? strokeWidth}) {\n  final visibleRect = game.camera.visibleWorldRect;\n  final topLeft = visibleRect.topLeft.toVector2();\n  final topRight = visibleRect.topRight.toVector2();\n  final bottomRight = visibleRect.bottomRight.toVector2();\n  final bottomLeft = visibleRect.bottomLeft.toVector2();\n\n  return [\n    Wall(topLeft, topRight, strokeWidth: strokeWidth),\n    Wall(topRight, bottomRight, strokeWidth: strokeWidth),\n    Wall(bottomLeft, bottomRight, strokeWidth: strokeWidth),\n    Wall(topLeft, bottomLeft, strokeWidth: strokeWidth),\n  ];\n}\n\nclass Wall extends BodyComponent {\n  final Vector2 start;\n  final Vector2 end;\n  final double strokeWidth;\n\n  Wall(this.start, this.end, {double? strokeWidth})\n    : strokeWidth = strokeWidth ?? 1;\n\n  @override\n  Body createBody() {\n    final shape = EdgeShape()..set(start, end);\n    final fixtureDef = FixtureDef(shape, friction: 0.3);\n    final bodyDef = BodyDef(\n      userData: this, // To be able to determine object in collision\n      position: Vector2.zero(),\n    );\n    paint.strokeWidth = strokeWidth;\n\n    return world.createBody(bodyDef)..createFixture(fixtureDef);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/utils/boxes.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\n\nclass Box extends BodyComponent {\n  final Vector2 startPosition;\n  final double width;\n  final double height;\n  final BodyType bodyType;\n\n  Box({\n    required this.startPosition,\n    required this.width,\n    required this.height,\n    this.bodyType = BodyType.dynamic,\n    Color? color,\n  }) {\n    if (color != null) {\n      paint = PaletteEntry(color).paint();\n    } else {\n      paint = randomPaint();\n    }\n  }\n\n  Paint randomPaint() => PaintExtension.random(withAlpha: 0.9, base: 100);\n\n  @override\n  Body createBody() {\n    final shape = PolygonShape()\n      ..setAsBox(width / 2, height / 2, Vector2.zero(), 0);\n    final fixtureDef = FixtureDef(\n      shape,\n      friction: 0.3,\n      restitution: 0.2,\n      density: 10,\n    );\n    final bodyDef = BodyDef(\n      userData: this, // To be able to determine object in collision\n      position: startPosition,\n      type: bodyType,\n    );\n\n    return world.createBody(bodyDef)..createFixture(fixtureDef);\n  }\n}\n\nclass DraggableBox extends Box with DragCallbacks {\n  MouseJoint? mouseJoint;\n  late final groundBody = world.createBody(BodyDef());\n  bool _destroyJoint = false;\n\n  DraggableBox({\n    required super.startPosition,\n    required super.width,\n    required super.height,\n  });\n\n  @override\n  void update(double dt) {\n    if (_destroyJoint && mouseJoint != null) {\n      world.destroyJoint(mouseJoint!);\n      mouseJoint = null;\n      _destroyJoint = false;\n    }\n  }\n\n  @override\n  void onDragStart(DragStartEvent event) {\n    super.onDragStart(event);\n\n    final target = game.screenToWorld(event.devicePosition);\n\n    final mouseJointDef = MouseJointDef()\n      ..maxForce = 5000 * body.mass\n      ..dampingRatio = 0.1\n      ..frequencyHz = 50\n      ..target.setFrom(target)\n      ..collideConnected = false\n      ..bodyA = groundBody\n      ..bodyB = body;\n    mouseJoint = MouseJoint(mouseJointDef);\n\n    world.createJoint(mouseJoint!);\n  }\n\n  @override\n  bool onDragUpdate(DragUpdateEvent event) {\n    mouseJoint?.setTarget(\n      game.screenToWorld(event.deviceEndPosition),\n    );\n\n    return false;\n  }\n\n  @override\n  void onDragEnd(DragEndEvent event) {\n    super.onDragEnd(event);\n\n    if (mouseJoint == null) {\n      return;\n    }\n\n    _destroyJoint = true;\n    event.continuePropagation = false;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_forge2d/widget_example.dart",
    "content": "import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart' hide Transform;\nimport 'package:flutter/material.dart';\n\nclass WidgetExample extends Forge2DGame {\n  static const String description = '''\n    This examples shows how to render a widget on top of a Forge2D body outside\n    of Flame.\n  ''';\n\n  final List<void Function()> updateStates = [];\n  final Map<int, Body> bodyIdMap = {};\n  final List<int> addLaterIds = [];\n\n  WidgetExample() : super(zoom: 20, gravity: Vector2(0, 10.0));\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    final boundaries = createBoundaries(this, strokeWidth: 0);\n    world.addAll(boundaries);\n  }\n\n  Body createBody() {\n    final bodyDef = BodyDef(\n      angularVelocity: 3,\n      position: Vector2.zero(),\n      type: BodyType.dynamic,\n    );\n    final body = world.createBody(bodyDef);\n\n    final shape = PolygonShape()..setAsBoxXY(4.6, 0.8);\n    final fixtureDef = FixtureDef(\n      shape,\n      restitution: 0.8,\n      friction: 0.2,\n    );\n    body.createFixture(fixtureDef);\n    return body;\n  }\n\n  int createBodyId(int id) {\n    addLaterIds.add(id);\n    return id;\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    addLaterIds.forEach((id) {\n      if (!bodyIdMap.containsKey(id)) {\n        bodyIdMap[id] = createBody();\n      }\n    });\n    addLaterIds.clear();\n    updateStates.forEach((f) => f());\n  }\n}\n\nclass BodyWidgetExample extends StatelessWidget {\n  const BodyWidgetExample({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return GameWidget<WidgetExample>(\n      game: WidgetExample(),\n      overlayBuilderMap: {\n        'button1': (ctx, game) {\n          return BodyButtonWidget(game, game.createBodyId(1));\n        },\n        'button2': (ctx, game) {\n          return BodyButtonWidget(game, game.createBodyId(2));\n        },\n      },\n      initialActiveOverlays: const ['button1', 'button2'],\n    );\n  }\n}\n\nclass BodyButtonWidget extends StatefulWidget {\n  final WidgetExample _game;\n  final int _bodyId;\n\n  const BodyButtonWidget(\n    this._game,\n    this._bodyId, {\n    super.key,\n  });\n\n  @override\n  State<StatefulWidget> createState() {\n    return _BodyButtonState(_game, _bodyId);\n  }\n}\n\nclass _BodyButtonState extends State<BodyButtonWidget> {\n  final WidgetExample _game;\n  final int _bodyId;\n  Body? _body;\n\n  _BodyButtonState(this._game, this._bodyId) {\n    _game.updateStates.add(() {\n      setState(() {\n        _body = _game.bodyIdMap[_bodyId];\n      });\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final body = _body;\n    if (body == null) {\n      return Container();\n    } else {\n      final bodyPosition = _game.worldToScreen(body.position);\n      return Positioned(\n        top: bodyPosition.y - 18,\n        left: bodyPosition.x - 90,\n        child: Transform.rotate(\n          angle: body.angle,\n          child: ElevatedButton(\n            onPressed: () {\n              setState(\n                () => body.applyLinearImpulse(Vector2(0.0, 1000)),\n              );\n            },\n            child: const Text(\n              'Flying button!',\n              textScaler: TextScaler.linear(2.0),\n            ),\n          ),\n        ),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_isolate/isolate.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/bridge_libraries/flame_isolate/simple_isolate_example.dart';\nimport 'package:flame/game.dart';\n\nvoid addFlameIsolateExample(Dashbook dashbook) {\n  dashbook\n      .storiesOf('FlameIsolate')\n      .add(\n        'Simple isolate example',\n        (_) => GameWidget(\n          game: SimpleIsolateExample(),\n        ),\n        codeLink: baseLink(\n          'bridge_libraries/flame_isolate/simple_isolate_example.dart',\n        ),\n        info: SimpleIsolateExample.description,\n      );\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_isolate/simple_isolate_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_isolate/flame_isolate.dart';\nimport 'package:flutter/material.dart';\n\nclass SimpleIsolateExample extends FlameGame {\n  static const String description = '''\n    This example showcases a simple FlameIsolate example, making it easy to \n    continually run heavy load without stutter.\n    \n    Tap the brown square to swap between running heavy load in an isolate or\n    synchronous.\n    \n    The selected backpressure strategy used for this example is\n    `DiscardNewBackPressureStrategy`. This strategy discards all new jobs added\n    to the queue if there is already a job in the queue.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final world = World();\n    final cameraComponent = CameraComponent.withFixedResolution(\n      world: world,\n      width: 400,\n      height: 600,\n    );\n    addAll([world, cameraComponent]);\n\n    const rect = Rect.fromLTRB(-120, -120, 120, 120);\n    final circle = Path()..addOval(rect);\n    final teal = Paint()..color = Colors.tealAccent;\n\n    for (var i = 0; i < 20; i++) {\n      world.add(\n        RectangleComponent.square(size: 10)\n          ..paint = teal\n          ..add(\n            MoveAlongPathEffect(\n              circle,\n              EffectController(\n                duration: 6,\n                startDelay: i * 0.3,\n                infinite: true,\n              ),\n              oriented: true,\n            ),\n          ),\n      );\n    }\n\n    world.add(CalculatePrimeNumber());\n  }\n}\n\nenum ComputeType {\n  isolate('Running in isolate'),\n  synchronous('Running synchronously')\n  ;\n\n  final String description;\n\n  const ComputeType(this.description);\n}\n\nclass CalculatePrimeNumber extends PositionComponent\n    with TapCallbacks, FlameIsolate {\n  CalculatePrimeNumber() : super(anchor: Anchor.center);\n\n  ComputeType computeType = ComputeType.isolate;\n  late Timer _interval;\n\n  @override\n  BackpressureStrategy get backpressureStrategy =>\n      DiscardNewBackPressureStrategy();\n\n  @override\n  void onLoad() {\n    width = 200;\n    height = 70;\n  }\n\n  @override\n  Future<void> onMount() {\n    _interval = Timer(0.4, repeat: true, onTick: _checkNextAgainstPrime)\n      ..start();\n    return super.onMount();\n  }\n\n  @override\n  void update(double dt) {\n    _interval.update(dt);\n  }\n\n  @override\n  void onRemove() {\n    _interval.stop();\n    super.onRemove();\n  }\n\n  static const _minStartValue = 500000000;\n  static const _maxStartValue = 600000000;\n  static final _primeStartNumber =\n      Random().nextInt(_maxStartValue - _minStartValue) + _minStartValue;\n\n  MapEntry<int, bool> _primeData = MapEntry(\n    _primeStartNumber,\n    _isPrime(_primeStartNumber),\n  );\n\n  Future<void> _checkNextAgainstPrime() async {\n    final nextInt = _primeData.key + 1;\n\n    try {\n      final isPrime = switch (computeType) {\n        ComputeType.isolate => await isolateCompute(_isPrime, nextInt),\n        ComputeType.synchronous => _isPrime(nextInt),\n      };\n\n      _primeData = MapEntry(nextInt, isPrime);\n    } on BackpressureDropException catch (_) {\n      debugPrint('Backpressure kicked in');\n    }\n  }\n\n  @override\n  void onTapDown(_) {\n    computeType =\n        ComputeType.values[(computeType.index + 1) % ComputeType.values.length];\n  }\n\n  final _paint = Paint()..color = Colors.green;\n\n  final _textPaint = TextPaint(\n    style: const TextStyle(\n      fontSize: 10,\n    ),\n  );\n\n  late final rect = Rect.fromLTWH(0, 0, width, height);\n  late final topLeftVector = rect.topLeft.toVector2() + Vector2.all(4);\n  late final centerVector = rect.center.toVector2();\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(rect, _paint);\n\n    _textPaint.render(\n      canvas,\n      computeType.description,\n      topLeftVector,\n    );\n\n    _textPaint.render(\n      canvas,\n      '${_primeData.key} is${_primeData.value ? '' : ' not'} a prime number',\n      centerVector,\n      anchor: Anchor.center,\n    );\n  }\n}\n\nbool _isPrime(int value) {\n  // Simulating heavy load\n  if (value == 1) {\n    return false;\n  }\n  for (var i = 2; i < value; ++i) {\n    if (value % i == 0) {\n      return false;\n    }\n  }\n  return true;\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_jenny/commons/commons.dart",
    "content": "String baseLink(String path) {\n  const basePath =\n      'https://github.com/flame-engine/flame/blob/main/examples/lib/stories/bridge_libraries/flame_jenny/';\n\n  return '$basePath$path';\n}\n\nconst double fontSize = 24;\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_jenny/components/button_row.dart",
    "content": "import 'package:examples/stories/bridge_libraries/flame_jenny/components/dialogue_button.dart';\nimport 'package:flame/components.dart';\nimport 'package:jenny/jenny.dart';\n\nclass ButtonRow extends PositionComponent {\n  ButtonRow({required super.size}) : super(position: Vector2(0, 96));\n\n  void removeButtons() {\n    final buttonList = children.query<DialogueButton>();\n    if (buttonList.isNotEmpty) {\n      for (final dialogueButton in buttonList) {\n        if (dialogueButton.parent != null) {\n          dialogueButton.removeFromParent();\n        }\n      }\n    }\n  }\n\n  void showNextButton(Function() onNextButtonPressed) {\n    removeButtons();\n    final nextButton = DialogueButton(\n      assetPath: 'green_button_sqr.png',\n      text: 'Next',\n      position: Vector2(size.x / 2, 0),\n      onPressed: () {\n        onNextButtonPressed();\n        removeButtons();\n      },\n    );\n    add(nextButton);\n  }\n\n  void showOptionButtons({\n    required Function(int optionNumber) onChoice,\n    required DialogueOption option1,\n    required DialogueOption option2,\n  }) {\n    removeButtons();\n    final optionButtons = <DialogueButton>[\n      DialogueButton(\n        assetPath: 'green_button_sqr.png',\n        text: option1.text,\n        position: Vector2(size.x / 4, 0),\n        onPressed: () {\n          onChoice(0);\n          removeButtons();\n        },\n      ),\n      DialogueButton(\n        assetPath: 'red_button_sqr.png',\n        text: option2.text,\n        position: Vector2(size.x * 3 / 4, 0),\n        onPressed: () {\n          onChoice(1);\n          removeButtons();\n        },\n      ),\n    ];\n    addAll(optionButtons);\n  }\n\n  void showCloseButton(Function() onClose) {\n    final closeButton = DialogueButton(\n      assetPath: 'green_button_sqr.png',\n      text: 'Close',\n      onPressed: () => onClose(),\n      position: Vector2(size.x / 2, 0),\n    );\n    add(closeButton);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_jenny/components/command_lifecycle_dialogue_controller.dart",
    "content": "import 'dart:async';\n\nimport 'package:examples/stories/bridge_libraries/flame_jenny/components/dialogue_controller_component.dart';\nimport 'package:jenny/jenny.dart';\n\nclass CommandLifecycleDialogueController extends DialogueControllerComponent {\n  CommandLifecycleDialogueController({\n    required this.onCommandOverride,\n    required this.onCommandFinishOverride,\n  });\n  final FutureOr<void> Function(UserDefinedCommand command) onCommandOverride;\n  final FutureOr<void> Function(UserDefinedCommand command)\n  onCommandFinishOverride;\n\n  @override\n  FutureOr<void> onCommand(UserDefinedCommand command) async {\n    await onCommandOverride(command);\n    return super.onCommand(command);\n  }\n\n  @override\n  FutureOr<void> onCommandFinish(UserDefinedCommand command) async {\n    await onCommandFinishOverride(command);\n    return super.onCommandFinish(command);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_jenny/components/dialogue_box.dart",
    "content": "import 'package:examples/stories/bridge_libraries/flame_jenny/components/button_row.dart';\nimport 'package:examples/stories/bridge_libraries/flame_jenny/components/dialogue_text_box.dart';\nimport 'package:flame/components.dart';\nimport 'package:jenny/jenny.dart';\n\nclass DialogueBoxComponent extends SpriteComponent with HasGameReference {\n  DialogueTextBox textBox = DialogueTextBox(text: '');\n  final Vector2 spriteSize = Vector2(736, 128);\n  late final ButtonRow buttonRow = ButtonRow(size: spriteSize);\n\n  @override\n  Future<void> onLoad() async {\n    position = Vector2(game.size.x / 2, 96);\n    anchor = Anchor.center;\n    sprite = await Sprite.load(\n      'dialogue_box.png',\n      srcSize: spriteSize,\n    );\n    await addAll([buttonRow, textBox]);\n    return super.onLoad();\n  }\n\n  void changeText(String newText, Function() goNextLine) {\n    textBox.text = newText;\n    buttonRow.showNextButton(goNextLine);\n  }\n\n  void showOptions({\n    required Function(int optionNumber) onChoice,\n    required DialogueOption option1,\n    required DialogueOption option2,\n  }) {\n    buttonRow.showOptionButtons(\n      onChoice: onChoice,\n      option1: option1,\n      option2: option2,\n    );\n  }\n\n  void showCloseButton(Function() onClose) {\n    buttonRow.showCloseButton(onClose);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_jenny/components/dialogue_button.dart",
    "content": "import 'package:examples/stories/bridge_libraries/flame_jenny/commons/commons.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/input.dart';\nimport 'package:flutter/material.dart';\n\nclass DialogueButton extends SpriteButtonComponent {\n  DialogueButton({\n    required super.position,\n    required this.assetPath,\n    required this.text,\n    required super.onPressed,\n    super.anchor = Anchor.center,\n  });\n\n  final String text;\n  final String assetPath;\n\n  @override\n  Future<void> onLoad() async {\n    button = await Sprite.load(assetPath);\n    add(\n      TextComponent(\n        text: text,\n        position: Vector2(48, 16),\n        anchor: Anchor.center,\n        size: Vector2(88, 28),\n        textRenderer: TextPaint(\n          style: const TextStyle(\n            fontSize: fontSize,\n            color: Colors.white70,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_jenny/components/dialogue_controller_component.dart",
    "content": "import 'dart:async';\nimport 'package:examples/stories/bridge_libraries/flame_jenny/components/dialogue_box.dart';\nimport 'package:flame/components.dart' hide Timer;\nimport 'package:jenny/jenny.dart';\n\nclass DialogueControllerComponent extends Component\n    with DialogueView, HasGameReference {\n  Completer<void> _forwardCompleter = Completer();\n  Completer<int> _choiceCompleter = Completer<int>();\n  Completer<void> _closeCompleter = Completer();\n  late final DialogueBoxComponent _dialogueBoxComponent =\n      DialogueBoxComponent();\n\n  @override\n  Future<void> onNodeStart(Node node) async {\n    _closeCompleter = Completer();\n    _addDialogueBox();\n  }\n\n  void _addDialogueBox() {\n    game.camera.viewport.add(_dialogueBoxComponent);\n  }\n\n  @override\n  Future<void> onNodeFinish(Node node) async {\n    _dialogueBoxComponent.showCloseButton(_onClose);\n    return _closeCompleter.future;\n  }\n\n  void _onClose() {\n    if (!_closeCompleter.isCompleted) {\n      _closeCompleter.complete();\n    }\n    final list = game.camera.viewport.children.query<DialogueBoxComponent>();\n    if (list.isNotEmpty) {\n      game.camera.viewport.removeAll(list);\n    }\n  }\n\n  Future<void> _advance() async {\n    return _forwardCompleter.future;\n  }\n\n  @override\n  FutureOr<bool> onLineStart(DialogueLine line) async {\n    _forwardCompleter = Completer();\n    _changeTextAndShowNextButton(line);\n    await _advance();\n    return super.onLineStart(line);\n  }\n\n  void _changeTextAndShowNextButton(DialogueLine line) {\n    final characterName = line.character?.name ?? '';\n    final dialogueLineText = '$characterName: ${line.text}';\n    _dialogueBoxComponent.changeText(dialogueLineText, _goNextLine);\n  }\n\n  void _goNextLine() {\n    if (!_forwardCompleter.isCompleted) {\n      _forwardCompleter.complete();\n    }\n  }\n\n  @override\n  FutureOr<int?> onChoiceStart(DialogueChoice choice) async {\n    _forwardCompleter = Completer();\n    _choiceCompleter = Completer<int>();\n    _dialogueBoxComponent.showOptions(\n      onChoice: _onChoice,\n      option1: choice.options[0],\n      option2: choice.options[1],\n    );\n    await _advance();\n    return _choiceCompleter.future;\n  }\n\n  void _onChoice(int optionNumber) {\n    if (!_forwardCompleter.isCompleted) {\n      _forwardCompleter.complete();\n    }\n    if (!_choiceCompleter.isCompleted) {\n      _choiceCompleter.complete(optionNumber);\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_jenny/components/dialogue_text_box.dart",
    "content": "import 'package:examples/stories/bridge_libraries/flame_jenny/commons/commons.dart';\nimport 'package:flame/components.dart';\nimport 'package:flutter/material.dart';\n\nclass DialogueTextBox extends TextBoxComponent {\n  DialogueTextBox({required super.text})\n    : super(\n        position: Vector2(16, 16),\n        size: Vector2(704, 96),\n        textRenderer: TextPaint(\n          style: const TextStyle(\n            fontSize: fontSize,\n            color: Colors.black,\n          ),\n        ),\n      );\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_jenny/components/menu_button.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame/text.dart';\nimport 'package:flutter/material.dart';\n\nclass MenuButton extends ButtonComponent {\n  MenuButton({\n    required super.position,\n    required super.onPressed,\n    required this.text,\n  }) : super(size: Vector2(128, 42));\n\n  late String text;\n\n  final Paint white = BasicPalette.white.paint();\n  final TextPaint topTextPaint = TextPaint(\n    style: TextStyle(color: BasicPalette.black.color),\n  );\n\n  @override\n  Future<void> onLoad() async {\n    button = RectangleComponent(paint: white, size: size);\n    anchor = Anchor.center;\n    add(\n      TextComponent(\n        text: text,\n        textRenderer: topTextPaint,\n        position: size / 2,\n        anchor: Anchor.center,\n        priority: 1,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_jenny/jenny.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/stories/bridge_libraries/flame_jenny/commons/commons.dart';\nimport 'package:examples/stories/bridge_libraries/flame_jenny/jenny_advanced_example.dart';\nimport 'package:examples/stories/bridge_libraries/flame_jenny/jenny_command_lifecycle_example.dart';\nimport 'package:examples/stories/bridge_libraries/flame_jenny/jenny_simple_example.dart';\nimport 'package:flame/game.dart';\n\nvoid addFlameJennyExample(Dashbook dashbook) {\n  dashbook.storiesOf('FlameJenny')\n    ..add(\n      'Simple Jenny example',\n      (_) => GameWidget(game: JennySimpleExample()),\n      codeLink: baseLink('jenny_simple_example.dart'),\n      info: JennySimpleExample.description,\n    )\n    ..add(\n      'Advanced Jenny example',\n      (_) => GameWidget(game: JennyAdvancedExample()),\n      codeLink: baseLink('jenny_advanced_example.dart'),\n      info: JennyAdvancedExample.description,\n    )\n    ..add(\n      'Command Lifecycle example',\n      (_) => GameWidget(game: JennyCommandLifecycleExample()),\n      codeLink: baseLink('jenny_command_lifecycle_example.dart'),\n      info: JennyCommandLifecycleExample.description,\n    );\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_jenny/jenny_advanced_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:examples/stories/bridge_libraries/flame_jenny/components/dialogue_controller_component.dart';\nimport 'package:examples/stories/bridge_libraries/flame_jenny/components/menu_button.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame/text.dart';\nimport 'package:flutter/services.dart';\nimport 'package:jenny/jenny.dart';\n\nclass JennyAdvancedExample extends FlameGame {\n  static const String description = '''\n    This is an advanced example of how to use the Jenny Package. \n    It includes implementing dialogue choices, setting custom variables,\n    using commands and implementing User-Defined Commands, .\n  ''';\n\n  int coins = 0;\n\n  final Paint white = BasicPalette.white.paint();\n  final TextPaint mainTextPaint = TextPaint(\n    style: TextStyle(color: BasicPalette.white.color),\n  );\n  final TextPaint buttonTextPaint = TextPaint(\n    style: TextStyle(color: BasicPalette.black.color),\n  );\n  final startButtonSize = Vector2(128, 56);\n\n  late final TextComponent header = TextComponent(\n    text: 'Select player name.',\n    position: Vector2(size.x / 2, 56),\n    size: startButtonSize,\n    anchor: Anchor.center,\n    textRenderer: mainTextPaint,\n  );\n\n  Future<void> startDialogue(String playerName) async {\n    final dialogueControllerComponent = DialogueControllerComponent();\n    add(dialogueControllerComponent);\n\n    final yarnProject = YarnProject();\n\n    yarnProject\n      ..commands.addCommand1('updateCoins', updateCoins)\n      ..variables.setVariable(r'$playerName', playerName)\n      ..parse(await rootBundle.loadString('assets/yarn/advanced.yarn'));\n    final dialogueRunner = DialogueRunner(\n      yarnProject: yarnProject,\n      dialogueViews: [dialogueControllerComponent],\n    );\n    dialogueRunner.startDialogue('gamble');\n  }\n\n  void updateCoins(int amountChange) {\n    coins += amountChange;\n    header.text = 'Select player name. Current coins: $coins';\n  }\n\n  @override\n  Future<void> onLoad() async {\n    addAll([\n      header,\n      MenuButton(\n        position: Vector2(size.x / 4, 128),\n        onPressed: () => startDialogue('Jessie'),\n        text: 'Jessie',\n      ),\n      MenuButton(\n        position: Vector2(size.x * 3 / 4, 128),\n        onPressed: () => startDialogue('James'),\n        text: 'James',\n      ),\n    ]);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_jenny/jenny_command_lifecycle_example.dart",
    "content": "import 'package:examples/stories/bridge_libraries/flame_jenny/components/command_lifecycle_dialogue_controller.dart';\nimport 'package:examples/stories/bridge_libraries/flame_jenny/components/menu_button.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/services.dart';\nimport 'package:jenny/jenny.dart';\n\nclass JennyCommandLifecycleExample extends FlameGame {\n  static const String description = '''\nThis is an example of how the lifecycle methods relating to user-defined\ncommands work.\n  ''';\n\n  final TextComponent onCommandLabel = TextComponent(text: '');\n  final TextComponent onCommandFinishLabel = TextComponent(text: '');\n\n  static const initialOnCommandLabelText = 'onCommand: ???';\n  static const initialOnCommandFinishLabelText = 'onCommandFinish: ???';\n\n  Future<void> startDialogue() async {\n    // Initialize the labels\n    onCommandLabel.text = initialOnCommandLabelText;\n    onCommandFinishLabel.text = initialOnCommandFinishLabelText;\n    final yarnProject = YarnProject();\n    final dialogueControllerComponent = CommandLifecycleDialogueController(\n      onCommandOverride: (command) async {\n        final exampleVariable = yarnProject.variables.getVariable(\n          r'$exampleVariable',\n        );\n        onCommandLabel.text = 'onCommand: $exampleVariable';\n      },\n      onCommandFinishOverride: (command) async {\n        final exampleVariable = yarnProject.variables.getVariable(\n          r'$exampleVariable',\n        );\n        onCommandFinishLabel.text = 'onCommandFinish: $exampleVariable';\n      },\n    );\n    add(dialogueControllerComponent);\n    add(\n      ColumnComponent(\n        children: [\n          onCommandLabel,\n          onCommandFinishLabel,\n        ],\n      ),\n    );\n    yarnProject.commands.addCommand1('exampleCommand', (String someData) async {\n      /// Placeholder for some asynchronous event, like... maybe a dice roll.\n      await Future.delayed(const Duration(milliseconds: 300));\n      yarnProject.variables.setVariable(r'$exampleVariable', someData);\n    });\n    yarnProject.parse(\n      await rootBundle.loadString('assets/yarn/command_lifecycle.yarn'),\n    );\n    final dialogueRunner = DialogueRunner(\n      yarnProject: yarnProject,\n      dialogueViews: [dialogueControllerComponent],\n    );\n    dialogueRunner.startDialogue('command_lifecycle');\n  }\n\n  @override\n  Future<void> onLoad() async {\n    addAll([\n      MenuButton(\n        position: Vector2(size.x / 2, 96),\n        onPressed: startDialogue,\n        text: 'Start conversation',\n      ),\n    ]);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_jenny/jenny_simple_example.dart",
    "content": "import 'package:examples/stories/bridge_libraries/flame_jenny/components/dialogue_controller_component.dart';\nimport 'package:examples/stories/bridge_libraries/flame_jenny/components/menu_button.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/services.dart';\nimport 'package:jenny/jenny.dart';\n\nclass JennySimpleExample extends FlameGame {\n  static const String description = '''\n    This is a simple example of how to use the Jenny Package. \n    It includes instantiating YarnProject and parsing a .yarn script.\n  ''';\n\n  Future<void> startDialogue() async {\n    final dialogueControllerComponent = DialogueControllerComponent();\n    add(dialogueControllerComponent);\n\n    final yarnProject = YarnProject();\n    yarnProject.parse(await rootBundle.loadString('assets/yarn/simple.yarn'));\n    final dialogueRunner = DialogueRunner(\n      yarnProject: yarnProject,\n      dialogueViews: [dialogueControllerComponent],\n    );\n    dialogueRunner.startDialogue('hello_world');\n  }\n\n  @override\n  Future<void> onLoad() async {\n    addAll([\n      MenuButton(\n        position: Vector2(size.x / 2, 96),\n        onPressed: startDialogue,\n        text: 'Start conversation',\n      ),\n    ]);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_lottie/lottie.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/bridge_libraries/flame_lottie/lottie_animation_example.dart';\nimport 'package:flame/game.dart';\n\nvoid addFlameLottieExample(Dashbook dashbook) {\n  dashbook\n      .storiesOf('FlameLottie')\n      .add(\n        'Lottie Animation example',\n        (_) => GameWidget(\n          game: LottieAnimationExample(),\n        ),\n        codeLink: baseLink(\n          'bridge_libraries/flame_lottie/lottie_animation_example.dart',\n        ),\n        info: LottieAnimationExample.description,\n      );\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_lottie/lottie_animation_example.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame_lottie/flame_lottie.dart';\n\nclass LottieAnimationExample extends FlameGame {\n  static const String description = '''\n    This example shows how to load a Lottie animation. It is configured to \n    continuously loop the animation and restart once its done.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final asset = await loadLottie(\n      Lottie.asset('assets/images/animations/lottieLogo.json'),\n    );\n\n    add(LottieComponent(asset, size: Vector2.all(400), repeating: true));\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_spine/basic_spine_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_spine/flame_spine.dart';\n\nclass FlameSpineExample extends FlameGame with TapCallbacks {\n  static const String description = '''\n    This example shows how to load a Spine animation. Tap on the screen to try\n    different states of the animation.\n  ''';\n\n  late final SpineComponent spineboy;\n\n  final states = [\n    'walk',\n    'aim',\n    'death',\n    'hoverboard',\n    'idle',\n    'jump',\n    'portal',\n    'run',\n    'shoot',\n  ];\n\n  int _stateIndex = 0;\n\n  @override\n  Future<void> onLoad() async {\n    await initSpineFlutter();\n    // Load the Spineboy atlas and skeleton data from asset files\n    // and create a SpineComponent from them, scaled down and\n    // centered on the screen\n    spineboy = await SpineComponent.fromAssets(\n      atlasFile: 'assets/spine/spineboy.atlas',\n      skeletonFile: 'assets/spine/spineboy-pro.skel',\n      scale: Vector2(0.4, 0.4),\n      anchor: Anchor.center,\n      position: Vector2(size.x / 2, size.y / 2),\n    );\n\n    // Set the \"walk\" animation on track 0 in looping mode\n    spineboy.animationState.setAnimation(0, 'walk', true);\n    await add(spineboy);\n  }\n\n  @override\n  void onTapDown(_) {\n    _stateIndex = (_stateIndex + 1) % states.length;\n    spineboy.animationState.setAnimation(0, states[_stateIndex], true);\n  }\n\n  @override\n  void onDetach() {\n    // Dispose the native resources that have been loaded for spineboy.\n    spineboy.dispose();\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_spine/flame_spine.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/bridge_libraries/flame_spine/basic_spine_example.dart';\nimport 'package:examples/stories/bridge_libraries/flame_spine/shared_data_spine_example.dart';\nimport 'package:flame/game.dart';\n\nvoid addFlameSpineExamples(Dashbook dashbook) {\n  dashbook.storiesOf('FlameSpine')\n    ..add(\n      'Basic Spine Animation',\n      (_) => GameWidget(\n        game: FlameSpineExample(),\n      ),\n      codeLink: baseLink(\n        'bridge_libraries/flame_spine/basic_spine_example.dart',\n      ),\n      info: FlameSpineExample.description,\n    )\n    ..add(\n      'SpineComponent with shared data',\n      (_) => GameWidget(\n        game: SharedDataSpineExample(),\n      ),\n      codeLink: baseLink(\n        'bridge_libraries/flame_spine/shared_data_spine_example.dart',\n      ),\n      info: SharedDataSpineExample.description,\n    );\n}\n"
  },
  {
    "path": "examples/lib/stories/bridge_libraries/flame_spine/shared_data_spine_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_spine/flame_spine.dart';\n\nclass SharedDataSpineExample extends FlameGame with TapCallbacks {\n  static const String description = '''\n    This example shows how to preload assets and share data between Spine\n    components.\n  ''';\n\n  late final SkeletonDataFlutter cachedSkeletonData;\n  late final AtlasFlutter cachedAtlas;\n  late final List<SpineComponent> spineboys = [];\n\n  @override\n  Future<void> onLoad() async {\n    await initSpineFlutter();\n    // Pre-load the atlas and skeleton data once.\n    cachedAtlas = await AtlasFlutter.fromAsset('assets/spine/spineboy.atlas');\n    cachedSkeletonData = await SkeletonDataFlutter.fromAsset(\n      cachedAtlas,\n      'assets/spine/spineboy-pro.skel',\n    );\n\n    // Instantiate many spineboys from the pre-loaded data. Each SpineComponent\n    // gets their own SkeletonDrawable copy derived from the cached data. The\n    // SkeletonDrawable copies do not own the underlying skeleton data and\n    // atlas.\n    final rng = Random();\n    for (var i = 0; i < 100; i++) {\n      final drawable = SkeletonDrawableFlutter(\n        cachedAtlas,\n        cachedSkeletonData,\n        false,\n      );\n      final scale = 0.1 + rng.nextDouble() * 0.2;\n      final position = Vector2.random(rng)..multiply(size);\n      final spineboy = SpineComponent(\n        drawable,\n        scale: Vector2.all(scale),\n        position: position,\n      );\n      spineboy.animationState.setAnimation(0, 'walk', true);\n      spineboys.add(spineboy);\n    }\n    await addAll(spineboys);\n  }\n\n  @override\n  void onTapDown(_) {\n    for (final spineboy in spineboys) {\n      spineboy.animationState.setAnimation(0, 'jump', false);\n      spineboy.animationState.setListener((type, track, event) {\n        if (type == EventType.complete) {\n          spineboy.animationState.setAnimation(0, 'walk', true);\n        }\n      });\n    }\n  }\n\n  @override\n  void onDetach() {\n    // Dispose the pre-loaded atlas and skeleton data when the game/scene is\n    // removed.\n    cachedAtlas.dispose();\n    cachedSkeletonData.dispose();\n\n    // Dispose each spineboy and its internal SkeletonDrawable.\n    for (final spineboy in spineboys) {\n      spineboy.dispose();\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/camera_and_viewport/camera_and_viewport.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/camera_and_viewport/camera_component_example.dart';\nimport 'package:examples/stories/camera_and_viewport/camera_component_properties_example.dart';\nimport 'package:examples/stories/camera_and_viewport/camera_follow_and_world_bounds.dart';\nimport 'package:examples/stories/camera_and_viewport/coordinate_systems_example.dart';\nimport 'package:examples/stories/camera_and_viewport/fixed_resolution_example.dart';\nimport 'package:examples/stories/camera_and_viewport/follow_component_example.dart';\nimport 'package:examples/stories/camera_and_viewport/static_components_example.dart';\nimport 'package:examples/stories/camera_and_viewport/zoom_example.dart';\nimport 'package:flame/game.dart';\n\nvoid addCameraAndViewportStories(Dashbook dashbook) {\n  dashbook.storiesOf('Camera & Viewport')\n    ..add(\n      'Follow Component',\n      (context) {\n        return GameWidget(\n          game: FollowComponentExample(\n            viewportResolution: Vector2(\n              context.numberProperty('viewport width', 500),\n              context.numberProperty('viewport height', 500),\n            ),\n          ),\n        );\n      },\n      codeLink: baseLink('camera_and_viewport/follow_component_example.dart'),\n      info: FollowComponentExample.description,\n    )\n    ..add(\n      'Zoom',\n      (context) {\n        return GameWidget(\n          game: ZoomExample(),\n        );\n      },\n      codeLink: baseLink('camera_and_viewport/zoom_example.dart'),\n      info: ZoomExample.description,\n    )\n    ..add(\n      'Fixed Resolution viewport',\n      (context) {\n        return const GameWidget.controlled(\n          gameFactory: FixedResolutionExample.new,\n        );\n      },\n      codeLink: baseLink('camera_and_viewport/fixed_resolution_example.dart'),\n      info: FixedResolutionExample.description,\n    )\n    ..add(\n      'HUDs and static components',\n      (context) {\n        return GameWidget(\n          game: StaticComponentsExample(\n            viewportResolution: Vector2(\n              context.numberProperty('viewport width', 500),\n              context.numberProperty('viewport height', 500),\n            ),\n          ),\n        );\n      },\n      codeLink: baseLink('camera_and_viewport/static_components_example.dart'),\n      info: StaticComponentsExample.description,\n    )\n    ..add(\n      'Coordinate Systems',\n      (context) => const CoordinateSystemsWidget(),\n      codeLink: baseLink('camera_and_viewport/coordinate_systems_example.dart'),\n      info: CoordinateSystemsExample.description,\n    )\n    ..add(\n      'CameraComponent',\n      (context) => GameWidget(game: CameraComponentExample()),\n      codeLink: baseLink('camera_and_viewport/camera_component_example.dart'),\n      info: CameraComponentExample.description,\n    )\n    ..add(\n      'CameraComponent properties',\n      (context) => GameWidget(game: CameraComponentPropertiesExample()),\n      codeLink: baseLink(\n        'camera_and_viewport/camera_component_properties_example.dart',\n      ),\n      info: CameraComponentPropertiesExample.description,\n    )\n    ..add(\n      'Follow and World bounds',\n      (_) => GameWidget(game: CameraFollowAndWorldBoundsExample()),\n      codeLink: baseLink(\n        'camera_and_viewport/camera_follow_and_world_bounds.dart',\n      ),\n      info: CameraFollowAndWorldBoundsExample.description,\n    );\n}\n"
  },
  {
    "path": "examples/lib/stories/camera_and_viewport/camera_component_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/camera.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart' show OffsetExtension, PathExtension;\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/input.dart';\nimport 'package:flutter/painting.dart';\n\nclass CameraComponentExample extends FlameGame<AntWorld> with PanDetector {\n  static const description = '''\n    This example shows how a camera can be dynamically added into a game using\n    a CameraComponent.\n    \n    Click and hold the mouse to bring up a magnifying glass, then have a better\n    look at the world underneath! \n  ''';\n\n  CameraComponentExample() : super(world: AntWorld());\n\n  late final CameraComponent magnifyingGlass;\n  late final Vector2 center;\n  static const zoom = 10.0;\n  static const radius = 130.0;\n\n  @override\n  Color backgroundColor() => const Color(0xFFeeeeee);\n\n  @override\n  Future<void> onLoad() async {\n    world.loaded.then((_) {\n      final offset = world.curve.boundingRect().center;\n      center = offset.toVector2();\n      camera.viewfinder.position = Vector2(center.x, center.y);\n    });\n\n    magnifyingGlass = CameraComponent(\n      world: world,\n      viewport: CircularViewport(radius),\n    );\n    magnifyingGlass.viewport.add(Bezel(radius));\n    magnifyingGlass.viewfinder.zoom = zoom;\n  }\n\n  @override\n  bool onPanStart(DragStartInfo info) {\n    _updateMagnifyingGlassPosition(info.eventPosition.widget);\n    add(magnifyingGlass);\n    return false;\n  }\n\n  @override\n  bool onPanUpdate(DragUpdateInfo info) {\n    _updateMagnifyingGlassPosition(info.eventPosition.widget);\n    return false;\n  }\n\n  @override\n  bool onPanEnd(DragEndInfo info) {\n    onPanCancel();\n    return false;\n  }\n\n  @override\n  bool onPanCancel() {\n    remove(magnifyingGlass);\n    return false;\n  }\n\n  void _updateMagnifyingGlassPosition(Vector2 point) {\n    // [point] is in the canvas coordinate system.\n    magnifyingGlass\n      ..viewport.position = point - Vector2.all(radius)\n      ..viewfinder.position = point - canvasSize / 2 + center;\n  }\n}\n\nclass Bezel extends PositionComponent {\n  Bezel(this.radius)\n    : super(\n        size: Vector2.all(2 * radius),\n        position: Vector2.all(radius),\n      );\n\n  final double radius;\n  late final Path rim;\n  late final Path rimBorder;\n  late final Path handle;\n  late final Path connector;\n  late final Path specularHighlight;\n  static const rimWidth = 20.0;\n  static const handleWidth = 40.0;\n  static const handleLength = 100.0;\n  late final Paint glassPaint;\n  late final Paint rimPaint;\n  late final Paint rimBorderPaint;\n  late final Paint handlePaint;\n  late final Paint connectorPaint;\n  late final Paint specularPaint;\n\n  @override\n  void onLoad() {\n    rim = Path()..addOval(Rect.fromLTRB(-radius, -radius, radius, radius));\n    final outer = radius + rimWidth / 2;\n    final inner = radius - rimWidth / 2;\n    rimBorder = Path()\n      ..addOval(Rect.fromLTRB(-outer, -outer, outer, outer))\n      ..addOval(Rect.fromLTRB(-inner, -inner, inner, inner));\n    handle =\n        (Path()..addRRect(\n              RRect.fromLTRBR(\n                radius,\n                -handleWidth / 2,\n                handleLength + radius,\n                handleWidth / 2,\n                const Radius.circular(5.0),\n              ),\n            ))\n            .transform32((Matrix4.identity()..rotateZ(pi / 4)).storage);\n    connector =\n        (Path()..addArc(\n              Rect.fromLTRB(-outer, -outer, outer, outer),\n              -0.22,\n              0.44,\n            ))\n            .transform32((Matrix4.identity()..rotateZ(pi / 4)).storage);\n    specularHighlight =\n        (Path()..addOval(Rect.fromLTWH(-radius * 0.8, -8, 16, radius * 0.3)))\n            .transform32((Matrix4.identity()..rotateZ(pi / 4)).storage);\n\n    glassPaint = Paint()..color = const Color(0x1400ffae);\n    rimBorderPaint = Paint()\n      ..strokeWidth = 1.0\n      ..style = PaintingStyle.stroke\n      ..color = const Color(0xff61382a);\n    rimPaint = Paint()\n      ..strokeWidth = rimWidth\n      ..style = PaintingStyle.stroke\n      ..color = const Color(0xffffdf70);\n    connectorPaint = Paint()\n      ..strokeWidth = 20.0\n      ..style = PaintingStyle.stroke\n      ..strokeCap = StrokeCap.round\n      ..color = const Color(0xff654510);\n    handlePaint = Paint()..color = const Color(0xffdbbf9f);\n    specularPaint = Paint()\n      ..color = const Color(0xccffffff)\n      ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawPath(rim, glassPaint);\n    canvas.drawPath(specularHighlight, specularPaint);\n    canvas.drawPath(handle, handlePaint);\n    canvas.drawPath(handle, rimBorderPaint);\n    canvas.drawPath(connector, connectorPaint);\n    canvas.drawPath(rim, rimPaint);\n    canvas.drawPath(rimBorder, rimBorderPaint);\n  }\n}\n\nclass AntWorld extends World {\n  late final DragonCurve curve;\n  late final Rect bgRect;\n  final Paint bgPaint = Paint()..color = const Color(0xffeeeeee);\n\n  @override\n  Future<void> onLoad() async {\n    final random = Random();\n    curve = DragonCurve();\n    await add(curve);\n    bgRect = curve.boundingRect().inflate(100);\n\n    const baseColor = HSVColor.fromAHSV(1, 38.5, 0.63, 0.68);\n    for (var i = 0; i < 20; i++) {\n      add(\n        Ant()\n          ..color = baseColor.withHue(random.nextDouble() * 360).toColor()\n          ..scale = Vector2.all(0.4)\n          ..setTravelPath(curve.path),\n      );\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    // Render white backdrop, to prevent the world in the magnifying glass from\n    // being \"see-through\"\n    canvas.drawRect(bgRect, bgPaint);\n  }\n}\n\nclass DragonCurve extends PositionComponent {\n  late final Paint borderPaint;\n  late final Paint mainPaint;\n  late final Path dragon;\n  late List<Vector2> path;\n  static const cellSize = 20.0;\n  static const notchSize = 4.0;\n\n  void initPath() {\n    path = [\n      Vector2(0, cellSize - notchSize),\n      Vector2(0, notchSize),\n    ];\n    final endPoint = Vector2(0, cellSize);\n    final transform = Transform2D()..angleDegrees = -90;\n    for (var i = 0; i < 8; i++) {\n      path += List.from(path.reversed.map<Vector2>(transform.localToGlobal));\n      final pivot = transform.localToGlobal(endPoint);\n      transform\n        ..position = pivot\n        ..offset = -pivot;\n    }\n  }\n\n  Rect boundingRect() {\n    var minX = double.infinity;\n    var minY = double.infinity;\n    var maxX = -double.infinity;\n    var maxY = -double.infinity;\n    for (final point in path) {\n      minX = min(minX, point.x);\n      minY = min(minY, point.y);\n      maxX = max(maxX, point.x);\n      maxY = max(maxY, point.y);\n    }\n    return Rect.fromLTRB(minX, minY, maxX, maxY);\n  }\n\n  @override\n  Future<void> onLoad() async {\n    initPath();\n    borderPaint = Paint()\n      ..color = const Color(0xFF041D1F)\n      ..style = PaintingStyle.stroke\n      ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 0.3)\n      ..strokeWidth = 4;\n    mainPaint = Paint()\n      ..color = const Color(0xffefe79c)\n      ..style = PaintingStyle.stroke\n      ..strokeWidth = 3.6;\n\n    dragon = Path()..moveTo(path[0].x, path[0].y);\n    for (final p in path) {\n      dragon.lineTo(p.x, p.y);\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawPath(dragon, borderPaint);\n    canvas.drawPath(dragon, mainPaint);\n  }\n}\n\nclass Ant extends PositionComponent {\n  Ant() : random = Random() {\n    size = Vector2(2, 5);\n    anchor = const Anchor(0.5, 0.4);\n  }\n\n  late final Color color;\n  final Random random;\n  static const black = Color(0xFF000000);\n  late final Paint bodyPaint;\n  late final Paint eyesPaint;\n  late final Paint legsPaint;\n  late final Paint facePaint;\n  late final Paint borderPaint;\n  late final Path head;\n  late final Path body;\n  late final Path pincers;\n  late final Path eyes;\n  late final Path antennae;\n  late final List<InsectLeg> legs;\n  Vector2 destinationPosition = Vector2.zero();\n  double destinationAngle = 0;\n  double movementTime = 0;\n  double rotationTime = 0;\n  double stepTime = 0;\n  double movementSpeed = 3; // mm/s\n  double rotationSpeed = 3; // angle/s\n  double probabilityToChangeDirection = 0.02;\n  bool moveLeftSide = false;\n  List<Vector2> targetLegsPositions = List.generate(6, (_) => Vector2.zero());\n  List<Vector2> travelPath = [];\n  int travelPathNodeIndex = 0;\n  int travelDirection = 1; // +1 or -1\n\n  bool legIsMoving(int i) => moveLeftSide == (i < 3);\n\n  void setTravelPath(List<Vector2> path) {\n    travelPath = path;\n    travelPathNodeIndex = random.nextInt(path.length - 1);\n    travelDirection = 1;\n    position = travelPath[travelPathNodeIndex];\n    destinationPosition = travelPath[travelPathNodeIndex + travelDirection];\n    angle = -(destinationPosition - position).angleToSigned(Vector2(0, -1));\n    destinationAngle = angle;\n  }\n\n  @override\n  Future<void> onLoad() async {\n    bodyPaint = Paint()..color = color;\n    eyesPaint = Paint()..color = black;\n    borderPaint = Paint()\n      ..color = Color.lerp(color, black, 0.6)!\n      ..style = PaintingStyle.stroke\n      ..strokeWidth = 0.06;\n    legsPaint = Paint()\n      ..color = Color.lerp(color, black, 0.4)!\n      ..style = PaintingStyle.stroke\n      ..strokeWidth = 0.2;\n    facePaint = Paint()\n      ..color = Color.lerp(color, black, 0.5)!\n      ..style = PaintingStyle.stroke\n      ..strokeWidth = 0.05;\n    head = Path()\n      ..moveTo(0, -0.3)\n      ..cubicTo(-0.5, -0.3, -0.7, -0.6, -0.7, -1)\n      ..cubicTo(-0.7, -1.3, -0.3, -2, 0, -2)\n      ..cubicTo(0.3, -2, 0.7, -1.3, 0.7, -1)\n      ..cubicTo(0.7, -0.6, 0.5, -0.3, 0, -0.3)\n      ..close();\n    body = Path()\n      ..moveTo(0, -0.3)\n      ..cubicTo(0.2, -0.3, 0.4, -0.2, 0.4, 0.2)\n      ..cubicTo(0.4, 0.4, 0.25, 1, 0, 1)\n      ..cubicTo(0.6, 1, 0.9, 1.4, 0.9, 1.8)\n      ..cubicTo(0.9, 2.6, 0.35, 3.1, 0, 3.1)\n      ..cubicTo(-0.35, 3.1, -0.9, 2.6, -0.9, 1.8)\n      ..cubicTo(-0.9, 1.4, -0.6, 1, 0, 1)\n      ..cubicTo(-0.25, 1, -0.4, 0.4, -0.4, 0.2)\n      ..cubicTo(-0.4, -0.2, -0.2, -0.3, 0, -0.3)\n      ..close();\n    pincers = Path()\n      ..moveTo(0.15, -2.15)\n      ..cubicTo(0.5, -1.5, -0.5, -1.5, -0.15, -2.15)\n      ..cubicTo(-0.3, -1.8, 0.3, -1.8, 0.15, -2.15)\n      ..close();\n    antennae = Path()\n      ..moveTo(0, -1.7)\n      ..lineTo(-0.7, -1.9)\n      ..lineTo(-1, -2.5)\n      ..lineTo(-1.5, -2.6)\n      ..moveTo(0, -1.7)\n      ..lineTo(0.7, -1.9)\n      ..lineTo(1, -2.5)\n      ..lineTo(1.5, -2.6);\n    eyes = Path()\n      ..moveTo(-0.5, -1.1)\n      ..cubicTo(-0.95, -1.1, -0.6, -1.8, -0.3, -1.8)\n      ..cubicTo(0, -1.8, 0, -1.1, -0.5, -1.1)\n      ..moveTo(0.5, -1.1)\n      ..cubicTo(0.95, -1.1, 0.6, -1.8, 0.3, -1.8)\n      ..cubicTo(0, -1.8, 0, -1.1, 0.5, -1.1)\n      ..close();\n    legs = [\n      InsectLeg(-0.3, 0.4, -2.6, 0.6, 1.1, 1.1, 0.5, mirrorBendDirection: true),\n      InsectLeg(-0.2, 0.7, -2.3, 2.6, 1.5, 1.5, 0.6, mirrorBendDirection: true),\n      InsectLeg(0.3, 0, 1.7, -2.3, 1.5, 1.3, 0.6, mirrorBendDirection: true),\n      InsectLeg(0.3, 0.4, 2.6, 0.6, 1.1, 1.1, 0.5, mirrorBendDirection: false),\n      InsectLeg(0.2, 0.7, 2.3, 2.6, 1.5, 1.5, 0.6, mirrorBendDirection: false),\n      InsectLeg(-0.3, 0, -1.7, -2.3, 1.5, 1.3, 0.6, mirrorBendDirection: false),\n    ];\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    if (movementTime <= 0 && rotationTime <= 0) {\n      planNextMove();\n    }\n    if (stepTime <= 0) {\n      planNextStep();\n    }\n    final feetPositions = [for (final leg in legs) positionOf(leg.foot)];\n    final fMove = movementTime > 0 ? min(dt / movementTime, 1) : 0;\n    final fRot = rotationTime > 0 ? min(dt / rotationTime, 1) : 0;\n    final deltaX = (destinationPosition.x - position.x) * fMove;\n    final deltaY = (destinationPosition.y - position.y) * fMove;\n    final deltaA = (destinationAngle - angle) * fRot;\n    position += Vector2(deltaX, deltaY);\n    angle += deltaA;\n    movementTime -= dt;\n    rotationTime -= dt;\n    for (var i = 0; i < 6; i++) {\n      var newFootPosition = feetPositions[i];\n      if (legIsMoving(i)) {\n        final fStep = min(dt / stepTime, 1.0);\n        final targetPosition = targetLegsPositions[i];\n        newFootPosition += (targetPosition - newFootPosition) * fStep;\n      }\n      legs[i].placeFoot(toLocal(newFootPosition));\n    }\n    stepTime -= dt;\n  }\n\n  void planNextStep() {\n    moveLeftSide = !moveLeftSide;\n    stepTime = 0.1;\n    final f = min(stepTime * 1.6 / movementTime, 1.0);\n    final deltaX = (destinationPosition.x - position.x) * f;\n    final deltaY = (destinationPosition.y - position.y) * f;\n    final deltaA = (destinationAngle - angle) * f;\n    position += Vector2(deltaX, deltaY);\n    angle += deltaA;\n    for (var i = 0; i < 6; i++) {\n      if (legIsMoving(i)) {\n        targetLegsPositions[i].setFrom(\n          positionOf(Vector2(legs[i].x1, legs[i].y1)),\n        );\n      }\n    }\n    position -= Vector2(deltaX, deltaY);\n    angle -= deltaA;\n  }\n\n  void planNextMove() {\n    if (travelPathNodeIndex == 0) {\n      travelDirection = 1;\n    } else if (travelPathNodeIndex == travelPath.length - 1) {\n      travelDirection = -1;\n    } else if (random.nextDouble() < probabilityToChangeDirection) {\n      travelDirection = -travelDirection;\n    }\n    final nextIndex = travelPathNodeIndex + travelDirection;\n    assert(\n      nextIndex >= 0 && nextIndex < travelPath.length,\n      'nextIndex is outside of the bounds of travelPath',\n    );\n    final nextPosition = travelPath[nextIndex];\n    var nextAngle = angle = -(nextPosition - position).angleToSigned(\n      Vector2(0, -1),\n    );\n    if (nextAngle - angle > tau / 2) {\n      nextAngle -= tau;\n    }\n    if (nextAngle - angle < -tau / 2) {\n      nextAngle += tau;\n    }\n    if ((nextAngle - angle).abs() > 1) {\n      destinationPosition = position;\n      destinationAngle = nextAngle;\n    } else {\n      destinationPosition = nextPosition;\n      destinationAngle = nextAngle;\n      travelPathNodeIndex = nextIndex;\n    }\n    rotationTime = (destinationAngle - angle) / rotationSpeed;\n    movementTime = (destinationPosition - position).length / movementSpeed;\n  }\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    canvas\n      ..save()\n      ..translate(1, 2)\n      ..drawPath(pincers, facePaint)\n      ..drawPath(antennae, facePaint)\n      ..drawPath(head, bodyPaint)\n      ..drawPath(head, borderPaint)\n      ..drawPath(eyes, eyesPaint)\n      ..drawPath(legs[0].path, legsPaint)\n      ..drawPath(legs[1].path, legsPaint)\n      ..drawPath(legs[2].path, legsPaint)\n      ..drawPath(legs[3].path, legsPaint)\n      ..drawPath(legs[4].path, legsPaint)\n      ..drawPath(legs[5].path, legsPaint)\n      ..drawPath(body, bodyPaint)\n      ..drawPath(body, borderPaint)\n      ..restore();\n  }\n}\n\nclass InsectLeg {\n  InsectLeg(\n    this.x0,\n    this.y0,\n    this.x1,\n    this.y1,\n    this.l1,\n    this.l2,\n    this.l3, {\n    required bool mirrorBendDirection,\n  }) : dir = mirrorBendDirection ? -1 : 1,\n       path = Path(),\n       foot = Vector2.zero() {\n    final ok = placeFoot(Vector2(x1, y1));\n    assert(ok, 'The foot was not properly placed');\n  }\n\n  /// Place where the leg is attached to the body\n  final double x0;\n  final double y0;\n\n  /// Place on the ground where the ant needs to place its foot\n  final double x1;\n  final double y1;\n\n  /// Lengths of the 3 segments of the leg: [l1] is nearest to the body, [l2]\n  /// is the middle part, and [l3] is the \"foot\".\n  final double l1;\n  final double l2;\n  final double l3;\n\n  /// +1 if the leg bends \"forward\", or -1 if backwards\n  final double dir;\n\n  /// The leg is drawn as a simple [path] polyline consisting of 3 segments.\n  final Path path;\n\n  /// This vector stores the position of the foot; it's equal to (x1, y1).\n  final Vector2 foot;\n\n  bool placeFoot(Vector2 pos) {\n    final r = l3 / 2;\n    final rr = distance(pos.x, pos.y, x0, y0);\n    if (rr < r) {\n      return false;\n    }\n    final d = rr - r;\n    final z = (d * d + l1 * l1 - l2 * l2) / (2 * d);\n    if (z > l1) {\n      return false;\n    }\n    final h = sqrt(l1 * l1 - z * z);\n    final xv = (pos.x - x0) / rr;\n    final yv = (pos.y - y0) / rr;\n    path\n      ..reset()\n      ..moveTo(x0, y0)\n      ..lineTo(x0 + xv * z + dir * yv * h, y0 + yv * z - dir * xv * h)\n      ..lineTo(x0 + xv * (rr - r), y0 + yv * (rr - r))\n      ..lineTo(x0 + xv * (rr + r), y0 + yv * (rr + r));\n    foot.setFrom(pos);\n    return true;\n  }\n}\n\ndouble distance(num x0, num y0, num x1, num y1) {\n  final dx = x1 - x0;\n  final dy = y1 - y0;\n  return sqrt(dx * dx + dy * dy);\n}\n"
  },
  {
    "path": "examples/lib/stories/camera_and_viewport/camera_component_properties_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/camera.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\n\nclass CameraComponentPropertiesExample extends FlameGame {\n  static const description = '''\n    This example uses FixedSizeViewport which is dynamically sized and \n    positioned based on the size of the game widget.\n    \n    The underlying world is represented as a simple coordinate plane, with\n    green dot being the origin. The viewfinder uses custom anchor in order to\n    declare its \"center\" half-way between the bottom left corner and the true\n    center.\n    \n    The thin yellow rectangle shows the camera's [visibleWorldRect]. It should\n    be visible along the edge of the viewport. \n    \n    Click at any point within the viewport to create a circle there.\n  ''';\n\n  CameraComponentPropertiesExample()\n    : super(\n        camera:\n            CameraComponent(\n                viewport: FixedSizeViewport(200, 200)..add(ViewportFrame()),\n              )\n              ..viewfinder.zoom = 5\n              ..viewfinder.anchor = const Anchor(0.25, 0.75),\n      );\n\n  late RectangleComponent _cullRect;\n\n  @override\n  Color backgroundColor() => const Color(0xff333333);\n\n  @override\n  Future<void> onLoad() async {\n    world.add(Background());\n    _cullRect = RectangleComponent.fromRect(\n      Rect.zero,\n      paint: Paint()\n        ..style = PaintingStyle.stroke\n        ..strokeWidth = 0.25\n        ..color = const Color(0xaaffff00),\n    );\n    await world.add(_cullRect);\n    camera.mounted.then((_) {\n      updateSize(canvasSize);\n    });\n  }\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    if (camera.isMounted) {\n      updateSize(size);\n    }\n  }\n\n  void updateSize(Vector2 size) {\n    camera.viewport.anchor = Anchor.center;\n    camera.viewport.size = size * 0.7;\n    camera.viewport.position = size * 0.6;\n    _cullRect.position = Vector2(\n      camera.visibleWorldRect.left + 1,\n      camera.visibleWorldRect.top + 1,\n    );\n    _cullRect.size = Vector2(\n      camera.visibleWorldRect.width - 2,\n      camera.visibleWorldRect.height - 2,\n    );\n  }\n}\n\nclass ViewportFrame extends Component {\n  final paint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 3\n    ..color = const Color(0xff87c4e2);\n\n  @override\n  void render(Canvas canvas) {\n    final size = (parent! as Viewport).size;\n    canvas.drawRRect(\n      RRect.fromRectAndRadius(\n        Rect.fromLTWH(0, 0, size.x, size.y),\n        const Radius.circular(5),\n      ),\n      paint,\n    );\n  }\n}\n\nclass Background extends Component with TapCallbacks {\n  final bgPaint = Paint()..color = const Color(0xffff0000);\n  final originPaint = Paint()..color = const Color(0xff19bf57);\n  final axisPaint = Paint()\n    ..strokeWidth = 1\n    ..style = PaintingStyle.stroke\n    ..color = const Color(0xff878787);\n  final gridPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 0\n    ..color = const Color(0xff555555);\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawColor(const Color(0xff000000), BlendMode.src);\n    for (var i = -100.0; i <= 100.0; i += 10) {\n      canvas.drawLine(Offset(i, -100), Offset(i, 100), gridPaint);\n      canvas.drawLine(Offset(-100, i), Offset(100, i), gridPaint);\n    }\n    canvas.drawLine(Offset.zero, const Offset(0, 10), axisPaint);\n    canvas.drawLine(Offset.zero, const Offset(10, 0), axisPaint);\n    canvas.drawCircle(Offset.zero, 1.0, originPaint);\n  }\n\n  @override\n  bool containsLocalPoint(Vector2 point) => true;\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    add(ExpandingCircle(event.localPosition.toOffset()));\n  }\n}\n\nclass ExpandingCircle extends CircleComponent {\n  ExpandingCircle(Offset center)\n    : super(\n        position: Vector2(center.dx, center.dy),\n        anchor: Anchor.center,\n        radius: 0,\n        paint: Paint()\n          ..color = const Color(0xffffffff)\n          ..style = PaintingStyle.stroke\n          ..strokeWidth = 1,\n      );\n\n  static const maxRadius = 50;\n\n  @override\n  void update(double dt) {\n    radius += dt * 10;\n    if (radius >= maxRadius) {\n      removeFromParent();\n    } else {\n      final opacity = 1 - radius / maxRadius;\n      paint.color = const Color(0xffffffff).withValues(alpha: opacity);\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/camera_and_viewport/camera_follow_and_world_bounds.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flutter/services.dart';\n\nclass CameraFollowAndWorldBoundsExample extends FlameGame\n    with HasKeyboardHandlerComponents {\n  static const description = '''\n    This example demonstrates camera following the player, but also obeying the\n    world bounds (which are set up to leave a small margin around the visible\n    part of the ground).\n    \n    Use arrows or keys W,A,D to move the player around. The camera should follow\n    the player horizontally, but not jump with the player.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final player = Player()..position = Vector2(250, 0);\n    camera\n      ..viewfinder.visibleGameSize = Vector2(400, 100)\n      ..follow(player, horizontalOnly: true)\n      ..setBounds(Rectangle.fromLTRB(190, -50, 810, 50));\n    world.add(Ground());\n    world.add(player);\n  }\n}\n\nclass Ground extends PositionComponent {\n  Ground() : pebbles = [], super(size: Vector2(1000, 30)) {\n    final random = Random();\n    for (var i = 0; i < 25; i++) {\n      pebbles.add(\n        Vector3(\n          random.nextDouble() * size.x,\n          random.nextDouble() * size.y / 3,\n          random.nextDouble() * 0.5 + 1,\n        ),\n      );\n    }\n  }\n\n  final Paint groundPaint = Paint()\n    ..shader = Gradient.linear(\n      Offset.zero,\n      const Offset(0, 30),\n      [const Color(0xFFC9C972), const Color(0x22FFFF88)],\n    );\n  final Paint pebblePaint = Paint()..color = const Color(0xFF685A2B);\n\n  final List<Vector3> pebbles;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(size.toRect(), groundPaint);\n    for (final pebble in pebbles) {\n      canvas.drawCircle(Offset(pebble.x, pebble.y), pebble.z, pebblePaint);\n    }\n  }\n}\n\nclass Player extends PositionComponent with KeyboardHandler {\n  Player()\n    : body = Path()\n        ..moveTo(10, 0)\n        ..cubicTo(17, 0, 28, 20, 10, 20)\n        ..cubicTo(-8, 20, 3, 0, 10, 0)\n        ..close(),\n      eyes = Path()\n        ..addOval(const Rect.fromLTWH(12.5, 9, 4, 6))\n        ..addOval(const Rect.fromLTWH(6.5, 9, 4, 6)),\n      pupils = Path()\n        ..addOval(const Rect.fromLTWH(14, 11, 2, 2))\n        ..addOval(const Rect.fromLTWH(8, 11, 2, 2)),\n      velocity = Vector2.zero(),\n      super(size: Vector2(20, 20), anchor: Anchor.bottomCenter);\n\n  final Path body;\n  final Path eyes;\n  final Path pupils;\n  final Paint borderPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 1\n    ..color = const Color(0xffffc67c);\n  final Paint innerPaint = Paint()..color = const Color(0xff9c0051);\n  final Paint eyesPaint = Paint()..color = const Color(0xFFFFFFFF);\n  final Paint pupilsPaint = Paint()..color = const Color(0xFF000000);\n  final Paint shadowPaint = Paint()\n    ..shader = Gradient.radial(\n      Offset.zero,\n      10,\n      [const Color(0x88000000), const Color(0x00000000)],\n    );\n\n  final Vector2 velocity;\n  final double runSpeed = 150.0;\n  final double jumpSpeed = 300.0;\n  final double gravity = 1000.0;\n  bool facingRight = true;\n  int nJumpsLeft = 2;\n\n  @override\n  void update(double dt) {\n    position.x += velocity.x * dt;\n    position.y += velocity.y * dt;\n    if (position.y > 0) {\n      position.y = 0;\n      velocity.y = 0;\n      nJumpsLeft = 2;\n    }\n    if (position.y < 0) {\n      velocity.y += gravity * dt;\n    }\n    if (position.x < 0) {\n      position.x = 0;\n    }\n    if (position.x > 1000) {\n      position.x = 1000;\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    {\n      final h = -position.y; // height above the ground\n      canvas.save();\n      canvas.translate(width / 2, height + 1 + h * 1.05);\n      canvas.scale(1 - h * 0.003, 0.3 - h * 0.001);\n      canvas.drawCircle(Offset.zero, 10, shadowPaint);\n      canvas.restore();\n    }\n    canvas.drawPath(body, innerPaint);\n    canvas.drawPath(body, borderPaint);\n    canvas.drawPath(eyes, eyesPaint);\n    canvas.drawPath(pupils, pupilsPaint);\n  }\n\n  @override\n  bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {\n    final isKeyDown = event is KeyDownEvent;\n    final keyLeft =\n        (event.logicalKey == LogicalKeyboardKey.arrowLeft) ||\n        (event.logicalKey == LogicalKeyboardKey.keyA);\n    final keyRight =\n        (event.logicalKey == LogicalKeyboardKey.arrowRight) ||\n        (event.logicalKey == LogicalKeyboardKey.keyD);\n    final keyUp =\n        (event.logicalKey == LogicalKeyboardKey.arrowUp) ||\n        (event.logicalKey == LogicalKeyboardKey.keyW);\n\n    if (isKeyDown) {\n      if (keyLeft) {\n        velocity.x = -runSpeed;\n      } else if (keyRight) {\n        velocity.x = runSpeed;\n      } else if (keyUp && nJumpsLeft > 0) {\n        velocity.y = -jumpSpeed;\n        nJumpsLeft -= 1;\n      }\n    } else {\n      final hasLeft =\n          keysPressed.contains(LogicalKeyboardKey.arrowLeft) ||\n          keysPressed.contains(LogicalKeyboardKey.keyA);\n      final hasRight =\n          keysPressed.contains(LogicalKeyboardKey.arrowRight) ||\n          keysPressed.contains(LogicalKeyboardKey.keyD);\n      if (hasLeft && hasRight) {\n        // Leave the current speed unchanged\n      } else if (hasLeft) {\n        velocity.x = -runSpeed;\n      } else if (hasRight) {\n        velocity.x = runSpeed;\n      } else {\n        velocity.x = 0;\n      }\n    }\n    if ((velocity.x > 0) && !facingRight) {\n      facingRight = true;\n      flipHorizontally();\n    }\n    if ((velocity.x < 0) && facingRight) {\n      facingRight = false;\n      flipHorizontally();\n    }\n    return super.onKeyEvent(event, keysPressed);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/camera_and_viewport/coordinate_systems_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\n/// A game that allows for camera control and displays Tap, Drag & Scroll\n/// events information on the screen, to allow exploration of the 3 coordinate\n/// systems of Flame (global, widget, game).\nclass CoordinateSystemsExample extends FlameGame\n    with\n        MultiTouchTapDetector,\n        MultiTouchDragDetector,\n        ScrollDetector,\n        KeyboardEvents {\n  static const String description = '''\n    Displays event data in all 3 coordinate systems (global, widget and game).\n    Use WASD to move the camera and Q/E to zoom in/out.\n    Trigger events to see the coordinates on each coordinate space.\n  ''';\n\n  static final _borderPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 2\n    ..color = BasicPalette.red.color;\n  static final _text = TextPaint(\n    style: TextStyle(color: BasicPalette.red.color, fontSize: 12),\n  );\n\n  String? lastEventDescription;\n  final cameraPosition = Vector2.zero();\n  final cameraVelocity = Vector2.zero();\n\n  @override\n  Future<void> onLoad() async {\n    final rectanglePosition = canvasSize / 4;\n    final rectangleSize = Vector2.all(20);\n    final positions = [\n      Vector2(rectanglePosition.x, rectanglePosition.y),\n      Vector2(rectanglePosition.x, -rectanglePosition.y),\n      Vector2(-rectanglePosition.x, rectanglePosition.y),\n      Vector2(-rectanglePosition.x, -rectanglePosition.y),\n    ];\n    world.addAll(\n      [\n        for (final position in positions)\n          RectangleComponent(\n            position: position,\n            size: rectangleSize,\n          ),\n      ],\n    );\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(canvasSize.toRect(), _borderPaint);\n    _text.render(\n      canvas,\n      'Camera: WASD to move, QE to zoom',\n      Vector2.all(5.0),\n    );\n    _text.render(\n      canvas,\n      'Camera: ${camera.viewfinder.position}, '\n      'zoom: ${camera.viewfinder.zoom}',\n      Vector2(canvasSize.x - 5, 5.0),\n      anchor: Anchor.topRight,\n    );\n    _text.render(\n      canvas,\n      'This is your Flame game!',\n      canvasSize - Vector2.all(5.0),\n      anchor: Anchor.bottomRight,\n    );\n    final lastEventDescription = this.lastEventDescription;\n    if (lastEventDescription != null) {\n      _text.render(\n        canvas,\n        lastEventDescription,\n        canvasSize / 2,\n        anchor: Anchor.center,\n      );\n    }\n    super.render(canvas);\n  }\n\n  @override\n  void onTapUp(int pointerId, TapUpInfo info) {\n    lastEventDescription = _describe('TapUp', info);\n  }\n\n  @override\n  void onTapDown(int pointerId, TapDownInfo info) {\n    lastEventDescription = _describe('TapDown', info);\n  }\n\n  @override\n  void onDragStart(int pointerId, DragStartInfo info) {\n    lastEventDescription = _describe('DragStart', info);\n  }\n\n  @override\n  void onDragUpdate(int pointerId, DragUpdateInfo info) {\n    lastEventDescription = _describe('DragUpdate', info);\n  }\n\n  @override\n  void onScroll(PointerScrollInfo info) {\n    lastEventDescription = _describe('Scroll', info);\n  }\n\n  /// Describes generic event information + some event specific details for\n  /// some events.\n  String _describe(String name, PositionInfo info) {\n    return [\n      name,\n      'Global: ${info.eventPosition.global}',\n      'Widget: ${info.eventPosition.widget}',\n      'World: ${camera.globalToLocal(info.eventPosition.global)}',\n      'Camera: ${camera.viewfinder.position}',\n      if (info is DragUpdateInfo) ...[\n        'Delta',\n        'Global: ${info.delta.global}',\n        'World: ${info.delta.global / camera.viewfinder.zoom}',\n      ],\n      if (info is PointerScrollInfo) ...[\n        'Scroll Delta',\n        'Global: ${info.scrollDelta.global}',\n        'World: ${info.scrollDelta.global / camera.viewfinder.zoom}',\n      ],\n    ].join('\\n');\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    cameraPosition.add(cameraVelocity * dt * 30);\n    // just make it look pretty\n    cameraPosition.x = _roundDouble(cameraPosition.x, 5);\n    cameraPosition.y = _roundDouble(cameraPosition.y, 5);\n    camera.viewfinder.position = cameraPosition;\n  }\n\n  /// Round [val] up to [places] decimal places.\n  static double _roundDouble(double val, int places) {\n    final mod = pow(10.0, places);\n    return (val * mod).round().toDouble() / mod;\n  }\n\n  /// Camera controls.\n  @override\n  KeyEventResult onKeyEvent(\n    KeyEvent event,\n    Set<LogicalKeyboardKey> keysPressed,\n  ) {\n    final isKeyDown = event is KeyDownEvent;\n\n    if (event.logicalKey == LogicalKeyboardKey.keyA) {\n      cameraVelocity.x = isKeyDown ? -1 : 0;\n    } else if (event.logicalKey == LogicalKeyboardKey.keyD) {\n      cameraVelocity.x = isKeyDown ? 1 : 0;\n    } else if (event.logicalKey == LogicalKeyboardKey.keyW) {\n      cameraVelocity.y = isKeyDown ? -1 : 0;\n    } else if (event.logicalKey == LogicalKeyboardKey.keyS) {\n      cameraVelocity.y = isKeyDown ? 1 : 0;\n    } else if (isKeyDown) {\n      if (event.logicalKey == LogicalKeyboardKey.keyQ) {\n        camera.viewfinder.zoom *= 2;\n      } else if (event.logicalKey == LogicalKeyboardKey.keyE) {\n        camera.viewfinder.zoom /= 2;\n      }\n    }\n\n    return KeyEventResult.handled;\n  }\n}\n\n/// A simple widget that \"wraps\" a Flame game with some Containers\n/// on each direction (top, bottom, left and right) and allow adding\n/// or removing containers.\nclass CoordinateSystemsWidget extends StatefulWidget {\n  const CoordinateSystemsWidget({super.key});\n\n  @override\n  State<StatefulWidget> createState() {\n    return _CoordinateSystemsState();\n  }\n}\n\nclass _CoordinateSystemsState extends State<CoordinateSystemsWidget> {\n  /// The number of blocks in each direction (top, left, right, bottom).\n  List<int> blocks = [1, 1, 1, 1];\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisAlignment: MainAxisAlignment.spaceEvenly,\n      crossAxisAlignment: CrossAxisAlignment.stretch,\n      children: [\n        ...createBlocks(index: 0, rotated: false, start: true),\n        Expanded(\n          child: Row(\n            mainAxisAlignment: MainAxisAlignment.spaceEvenly,\n            crossAxisAlignment: CrossAxisAlignment.stretch,\n            children: [\n              ...createBlocks(index: 1, rotated: true, start: true),\n              Expanded(\n                child: GameWidget(game: CoordinateSystemsExample()),\n              ),\n              ...createBlocks(index: 2, rotated: true, start: false),\n            ],\n          ),\n        ),\n        ...createBlocks(index: 3, rotated: false, start: false),\n      ],\n    );\n  }\n\n  /// Just creates a list of Widgets + the \"add\" button\n  List<Widget> createBlocks({\n    /// Index on the [blocks] array\n    required int index,\n\n    /// If true, render vertical text\n    required bool rotated,\n\n    /// Whether to render the \"add\" button before or after\n    required bool start,\n  }) {\n    final add = Container(\n      margin: const EdgeInsets.all(32),\n      child: Center(\n        child: TextButton(\n          child: const Text('+'),\n          onPressed: () => setState(() => blocks[index]++),\n        ),\n      ),\n    );\n    return [\n      if (start) add,\n      for (int i = 1; i <= blocks[index]; i++)\n        GestureDetector(\n          child: Container(\n            margin: const EdgeInsets.all(32),\n            child: Center(\n              child: RotatedBox(\n                quarterTurns: rotated ? 1 : 0,\n                child: Text('Block $i'),\n              ),\n            ),\n          ),\n          onTap: () => setState(() => blocks[index]--),\n        ),\n      if (!start) add,\n    ];\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/camera_and_viewport/fixed_resolution_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame/text.dart';\nimport 'package:flutter/material.dart';\n\nclass FixedResolutionExample extends FlameGame\n    with ScrollDetector, ScaleDetector {\n  static const description = '''\n    This example shows how to create a viewport with a fixed resolution.\n    It is useful when you want the visible part of the game to be the same on\n    all devices no matter the actual screen size of the device.\n    Resize the window or change device orientation to see the difference.\n    \n    If you tap once you will set the zoom to 2 and if you tap again it goes back\n    to 1, so that you can test how it works with a zoom level.\n  ''';\n\n  FixedResolutionExample()\n    : super(\n        camera: CameraComponent.withFixedResolution(width: 600, height: 1024),\n        world: FixedResolutionWorld(),\n      );\n\n  @override\n  Future<void> onLoad() async {\n    final textRenderer = TextPaint(\n      style: TextStyle(fontSize: 25, color: BasicPalette.black.color),\n    );\n    camera.viewport.add(\n      TextButton(\n        text: 'Viewport\\ncomponent',\n        position: Vector2.all(10),\n        textRenderer: textRenderer,\n      ),\n    );\n    camera.viewfinder.add(\n      TextButton(\n        text: 'Viewfinder\\ncomponent',\n        textRenderer: textRenderer,\n        position: Vector2(0, 200),\n        anchor: Anchor.center,\n      ),\n    );\n    camera.viewport.add(\n      TextButton(\n        text: 'Viewport\\ncomponent',\n        position: camera.viewport.virtualSize - Vector2.all(10),\n        textRenderer: textRenderer,\n        anchor: Anchor.bottomRight,\n      ),\n    );\n  }\n}\n\nclass FixedResolutionWorld extends World\n    with HasGameReference, TapCallbacks, DoubleTapCallbacks {\n  final red = BasicPalette.red.paint();\n\n  @override\n  Future<void> onLoad() async {\n    final flameSprite = await game.loadSprite('layers/player.png');\n\n    add(Background());\n    add(\n      SpriteComponent(\n        sprite: flameSprite,\n        size: Vector2(149, 211),\n      )..anchor = Anchor.center,\n    );\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    add(\n      CircleComponent(\n        radius: 2,\n        position: event.localPosition,\n        paint: red,\n      ),\n    );\n  }\n\n  @override\n  void onDoubleTapDown(DoubleTapDownEvent event) {\n    final currentZoom = game.camera.viewfinder.zoom;\n    game.camera.viewfinder.zoom = currentZoom > 1 ? 1 : 2;\n  }\n}\n\nclass Background extends PositionComponent {\n  @override\n  int priority = -1;\n\n  late Paint white;\n  late final Rect hugeRect;\n\n  Background() : super(size: Vector2.all(100000), anchor: Anchor.center);\n\n  @override\n  Future<void> onLoad() async {\n    white = BasicPalette.white.paint();\n    hugeRect = size.toRect();\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(hugeRect, white);\n  }\n}\n\nclass TextButton extends ButtonComponent {\n  TextButton({\n    required String text,\n    required super.position,\n    super.anchor,\n    TextRenderer? textRenderer,\n  }) : super(\n         button: RectangleComponent(\n           size: Vector2(200, 100),\n           paint: Paint()\n             ..color = Colors.orange\n             ..strokeWidth = 2\n             ..style = PaintingStyle.stroke,\n         ),\n         buttonDown: RectangleComponent(\n           size: Vector2(200, 100),\n           paint: Paint()\n             ..color = BasicPalette.orange.color.withValues(alpha: 0.5),\n         ),\n         children: [\n           TextComponent(\n             text: text,\n             textRenderer: textRenderer,\n             position: Vector2(100, 50),\n             anchor: Anchor.center,\n           ),\n         ],\n       );\n}\n"
  },
  {
    "path": "examples/lib/stories/camera_and_viewport/follow_component_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:examples/commons/ember.dart';\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nclass FollowComponentExample extends FlameGame\n    with HasCollisionDetection, HasKeyboardHandlerComponents {\n  static const String description = '''\n    Move around with W, A, S, D and notice how the camera follows the ember \n    sprite.\\n\n    If you collide with the gray squares, the camera reference is changed from\n    center to topCenter.\\n\n    The gray squares can also be clicked to show how the coordinate system\n    respects the camera transformation.\n  ''';\n\n  FollowComponentExample({required this.viewportResolution})\n    : super(\n        camera: CameraComponent.withFixedResolution(\n          width: viewportResolution.x,\n          height: viewportResolution.y,\n        ),\n      );\n\n  late MovableEmber ember;\n  final Vector2 viewportResolution;\n\n  @override\n  Future<void> onLoad() async {\n    world.add(Map());\n    world.add(ember = MovableEmber());\n    camera.setBounds(Map.bounds);\n    camera.follow(ember, maxSpeed: 250);\n\n    world.addAll(\n      List.generate(30, (_) => Rock(Map.generateCoordinates())),\n    );\n  }\n}\n\nclass MovableEmber extends Ember<FollowComponentExample>\n    with CollisionCallbacks, KeyboardHandler {\n  static const double speed = 300;\n  static final TextPaint textRenderer = TextPaint(\n    style: const TextStyle(color: Colors.white70, fontSize: 12),\n  );\n\n  final Vector2 velocity = Vector2.zero();\n  late final TextComponent positionText;\n  late final Vector2 textPosition;\n  late final maxPosition = Vector2.all(Map.size - size.x / 2);\n  late final minPosition = -maxPosition;\n\n  MovableEmber() : super(priority: 2);\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    positionText = TextComponent(\n      textRenderer: textRenderer,\n      position: (size / 2)..y = size.y / 2 + 30,\n      anchor: Anchor.center,\n    );\n    add(positionText);\n    add(CircleHitbox());\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    final deltaPosition = velocity * (speed * dt);\n    position.add(deltaPosition);\n    position.clamp(minPosition, maxPosition);\n    positionText.text = '(${x.toInt()}, ${y.toInt()})';\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n    if (other is Rock) {\n      other.add(\n        ScaleEffect.to(\n          Vector2.all(1.5),\n          EffectController(duration: 0.2, alternate: true),\n        ),\n      );\n    }\n  }\n\n  @override\n  bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {\n    if (event is KeyRepeatEvent) {\n      return super.onKeyEvent(event, keysPressed);\n    }\n    final isKeyDown = event is KeyDownEvent;\n\n    final bool handled;\n    if (event.logicalKey == LogicalKeyboardKey.keyA) {\n      velocity.x = isKeyDown ? -1 : 0;\n      handled = true;\n    } else if (event.logicalKey == LogicalKeyboardKey.keyD) {\n      velocity.x = isKeyDown ? 1 : 0;\n      handled = true;\n    } else if (event.logicalKey == LogicalKeyboardKey.keyW) {\n      velocity.y = isKeyDown ? -1 : 0;\n      handled = true;\n    } else if (event.logicalKey == LogicalKeyboardKey.keyS) {\n      velocity.y = isKeyDown ? 1 : 0;\n      handled = true;\n    } else {\n      handled = false;\n    }\n\n    if (handled) {\n      angle = -velocity.angleToSigned(Vector2(1, 0));\n      return false;\n    } else {\n      return super.onKeyEvent(event, keysPressed);\n    }\n  }\n}\n\nclass Map extends Component {\n  static const double size = 1500;\n  static const Rect _bounds = Rect.fromLTRB(-size, -size, size, size);\n  static final Rectangle bounds = Rectangle.fromLTRB(-size, -size, size, size);\n\n  static final Paint _paintBorder = Paint()\n    ..color = Colors.white12\n    ..strokeWidth = 10\n    ..style = PaintingStyle.stroke;\n  static final Paint _paintBg = Paint()..color = const Color(0xFF333333);\n\n  static final _rng = Random();\n\n  late final List<Paint> _paintPool;\n  late final List<Rect> _rectPool;\n\n  Map() : super(priority: 0) {\n    _paintPool = List<Paint>.generate(\n      (size / 50).ceil(),\n      (_) => PaintExtension.random(rng: _rng)\n        ..style = PaintingStyle.stroke\n        ..strokeWidth = 2,\n      growable: false,\n    );\n    _rectPool = List<Rect>.generate(\n      (size / 50).ceil(),\n      (i) => Rect.fromCircle(center: Offset.zero, radius: size - i * 50),\n      growable: false,\n    );\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(_bounds, _paintBg);\n    canvas.drawRect(_bounds, _paintBorder);\n    for (var i = 0; i < (size / 50).ceil(); i++) {\n      canvas.drawCircle(Offset.zero, size - i * 50, _paintPool[i]);\n      canvas.drawRect(_rectPool[i], _paintBorder);\n    }\n  }\n\n  static Vector2 generateCoordinates() {\n    return Vector2.random()\n      ..scale(2 * size)\n      ..sub(Vector2.all(size));\n  }\n}\n\nclass Rock extends SpriteComponent with HasGameReference, TapCallbacks {\n  Rock(Vector2 position)\n    : super(\n        position: position,\n        size: Vector2.all(50),\n        priority: 1,\n        anchor: Anchor.center,\n      );\n\n  @override\n  Future<void> onLoad() async {\n    sprite = await game.loadSprite('nine-box.png');\n    paint = Paint()..color = Colors.white;\n    add(RectangleHitbox());\n  }\n\n  @override\n  void onTapDown(_) {\n    add(\n      ScaleEffect.to(\n        Vector2.all(scale.x >= 2.0 ? 1 : 2),\n        EffectController(duration: 0.3),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/camera_and_viewport/static_components_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/parallax.dart';\n\nclass StaticComponentsExample extends FlameGame\n    with ScrollDetector, ScaleDetector {\n  static const description = '''\n  This example shows a parallax which is attached to the viewport (behind the\n  world), four Flame logos that are added to the world, and a player added to\n  the world which is also followed by the camera when you click somewhere.\n  The text components that are added are self-explanatory.\n  ''';\n\n  late final ParallaxComponent myParallax;\n\n  StaticComponentsExample({\n    required Vector2 viewportResolution,\n  }) : super(\n         camera: CameraComponent.withFixedResolution(\n           width: viewportResolution.x,\n           height: viewportResolution.y,\n         ),\n         world: _StaticComponentWorld(),\n       );\n\n  @override\n  Future<void> onLoad() async {\n    myParallax = MyParallaxComponent()..parallax?.baseVelocity.setZero();\n    camera.backdrop.addAll([\n      myParallax,\n      TextComponent(\n        text: 'Center backdrop Component',\n        position: camera.viewport.virtualSize / 2 + Vector2(0, 30),\n        anchor: Anchor.center,\n      ),\n    ]);\n    camera.viewport.addAll(\n      [\n        TextComponent(\n          text: 'Corner Viewport Component',\n          position: Vector2.all(10),\n        ),\n        TextComponent(\n          text: 'Center Viewport Component',\n          position: camera.viewport.virtualSize / 2,\n          anchor: Anchor.center,\n        ),\n      ],\n    );\n  }\n}\n\nclass _StaticComponentWorld extends World\n    with\n        HasGameReference<StaticComponentsExample>,\n        TapCallbacks,\n        DoubleTapCallbacks {\n  late SpriteComponent player;\n  @override\n  Future<void> onLoad() async {\n    final playerSprite = await game.loadSprite('layers/player.png');\n    final flameSprite = await game.loadSprite('flame.png');\n    final visibleSize = game.camera.visibleWorldRect.toVector2();\n    add(player = SpriteComponent(sprite: playerSprite, anchor: Anchor.center));\n    addAll([\n      SpriteComponent(\n        sprite: flameSprite,\n        anchor: Anchor.center,\n        position: -visibleSize / 8,\n        size: Vector2(20, 30),\n      ),\n      SpriteComponent(\n        sprite: flameSprite,\n        anchor: Anchor.center,\n        position: visibleSize / 8,\n        size: Vector2(20, 30),\n      ),\n      SpriteComponent(\n        sprite: flameSprite,\n        anchor: Anchor.center,\n        position: (visibleSize / 8)..multiply(Vector2(-1, 1)),\n        size: Vector2(20, 30),\n      ),\n      SpriteComponent(\n        sprite: flameSprite,\n        anchor: Anchor.center,\n        position: (visibleSize / 8)..multiply(Vector2(1, -1)),\n        size: Vector2(20, 30),\n      ),\n    ]);\n    game.camera.follow(player, maxSpeed: 100);\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    const moveDuration = 1.0;\n    final deltaX = (event.localPosition - player.position).x;\n    player.add(\n      MoveToEffect(\n        event.localPosition,\n        EffectController(\n          duration: moveDuration,\n        ),\n        onComplete: () => game.myParallax.parallax?.baseVelocity.setZero(),\n      ),\n    );\n    final moveSpeedX = deltaX / moveDuration;\n    game.myParallax.parallax?.baseVelocity.setValues(moveSpeedX, 0);\n  }\n}\n\nclass MyParallaxComponent extends ParallaxComponent {\n  @override\n  Future<void> onLoad() async {\n    parallax = await game.loadParallax(\n      [\n        ParallaxImageData('parallax/bg.png'),\n        ParallaxImageData('parallax/mountain-far.png'),\n        ParallaxImageData('parallax/mountains.png'),\n        ParallaxImageData('parallax/trees.png'),\n        ParallaxImageData('parallax/foreground-trees.png'),\n      ],\n      baseVelocity: Vector2(0, 0),\n      velocityMultiplierDelta: Vector2(1.8, 1.0),\n      filterQuality: FilterQuality.none,\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/camera_and_viewport/zoom_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\n\nclass ZoomExample extends FlameGame with ScrollDetector, ScaleDetector {\n  static const String description = '''\n    On web: use scroll to zoom in and out.\\n\n    On mobile: use scale gesture to zoom in and out.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final flameSprite = await loadSprite('flame.png');\n\n    world.add(\n      SpriteComponent(\n        sprite: flameSprite,\n        size: Vector2(149, 211),\n      )..anchor = Anchor.center,\n    );\n  }\n\n  void clampZoom() {\n    camera.viewfinder.zoom = camera.viewfinder.zoom.clamp(0.05, 3.0);\n  }\n\n  static const zoomPerScrollUnit = 0.02;\n\n  @override\n  void onScroll(PointerScrollInfo info) {\n    camera.viewfinder.zoom +=\n        info.scrollDelta.global.y.sign * zoomPerScrollUnit;\n    clampZoom();\n  }\n\n  late double startZoom;\n\n  @override\n  void onScaleStart(_) {\n    startZoom = camera.viewfinder.zoom;\n  }\n\n  @override\n  void onScaleUpdate(ScaleUpdateInfo info) {\n    final currentScale = info.scale.global;\n    if (!currentScale.isIdentity()) {\n      camera.viewfinder.zoom = startZoom * currentScale.y;\n      clampZoom();\n    } else {\n      final zoom = camera.viewfinder.zoom;\n      final delta = (info.delta.global..negate()) / zoom;\n      camera.moveBy(delta);\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/collision_detection/bouncing_ball_example.dart",
    "content": "import 'dart:math' as math;\nimport 'dart:ui';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass BouncingBallExample extends FlameGame with HasCollisionDetection {\n  static const description = '''\n    This example shows how you can use the Collisions detection api to know when a ball\n    collides with the screen boundaries and then update it to bounce off these boundaries.\n  ''';\n  @override\n  void onLoad() {\n    addAll([\n      ScreenHitbox(),\n      Ball(),\n    ]);\n  }\n}\n\nclass Ball extends CircleComponent\n    with HasGameReference<FlameGame>, CollisionCallbacks {\n  late Vector2 velocity;\n\n  Ball() {\n    paint = Paint()..color = Colors.white;\n    radius = 10;\n  }\n\n  static const double speed = 500;\n  static const degree = math.pi / 180;\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    _resetBall;\n    final hitBox = CircleHitbox(\n      radius: radius,\n    );\n\n    addAll([\n      hitBox,\n    ]);\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    position += velocity * dt;\n  }\n\n  void get _resetBall {\n    position = game.size / 2;\n    final spawnAngle = getSpawnAngle;\n\n    final vx = math.cos(spawnAngle * degree) * speed;\n    final vy = math.sin(spawnAngle * degree) * speed;\n    velocity = Vector2(\n      vx,\n      vy,\n    );\n  }\n\n  double get getSpawnAngle {\n    final random = math.Random().nextDouble();\n    final spawnAngle = lerpDouble(0, 360, random)!;\n\n    return spawnAngle;\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n\n    if (other is ScreenHitbox) {\n      final collisionPoint = intersectionPoints.first;\n\n      // Left Side Collision\n      if (collisionPoint.x == 0) {\n        velocity.x = -velocity.x;\n        velocity.y = velocity.y;\n      }\n      // Right Side Collision\n      if (collisionPoint.x == game.size.x) {\n        velocity.x = -velocity.x;\n        velocity.y = velocity.y;\n      }\n      // Top Side Collision\n      if (collisionPoint.y == 0) {\n        velocity.x = velocity.x;\n        velocity.y = -velocity.y;\n      }\n      // Bottom Side Collision\n      if (collisionPoint.y == game.size.y) {\n        velocity.x = velocity.x;\n        velocity.y = -velocity.y;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/collision_detection/circles_example.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart' hide Image, Draggable;\n\nclass CirclesExample extends FlameGame {\n  static const description = '''\n    This example will create a circle every time you tap on the screen. It will\n    have the initial velocity towards the center of the screen and if it touches\n    another circle both of them will change color.\n  ''';\n\n  CirclesExample()\n    : super(\n        camera: CameraComponent.withFixedResolution(width: 600, height: 400),\n        world: MyWorld(),\n      );\n}\n\nclass MyWorld extends World with TapCallbacks, HasCollisionDetection {\n  MyWorld() : super(children: [ScreenHitbox()..debugMode = true]);\n\n  @override\n  void onTapDown(TapDownEvent info) {\n    add(MyCollidable(position: info.localPosition));\n  }\n}\n\nclass MyCollidable extends PositionComponent\n    with HasGameReference<CirclesExample>, CollisionCallbacks {\n  MyCollidable({super.position})\n    : super(size: Vector2.all(30), anchor: Anchor.center);\n\n  late Vector2 velocity;\n  final _collisionColor = Colors.amber;\n  final _defaultColor = Colors.cyan;\n  late ShapeHitbox hitbox;\n\n  @override\n  Future<void> onLoad() async {\n    final defaultPaint = Paint()\n      ..color = _defaultColor\n      ..style = PaintingStyle.stroke;\n    hitbox = CircleHitbox()\n      ..paint = defaultPaint\n      ..renderShape = true;\n    add(hitbox);\n    velocity = -position\n      ..scaleTo(50);\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    position.add(velocity * dt);\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n    hitbox.paint.color = _collisionColor;\n    if (other is ScreenHitbox) {\n      removeFromParent();\n      return;\n    }\n  }\n\n  @override\n  void onCollisionEnd(PositionComponent other) {\n    super.onCollisionEnd(other);\n    if (!isColliding) {\n      hitbox.paint.color = _defaultColor;\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/collision_detection/collidable_animation_example.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/palette.dart';\n\nclass CollidableAnimationExample extends FlameGame with HasCollisionDetection {\n  static const description = '''\n    In this example you can see four animated birds which are flying straight\n    along the same route until they hit either another bird or the wall, which\n    makes them turn. The birds have PolygonHitboxes which are marked with the\n    white lines.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    add(ScreenHitbox());\n    final componentSize = Vector2(150, 100);\n    // Top left component\n    add(\n      AnimatedComponent(Vector2.all(200), Vector2.all(100), componentSize)\n        ..flipVertically(),\n    );\n    // Bottom right component\n    add(\n      AnimatedComponent(\n        Vector2(-100, -100),\n        size.clone()..sub(Vector2.all(200)),\n        componentSize / 2,\n      ),\n    );\n    // Bottom left component\n    add(\n      AnimatedComponent(\n        Vector2(100, -100),\n        Vector2(100, size.y - 100),\n        componentSize * 1.5,\n        angle: pi / 4,\n      ),\n    );\n    // Top right component\n    add(\n      AnimatedComponent(\n        Vector2(-300, 300),\n        Vector2(size.x - 100, 100),\n        componentSize / 3,\n        angle: pi / 4,\n      )..flipVertically(),\n    );\n  }\n}\n\nclass AnimatedComponent extends SpriteAnimationComponent\n    with CollisionCallbacks, HasGameReference {\n  final Vector2 velocity;\n\n  AnimatedComponent(\n    this.velocity,\n    Vector2 position,\n    Vector2 size, {\n    double angle = -pi / 4,\n  }) : super(\n         position: position,\n         size: size,\n         angle: angle,\n         anchor: Anchor.center,\n       );\n\n  @override\n  Future<void> onLoad() async {\n    animation = await game.loadSpriteAnimation(\n      'bomb_ptero.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        stepTime: 0.2,\n        textureSize: Vector2.all(48),\n      ),\n    );\n    final hitboxPaint = BasicPalette.white.paint()\n      ..style = PaintingStyle.stroke;\n    add(\n      PolygonHitbox.relative(\n          [\n            Vector2(0.0, -1.0),\n            Vector2(-1.0, -0.1),\n            Vector2(-0.2, 0.4),\n            Vector2(0.2, 0.4),\n            Vector2(1.0, -0.1),\n          ],\n          parentSize: size,\n        )\n        ..paint = hitboxPaint\n        ..renderShape = true,\n    );\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    position += velocity * dt;\n  }\n\n  final Paint hitboxPaint = BasicPalette.green.paint()\n    ..style = PaintingStyle.stroke;\n  final Paint dotPaint = BasicPalette.red.paint()..style = PaintingStyle.stroke;\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n    velocity.negate();\n    flipVertically();\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/collision_detection/collision_detection.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/collision_detection/bouncing_ball_example.dart';\nimport 'package:examples/stories/collision_detection/circles_example.dart';\nimport 'package:examples/stories/collision_detection/collidable_animation_example.dart';\nimport 'package:examples/stories/collision_detection/multiple_shapes_example.dart';\nimport 'package:examples/stories/collision_detection/multiple_worlds_example.dart';\nimport 'package:examples/stories/collision_detection/quadtree_example.dart';\nimport 'package:examples/stories/collision_detection/raycast_example.dart';\nimport 'package:examples/stories/collision_detection/raycast_light_example.dart';\nimport 'package:examples/stories/collision_detection/raycast_max_distance_example.dart';\nimport 'package:examples/stories/collision_detection/rays_in_shape_example.dart';\nimport 'package:examples/stories/collision_detection/raytrace_example.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart';\n\nvoid addCollisionDetectionStories(Dashbook dashbook) {\n  dashbook.storiesOf('Collision Detection')\n    ..add(\n      'Collidable AnimationComponent',\n      (_) => GameWidget(game: CollidableAnimationExample()),\n      codeLink: baseLink(\n        'collision_detection/collidable_animation_example.dart',\n      ),\n      info: CollidableAnimationExample.description,\n    )\n    ..add(\n      'Circles',\n      (_) => GameWidget(game: CirclesExample()),\n      codeLink: baseLink('collision_detection/circles_example.dart'),\n      info: CirclesExample.description,\n    )\n    ..add(\n      'Bouncing Ball',\n      (_) => GameWidget(game: BouncingBallExample()),\n      codeLink: baseLink('collision_detection/bouncing_ball_example.dart'),\n      info: BouncingBallExample.description,\n    )\n    ..add(\n      'Multiple shapes',\n      (_) => ClipRect(child: GameWidget(game: MultipleShapesExample())),\n      codeLink: baseLink('collision_detection/multiple_shapes_example.dart'),\n      info: MultipleShapesExample.description,\n    )\n    ..add(\n      'Multiple worlds',\n      (_) => GameWidget(game: MultipleWorldsExample()),\n      codeLink: baseLink('collision_detection/multiple_worlds_example.dart'),\n      info: MultipleWorldsExample.description,\n    )\n    ..add(\n      'QuadTree collision',\n      (_) => GameWidget(game: QuadTreeExample()),\n      codeLink: baseLink('collision_detection/quadtree_example.dart'),\n      info: QuadTreeExample.description,\n    )\n    ..add(\n      'Raycasting (light)',\n      (_) => GameWidget(game: RaycastLightExample()),\n      codeLink: baseLink('collision_detection/raycast_light_example.dart'),\n      info: RaycastLightExample.description,\n    )\n    ..add(\n      'Raycasting',\n      (_) => GameWidget(game: RaycastExample()),\n      codeLink: baseLink('collision_detection/raycast_example.dart'),\n      info: RaycastExample.description,\n    )\n    ..add(\n      'Raytracing',\n      (_) => GameWidget(game: RaytraceExample()),\n      codeLink: baseLink('collision_detection/raytrace_example.dart'),\n      info: RaytraceExample.description,\n    )\n    ..add(\n      'Raycasting Max Distance',\n      (_) => GameWidget(game: RaycastMaxDistanceExample()),\n      codeLink: baseLink(\n        'collision_detection/raycast_max_distance_example.dart',\n      ),\n      info: RaycastMaxDistanceExample.description,\n    )\n    ..add(\n      'Ray inside/outside shapes',\n      (_) => GameWidget(game: RaysInShapeExample()),\n      codeLink: baseLink('collision_detection/rays_in_shape_example.dart'),\n      info: RaysInShapeExample.description,\n    );\n}\n"
  },
  {
    "path": "examples/lib/stories/collision_detection/multiple_shapes_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/material.dart' hide Image, Draggable;\n\nenum Shapes { circle, rectangle, polygon }\n\nclass MultipleShapesExample extends FlameGame with HasCollisionDetection {\n  static const description = '''\n    An example with many hitboxes that move around on the screen and during\n    collisions they change color depending on what it is that they have collided\n    with. \n    \n    The snowman, the component built with three circles on top of each other, \n    works a little bit differently than the other components to show that you\n    can have multiple hitboxes within one component.\n    \n    On this example, you can \"throw\" the components by dragging them quickly in\n    any direction.\n  ''';\n\n  MultipleShapesExample()\n    : super(\n        world: MultiShapesWorld(),\n        camera: CameraComponent()..viewfinder.anchor = Anchor.topLeft,\n      );\n}\n\nclass MultiShapesWorld extends World with HasGameReference {\n  @override\n  Future<void> onLoad() async {\n    add(FpsTextComponent(position: Vector2(0, game.size.y - 24)));\n    final screenHitbox = ScreenHitbox();\n    final snowman = CollidableSnowman(\n      Vector2.all(150),\n      Vector2(120, 250),\n      Vector2(-100, 100),\n      screenHitbox,\n    );\n    MyCollidable lastToAdd = snowman;\n    add(screenHitbox);\n    add(snowman);\n    var totalAdded = 1;\n    while (totalAdded < 1000) {\n      lastToAdd = nextRandomCollidable(lastToAdd, screenHitbox);\n      final lastBottomRight = lastToAdd.toAbsoluteRect().bottomRight;\n      if (lastBottomRight.dx < game.size.x &&\n          lastBottomRight.dy < game.size.y) {\n        add(lastToAdd);\n        totalAdded++;\n      } else {\n        break;\n      }\n    }\n  }\n\n  final _rng = Random();\n  final _distance = Vector2(100, 0);\n\n  MyCollidable nextRandomCollidable(\n    MyCollidable lastCollidable,\n    ScreenHitbox screenHitbox,\n  ) {\n    final collidableSize = Vector2.all(50) + Vector2.random(_rng) * 100;\n    final isXOverflow =\n        lastCollidable.position.x +\n            lastCollidable.size.x / 2 +\n            _distance.x +\n            collidableSize.x >\n        game.size.x;\n    var position = _distance + Vector2(0, lastCollidable.position.y + 200);\n    if (!isXOverflow) {\n      position = (lastCollidable.position + _distance)\n        ..x += collidableSize.x / 2;\n    }\n    final velocity = (Vector2.random(_rng) - Vector2.random(_rng)) * 400;\n    return randomCollidable(\n      position,\n      collidableSize,\n      velocity,\n      screenHitbox,\n      random: _rng,\n    );\n  }\n}\n\nabstract class MyCollidable extends PositionComponent\n    with DragCallbacks, CollisionCallbacks, GestureHitboxes {\n  double rotationSpeed = 0.0;\n  final Vector2 velocity;\n  final delta = Vector2.zero();\n  double angleDelta = 0;\n  final Color _defaultColor = Colors.blue.withValues(alpha: 0.8);\n  final Color _collisionColor = Colors.green.withValues(alpha: 0.8);\n  late final Paint _dragIndicatorPaint;\n  final ScreenHitbox screenHitbox;\n  ShapeHitbox? hitbox;\n\n  MyCollidable(\n    Vector2 position,\n    Vector2 size,\n    this.velocity,\n    this.screenHitbox,\n  ) : super(position: position, size: size, anchor: Anchor.center) {\n    _dragIndicatorPaint = BasicPalette.white.paint();\n  }\n\n  @override\n  void onMount() {\n    hitbox?.paint.color = _defaultColor;\n    super.onMount();\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    if (isDragged) {\n      return;\n    }\n    delta.setFrom(velocity * dt);\n    position.add(delta);\n    angleDelta = dt * rotationSpeed;\n    angle = (angle + angleDelta) % (2 * pi);\n    // Takes rotation into consideration (which topLeftPosition doesn't)\n    final topLeft = absoluteCenter - (scaledSize / 2);\n    if (topLeft.x + scaledSize.x < 0 ||\n        topLeft.y + scaledSize.y < 0 ||\n        topLeft.x > screenHitbox.scaledSize.x ||\n        topLeft.y > screenHitbox.scaledSize.y) {\n      final moduloSize = screenHitbox.scaledSize + scaledSize;\n      topLeftPosition = topLeftPosition % moduloSize;\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    if (isDragged) {\n      final localCenter = scaledSize.toOffset() / 2;\n      canvas.drawCircle(localCenter, 5, _dragIndicatorPaint);\n    }\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n    hitbox?.paint.color = _collisionColor;\n  }\n\n  @override\n  void onCollisionEnd(PositionComponent other) {\n    super.onCollisionEnd(other);\n    if (!isColliding) {\n      hitbox?.paint.color = _defaultColor;\n    }\n  }\n\n  @override\n  void onDragEnd(DragEndEvent event) {\n    super.onDragEnd(event);\n    velocity.setFrom(event.velocity / 10);\n  }\n}\n\nclass CollidablePolygon extends MyCollidable {\n  CollidablePolygon(\n    Vector2 position,\n    Vector2 size,\n    Vector2 velocity,\n    ScreenHitbox screenHitbox,\n  ) : super(position, size, velocity, screenHitbox) {\n    hitbox = PolygonHitbox.relative(\n      [\n        Vector2(-1.0, 0.0),\n        Vector2(-0.8, 0.6),\n        Vector2(0.0, 1.0),\n        Vector2(0.6, 0.9),\n        Vector2(1.0, 0.0),\n        Vector2(0.6, -0.8),\n        Vector2(0, -1.0),\n        Vector2(-0.8, -0.8),\n      ],\n      parentSize: size,\n    )..renderShape = true;\n    add(hitbox!);\n  }\n}\n\nclass CollidableRectangle extends MyCollidable {\n  CollidableRectangle(\n    super.position,\n    super.size,\n    super.velocity,\n    super.screenHitbox,\n  ) {\n    hitbox = RectangleHitbox()..renderShape = true;\n    add(hitbox!);\n  }\n}\n\nclass CollidableCircle extends MyCollidable {\n  CollidableCircle(\n    super.position,\n    super.size,\n    super.velocity,\n    super.screenHitbox,\n  ) {\n    hitbox = CircleHitbox()..renderShape = true;\n    add(hitbox!);\n  }\n}\n\nclass SnowmanPart extends CircleHitbox {\n  @override\n  final renderShape = true;\n  final startColor = Colors.white.withValues(alpha: 0.8);\n  final Color hitColor;\n\n  SnowmanPart(double radius, Vector2 position, this.hitColor)\n    : super(radius: radius, position: position, anchor: Anchor.center) {\n    paint.color = startColor;\n  }\n\n  @override\n  void onCollisionStart(Set<Vector2> intersectionPoints, ShapeHitbox other) {\n    super.onCollisionStart(intersectionPoints, other);\n\n    if (other.hitboxParent is ScreenHitbox) {\n      paint.color = startColor;\n    } else {\n      paint.color = hitColor.withValues(alpha: 0.8);\n    }\n  }\n\n  @override\n  void onCollisionEnd(ShapeHitbox other) {\n    super.onCollisionEnd(other);\n    if (!isColliding) {\n      paint.color = startColor;\n    }\n  }\n}\n\nclass CollidableSnowman extends MyCollidable {\n  CollidableSnowman(\n    Vector2 position,\n    Vector2 size,\n    Vector2 velocity,\n    ScreenHitbox screenHitbox,\n  ) : super(position, size, velocity, screenHitbox) {\n    rotationSpeed = 0.3;\n    anchor = Anchor.topLeft;\n    final top = SnowmanPart(\n      size.x * 0.3,\n      Vector2(size.x / 2, size.y * 0.15),\n      Colors.red,\n    );\n    final middle = SnowmanPart(\n      size.x * 0.4,\n      Vector2(size.x / 2, size.y * 0.4),\n      Colors.yellow,\n    );\n    final bottom = SnowmanPart(\n      size.x / 2,\n      Vector2(size.x / 2, size.y - size.y / 4),\n      Colors.green,\n    );\n    add(bottom);\n    add(middle);\n    add(top);\n  }\n}\n\nMyCollidable randomCollidable(\n  Vector2 position,\n  Vector2 size,\n  Vector2 velocity,\n  ScreenHitbox screenHitbox, {\n  Random? random,\n}) {\n  final rng = random ?? Random();\n  final rotationSpeed = 0.5 - rng.nextDouble();\n  final shapeType = Shapes.values[rng.nextInt(Shapes.values.length)];\n  return switch (shapeType) {\n    Shapes.circle => CollidableCircle(\n      position,\n      size,\n      velocity,\n      screenHitbox,\n    )..rotationSpeed = rotationSpeed,\n    Shapes.rectangle => CollidableRectangle(\n      position,\n      size,\n      velocity,\n      screenHitbox,\n    )..rotationSpeed = rotationSpeed,\n    Shapes.polygon => CollidablePolygon(\n      position,\n      size,\n      velocity,\n      screenHitbox,\n    )..rotationSpeed = rotationSpeed,\n  };\n}\n"
  },
  {
    "path": "examples/lib/stories/collision_detection/multiple_worlds_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:examples/commons/ember.dart';\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass MultipleWorldsExample extends FlameGame {\n  static const description = '''\n    This example shows how multiple worlds can have discrete collision\n    detection.\n    \n    The top two Embers live in one world and turn green when they collide and\n    the bottom two embers live in another world and turn red when they collide,\n    you can see that when one of the top ones collide with one of the bottom\n    ones, neither change their colors since they are in different worlds.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final world1 = CollisionDetectionWorld();\n    final world2 = CollisionDetectionWorld();\n    final camera1 = CameraComponent(world: world1);\n    final camera2 = CameraComponent(world: world2);\n    await addAll([world1, world2, camera1, camera2]);\n    final ember1 = CollidableEmber(position: Vector2(75, 75));\n    final ember2 = CollidableEmber(position: Vector2(-75, 75));\n    final ember3 = CollidableEmber(position: Vector2(75, -75));\n    final ember4 = CollidableEmber(position: Vector2(-75, -75));\n    world1.addAll([ember1, ember2]);\n    world2.addAll([ember3, ember4]);\n  }\n}\n\nclass CollisionDetectionWorld extends World with HasCollisionDetection {}\n\nclass CollidableEmber extends Ember with CollisionCallbacks {\n  CollidableEmber({super.position});\n\n  static final Random _rng = Random();\n  int get index =>\n      (position.x.isNegative ? 1 : 0) + (position.y.isNegative ? 2 : 0);\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    add(CircleHitbox());\n    add(\n      MoveToEffect(\n        Vector2.zero(),\n        EffectController(\n          duration: 0.5 + _rng.nextDouble(),\n          infinite: true,\n          alternate: true,\n        ),\n      ),\n    );\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n\n    add(\n      ColorEffect(\n        index < 2 ? Colors.red : Colors.green,\n        EffectController(\n          duration: 0.2,\n          alternate: true,\n        ),\n        opacityTo: 0.9,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/collision_detection/quadtree_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/layers.dart';\nimport 'package:flutter/material.dart' hide Image, Draggable;\nimport 'package:flutter/services.dart';\n\nconst tileSize = 8.0;\n\nclass QuadTreeExample extends FlameGame\n    with HasQuadTreeCollisionDetection, KeyboardEvents, ScrollDetector {\n  QuadTreeExample();\n\n  static const description = '''\nIn this example the standard \"Sweep and Prune\" algorithm is replaced by  \n\"Quad Tree\". Quad Tree is often a more efficient approach of handling collisions,\nits efficiency is shown especially on huge maps with big amounts of collidable \ncomponents.\nSome bricks are highlighted when placed on an edge of a quadrant. It is\nimportant to understand that handling hitboxes on edges requires more\nresources.\nBlue lines visualize the quad tree's quadrant positions.\n\nUse WASD to move the player and use the mouse scroll to change zoom.\nHold direction button and press space to fire a bullet. \nNotice that bullet will fly above water but collides with bricks.\n\nAlso notice that creating a lot of bullets at once leads to generating new\nquadrants on the map since it becomes more than 25 objects in one quadrant.\n\nPress O button to rescan the tree and optimize it, removing unused quadrants.\n\nPress T button to toggle player to collide with other objects.\n  ''';\n\n  static const mapSize = 300;\n  static const bricksCount = 8000;\n  late final Player player;\n  final staticLayer = StaticLayer();\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n\n    const mapWidth = mapSize * tileSize;\n    const mapHeight = mapSize * tileSize;\n    initializeCollisionDetection(\n      mapDimensions: const Rect.fromLTWH(0, 0, mapWidth, mapHeight),\n      minimumDistance: 10,\n    );\n\n    final random = Random();\n    final spriteBrick = await Sprite.load(\n      'retro_tiles.png',\n      srcPosition: Vector2.all(0),\n      srcSize: Vector2.all(tileSize),\n    );\n\n    final spriteWater = await Sprite.load(\n      'retro_tiles.png',\n      srcPosition: Vector2(0, tileSize),\n      srcSize: Vector2.all(tileSize),\n    );\n\n    for (var i = 0; i < bricksCount; i++) {\n      final x = random.nextInt(mapSize);\n      final y = random.nextInt(mapSize);\n      final brick = Brick(\n        position: Vector2(x * tileSize, y * tileSize),\n        size: Vector2.all(tileSize),\n        priority: 0,\n        sprite: spriteBrick,\n      );\n      world.add(brick);\n      staticLayer.components.add(brick);\n    }\n\n    staticLayer.reRender();\n    camera = CameraComponent.withFixedResolution(\n      world: world,\n      width: 500,\n      height: 250,\n    );\n\n    player = Player(\n      position: Vector2.all(mapSize * tileSize / 2),\n      size: Vector2.all(tileSize),\n      priority: 2,\n    );\n    world.add(player);\n    camera.follow(player);\n\n    final brick = Brick(\n      position: player.position.translated(0, -tileSize * 2),\n      size: Vector2.all(tileSize),\n      priority: 0,\n      sprite: spriteBrick,\n    );\n    world.add(brick);\n    staticLayer.components.add(brick);\n\n    final water1 = Water(\n      position: player.position.translated(0, tileSize * 2),\n      size: Vector2.all(tileSize),\n      priority: 0,\n      sprite: spriteWater,\n    );\n    world.add(water1);\n\n    final water2 = Water(\n      position: player.position.translated(tileSize * 2, 0),\n      size: Vector2.all(tileSize),\n      priority: 0,\n      sprite: spriteWater,\n    );\n    world.add(water2);\n\n    final water3 = Water(\n      position: player.position.translated(-tileSize * 2, 0),\n      size: Vector2.all(tileSize),\n      priority: 0,\n      sprite: spriteWater,\n    );\n    world.add(water3);\n\n    world.add(QuadTreeDebugComponent(collisionDetection));\n    world.add(LayerComponent(staticLayer));\n    camera.viewport.add(FpsTextComponent());\n  }\n\n  final elapsedMicroseconds = <double>[];\n\n  final _playerDisplacement = Vector2.zero();\n  var _fireBullet = false;\n\n  static const stepSize = 1.0;\n\n  @override\n  KeyEventResult onKeyEvent(\n    KeyEvent event,\n    Set<LogicalKeyboardKey> keysPressed,\n  ) {\n    for (final key in keysPressed) {\n      if (key == LogicalKeyboardKey.keyW && player.canMoveTop) {\n        _playerDisplacement.setValues(0, -stepSize);\n        player.position.translate(0, -stepSize);\n      }\n      if (key == LogicalKeyboardKey.keyA && player.canMoveLeft) {\n        _playerDisplacement.setValues(-stepSize, 0);\n        player.position.translate(-stepSize, 0);\n      }\n      if (key == LogicalKeyboardKey.keyS && player.canMoveBottom) {\n        _playerDisplacement.setValues(0, stepSize);\n        player.position.translate(0, stepSize);\n      }\n      if (key == LogicalKeyboardKey.keyD && player.canMoveRight) {\n        _playerDisplacement.setValues(stepSize, 0);\n        player.position.translate(stepSize, 0);\n      }\n      if (key == LogicalKeyboardKey.space) {\n        _fireBullet = true;\n      }\n      if (key == LogicalKeyboardKey.keyT) {\n        final collisionType = player.hitbox.collisionType;\n        if (collisionType == CollisionType.active) {\n          player.hitbox.collisionType = CollisionType.inactive;\n        } else if (collisionType == CollisionType.inactive) {\n          player.hitbox.collisionType = CollisionType.active;\n        }\n      }\n      if (key == LogicalKeyboardKey.keyO) {\n        collisionDetection.broadphase.tree.optimize();\n      }\n    }\n    if (_fireBullet && !_playerDisplacement.isZero()) {\n      final bullet = Bullet(\n        position: player.position,\n        displacement: _playerDisplacement * 50,\n      );\n      add(bullet);\n      _playerDisplacement.setZero();\n      _fireBullet = false;\n    }\n\n    return KeyEventResult.handled;\n  }\n\n  @override\n  void onScroll(PointerScrollInfo info) {\n    camera.viewfinder.zoom += info.scrollDelta.global.y.sign * 0.08;\n    camera.viewfinder.zoom = camera.viewfinder.zoom.clamp(0.05, 5.0);\n  }\n}\n\n//#region Player\n\nclass Player extends SpriteComponent\n    with CollisionCallbacks, HasGameReference<QuadTreeExample> {\n  Player({\n    required super.position,\n    required super.size,\n    required super.priority,\n  });\n\n  bool canMoveLeft = true;\n  bool canMoveRight = true;\n  bool canMoveTop = true;\n  bool canMoveBottom = true;\n  final hitbox = RectangleHitbox();\n\n  @override\n  Future<void> onLoad() async {\n    sprite = await Sprite.load(\n      'retro_tiles.png',\n      srcSize: Vector2.all(tileSize),\n      srcPosition: Vector2(tileSize * 3, tileSize),\n    );\n\n    add(hitbox);\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    final myCenter = Vector2(\n      position.x + tileSize / 2,\n      position.y + tileSize / 2,\n    );\n    if (other is GameCollidable) {\n      final diffX = myCenter.x - other.cachedCenter.x;\n      if (diffX < 0) {\n        canMoveRight = false;\n      } else if (diffX > 0) {\n        canMoveLeft = false;\n      }\n\n      final diffY = myCenter.y - other.cachedCenter.y;\n      if (diffY < 0) {\n        canMoveBottom = false;\n      } else if (diffY > 0) {\n        canMoveTop = false;\n      }\n      final newPos = Vector2(position.x + diffX / 3, position.y + diffY / 3);\n      position = newPos;\n    }\n    super.onCollisionStart(intersectionPoints, other);\n  }\n\n  @override\n  void onCollisionEnd(PositionComponent other) {\n    canMoveLeft = true;\n    canMoveRight = true;\n    canMoveTop = true;\n    canMoveBottom = true;\n    super.onCollisionEnd(other);\n  }\n}\n\nclass Bullet extends PositionComponent with CollisionCallbacks, HasPaint {\n  Bullet({required super.position, required this.displacement}) {\n    paint.color = Colors.deepOrange;\n    priority = 10;\n    size = Vector2.all(1);\n    add(RectangleHitbox());\n  }\n\n  final Vector2 displacement;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawCircle(Offset.zero, 1, paint);\n  }\n\n  @override\n  void update(double dt) {\n    final d = displacement * dt;\n    position = Vector2(position.x + d.x, position.y + d.y);\n    super.update(dt);\n  }\n\n  @override\n  bool onComponentTypeCheck(PositionComponent other) {\n    if (other is Player || other is Water) {\n      return false;\n    }\n    return super.onComponentTypeCheck(other);\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    if (other is Brick) {\n      removeFromParent();\n    }\n    super.onCollisionStart(intersectionPoints, other);\n  }\n}\n\n//#endregion\n\n//#region Environment\n\nclass Brick extends SpriteComponent\n    with CollisionCallbacks, GameCollidable, UpdateOnce {\n  Brick({\n    required super.position,\n    required super.size,\n    required super.priority,\n    required super.sprite,\n  }) {\n    initCenter();\n    initCollision();\n  }\n\n  bool rendered = false;\n\n  @override\n  void renderTree(Canvas canvas) {\n    if (!rendered) {\n      super.renderTree(canvas);\n    }\n  }\n}\n\nclass Water extends SpriteComponent\n    with CollisionCallbacks, GameCollidable, UpdateOnce {\n  Water({\n    required super.position,\n    required super.size,\n    required super.priority,\n    required super.sprite,\n  }) {\n    initCenter();\n    initCollision();\n  }\n}\n\nmixin GameCollidable on PositionComponent {\n  void initCollision() {\n    add(RectangleHitbox(collisionType: CollisionType.passive));\n  }\n\n  void initCenter() {\n    cachedCenter = Vector2(\n      position.x + tileSize / 2,\n      position.y + tileSize / 2,\n    );\n  }\n\n  late final Vector2 cachedCenter;\n}\n\n//#endregion\n\n//#region Utils\n\nmixin UpdateOnce on PositionComponent {\n  bool updateOnce = true;\n\n  @override\n  void updateTree(double dt) {\n    if (updateOnce) {\n      super.updateTree(dt);\n      updateOnce = false;\n    }\n  }\n}\n\nclass StaticLayer extends PreRenderedLayer {\n  StaticLayer();\n\n  List<PositionComponent> components = [];\n\n  @override\n  void drawLayer() {\n    for (final element in components) {\n      if (element is Brick) {\n        element.rendered = false;\n        element.renderTree(canvas);\n        element.rendered = true;\n      }\n    }\n  }\n}\n\nclass LayerComponent extends PositionComponent {\n  LayerComponent(this.layer);\n\n  StaticLayer layer;\n\n  @override\n  void render(Canvas canvas) {\n    layer.render(canvas);\n  }\n}\n\nclass QuadTreeDebugComponent extends PositionComponent with HasPaint {\n  QuadTreeDebugComponent(QuadTreeCollisionDetection cd) {\n    dbg = QuadTreeNodeDebugInfo.init(cd);\n    paint.color = Colors.blue;\n    paint.style = PaintingStyle.stroke;\n    priority = 10;\n  }\n\n  late final QuadTreeNodeDebugInfo dbg;\n\n  final _boxPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..color = Colors.lightGreenAccent\n    ..strokeWidth = 1;\n\n  @override\n  void render(Canvas canvas) {\n    final nodes = dbg.nodes;\n    for (final node in nodes) {\n      canvas.drawRect(node.rect, paint);\n      final nodeElements = node.ownElements;\n\n      final shouldPaint = !node.noChildren && nodeElements.isNotEmpty;\n      for (final box in nodeElements) {\n        if (shouldPaint) {\n          canvas.drawRect(box.aabb.toRect(), _boxPaint);\n        }\n      }\n    }\n  }\n}\n\n//#endregion\n"
  },
  {
    "path": "examples/lib/stories/collision_detection/raycast_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/material.dart';\n\nclass RaycastExample extends FlameGame with HasCollisionDetection {\n  static const description = '''\nIn this example the raycast functionality is showcased. The circle moves around\nand casts 10 rays and checks how far the nearest hitboxes are and naively moves\naround trying not to hit them.\n  ''';\n\n  Ray2? ray;\n  Ray2? reflection;\n  Vector2 origin = Vector2(250, 100);\n  Paint paint = Paint()..color = Colors.amber.withValues(alpha: 0.6);\n  final speed = 100;\n  final inertia = 3.0;\n  final safetyDistance = 50;\n  final direction = Vector2(0, 1);\n  final velocity = Vector2.zero();\n  final random = Random();\n\n  static const numberOfRays = 10;\n  final List<Ray2> rays = [];\n  final List<RaycastResult<ShapeHitbox>> results = [];\n\n  late Path path;\n  @override\n  Future<void> onLoad() async {\n    final paint = BasicPalette.gray.paint()\n      ..style = PaintingStyle.stroke\n      ..strokeWidth = 2.0;\n    add(ScreenHitbox());\n    add(\n      CircleComponent(\n        position: Vector2(100, 100),\n        radius: 50,\n        paint: paint,\n        children: [CircleHitbox()],\n      ),\n    );\n    add(\n      CircleComponent(\n        position: Vector2(150, 500),\n        radius: 50,\n        paint: paint,\n        children: [CircleHitbox()],\n      ),\n    );\n    add(\n      RectangleComponent(\n        position: Vector2.all(300),\n        size: Vector2.all(100),\n        paint: paint,\n        children: [RectangleHitbox()],\n      ),\n    );\n    add(\n      RectangleComponent(\n        position: Vector2.all(500),\n        size: Vector2(100, 200),\n        paint: paint,\n        children: [RectangleHitbox()],\n      ),\n    );\n    add(\n      RectangleComponent(\n        position: Vector2(550, 200),\n        size: Vector2(200, 150),\n        paint: paint,\n        children: [RectangleHitbox()],\n      ),\n    );\n  }\n\n  final _velocityModifier = Vector2.zero();\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    collisionDetection.raycastAll(\n      origin,\n      numberOfRays: numberOfRays,\n      rays: rays,\n      out: results,\n    );\n    velocity.scale(inertia);\n    for (final result in results) {\n      _velocityModifier\n        ..setFrom(result.intersectionPoint!)\n        ..sub(origin)\n        ..normalize();\n      if (result.distance! < safetyDistance) {\n        _velocityModifier.negate();\n      } else if (random.nextDouble() < 0.2) {\n        velocity.add(_velocityModifier);\n      }\n      velocity.add(_velocityModifier);\n    }\n    velocity\n      ..normalize()\n      ..scale(speed * dt);\n    origin.add(velocity);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    renderResult(canvas, origin, results, paint);\n  }\n\n  void renderResult(\n    Canvas canvas,\n    Vector2 origin,\n    List<RaycastResult<ShapeHitbox>> results,\n    Paint paint,\n  ) {\n    final originOffset = origin.toOffset();\n    for (final result in results) {\n      if (!result.isActive) {\n        continue;\n      }\n      final intersectionPoint = result.intersectionPoint!.toOffset();\n      canvas.drawLine(\n        originOffset,\n        intersectionPoint,\n        paint,\n      );\n    }\n    canvas.drawCircle(originOffset, 5, paint);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/collision_detection/raycast_light_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/material.dart';\n\nclass RaycastLightExample extends FlameGame\n    with HasCollisionDetection, TapCallbacks, MouseMovementDetector {\n  static const description = '''\nIn this example the raycast functionality is showcased by using it as a light\nsource, if you move the mouse around the canvas the rays will be cast from its\nlocation. You can also tap to create a permanent source of rays that wont move\nwith with mouse.\n  ''';\n\n  Ray2? ray;\n  Ray2? reflection;\n  Vector2? origin;\n  Vector2? tapOrigin;\n  bool isOriginCasted = false;\n  bool isTapOriginCasted = false;\n  Paint paint = Paint();\n  Paint tapPaint = Paint();\n\n  final _colorTween = ColorTween(\n    begin: Colors.blue.withValues(alpha: 0.2),\n    end: Colors.red.withValues(alpha: 0.2),\n  );\n\n  static const numberOfRays = 2000;\n  final List<Ray2> rays = [];\n  final List<Ray2> tapRays = [];\n  final List<RaycastResult<ShapeHitbox>> results = [];\n  final List<RaycastResult<ShapeHitbox>> tapResults = [];\n\n  late Path path;\n  @override\n  Future<void> onLoad() async {\n    final paint = BasicPalette.gray.paint()\n      ..style = PaintingStyle.stroke\n      ..strokeWidth = 2.0;\n    add(ScreenHitbox());\n    add(\n      CircleComponent(\n        position: Vector2(100, 100),\n        radius: 50,\n        paint: paint,\n        children: [CircleHitbox()],\n      ),\n    );\n    add(\n      CircleComponent(\n        position: Vector2(150, 500),\n        radius: 50,\n        paint: paint,\n        children: [CircleHitbox()],\n      ),\n    );\n    add(\n      RectangleComponent(\n        position: Vector2.all(300),\n        size: Vector2.all(100),\n        paint: paint,\n        children: [RectangleHitbox()],\n      ),\n    );\n    add(\n      RectangleComponent(\n        position: Vector2.all(500),\n        size: Vector2(100, 200),\n        paint: paint,\n        children: [RectangleHitbox()],\n      ),\n    );\n    add(\n      RectangleComponent(\n        position: Vector2(550, 200),\n        size: Vector2(200, 150),\n        paint: paint,\n        children: [RectangleHitbox()],\n      ),\n    );\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    final origin = event.canvasPosition;\n    isTapOriginCasted = origin == tapOrigin;\n    tapOrigin = origin;\n  }\n\n  @override\n  void onMouseMove(PointerHoverInfo info) {\n    final origin = info.eventPosition.widget;\n    isOriginCasted = origin == this.origin;\n    this.origin = origin;\n  }\n\n  var _timePassed = 0.0;\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    _timePassed += dt;\n    paint.color = _colorTween.transform(0.5 + (sin(_timePassed) / 2))!;\n    tapPaint.color = _colorTween.transform(0.5 + (cos(_timePassed) / 2))!;\n    if (origin != null && !isOriginCasted) {\n      collisionDetection.raycastAll(\n        origin!,\n        numberOfRays: numberOfRays,\n        rays: rays,\n        out: results,\n      );\n      isOriginCasted = true;\n    }\n    if (tapOrigin != null && !isTapOriginCasted) {\n      collisionDetection.raycastAll(\n        tapOrigin!,\n        numberOfRays: numberOfRays,\n        rays: tapRays,\n        out: tapResults,\n      );\n      isTapOriginCasted = true;\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    if (origin != null) {\n      renderResult(canvas, origin!, results, paint);\n    }\n    if (tapOrigin != null) {\n      renderResult(canvas, tapOrigin!, tapResults, tapPaint);\n    }\n  }\n\n  void renderResult(\n    Canvas canvas,\n    Vector2 origin,\n    List<RaycastResult<ShapeHitbox>> results,\n    Paint paint,\n  ) {\n    final originOffset = origin.toOffset();\n    for (final result in results) {\n      if (!result.isActive) {\n        continue;\n      }\n      final intersectionPoint = result.intersectionPoint!.toOffset();\n      canvas.drawLine(\n        originOffset,\n        intersectionPoint,\n        paint,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/collision_detection/raycast_max_distance_example.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame_noise/flame_noise.dart';\nimport 'package:flutter/material.dart';\n\nclass RaycastMaxDistanceExample extends FlameGame with HasCollisionDetection {\n  static const description = '''\nThis examples showcases how raycast APIs can be used to detect hits within certain range.\n''';\n\n  static const _maxDistance = 50.0;\n\n  late Ray2 _ray;\n  late _Character _character;\n  final _result = RaycastResult<ShapeHitbox>();\n\n  final _text = TextComponent(\n    text: \"Hey! Who's there?\",\n    anchor: Anchor.center,\n    textRenderer: TextPaint(\n      style: const TextStyle(\n        fontSize: 8,\n        color: Colors.amber,\n      ),\n    ),\n  );\n\n  @override\n  void onLoad() {\n    camera = CameraComponent.withFixedResolution(\n      world: world,\n      width: 320,\n      height: 180,\n    );\n\n    _addMovingWall();\n\n    world.add(\n      _character = _Character(\n        maxDistance: _maxDistance,\n        position: Vector2(-50, 0),\n        anchor: Anchor.center,\n      ),\n    );\n\n    _text.position = _character.position - Vector2(0, 50);\n\n    _ray = Ray2(\n      origin: _character.absolutePosition,\n      direction: Vector2(1, 0),\n    );\n  }\n\n  void _addMovingWall() {\n    world.add(\n      RectangleComponent(\n        size: Vector2(20, 40),\n        anchor: Anchor.center,\n        paint: BasicPalette.red.paint(),\n        children: [\n          RectangleHitbox(),\n          MoveByEffect(\n            Vector2(50, 0),\n            EffectController(\n              duration: 2,\n              alternate: true,\n              infinite: true,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  @override\n  void update(double dt) {\n    collisionDetection.raycast(_ray, maxDistance: _maxDistance, out: _result);\n    if (_result.isActive) {\n      if (camera.viewfinder.children.query<Effect>().isEmpty) {\n        camera.viewfinder.add(\n          MoveEffect.by(\n            Vector2(5, 5),\n            NoiseEffectController(\n              duration: 0.2,\n              noise: PerlinNoise(frequency: 400),\n            ),\n          ),\n        );\n      }\n      if (!_text.isMounted) {\n        world.add(_text);\n      }\n    } else {\n      _text.removeFromParent();\n    }\n    super.update(dt);\n  }\n}\n\nclass _Character extends PositionComponent {\n  _Character({required this.maxDistance, super.position, super.anchor});\n\n  final double maxDistance;\n\n  final _rayOriginPoint = Offset.zero;\n  late final _rayEndPoint = Offset(maxDistance, 0);\n  final _rayPaint = BasicPalette.gray.paint();\n\n  @override\n  Future<void>? onLoad() async {\n    addAll([\n      CircleComponent(\n        radius: 20,\n        anchor: Anchor.center,\n        paint: BasicPalette.green.paint(),\n      )..scale = Vector2(0.55, 1),\n      CircleComponent(\n        radius: 10,\n        anchor: Anchor.center,\n        paint: _rayPaint,\n      ),\n      RectangleComponent(\n        size: Vector2(10, 3),\n        position: Vector2(12, 5),\n      ),\n    ]);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawLine(_rayOriginPoint, _rayEndPoint, _rayPaint);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/collision_detection/rays_in_shape_example.dart",
    "content": "import 'dart:async';\nimport 'dart:math';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flutter/material.dart';\n\nconst playArea = Rect.fromLTRB(-100, -100, 100, 100);\n\nclass RaysInShapeExample extends FlameGame {\n  static const description = '''\nIn this example we showcase the raytrace functionality where you can see whether\nthe rays are inside the shapes or not. Click to change the shape that the rays\nare casted against. The rays originates from small circles, and if the circle is\ninside the shape it will be red, otherwise green. And if the ray doesn't hit any\nshape it will be gray.\n''';\n\n  RaysInShapeExample()\n    : super(\n        world: RaysInShapeWorld(),\n        camera: CameraComponent.withFixedResolution(\n          width: playArea.width,\n          height: playArea.height,\n        ),\n      );\n}\n\nfinal whiteStroke = Paint()\n  ..color = const Color(0xffffffff)\n  ..style = PaintingStyle.stroke;\n\nfinal lightStroke = Paint()\n  ..color = const Color(0x50ffffff)\n  ..style = PaintingStyle.stroke;\n\nfinal greenStroke = Paint()\n  ..color = const Color(0xff00ff00)\n  ..style = PaintingStyle.stroke;\n\nfinal redStroke = Paint()\n  ..color = const Color(0xffff0000)\n  ..style = PaintingStyle.stroke;\n\nclass RaysInShapeWorld extends World\n    with\n        HasGameReference<RaysInShapeExample>,\n        HasCollisionDetection,\n        TapCallbacks {\n  final _rng = Random();\n  List<Ray2> _rays = [];\n\n  List<Ray2> randomRays(int count) => List<Ray2>.generate(\n    count,\n    (index) => Ray2(\n      origin:\n          (Vector2.random(_rng)) * playArea.size.width -\n          playArea.size.toVector2() / 2,\n      direction: (Vector2.random(_rng) - Vector2(0.5, 0.5)).normalized(),\n    ),\n  );\n\n  int _componentIndex = 0;\n\n  final _components = [\n    CircleComponent(\n      radius: 60,\n      anchor: Anchor.center,\n      position: Vector2.zero(),\n      paint: whiteStroke,\n      children: [CircleHitbox()],\n    ),\n    RectangleComponent(\n      size: Vector2(100, 100),\n      anchor: Anchor.center,\n      position: Vector2.zero(),\n      paint: whiteStroke,\n      children: [RectangleHitbox()],\n    ),\n    PositionComponent(\n      position: Vector2.zero(),\n      children: [\n        PolygonHitbox.relative(\n            [\n              Vector2(-0.7, -1),\n              Vector2(1, -0.4),\n              Vector2(0.3, 1),\n              Vector2(-1, 0.6),\n            ],\n            parentSize: Vector2(100, 100),\n            anchor: Anchor.center,\n            position: Vector2.zero(),\n          )\n          ..paint = whiteStroke\n          ..renderShape = true,\n      ],\n    ),\n  ];\n\n  @override\n  FutureOr<void> onLoad() {\n    super.onLoad();\n    add(_components[_componentIndex]);\n    _rays = randomRays(200);\n  }\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    super.onTapUp(event);\n    remove(_components[_componentIndex]);\n    _componentIndex = (_componentIndex + 1) % _components.length;\n    add(_components[_componentIndex]);\n    _recording.clear();\n    _rays = randomRays(200);\n  }\n\n  final Map<Ray2, RaycastResult<ShapeHitbox>?> _recording = {};\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    for (final ray in _rays) {\n      final result = collisionDetection.raycast(ray);\n      _recording.addAll({ray: result});\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    for (final ray in _recording.keys) {\n      final result = _recording[ray];\n      if (result == null) {\n        canvas.drawLine(\n          ray.origin.toOffset(),\n          (ray.origin + ray.direction.scaled(10)).toOffset(),\n          lightStroke,\n        );\n        canvas.drawCircle(ray.origin.toOffset(), 1, lightStroke);\n      } else {\n        canvas.drawLine(\n          ray.origin.toOffset(),\n          result.intersectionPoint!.toOffset(),\n          lightStroke,\n        );\n        canvas.drawCircle(\n          ray.origin.toOffset(),\n          1,\n          result.isInsideHitbox ? redStroke : greenStroke,\n        );\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/collision_detection/raytrace_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/material.dart';\n\nclass RaytraceExample extends FlameGame\n    with HasCollisionDetection, MouseMovementDetector, TapCallbacks {\n  static const description = '''\nIn this example the raytrace functionality is showcased.\nClick to start sending out a ray which will bounce around to visualize how it\nworks. If you move the mouse around the canvas, rays and their reflections will\nbe moved rendered and if you click again some more objects that the rays can\nbounce on will appear.\n  ''';\n\n  final _colorTween = ColorTween(\n    begin: Colors.amber.withValues(alpha: 1.0),\n    end: Colors.lightBlueAccent.withValues(alpha: 1.0),\n  );\n  final random = Random();\n  Ray2? ray;\n  Ray2? reflection;\n  Vector2? origin;\n  bool isOriginCasted = false;\n  Paint rayPaint = Paint();\n  final boxPaint = BasicPalette.gray.paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 2.0;\n\n  final List<Ray2> rays = [];\n  final List<RaycastResult<ShapeHitbox>> results = [];\n\n  late Path path;\n  @override\n  Future<void> onLoad() async {\n    addAll([\n      ScreenHitbox(),\n      CircleComponent(\n        radius: min(canvasSize.x, canvasSize.y) / 2,\n        paint: boxPaint,\n        children: [CircleHitbox()],\n      ),\n    ]);\n  }\n\n  bool isClicked = false;\n  final extraChildren = <Component>[];\n\n  @override\n  void onTapDown(_) {\n    if (!isClicked) {\n      isClicked = true;\n      return;\n    }\n    _timePassed = 0;\n    if (extraChildren.isEmpty) {\n      addAll(\n        extraChildren..addAll(\n          [\n            CircleComponent(\n              position: Vector2(100, 100),\n              radius: 50,\n              paint: boxPaint,\n              children: [CircleHitbox()],\n            ),\n            CircleComponent(\n              position: Vector2(150, 500),\n              radius: 50,\n              paint: boxPaint,\n              anchor: Anchor.center,\n              children: [CircleHitbox()],\n            ),\n            CircleComponent(\n              position: Vector2(150, 500),\n              radius: 150,\n              paint: boxPaint,\n              anchor: Anchor.center,\n              children: [CircleHitbox()],\n            ),\n            RectangleComponent(\n              position: Vector2.all(300),\n              size: Vector2.all(100),\n              paint: boxPaint,\n              children: [RectangleHitbox()],\n            ),\n            RectangleComponent(\n              position: Vector2.all(500),\n              size: Vector2(100, 200),\n              paint: boxPaint,\n              children: [RectangleHitbox()],\n            ),\n            CircleComponent(\n              position: Vector2(650, 275),\n              radius: 50,\n              paint: boxPaint,\n              anchor: Anchor.center,\n              children: [CircleHitbox()],\n            ),\n            RectangleComponent(\n              position: Vector2(550, 200),\n              size: Vector2(200, 150),\n              paint: boxPaint,\n              children: [RectangleHitbox()],\n            ),\n            RectangleComponent(\n              position: Vector2(350, 30),\n              size: Vector2(200, 150),\n              paint: boxPaint,\n              angle: tau / 10,\n              children: [RectangleHitbox()],\n            ),\n          ],\n        ),\n      );\n    } else {\n      removeAll(extraChildren);\n      extraChildren.clear();\n    }\n  }\n\n  @override\n  void onMouseMove(PointerHoverInfo info) {\n    final origin = info.eventPosition.widget;\n    isOriginCasted = origin == this.origin;\n    this.origin = origin;\n  }\n\n  final Ray2 _ray = Ray2.zero();\n  final _rayDirection = Vector2(1, 1)..normalize();\n  var _timePassed = 0.0;\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    if (isClicked) {\n      _timePassed += dt;\n    }\n    rayPaint.color = _colorTween.transform(0.5 + (sin(_timePassed) / 2))!;\n    if (origin != null) {\n      _ray.origin.setFrom(origin!);\n      _ray.direction = _rayDirection;\n      collisionDetection\n          .raytrace(\n            _ray,\n            maxDepth: min((_timePassed * 8).ceil(), 1000),\n            out: results,\n          )\n          .toList();\n      isOriginCasted = true;\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    if (origin != null) {\n      renderResult(canvas, origin!, results, rayPaint);\n    }\n  }\n\n  void renderResult(\n    Canvas canvas,\n    Vector2 origin,\n    List<RaycastResult<ShapeHitbox>> results,\n    Paint paint,\n  ) {\n    var originOffset = origin.toOffset();\n    for (final result in results) {\n      if (!result.isActive) {\n        continue;\n      }\n      final intersectionPoint = result.intersectionPoint!.toOffset();\n      canvas.drawLine(\n        originOffset,\n        intersectionPoint,\n        paint,\n      );\n      originOffset = intersectionPoint;\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/components/clip_component_example.dart",
    "content": "import 'package:examples/commons/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\n\nclass TappableEmber extends Ember with TapCallbacks {\n  TappableEmber({required Vector2 position, required Vector2 size})\n    : super(position: position, size: size);\n\n  @override\n  bool onTapDown(TapDownEvent event) {\n    size += Vector2.all(10);\n    return true;\n  }\n}\n\nclass ClipComponentExample extends FlameGame {\n  static const String description =\n      '''Tap on the objects to increase their size and see how the clip component\nworks.''';\n\n  late final _embers = <TappableEmber>[\n    TappableEmber(size: Vector2.all(200), position: Vector2.all(100)),\n    TappableEmber(size: Vector2.all(300), position: Vector2.all(100)),\n    TappableEmber(size: Vector2.all(200), position: Vector2.all(125)),\n  ];\n\n  @override\n  Future<void> onLoad() async {\n    addAll(\n      [\n        ClipComponent.circle(\n          position: Vector2.all(200),\n          size: Vector2.all(200),\n          children: [_embers[0]],\n        ),\n        ClipComponent.rectangle(\n          position: Vector2(600, 200),\n          size: Vector2.all(200),\n          children: [_embers[1]],\n        ),\n        ClipComponent.polygon(\n          points: [\n            Vector2(1, 0),\n            Vector2(1, 1),\n            Vector2(0, 1),\n            Vector2(1, 0),\n          ],\n          position: Vector2(200, 500),\n          size: Vector2.all(200),\n          children: [_embers[2]],\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/components/component_pool_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass ComponentPoolExample extends FlameGame {\n  static const String description =\n      'Tap on the screen to spawn a burst of pooled balls. '\n      'Watch the stats to see active vs pooled balls and observe '\n      'how the pool efficiently reuses objects.';\n\n  static const gameWidth = 800.0;\n  static const gameHeight = 600.0;\n\n  ComponentPoolExample()\n    : super(\n        world: _BallWorld(),\n        camera: CameraComponent.withFixedResolution(\n          width: gameWidth,\n          height: gameHeight,\n        ),\n      ) {\n    camera.moveTo(Vector2(gameWidth / 2, gameHeight / 2));\n  }\n}\n\nclass _BallWorld extends World with TapCallbacks {\n  late final ComponentPool<_PooledBall> ballPool;\n  late final _StatsDisplay statsDisplay;\n  final Random _random = Random();\n\n  static const ballsPerTap = 12;\n\n  @override\n  Future<void> onLoad() async {\n    // Add a background\n    add(\n      RectangleComponent(\n        size: Vector2(\n          ComponentPoolExample.gameWidth,\n          ComponentPoolExample.gameHeight,\n        ),\n        paint: Paint()..color = Colors.green,\n      ),\n    );\n\n    // Create a pool with a larger initial size to handle bursts\n    // and a maximum size to accommodate multiple ball bursts\n    ballPool = ComponentPool<_PooledBall>(\n      factory: _PooledBall.new,\n      initialSize: 30,\n      maxSize: 200,\n    );\n\n    // Add a stats display to show pool information\n    statsDisplay = _StatsDisplay(pool: ballPool);\n    await add(statsDisplay);\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    final tapPosition = event.localPosition;\n\n    for (var i = 0; i < ballsPerTap; i++) {\n      final ball = ballPool.acquire();\n      ball.position.setFrom(tapPosition);\n\n      // Create a spread pattern - balls go out in all directions\n      final angle = (i / ballsPerTap) * 2 * pi;\n      final speed = 150.0 + _random.nextDouble() * 100;\n      ball.velocity = Vector2(cos(angle) * speed, sin(angle) * speed);\n\n      // Add slight random variation to velocity\n      ball.velocity.add(\n        Vector2(\n          (_random.nextDouble() - 0.5) * 30,\n          (_random.nextDouble() - 0.5) * 30,\n        ),\n      );\n\n      add(ball);\n    }\n  }\n}\n\n/// A ball component that can be pooled.\n///\n/// Uses two child [CircleComponent]s for visuals: a shadow (rendered first via\n/// lower priority) and the ball itself on top. A bouncing scale effect\n/// simulates the ball bouncing up and down as it travels outward.\nclass _PooledBall extends PositionComponent\n    with HasGameReference, ParentIsA<_BallWorld> {\n  static const _radius = 4.0;\n  static const _bounceSpeed = 8.0;\n  static const _maxShadowOffset = 16.0;\n\n  Vector2 velocity = Vector2.zero();\n  double _bouncePhase = 0;\n\n  late final CircleComponent _shadow;\n  late final CircleComponent _ball;\n\n  _PooledBall() : super(anchor: Anchor.center);\n\n  @override\n  Future<void> onLoad() async {\n    _shadow = CircleComponent(\n      radius: _radius,\n      anchor: Anchor.center,\n      position: Vector2(0, _maxShadowOffset),\n      priority: 0,\n      paint: Paint()..color = Colors.black.withValues(alpha: 0.6),\n    );\n\n    _ball = CircleComponent(\n      radius: _radius,\n      anchor: Anchor.center,\n      priority: 1,\n      paint: Paint()\n        ..color = Colors.yellowAccent\n        ..style = PaintingStyle.fill,\n    );\n\n    addAll([_shadow, _ball]);\n  }\n\n  @override\n  void onMount() {\n    super.onMount();\n    // Only reset internal visual state — velocity and position are set by the\n    // caller between acquire() and add(), so we must not touch them here.\n    _bouncePhase = 0;\n    _ball.scale = Vector2.all(1);\n  }\n\n  @override\n  void update(double dt) {\n    position.add(velocity * dt);\n    _bouncePhase += dt * _bounceSpeed;\n\n    // t ranges from 0 (ground) to 1 (peak of bounce)\n    final t = (1 + sin(_bouncePhase)) / 2;\n\n    // Ball grows as it \"rises\" toward the camera\n    _ball.scale = Vector2.all(1.0 + 0.5 * t);\n\n    // Shadow drifts down and shrinks as ball rises away from ground\n    _shadow.position.y = _maxShadowOffset * t;\n    _shadow.scale = Vector2.all(1.0 - 0.2 * t);\n\n    // Check if ball is outside the game bounds\n    final isOutOfBounds =\n        position.x < 0 ||\n        position.x > ComponentPoolExample.gameWidth ||\n        position.y < 0 ||\n        position.y > ComponentPoolExample.gameHeight;\n\n    if (isOutOfBounds) {\n      removeFromParent();\n    }\n  }\n}\n\n/// Displays statistics about the ball pool.\nclass _StatsDisplay extends TextComponent with ParentIsA<_BallWorld> {\n  final ComponentPool<_PooledBall> pool;\n  int _activeBalls = 0;\n\n  _StatsDisplay({required this.pool})\n    : super(\n        position: Vector2(10, 10),\n        textRenderer: TextPaint(\n          style: const TextStyle(\n            color: Colors.white,\n            fontSize: 14,\n            fontWeight: FontWeight.bold,\n            shadows: [\n              Shadow(\n                offset: Offset(1, 1),\n                blurRadius: 2,\n              ),\n            ],\n          ),\n        ),\n      );\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    // Count active balls (in the world but not in the pool)\n    _activeBalls = parent.children.whereType<_PooledBall>().length;\n\n    text =\n        'Active Balls: $_activeBalls\\n'\n        'In Pool (Ready): ${pool.availableCount}\\n'\n        '\\nTap to spawn ${_BallWorld.ballsPerTap} balls!';\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/components/components.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/components/clip_component_example.dart';\nimport 'package:examples/stories/components/component_pool_example.dart';\nimport 'package:examples/stories/components/components_notifier_example.dart';\nimport 'package:examples/stories/components/components_notifier_provider_example.dart';\nimport 'package:examples/stories/components/composability_example.dart';\nimport 'package:examples/stories/components/debug_example.dart';\nimport 'package:examples/stories/components/has_visibility_example.dart';\nimport 'package:examples/stories/components/icon_component_example.dart';\nimport 'package:examples/stories/components/keys_example.dart';\nimport 'package:examples/stories/components/look_at_example.dart';\nimport 'package:examples/stories/components/look_at_smooth_example.dart';\nimport 'package:examples/stories/components/priority_example.dart';\nimport 'package:examples/stories/components/skip_text_box_component_example.dart';\nimport 'package:examples/stories/components/spawn_component_example.dart';\nimport 'package:examples/stories/components/time_scale_example.dart';\nimport 'package:flame/game.dart';\n\nvoid addComponentsStories(Dashbook dashbook) {\n  dashbook.storiesOf('Components')\n    ..add(\n      'Composability',\n      (_) => GameWidget(game: ComposabilityExample()),\n      codeLink: baseLink('components/composability_example.dart'),\n      info: ComposabilityExample.description,\n    )\n    ..add(\n      'Priority',\n      (_) => GameWidget(game: PriorityExample()),\n      codeLink: baseLink('components/priority_example.dart'),\n      info: PriorityExample.description,\n    )\n    ..add(\n      'Debug',\n      (_) => GameWidget(game: DebugExample()),\n      codeLink: baseLink('components/debug_example.dart'),\n      info: DebugExample.description,\n    )\n    ..add(\n      'ClipComponent',\n      (context) => GameWidget(game: ClipComponentExample()),\n      codeLink: baseLink('components/clip_component_example.dart'),\n      info: ClipComponentExample.description,\n    )\n    ..add(\n      'Component Pool',\n      (_) => const GameWidget.controlled(\n        gameFactory: ComponentPoolExample.new,\n      ),\n      codeLink: baseLink('components/component_pool_example.dart'),\n      info: ComponentPoolExample.description,\n    )\n    ..add(\n      'Look At',\n      (_) => GameWidget(game: LookAtExample()),\n      codeLink: baseLink('components/look_at_example.dart'),\n      info: LookAtExample.description,\n    )\n    ..add(\n      'Look At Smooth',\n      (_) => GameWidget(game: LookAtSmoothExample()),\n      codeLink: baseLink('components/look_at_smooth_example.dart'),\n      info: LookAtExample.description,\n    )\n    ..add(\n      'Component Notifier',\n      (_) => const ComponentsNotifierExampleWidget(),\n      codeLink: baseLink('components/components_notifier_example.dart'),\n      info: ComponentsNotifierExampleWidget.description,\n    )\n    ..add(\n      'Component Notifier (with provider)',\n      (_) => const ComponentsNotifierProviderExampleWidget(),\n      codeLink: baseLink(\n        'components/components_notifier_provider_example.dart',\n      ),\n      info: ComponentsNotifierProviderExampleWidget.description,\n    )\n    ..add(\n      'Spawn Component',\n      (_) => const GameWidget.controlled(\n        gameFactory: SpawnComponentExample.new,\n      ),\n      codeLink: baseLink('components/spawn_component_example.dart'),\n      info: SpawnComponentExample.description,\n    )\n    ..add(\n      'Time Scale',\n      (_) => const GameWidget.controlled(\n        gameFactory: TimeScaleExample.new,\n      ),\n      codeLink: baseLink('components/time_scale_example.dart'),\n      info: TimeScaleExample.description,\n    )\n    ..add(\n      'Component Keys',\n      (_) => const KeysExampleWidget(),\n      codeLink: baseLink('components/keys_example.dart'),\n      info: KeysExampleWidget.description,\n    )\n    ..add(\n      'Icon Component',\n      (_) => GameWidget(game: IconComponentExample()),\n      codeLink: baseLink('components/icon_component_example.dart'),\n      info: IconComponentExample.description,\n    )\n    ..add(\n      'HasVisibility',\n      (_) => GameWidget(game: HasVisibilityExample()),\n      codeLink: baseLink('components/has_visibility_example.dart'),\n      info: HasVisibilityExample.description,\n    )\n    ..add(\n      'Skip TextBoxComponent',\n      (_) => GameWidget(game: SkipTextBoxComponentExample()),\n      codeLink: baseLink('components/skip_text_box_component_example.dart'),\n      info: SkipTextBoxComponentExample.description,\n    );\n}\n"
  },
  {
    "path": "examples/lib/stories/components/components_notifier_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/widgets.dart';\nimport 'package:flutter/material.dart';\n\nclass ComponentsNotifierExampleWidget extends StatefulWidget {\n  const ComponentsNotifierExampleWidget({super.key});\n\n  static const String description = '''\n      Showcases how the components notifier can be used between\n      a flame game instance and widgets.\n\n      Tap the red dots to defeat the enemies and see the hud being updated\n      to reflect the current state of the game.\n''';\n\n  @override\n  State<ComponentsNotifierExampleWidget> createState() =>\n      _ComponentsNotifierExampleWidgetState();\n}\n\nclass _ComponentsNotifierExampleWidgetState\n    extends State<ComponentsNotifierExampleWidget> {\n  @override\n  void initState() {\n    super.initState();\n\n    game = ComponentNotifierExample();\n  }\n\n  late final ComponentNotifierExample game;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Stack(\n        children: [\n          Positioned.fill(\n            child: GameWidget(game: game),\n          ),\n          Positioned(\n            left: 16,\n            top: 16,\n            child: ComponentsNotifierBuilder<Enemy>(\n              notifier: game.componentsNotifier<Enemy>(),\n              builder: (context, notifier) {\n                return GameHud(\n                  remainingEnemies: notifier.components.length,\n                  onReplay: game.replay,\n                );\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass GameHud extends StatelessWidget {\n  const GameHud({\n    required this.remainingEnemies,\n    required this.onReplay,\n    super.key,\n  });\n\n  final int remainingEnemies;\n  final VoidCallback onReplay;\n\n  @override\n  Widget build(BuildContext context) {\n    return Card(\n      child: Padding(\n        padding: const EdgeInsets.all(16),\n        child: remainingEnemies == 0\n            ? ElevatedButton(\n                onPressed: onReplay,\n                child: const Text('Play again'),\n              )\n            : Text('Remaining enemies: $remainingEnemies'),\n      ),\n    );\n  }\n}\n\nclass Enemy extends CircleComponent with TapCallbacks, Notifier {\n  Enemy({super.position})\n    : super(\n        radius: 20,\n        paint: Paint()..color = const Color(0xFFFF0000),\n      );\n\n  @override\n  void onTapUp(_) {\n    removeFromParent();\n  }\n}\n\nclass ComponentNotifierExample extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    replay();\n  }\n\n  void replay() {\n    add(Enemy(position: Vector2(100, 100)));\n    add(Enemy(position: Vector2(200, 100)));\n    add(Enemy(position: Vector2(300, 100)));\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/components/components_notifier_provider_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass ComponentsNotifierProviderExampleWidget extends StatefulWidget {\n  const ComponentsNotifierProviderExampleWidget({super.key});\n\n  static const String description = '''\n      Similar to the Components Notifier example, but uses provider\n      instead of the built in ComponentsNotifierBuilder widget.\n''';\n\n  @override\n  State<ComponentsNotifierProviderExampleWidget> createState() =>\n      _ComponentsNotifierProviderExampleWidgetState();\n}\n\nclass _ComponentsNotifierProviderExampleWidgetState\n    extends State<ComponentsNotifierProviderExampleWidget> {\n  @override\n  void initState() {\n    super.initState();\n\n    game = ComponentNotifierExample();\n  }\n\n  late final ComponentNotifierExample game;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: MultiProvider(\n        providers: [\n          Provider<ComponentNotifierExample>.value(value: game),\n          ChangeNotifierProvider<ComponentsNotifier<Enemy>>(\n            create: (_) => game.componentsNotifier<Enemy>(),\n          ),\n        ],\n        child: Stack(\n          children: [\n            Positioned.fill(\n              child: GameWidget(game: game),\n            ),\n            const Positioned(\n              left: 16,\n              top: 16,\n              child: GameHud(),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass GameHud extends StatelessWidget {\n  const GameHud({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final enemies = context.watch<ComponentsNotifier<Enemy>>().components;\n\n    return Card(\n      child: Padding(\n        padding: const EdgeInsets.all(16),\n        child: enemies.isEmpty\n            ? ElevatedButton(\n                child: const Text('Play again'),\n                onPressed: () {\n                  context.read<ComponentNotifierExample>().replay();\n                },\n              )\n            : Text('Remaining enemies: ${enemies.length}'),\n      ),\n    );\n  }\n}\n\nclass Enemy extends CircleComponent with TapCallbacks, Notifier {\n  Enemy({super.position})\n    : super(\n        radius: 20,\n        paint: Paint()..color = const Color(0xFFFF0000),\n      );\n\n  @override\n  void onTapUp(_) {\n    removeFromParent();\n  }\n}\n\nclass ComponentNotifierExample extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    replay();\n  }\n\n  void replay() {\n    add(Enemy(position: Vector2(100, 100)));\n    add(Enemy(position: Vector2(200, 100)));\n    add(Enemy(position: Vector2(300, 100)));\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/components/composability_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/palette.dart';\n\nclass ComposabilityExample extends FlameGame {\n  static const String description = '''\n    In this example we showcase how you can add children to a component and how\n    they transform together with their parent, if the parent is a\n    `PositionComponent`. This example is not interactive.\n  ''';\n\n  late ParentSquare parentSquare;\n\n  @override\n  bool debugMode = true;\n\n  @override\n  Future<void> onLoad() async {\n    parentSquare = ParentSquare(Vector2.all(200), Vector2.all(300))\n      ..anchor = Anchor.center;\n    add(parentSquare);\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    parentSquare.angle += dt;\n  }\n}\n\nclass ParentSquare extends RectangleComponent with HasGameReference {\n  static final defaultPaint = BasicPalette.white.paint()\n    ..style = PaintingStyle.stroke;\n\n  ParentSquare(Vector2 position, Vector2 size)\n    : super(\n        position: position,\n        size: size,\n        paint: defaultPaint,\n      );\n\n  @override\n  Future<void> onLoad() async {\n    createChildren();\n  }\n\n  void createChildren() {\n    // All positions here are in relation to the parent's position\n    const childSize = 50.0;\n    final children = [\n      RectangleComponent.square(\n        position: Vector2(100, 100),\n        size: childSize,\n        angle: 2,\n        paint: defaultPaint,\n      ),\n      RectangleComponent.square(\n        position: Vector2(160, 100),\n        size: childSize,\n        angle: 3,\n        paint: defaultPaint,\n      ),\n      RectangleComponent.square(\n        position: Vector2(170, 150),\n        size: childSize,\n        angle: 4,\n        paint: defaultPaint,\n      ),\n      RectangleComponent.square(\n        position: Vector2(70, 200),\n        size: childSize,\n        angle: 5,\n        paint: defaultPaint,\n      ),\n    ];\n\n    addAll(children);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/components/debug_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\n\nclass DebugExample extends FlameGame {\n  static const String description = '''\n    In this example we show what you will see when setting `debugMode = true`\n    and add the `FPSTextComponent` to your game.\n    This is a non-interactive example.\n  ''';\n\n  @override\n  bool debugMode = true;\n\n  @override\n  Future<void> onLoad() async {\n    final flameLogo = await loadSprite('flame.png');\n\n    final flame1 = LogoComponent(flameLogo);\n    flame1.x = 100;\n    flame1.y = 400;\n\n    final flame2 = LogoComponent(flameLogo);\n    flame2.x = 100;\n    flame2.y = 400;\n    flame2.yDirection = -1;\n\n    final flame3 = LogoComponent(flameLogo);\n    flame3.x = 100;\n    flame3.y = 400;\n    flame3.xDirection = -1;\n\n    add(flame1);\n    add(flame2);\n    add(flame3);\n\n    add(FpsTextComponent(position: Vector2(0, size.y - 24)));\n  }\n}\n\nclass LogoComponent extends SpriteComponent\n    with HasGameReference<DebugExample> {\n  static const int speed = 150;\n\n  int xDirection = 1;\n  int yDirection = 1;\n\n  LogoComponent(Sprite sprite) : super(sprite: sprite, size: sprite.srcSize);\n\n  @override\n  void update(double dt) {\n    x += xDirection * speed * dt;\n\n    final rect = toRect();\n\n    if ((x <= 0 && xDirection == -1) ||\n        (rect.right >= game.size.x && xDirection == 1)) {\n      xDirection = xDirection * -1;\n    }\n\n    y += yDirection * speed * dt;\n\n    if ((y <= 0 && yDirection == -1) ||\n        (rect.bottom >= game.size.y && yDirection == 1)) {\n      yDirection = yDirection * -1;\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/components/has_visibility_example.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart' hide Timer;\nimport 'package:flame/game.dart';\n\nclass HasVisibilityExample extends FlameGame {\n  static const String description = '''\n    In this example we use the `HasVisibility` mixin to toggle the\n    visibility of a component without removing it from the parent\n    component.\n    This is a non-interactive example.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final flameLogoComponent = LogoComponent(await loadSprite('flame.png'));\n    add(flameLogoComponent);\n\n    // Toggle visibility every second\n    const oneSecDuration = Duration(seconds: 1);\n    Timer.periodic(\n      oneSecDuration,\n      (Timer t) => flameLogoComponent.isVisible = !flameLogoComponent.isVisible,\n    );\n  }\n}\n\nclass LogoComponent extends SpriteComponent with HasVisibility {\n  LogoComponent(Sprite sprite) : super(sprite: sprite, size: sprite.srcSize);\n}\n"
  },
  {
    "path": "examples/lib/stories/components/icon_component_example.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart' show IconData;\n\nclass IconComponentExample extends FlameGame {\n  static const String description = '''\n    In this example we showcase the `IconComponent`, which renders Flutter\n    `IconData` as Flame components. The icons are rasterized to images on load,\n    enabling paint-based effects like tinting, opacity, color effects, and\n    glow effects.\n  ''';\n\n  static const _heartIcon = IconData(\n    0xe87d,\n    fontFamily: 'MaterialIcons',\n  );\n  static const _homeIcon = IconData(\n    0xe318,\n    fontFamily: 'MaterialIcons',\n  );\n  static const _settingsIcon = IconData(\n    0xe8b8,\n    fontFamily: 'MaterialIcons',\n  );\n  static const _checkCircleIcon = IconData(\n    0xe86c,\n    fontFamily: 'MaterialIcons',\n  );\n  static const _flashOnIcon = IconData(\n    0xe3e7,\n    fontFamily: 'MaterialIcons',\n  );\n  static const _starIcon = IconData(\n    0xe838,\n    fontFamily: 'MaterialIcons',\n  );\n\n  @override\n  Future<void> onLoad() async {\n    // A row of star icons with different colors\n    final colors = [\n      const Color(0xFFFFD700), // Gold\n      const Color(0xFFFF4444), // Red\n      const Color(0xFF44AAFF), // Blue\n      const Color(0xFF44FF44), // Green\n      const Color(0xFFFF44FF), // Magenta\n    ];\n    final stars = colors.map((color) {\n      return IconComponent(\n        icon: _starIcon,\n        anchor: Anchor.center,\n      )..tint(color);\n    }).toList();\n\n    const spacing = 100.0;\n    final startX = (size.x - (stars.length - 1) * spacing) / 2;\n    for (var i = 0; i < stars.length; i++) {\n      stars[i].position = Vector2(startX + i * spacing, size.y * 0.2);\n      add(stars[i]);\n    }\n\n    // An icon with a pulsing opacity effect\n    final pulsingHeart = IconComponent(\n      icon: _heartIcon,\n      iconSize: 80,\n      anchor: Anchor.center,\n      position: Vector2(size.x / 2, size.y * 0.45),\n    )..tint(const Color(0xFFFF2222));\n    pulsingHeart.add(\n      OpacityEffect.to(\n        0.2,\n        EffectController(\n          duration: 1,\n          reverseDuration: 1,\n          infinite: true,\n        ),\n      ),\n    );\n    add(pulsingHeart);\n\n    // A row of various icons at the bottom\n    final miscEntries = [\n      (_heartIcon, const Color(0xFFFF6B6B)),\n      (_homeIcon, const Color(0xFF4FC3F7)),\n      (_settingsIcon, const Color(0xFFFFB74D)),\n      (_checkCircleIcon, const Color(0xFF81C784)),\n      (_flashOnIcon, const Color(0xFFCE93D8)),\n    ];\n    final miscIcons = miscEntries.map((entry) {\n      return IconComponent(\n        icon: entry.$1,\n        iconSize: 48,\n        anchor: Anchor.center,\n      )..tint(entry.$2);\n    }).toList();\n\n    final bottomStartX = (size.x - (miscIcons.length - 1) * spacing) / 2;\n    for (var i = 0; i < miscIcons.length; i++) {\n      miscIcons[i].position = Vector2(\n        bottomStartX + i * spacing,\n        size.y * 0.75,\n      );\n      add(miscIcons[i]);\n    }\n\n    // Add a rotating icon\n    final rotatingIcon = IconComponent(\n      icon: _settingsIcon,\n      anchor: Anchor.center,\n      position: Vector2(size.x / 2, size.y * 0.6),\n    )..tint(const Color(0xFFFFAB40));\n    rotatingIcon.add(\n      RotateEffect.by(\n        2 * pi,\n        EffectController(duration: 4, infinite: true),\n      ),\n    );\n    add(rotatingIcon);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/components/keys_example.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass KeysExampleWidget extends StatefulWidget {\n  const KeysExampleWidget({super.key});\n\n  static const String description = '''\n      Showcases how component keys can be used to find components\n      from a flame game instance.\n\n      Use the buttons to select or deselect the heroes.\n''';\n\n  @override\n  State<KeysExampleWidget> createState() => _KeysExampleWidgetState();\n}\n\nclass _KeysExampleWidgetState extends State<KeysExampleWidget> {\n  late final KeysExampleGame game = KeysExampleGame();\n\n  void selectHero(ComponentKey key) {\n    final hero = game.findByKey<SelectableClass>(key);\n    if (hero != null) {\n      hero.selected = !hero.selected;\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        Positioned.fill(\n          child: GameWidget(game: game),\n        ),\n        Positioned(\n          left: 20,\n          top: 222,\n          width: 300,\n          child: Row(\n            mainAxisAlignment: MainAxisAlignment.spaceBetween,\n            children: [\n              ElevatedButton(\n                onPressed: () {\n                  selectHero(ComponentKey.named('knight'));\n                },\n                child: const Text('Knight'),\n              ),\n              ElevatedButton(\n                onPressed: () {\n                  selectHero(ComponentKey.named('mage'));\n                },\n                child: const Text('Mage'),\n              ),\n              ElevatedButton(\n                onPressed: () {\n                  selectHero(ComponentKey.named('ranger'));\n                },\n                child: const Text('Ranger'),\n              ),\n            ],\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass KeysExampleGame extends FlameGame {\n  @override\n  FutureOr<void> onLoad() async {\n    await super.onLoad();\n\n    final knight = await loadSprite('knight.png');\n    final mage = await loadSprite('mage.png');\n    final ranger = await loadSprite('ranger.png');\n\n    await addAll([\n      SelectableClass(\n        key: ComponentKey.named('knight'),\n        sprite: knight,\n        size: Vector2.all(100),\n        position: Vector2(0, 100),\n      ),\n      SelectableClass(\n        key: ComponentKey.named('mage'),\n        sprite: mage,\n        size: Vector2.all(100),\n        position: Vector2(120, 100),\n      ),\n      SelectableClass(\n        key: ComponentKey.named('ranger'),\n        sprite: ranger,\n        size: Vector2.all(100),\n        position: Vector2(240, 100),\n      ),\n    ]);\n  }\n}\n\nclass SelectableClass extends SpriteComponent {\n  SelectableClass({\n    super.position,\n    super.size,\n    super.key,\n    super.sprite,\n  }) : super(paint: Paint()..color = Colors.white.withValues(alpha: 0.5));\n\n  bool _selected = false;\n  bool get selected => _selected;\n  set selected(bool value) {\n    _selected = value;\n    paint = Paint()\n      ..color = value ? Colors.white : Colors.white.withValues(alpha: 0.5);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/components/look_at_example.dart",
    "content": "import 'dart:async';\nimport 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame/sprite.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nclass LookAtExample extends FlameGame<_TapWorld>\n    with HasKeyboardHandlerComponents {\n  static const description =\n      'This example demonstrates how a component can be '\n      'made to look at a specific target using the lookAt method. Tap anywhere '\n      'to change the target point for both the choppers. '\n      'It also shows how nativeAngle can be used to make the component '\n      'oriented in the desired direction if the image is not facing the '\n      'correct direction.';\n\n  LookAtExample() : super(world: _TapWorld());\n\n  late List<_ChopperParent> _choppers;\n\n  @override\n  Color backgroundColor() => const Color.fromARGB(255, 96, 145, 112);\n\n  @override\n  Future<void> onLoad() async {\n    final spriteSheet = SpriteSheet(\n      image: await images.load('animations/chopper.png'),\n      srcSize: Vector2.all(48),\n    );\n\n    _spawnChoppers(spriteSheet);\n  }\n\n  void _spawnChoppers(SpriteSheet spriteSheet) {\n    _choppers = [\n      // Notice now the nativeAngle is set to pi because the chopper\n      // is facing in down/south direction in the original image.\n      _ChopperParent(\n        position: Vector2(0, -200),\n        chopper: SpriteAnimationComponent(\n          nativeAngle: pi,\n          size: Vector2.all(128),\n          anchor: Anchor.center,\n          animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05),\n        ),\n      ),\n      // This chopper does not use correct nativeAngle, hence using\n      // lookAt on it results in the sprite pointing in incorrect\n      // direction visually.\n      _ChopperParent(\n        position: Vector2(0, 200),\n        chopper: SpriteAnimationComponent(\n          size: Vector2.all(128),\n          anchor: Anchor.center,\n          animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05),\n        ),\n      ),\n    ];\n    world.addAll(_choppers);\n  }\n}\n\nclass _TapWorld extends World\n    with TapCallbacks, KeyboardHandler, HasGameReference<LookAtExample> {\n  final CircleComponent target = CircleComponent(\n    radius: 5,\n    anchor: Anchor.center,\n    paint: BasicPalette.black.paint(),\n  );\n\n  int _currentFlipIndex = 0;\n  final _flips = [\n    (Vector2(1, 1), Vector2(1, 1)),\n    (Vector2(1, 1), Vector2(1, -1)),\n    (Vector2(1, 1), Vector2(-1, 1)),\n    (Vector2(1, 1), Vector2(-1, -1)),\n    (Vector2(1, -1), Vector2(1, 1)),\n    (Vector2(1, -1), Vector2(1, -1)),\n    (Vector2(1, -1), Vector2(-1, 1)),\n    (Vector2(1, -1), Vector2(-1, -1)),\n    (Vector2(-1, 1), Vector2(1, 1)),\n    (Vector2(-1, 1), Vector2(1, -1)),\n    (Vector2(-1, 1), Vector2(-1, 1)),\n    (Vector2(-1, 1), Vector2(-1, -1)),\n    (Vector2(-1, -1), Vector2(1, 1)),\n    (Vector2(-1, -1), Vector2(1, -1)),\n    (Vector2(-1, -1), Vector2(-1, 1)),\n    (Vector2(-1, -1), Vector2(-1, -1)),\n  ];\n\n  @override\n  bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {\n    if (event is KeyDownEvent) {\n      if (keysPressed.contains(LogicalKeyboardKey.keyF)) {\n        _cycleFlips();\n        return true;\n      }\n    }\n    return false;\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    _updatePosition(event.localPosition);\n  }\n\n  void _cycleFlips() {\n    _currentFlipIndex = (_currentFlipIndex + 1) % _flips.length;\n    final nextFlip = _flips[_currentFlipIndex];\n    for (final parent in game._choppers) {\n      parent.scale = nextFlip.$1;\n      parent.chopper.scale = nextFlip.$2;\n    }\n  }\n\n  void _updatePosition(Vector2 position) {\n    if (!target.isMounted) {\n      add(target);\n    }\n    target.position = position;\n    for (final parent in game._choppers) {\n      parent.chopper.lookAt(position);\n    }\n  }\n}\n\nclass _ChopperParent extends PositionComponent\n    with HasGameReference<LookAtExample> {\n  final PositionComponent chopper;\n  late TextBoxComponent textBox;\n\n  _ChopperParent({\n    required super.position,\n    required this.chopper,\n  }) : super(children: [chopper]);\n\n  @override\n  FutureOr<void> onLoad() {\n    final shaded = TextPaint(\n      style: TextStyle(\n        color: BasicPalette.white.color,\n        fontSize: 30.0,\n        shadows: const [\n          Shadow(offset: Offset(1, 1), blurRadius: 1),\n        ],\n      ),\n    );\n    parent!.add(\n      textBox = TextBoxComponent(\n        text: '-',\n        position: position + Vector2(0, -150),\n        anchor: Anchor.center,\n        align: Anchor.topCenter,\n        textRenderer: shaded,\n        boxConfig: const TextBoxConfig(\n          maxWidth: 600,\n        ),\n      ),\n    );\n    return super.onLoad();\n  }\n\n  @override\n  void update(double dt) {\n    final angleTo = chopper.angleTo(game.world.target.position);\n    textBox.text =\n        '''\n      nativeAngle = ${chopper.nativeAngle.toStringAsFixed(2)}\n      angleTo = ${angleTo.toStringAsFixed(2)}\n      absoluteAngle = ${chopper.absoluteAngle.toStringAsFixed(2)}\n      absoluteScale = ${_asSigns(chopper.absoluteScale)} (${_asSigns(absoluteScale)} * ${_asSigns(chopper.scale)})\n    ''';\n  }\n\n  String _asSigns(Vector2 v) {\n    return '[${_asSign(v.x)}, ${_asSign(v.y)}]';\n  }\n\n  String _asSign(double value) {\n    return switch (value.sign) {\n      1 => '+',\n      -1 => '-',\n      _ => '0',\n    };\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/components/look_at_smooth_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame/sprite.dart';\nimport 'package:flutter/material.dart';\n\nclass LookAtSmoothExample extends FlameGame {\n  static const description =\n      'This example demonstrates how a component can be '\n      'made to smoothly rotate towards a target using the angleTo method. '\n      'Tap anywhere to change the target point for both the choppers. '\n      'It also shows how nativeAngle can be used to make the component '\n      'oriented in the desired direction if the image is not facing the '\n      'correct direction.';\n\n  LookAtSmoothExample() : super(world: _TapWorld());\n\n  late SpriteAnimationComponent _chopper1;\n  late SpriteAnimationComponent _chopper2;\n\n  @override\n  Color backgroundColor() => const Color.fromARGB(255, 96, 145, 112);\n\n  @override\n  Future<void> onLoad() async {\n    final spriteSheet = SpriteSheet(\n      image: await images.load('animations/chopper.png'),\n      srcSize: Vector2.all(48),\n    );\n\n    _spawnChoppers(spriteSheet);\n    _spawnInfoText();\n  }\n\n  void _spawnChoppers(SpriteSheet spriteSheet) {\n    // Notice now the nativeAngle is set to pi because the chopper\n    // is facing in down/south direction in the original image.\n    world.add(\n      _chopper1 = SpriteAnimationComponent(\n        nativeAngle: pi,\n        size: Vector2.all(128),\n        anchor: Anchor.center,\n        animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05),\n      ),\n    );\n\n    // This chopper does not use correct nativeAngle, hence using\n    // lookAt on it results in the sprite pointing in incorrect\n    // direction visually.\n    world.add(\n      _chopper2 = SpriteAnimationComponent(\n        size: Vector2.all(128),\n        anchor: Anchor.center,\n        animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05),\n        position: Vector2(0, 160),\n      ),\n    );\n  }\n\n  // Just displays some information. No functional contribution to the example.\n  void _spawnInfoText() {\n    final shaded = TextPaint(\n      style: TextStyle(\n        color: BasicPalette.white.color,\n        fontSize: 30.0,\n        shadows: const [\n          Shadow(offset: Offset(1, 1), blurRadius: 1),\n        ],\n      ),\n    );\n\n    world.add(\n      TextComponent(\n        text: 'nativeAngle = pi',\n        textRenderer: shaded,\n        anchor: Anchor.center,\n        position: _chopper1.absolutePosition + Vector2(0, -70),\n      ),\n    );\n\n    world.add(\n      TextComponent(\n        text: 'nativeAngle = 0',\n        textRenderer: shaded,\n        anchor: Anchor.center,\n        position: _chopper2.absolutePosition + Vector2(0, -70),\n      ),\n    );\n  }\n}\n\nclass _TapWorld extends World with TapCallbacks {\n  bool _isRotating = false;\n\n  final CircleComponent _targetComponent = CircleComponent(\n    radius: 5,\n    anchor: Anchor.center,\n    paint: BasicPalette.black.paint(),\n  );\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    if (!_targetComponent.isMounted) {\n      add(_targetComponent);\n    }\n\n    // Ignore if choppers are already rotating.\n    if (!_isRotating) {\n      _isRotating = true;\n      _targetComponent.position = event.localPosition;\n\n      final choppers = children.query<SpriteAnimationComponent>();\n      for (final chopper in choppers) {\n        chopper.add(\n          RotateEffect.by(\n            chopper.angleTo(_targetComponent.absolutePosition),\n            LinearEffectController(1),\n            onComplete: () => _isRotating = false,\n          ),\n        );\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/components/priority_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\n\nclass PriorityExample extends FlameGame {\n  static const String description = '''\n    On this example, click on the square to bring them to the front by changing\n    the priority.\n  ''';\n\n  PriorityExample()\n    : super(\n        children: [\n          Square(Vector2(100, 100)),\n          Square(Vector2(160, 100)),\n          Square(Vector2(170, 150)),\n          Square(Vector2(110, 150)),\n        ],\n      );\n}\n\nclass Square extends RectangleComponent\n    with HasGameReference<PriorityExample>, TapCallbacks {\n  Square(Vector2 position)\n    : super(\n        position: position,\n        size: Vector2.all(100),\n        paint: PaintExtension.random(withAlpha: 0.9, base: 100),\n      );\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    final topComponent = game.children.last;\n    if (topComponent != this) {\n      priority = topComponent.priority + 1;\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/components/skip_text_box_component_example.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\n\nclass SkipTextBoxComponentExample extends FlameGame {\n  static const String description = '''\n    On this example, click on the \"Skip\" button to display all the text at once.\n  ''';\n\n  @override\n  FutureOr<void> onLoad() {\n    final textBoxComponent = TextBoxComponent(\n      text: samplePassage,\n      position: Vector2(48, 48 * 2),\n      boxConfig: const TextBoxConfig(\n        maxWidth: 480,\n        timePerChar: 0.01,\n      ),\n    );\n    addAll([\n      ButtonComponent(\n        position: Vector2(48, 48),\n        button: TextComponent(text: 'Skip'),\n        onReleased: textBoxComponent.skip,\n      ),\n      textBoxComponent,\n    ]);\n  }\n\n  static const String samplePassage = '''\nLook again at that dot. That's here. That's home. That's us. On it everyone you love, everyone you know, everyone you ever heard of, every human being who ever was, lived out their lives. The aggregate of our joy and suffering, thousands of confident religions, ideologies, and economic doctrines, every hunter and forager, every hero and coward, every creator and destroyer of civilization, every king and peasant, every young couple in love, every mother and father, hopeful child, inventor and explorer, every teacher of morals, every corrupt politician, every \"superstar,\" every \"supreme leader,\" every saint and sinner in the history of our species lived there--on a mote of dust suspended in a sunbeam.\n\nThe Earth is a very small stage in a vast cosmic arena. Think of the rivers of blood spilled by all those generals and emperors so that, in glory and triumph, they could become the momentary masters of a fraction of a dot. Think of the endless cruelties visited by the inhabitants of one corner of this pixel on the scarcely distinguishable inhabitants of some other corner, how frequent their misunderstandings, how eager they are to kill one another, how fervent their hatreds.\n\nOur posturings, our imagined self-importance, the delusion that we have some privileged position in the Universe, are challenged by this point of pale light. Our planet is a lonely speck in the great enveloping cosmic dark. In our obscurity, in all this vastness, there is no hint that help will come from elsewhere to save us from ourselves.\n\nThe Earth is the only world known so far to harbor life. There is nowhere else, at least in the near future, to which our species could migrate. Visit, yes. Settle, not yet. Like it or not, for the moment the Earth is where we make our stand.\n\nIt has been said that astronomy is a humbling and character-building experience. There is perhaps no better demonstration of the folly of human conceits than this distant image of our tiny world. To me, it underscores our responsibility to deal more kindly with one another, and to preserve and cherish the pale blue dot, the only home we've ever known.\n\n— Carl Sagan, Pale Blue Dot, 1994\n''';\n}\n"
  },
  {
    "path": "examples/lib/stories/components/spawn_component_example.dart",
    "content": "import 'package:examples/commons/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/math.dart';\n\nclass SpawnComponentExample extends FlameGame {\n  static const String description =\n      'Tap on the screen to start spawning Embers within different shapes.';\n\n  SpawnComponentExample() : super(world: SpawnComponentWorld());\n}\n\nclass SpawnComponentWorld extends World with TapCallbacks {\n  @override\n  void onTapDown(TapDownEvent info) {\n    final shapeType = Shapes.values.random();\n\n    final position = info.localPosition;\n    final shape = switch (shapeType) {\n      Shapes.rectangle => Rectangle.fromCenter(\n        center: position,\n        size: Vector2.all(200),\n      ),\n      Shapes.circle => Circle(position, 150),\n      Shapes.polygon => Polygon(\n        [\n          Vector2(-1.0, 0.0),\n          Vector2(-0.8, 0.6),\n          Vector2(0.0, 1.0),\n          Vector2(0.6, 0.9),\n          Vector2(1.0, 0.0),\n          Vector2(0.3, -0.2),\n          Vector2(0.0, -1.0),\n          Vector2(-0.8, -0.5),\n        ].map((vertex) {\n          return vertex\n            ..scale(200)\n            ..add(position);\n        }).toList(),\n      ),\n    };\n\n    add(\n      SpawnComponent(\n        factory: (_) => Ember(),\n        period: 0.5,\n        area: shape,\n        within: randomFallback.nextBool(),\n      ),\n    );\n  }\n}\n\nenum Shapes {\n  rectangle,\n  circle,\n  polygon,\n}\n"
  },
  {
    "path": "examples/lib/stories/components/time_scale_example.dart",
    "content": "import 'dart:async';\nimport 'dart:math';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/image_composition.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame/sprite.dart';\nimport 'package:flutter/rendering.dart';\n\nclass TimeScaleExample extends FlameGame\n    with HasTimeScale, HasCollisionDetection {\n  static const description =\n      'This example shows how time scale can be used to control game speed.';\n\n  TimeScaleExample()\n    : super(\n        camera: CameraComponent.withFixedResolution(\n          width: 640,\n          height: 360,\n        ),\n      );\n\n  final gameSpeedText = TextComponent(\n    text: 'Time Scale: 1',\n    textRenderer: TextPaint(\n      style: TextStyle(\n        color: BasicPalette.white.color,\n        fontSize: 20.0,\n        shadows: const [\n          Shadow(offset: Offset(1, 1), blurRadius: 1),\n        ],\n      ),\n    ),\n    anchor: Anchor.center,\n  );\n\n  @override\n  Color backgroundColor() => const Color.fromARGB(255, 88, 114, 97);\n\n  @override\n  Future<void> onLoad() async {\n    final spriteSheet = SpriteSheet(\n      image: await images.load('animations/chopper.png'),\n      srcSize: Vector2.all(48),\n    );\n    gameSpeedText.position = Vector2(size.x * 0.5, size.y * 0.8);\n\n    await world.addAll([\n      _Chopper(\n        position: Vector2(-100, -10),\n        size: Vector2.all(64),\n        anchor: Anchor.center,\n        angle: -pi / 2,\n        animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05),\n      ),\n      _Chopper(\n        position: Vector2(100, 10),\n        size: Vector2.all(64),\n        anchor: Anchor.center,\n        angle: pi / 2,\n        animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05),\n      ),\n      gameSpeedText,\n    ]);\n    return super.onLoad();\n  }\n\n  @override\n  void update(double dt) {\n    gameSpeedText.text = 'Time Scale : $timeScale';\n    super.update(dt);\n  }\n}\n\nclass _Chopper extends SpriteAnimationComponent\n    with HasGameReference<TimeScaleExample>, CollisionCallbacks {\n  _Chopper({\n    super.animation,\n    super.position,\n    super.size,\n    super.angle,\n    super.anchor,\n  }) : _moveDirection = Vector2(0, 1)..rotate(angle ?? 0),\n       _initialPosition = position?.clone() ?? Vector2.zero();\n\n  final Vector2 _moveDirection;\n  final _speed = 80.0;\n  final Vector2 _initialPosition;\n  late final _timer = TimerComponent(\n    period: 2,\n    onTick: _reset,\n    autoStart: false,\n  );\n\n  @override\n  Future<void> onLoad() async {\n    await add(CircleHitbox());\n    await add(_timer);\n    return super.onLoad();\n  }\n\n  @override\n  void updateTree(double dt) {\n    position.setFrom(position + _moveDirection * _speed * dt);\n    super.updateTree(dt);\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    if (other is _Chopper) {\n      game.timeScale = 0.25;\n    }\n    super.onCollisionStart(intersectionPoints, other);\n  }\n\n  @override\n  void onCollisionEnd(PositionComponent other) {\n    if (other is _Chopper) {\n      game.timeScale = 1.0;\n      _timer.timer.start();\n    }\n    super.onCollisionEnd(other);\n  }\n\n  void _reset() {\n    position.setFrom(_initialPosition);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/effects/color_effect_example.dart",
    "content": "import 'package:examples/commons/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass ColorEffectExample extends FlameGame {\n  static const String description = '''\n    In this example we show how the `ColorEffect` can be used.\n    Ember will constantly pulse in and out of a blue color.\n  ''';\n\n  late final SpriteComponent sprite;\n\n  @override\n  Future<void> onLoad() async {\n    add(\n      Ember(\n        position: Vector2(180, 230),\n        size: Vector2.all(100),\n      )..add(\n        ColorEffect(\n          Colors.blue,\n          EffectController(\n            duration: 1.5,\n            reverseDuration: 1.5,\n            infinite: true,\n          ),\n          // Means, applies from 0% to 80% of the color\n          opacityTo: 0.8,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/effects/combined_effect_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\n\nclass CombinedEffectExample extends FlameGame {\n  static const String description = '''\n    A combination of effects, consisting of a move effect, a rotate effect, and\n    a scale effect. The combination of effects then runs in the opposite order\n    (alternate = true) and loops infinitely (infinite = true).\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    EffectController duration(double x) => EffectController(duration: x);\n    add(\n      Player()\n        ..position = Vector2(200, 300)\n        ..add(\n          CombinedEffect(\n            [\n              MoveEffect.by(Vector2(200, 0), duration(1)),\n              RotateEffect.by(tau / 4, duration(2)),\n              ScaleEffect.by(Vector2.all(1.5), duration(1)),\n            ],\n            alternate: true,\n            infinite: true,\n          ),\n        ),\n    );\n  }\n}\n\nclass Player extends PositionComponent {\n  Player()\n    : path = Path()\n        ..lineTo(40, 20)\n        ..lineTo(0, 40)\n        ..quadraticBezierTo(8, 20, 0, 0)\n        ..close(),\n      bodyPaint = Paint()..color = const Color(0x887F99B3),\n      borderPaint = Paint()\n        ..style = PaintingStyle.stroke\n        ..strokeWidth = 3\n        ..color = const Color(0xFFFFFD9A),\n      super(anchor: Anchor.center, size: Vector2(40, 40));\n\n  final Path path;\n  final Paint borderPaint;\n  final Paint bodyPaint;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawPath(path, bodyPaint);\n    canvas.drawPath(path, borderPaint);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/effects/dual_effect_removal_example.dart",
    "content": "import 'package:flame/components.dart';\r\nimport 'package:flame/effects.dart';\r\nimport 'package:flame/events.dart';\r\nimport 'package:flame/game.dart';\r\nimport 'package:flutter/material.dart';\r\n\r\nclass DualEffectRemovalExample extends FlameGame with TapCallbacks {\r\n  static const String description = '''\r\n    In this example we show how a dual effect can be used and removed.\r\n    To remove an effect, tap anywhere on the screen and the first tap will\r\n    remove the OpacityEffect and the second tap removes the ColorEffect.\r\n    In this example, when an effect is removed the component is reset to\r\n    the state (the part of the state that was affected by the running effect)\r\n    that it had before the effect started running.\r\n  ''';\r\n\r\n  late ColorEffect colorEffect;\r\n  late OpacityEffect opacityEffect;\r\n\r\n  @override\r\n  Future<void> onLoad() async {\r\n    final mySprite = SpriteComponent(\r\n      sprite: await loadSprite('flame.png'),\r\n      position: Vector2(50, 50),\r\n    );\r\n\r\n    add(mySprite);\r\n\r\n    final colorController = EffectController(\r\n      duration: 2,\r\n      reverseDuration: 2,\r\n      infinite: true,\r\n    );\r\n    colorEffect = ColorEffect(\r\n      Colors.blue,\r\n      colorController,\r\n      opacityTo: 0.8,\r\n    );\r\n    mySprite.add(colorEffect);\r\n\r\n    final opacityController = EffectController(\r\n      duration: 1,\r\n      reverseDuration: 1,\r\n      infinite: true,\r\n    );\r\n    opacityEffect = OpacityEffect.fadeOut(opacityController);\r\n    mySprite.add(opacityEffect);\r\n  }\r\n\r\n  @override\r\n  void onTapDown(_) {\r\n    // apply(0) sends the animation to its initial starting state.\r\n    // If this isn't called, the effect would be removed and leave the\r\n    // component at its current state.\r\n    // Hence when you want an effect to be removed and the component to go\r\n    // back to how it looked prior to the effect, you must call apply(0) before\r\n    // you call removeFromParent().\r\n    if (opacityEffect.isMounted) {\r\n      opacityEffect.apply(0);\r\n      opacityEffect.removeFromParent();\r\n    } else if (colorEffect.isMounted) {\r\n      colorEffect.apply(0);\r\n      colorEffect.removeFromParent();\r\n    }\r\n  }\r\n}\r\n"
  },
  {
    "path": "examples/lib/stories/effects/effect_controllers_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/camera.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\n\nclass EffectControllersExample extends FlameGame {\n  static const description = '''\n    This page demonstrates application of various non-standard effect \n    controllers.\n\n    The first white square has a ZigzagEffectController with period 1. The\n    orange square next to it has two move effects, each with a\n    ZigzagEffectController.\n\n    The lime square has a SineEffectController with the same period of 1s. The\n    violet square next to it has two move effects, each with a\n    SineEffectController with periods, but one of the effects is slightly \n    delayed.\n  ''';\n\n  EffectControllersExample()\n    : super(\n        camera: CameraComponent.withFixedResolution(\n          width: 400,\n          height: 600,\n        ),\n        world: _EffectControllerWorld(),\n      );\n}\n\nclass _EffectControllerWorld extends World {\n  @override\n  void onLoad() {\n    add(\n      RectangleComponent.square(\n        position: Vector2(-140, 0),\n        size: 20,\n      )..add(\n        MoveEffect.by(\n          Vector2(0, 20),\n          InfiniteEffectController(ZigzagEffectController(period: 1)),\n        ),\n      ),\n    );\n    add(\n      RectangleComponent.square(\n        position: Vector2(-50, 0),\n        size: 20,\n        paint: Paint()..color = const Color(0xffffbc63),\n      )..addAll([\n        MoveEffect.by(\n          Vector2(0, 20),\n          InfiniteEffectController(ZigzagEffectController(period: 8 / 7)),\n        ),\n        MoveEffect.by(\n          Vector2(10, 0),\n          InfiniteEffectController(ZigzagEffectController(period: 2 / 3)),\n        ),\n      ]),\n    );\n\n    add(\n      RectangleComponent.square(\n        position: Vector2(50, 0),\n        size: 20,\n        paint: Paint()..color = const Color(0xffbeff63),\n      )..add(\n        MoveEffect.by(\n          Vector2(0, 20),\n          InfiniteEffectController(SineEffectController(period: 1)),\n        ),\n      ),\n    );\n    add(\n      RectangleComponent.square(\n        position: Vector2(140, 0),\n        size: 10,\n        paint: Paint()..color = const Color(0xffb663ff),\n      )..addAll([\n        MoveEffect.by(\n          Vector2(0, 20),\n          InfiniteEffectController(SineEffectController(period: 1))\n            ..advance(0.25),\n        ),\n        MoveEffect.by(\n          Vector2(10, 0),\n          InfiniteEffectController(SineEffectController(period: 1)),\n        ),\n      ]),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/effects/effects.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/effects/color_effect_example.dart';\nimport 'package:examples/stories/effects/combined_effect_example.dart';\nimport 'package:examples/stories/effects/dual_effect_removal_example.dart';\nimport 'package:examples/stories/effects/effect_controllers_example.dart';\nimport 'package:examples/stories/effects/function_effect_example.dart';\nimport 'package:examples/stories/effects/hue_effect_example.dart';\nimport 'package:examples/stories/effects/move_effect_example.dart';\nimport 'package:examples/stories/effects/opacity_effect_example.dart';\nimport 'package:examples/stories/effects/remove_effect_example.dart';\nimport 'package:examples/stories/effects/rotate_around_effect_example.dart';\nimport 'package:examples/stories/effects/rotate_effect_example.dart';\nimport 'package:examples/stories/effects/scale_effect_example.dart';\nimport 'package:examples/stories/effects/sequence_effect_example.dart';\nimport 'package:examples/stories/effects/size_effect_example.dart';\nimport 'package:flame/game.dart';\n\nvoid addEffectsStories(Dashbook dashbook) {\n  dashbook.storiesOf('Effects')\n    ..add(\n      'Move Effect',\n      (_) => GameWidget(game: MoveEffectExample()),\n      codeLink: baseLink('effects/move_effect_example.dart'),\n      info: MoveEffectExample.description,\n    )\n    ..add(\n      'Dual Effect Removal',\n      (_) => GameWidget(game: DualEffectRemovalExample()),\n      codeLink: baseLink('effects/dual_effect_removal_example.dart'),\n      info: DualEffectRemovalExample.description,\n    )\n    ..add(\n      'Rotate Effect',\n      (_) => GameWidget(game: RotateEffectExample()),\n      codeLink: baseLink('effects/rotate_effect_example.dart'),\n      info: RotateEffectExample.description,\n    )\n    ..add(\n      'Rotate Around Effect',\n      (_) => GameWidget(game: RotateAroundEffectExample()),\n      codeLink: baseLink('effects/rotate_around_effect_example.dart'),\n      info: RotateAroundEffectExample.description,\n    )\n    ..add(\n      'Size Effect',\n      (_) => GameWidget(game: SizeEffectExample()),\n      codeLink: baseLink('effects/size_effect_example.dart'),\n      info: SizeEffectExample.description,\n    )\n    ..add(\n      'Scale Effect',\n      (_) => GameWidget(game: ScaleEffectExample()),\n      codeLink: baseLink('effects/scale_effect_example.dart'),\n      info: ScaleEffectExample.description,\n    )\n    ..add(\n      'Opacity Effect',\n      (_) => GameWidget(game: OpacityEffectExample()),\n      codeLink: baseLink('effects/opacity_effect_example.dart'),\n      info: OpacityEffectExample.description,\n    )\n    ..add(\n      'Hue Effect',\n      (_) => GameWidget(game: HueEffectExample()),\n      codeLink: baseLink('effects/hue_effect_example.dart'),\n      info: HueEffectExample.description,\n    )\n    ..add(\n      'Color Effect',\n      (_) => GameWidget(game: ColorEffectExample()),\n      codeLink: baseLink('effects/color_effect_example.dart'),\n      info: ColorEffectExample.description,\n    )\n    ..add(\n      'Sequence Effect',\n      (_) => GameWidget(game: SequenceEffectExample()),\n      codeLink: baseLink('effects/sequence_effect_example.dart'),\n      info: SequenceEffectExample.description,\n    )\n    ..add(\n      'Combined Effect',\n      (_) => GameWidget(game: CombinedEffectExample()),\n      codeLink: baseLink('effects/combined_effect_example.dart'),\n      info: CombinedEffectExample.description,\n    )\n    ..add(\n      'Remove Effect',\n      (_) => GameWidget(game: RemoveEffectExample()),\n      codeLink: baseLink('effects/remove_effect_example.dart'),\n      info: RemoveEffectExample.description,\n    )\n    ..add(\n      'Function Effect',\n      (_) => GameWidget(game: FunctionEffectExample()),\n      codeLink: baseLink('effects/function_effect_example.dart'),\n      info: FunctionEffectExample.description,\n    )\n    ..add(\n      'EffectControllers',\n      (_) => GameWidget(game: EffectControllersExample()),\n      codeLink: baseLink('effects/effect_controllers_example.dart'),\n      info: EffectControllersExample.description,\n    );\n}\n"
  },
  {
    "path": "examples/lib/stories/effects/function_effect_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\n\nenum RobotState {\n  idle,\n  running,\n}\n\nclass FunctionEffectExample extends FlameGame with TapCallbacks {\n  static const String description = '''\nThis example shows how to use the FunctionEffect to create custom effects.\n\nThe robot will switch between running and idle animations over the duration of\n10 seconds.\n''';\n\n  @override\n  Future<void> onLoad() async {\n    final running = await loadSpriteAnimation(\n      'animations/robot.png',\n      SpriteAnimationData.sequenced(\n        amount: 8,\n        stepTime: 0.2,\n        textureSize: Vector2(16, 18),\n      ),\n    );\n    final idle = await loadSpriteAnimation(\n      'animations/robot-idle.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        stepTime: 0.4,\n        textureSize: Vector2(16, 18),\n      ),\n    );\n    final robotSize = Vector2(64, 72);\n\n    final functionEffect =\n        FunctionEffect<SpriteAnimationGroupComponent<RobotState>>(\n          (target, progress) {\n            if (progress > 0.7) {\n              target.current = RobotState.idle;\n            } else if (progress > 0.3) {\n              target.current = RobotState.running;\n            }\n          },\n          EffectController(duration: 10.0, infinite: true),\n        );\n    final component = SpriteAnimationGroupComponent<RobotState>(\n      animations: {\n        RobotState.running: running,\n        RobotState.idle: idle,\n      },\n      current: RobotState.idle,\n      position: size / 2,\n      anchor: Anchor.center,\n      size: robotSize,\n      children: [functionEffect],\n    );\n\n    add(component);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/effects/glow_effect_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(GameWidget(game: GlowEffectExample()));\n}\n\nclass GlowEffectExample extends FlameGame with TapCallbacks {\n  static const String description = '''\n    In this example we show how the `GlowEffect` can be used.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final paint = Paint()\n      ..color = const Color(0xff39FF14)\n      ..style = PaintingStyle.stroke;\n\n    add(\n      CircleComponent(\n        radius: 50,\n        position: Vector2(300, 400),\n        paint: paint,\n      )..add(\n        GlowEffect(\n          10.0,\n          EffectController(\n            duration: 3,\n            reverseDuration: 1.5,\n            infinite: true,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/effects/hue_effect_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:examples/commons/ember.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\n\nclass HueEffectExample extends FlameGame {\n  static const String description = '''\nIn this example we show how the `HueEffect` can be used.\nEmber will shift its hue over time.\n''';\n\n  @override\n  Future<void> onLoad() async {\n    add(\n      Ember(\n        position: size / 2,\n        size: Vector2.all(100),\n      )..add(\n        HueEffect.by(\n          2 * pi,\n          EffectController(\n            duration: 3,\n            infinite: true,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/effects/move_effect_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame_noise/flame_noise.dart';\nimport 'package:flutter/material.dart';\n\nclass MoveEffectExample extends FlameGame {\n  static const description = '''\n    Top square has `MoveEffect.to` effect that makes the component move along a\n    straight line back and forth. The effect uses a non-linear progression\n    curve, which makes the movement non-uniform.\n\n    The middle green square has a combination of two movement effects: a\n    `MoveEffect.to` and a `MoveEffect.by` which forces it to periodically jump.\n\n    The purple square executes a sequence of shake effects.\n\n    At the bottom there are 60 more components which demonstrate movement along\n    an arbitrary path using `MoveEffect.along`.\n  ''';\n\n  MoveEffectExample()\n    : super(\n        camera: CameraComponent.withFixedResolution(\n          width: 400,\n          height: 600,\n        )..viewfinder.anchor = Anchor.topLeft,\n        world: _MoveEffectWorld(),\n      );\n}\n\nclass _MoveEffectWorld extends World {\n  @override\n  void onLoad() {\n    final paint1 = Paint()\n      ..style = PaintingStyle.stroke\n      ..strokeWidth = 5.0\n      ..color = Colors.deepOrange;\n    final paint2 = Paint()\n      ..style = PaintingStyle.stroke\n      ..strokeWidth = 5.0\n      ..color = Colors.greenAccent;\n    final paint3 = Paint()..color = const Color(0xffb372dc);\n\n    // Red square, moving back and forth\n    add(\n      RectangleComponent.square(\n        position: Vector2(20, 50),\n        size: 20,\n        paint: paint1,\n      )..add(\n        MoveEffect.to(\n          Vector2(380, 50),\n          EffectController(\n            duration: 3,\n            reverseDuration: 3,\n            infinite: true,\n            curve: Curves.easeOut,\n          ),\n        ),\n      ),\n    );\n\n    // Green square, moving and jumping\n    add(\n      RectangleComponent.square(\n          position: Vector2(20, 150),\n          size: 20,\n          paint: paint2,\n        )\n        ..add(\n          MoveEffect.to(\n            Vector2(380, 150),\n            EffectController(\n              duration: 3,\n              reverseDuration: 3,\n              infinite: true,\n            ),\n          ),\n        )\n        ..add(\n          MoveEffect.by(\n            Vector2(0, -50),\n            EffectController(\n              duration: 0.25,\n              reverseDuration: 0.25,\n              startDelay: 1,\n              atMinDuration: 2,\n              curve: Curves.ease,\n              infinite: true,\n            ),\n          ),\n        ),\n    );\n\n    // Purple square, vibrating from two noise controllers.\n    add(\n      RectangleComponent.square(\n        size: 15,\n        position: Vector2(40, 240),\n        paint: paint3,\n      )..add(\n        SequenceEffect(\n          [\n            MoveEffect.by(\n              Vector2(5, 0),\n              NoiseEffectController(\n                duration: 1,\n                noise: PerlinNoise(frequency: 20),\n              ),\n            ),\n            MoveEffect.by(Vector2.zero(), LinearEffectController(2)),\n            MoveEffect.by(\n              Vector2(0, 10),\n              NoiseEffectController(\n                duration: 1,\n                noise: PerlinNoise(frequency: 10),\n              ),\n            ),\n          ],\n          infinite: true,\n        ),\n      ),\n    );\n\n    // A circle of moving rectangles.\n    final path2 = Path()..addOval(const Rect.fromLTRB(80, 230, 320, 470));\n    for (var i = 0; i < 20; i++) {\n      add(\n        RectangleComponent.square(size: 10)\n          ..position = Vector2(i * 10, 0)\n          ..paint = (Paint()..color = Colors.tealAccent)\n          ..add(\n            MoveAlongPathEffect(\n              path2,\n              EffectController(\n                duration: 6,\n                startDelay: i * 0.3,\n                infinite: true,\n              ),\n              absolute: true,\n              oriented: true,\n            ),\n          ),\n      );\n    }\n\n    // A star of moving rectangles.\n    final path1 = Path()..moveTo(200, 250);\n    for (var i = 1; i <= 5; i++) {\n      final x = 200 + 100 * sin(i * tau * 2 / 5);\n      final y = 350 - 100 * cos(i * tau * 2 / 5);\n      path1.lineTo(x, y);\n    }\n    for (var i = 0; i < 40; i++) {\n      add(\n        CircleComponent(radius: 5)\n          ..position = Vector2(i * 10, 0)\n          ..add(\n            MoveAlongPathEffect(\n              path1,\n              EffectController(\n                duration: 10,\n                startDelay: i * 0.2,\n                infinite: true,\n              ),\n              absolute: true,\n            ),\n          ),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/effects/opacity_effect_example.dart",
    "content": "import 'package:examples/commons/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\n\nclass OpacityEffectExample extends FlameGame with TapCallbacks {\n  static const String description = '''\n    In this example we show how the `OpacityEffect` can be used in two ways.\n    The left Ember will constantly pulse in and out of opacity and the right\n    flame will change opacity when you click the screen.\n  ''';\n\n  late final SpriteComponent sprite;\n\n  @override\n  Future<void> onLoad() async {\n    final flameSprite = await loadSprite('flame.png');\n    add(\n      sprite = SpriteComponent(\n        sprite: flameSprite,\n        position: Vector2(300, 100),\n        size: Vector2(149, 211),\n      ),\n    );\n\n    add(\n      Ember(\n        position: Vector2(180, 230),\n        size: Vector2.all(100),\n      )..add(\n        OpacityEffect.fadeOut(\n          EffectController(\n            duration: 1.5,\n            reverseDuration: 1.5,\n            infinite: true,\n          ),\n        ),\n      ),\n    );\n  }\n\n  @override\n  void onTapDown(_) {\n    final opacity = sprite.paint.color.a;\n    if (opacity >= 0.5) {\n      sprite.add(OpacityEffect.fadeOut(EffectController(duration: 1)));\n    } else {\n      sprite.add(OpacityEffect.fadeIn(EffectController(duration: 1)));\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/effects/remove_effect_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass RemoveEffectExample extends FlameGame {\n  static const description = '''\n    Click on any circle to apply a RemoveEffect, which will make the circle\n    disappear after a 0.5 second delay.\n  ''';\n\n  RemoveEffectExample()\n    : super(\n        camera: CameraComponent.withFixedResolution(\n          width: 400,\n          height: 600,\n        )..viewfinder.anchor = Anchor.topLeft,\n        world: _RemoveEffectWorld(),\n      );\n}\n\nclass _RemoveEffectWorld extends World {\n  @override\n  void onLoad() {\n    super.onLoad();\n    final rng = Random();\n    for (var i = 0; i < 20; i++) {\n      add(_RandomCircle.random(rng));\n    }\n  }\n}\n\nclass _RandomCircle extends CircleComponent with TapCallbacks {\n  _RandomCircle(double radius, {super.position, super.paint})\n    : super(radius: radius);\n\n  factory _RandomCircle.random(Random rng) {\n    final radius = rng.nextDouble() * 30 + 10;\n    final position = Vector2(\n      rng.nextDouble() * 320 + 40,\n      rng.nextDouble() * 520 + 40,\n    );\n    final paint = Paint()\n      ..color = Colors.primaries[rng.nextInt(Colors.primaries.length)];\n    return _RandomCircle(radius, position: position, paint: paint);\n  }\n\n  @override\n  void onTapDown(TapDownEvent info) {\n    add(RemoveEffect(delay: 0.5));\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/effects/rotate_around_effect_example.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flutter/material.dart' as material;\n\nclass RotateAroundEffectExample extends FlameGame {\n  static const description = '''\nThis example shows how to use the RotateAroundEffect to rotate a component\naround a fixed point.\n''';\n\n  RotateAroundEffectExample()\n    : super(\n        camera: CameraComponent.withFixedResolution(\n          width: 400,\n          height: 600,\n        ),\n        world: _RotateAroundEffectWorld(),\n      );\n}\n\nclass _RotateAroundEffectWorld extends World {\n  @override\n  void onLoad() {\n    add(_GlowingBall(position: Vector2.zero(), radius: 30));\n    final rotatingBalls = List.generate(\n      4,\n      (i) =>\n          _GlowingBall(\n            position: Vector2(100 + 10.0 * i, 0),\n            radius: 10,\n          )..add(\n            RotateAroundEffect(\n              tau,\n              center: Vector2.zero(),\n              EffectController(\n                speed: 0.4 + 1.02 * i,\n                infinite: true,\n              ),\n            ),\n          ),\n    );\n    addAll(rotatingBalls);\n  }\n}\n\nclass _GlowingBall extends CircleComponent {\n  _GlowingBall({\n    required super.position,\n    required super.radius,\n  }) : super(anchor: Anchor.center);\n\n  static final random = Random(6);\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    paint = Paint()\n      ..color = material.Colors.white\n      ..shader = Gradient.radial(\n        (size / 2).toOffset(),\n        radius,\n        [\n          ColorExtension.random(base: 100, rng: random),\n          ColorExtension.random(withAlpha: 0.2, rng: random),\n        ],\n        null,\n        TileMode.clamp,\n        null,\n        Offset(radius / 2, radius / 2),\n      );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/effects/rotate_effect_example.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flutter/animation.dart';\n\nclass RotateEffectExample extends FlameGame {\n  static const description = '''\n    The outer rim rotates at a different speed forward and reverse, and\n    uses the \"ease\" animation curve.\n\n    The compass arrow has 3 rotation effects applied to it at the same\n    time: one effect rotates the arrow at a constant speed, and two more\n    add small amounts of wobble, creating quasi-chaotic movement.\n  ''';\n\n  RotateEffectExample()\n    : super(\n        camera: CameraComponent.withFixedResolution(\n          width: 400,\n          height: 600,\n        ),\n        world: _RotateEffectWorld(),\n      );\n}\n\nclass _RotateEffectWorld extends World {\n  @override\n  void onLoad() {\n    final compass = Compass(size: 200);\n    add(compass);\n\n    compass.rim.add(\n      RotateEffect.by(\n        1.0,\n        EffectController(\n          duration: 6,\n          reverseDuration: 3,\n          curve: Curves.ease,\n          infinite: true,\n        ),\n      ),\n    );\n    compass.arrow\n      ..add(\n        RotateEffect.to(\n          tau,\n          EffectController(\n            duration: 20,\n            infinite: true,\n          ),\n        ),\n      )\n      ..add(\n        RotateEffect.by(\n          tau * 0.015,\n          EffectController(\n            duration: 0.1,\n            reverseDuration: 0.1,\n            infinite: true,\n          ),\n        ),\n      )\n      ..add(\n        RotateEffect.by(\n          tau * 0.021,\n          EffectController(\n            duration: 0.13,\n            reverseDuration: 0.13,\n            infinite: true,\n          ),\n        ),\n      );\n  }\n}\n\nclass Compass extends PositionComponent {\n  Compass({required double size})\n    : _radius = size / 2,\n      super(\n        size: Vector2.all(size),\n        anchor: Anchor.center,\n      );\n\n  late PositionComponent arrow;\n  late PositionComponent rim;\n\n  final double _radius;\n  final _bgPaint = Paint()..color = const Color(0xffeacb31);\n  final _marksPaint = Paint()\n    ..color = const Color(0xFF7F6D36)\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 1.5;\n  late Path _marksPath;\n\n  @override\n  Future<void> onLoad() async {\n    _marksPath = Path();\n    for (var i = 0; i < 12; i++) {\n      final angle = tau * (i / 12);\n      // Note: rim takes up 0.1radius, so the lengths must be > than that\n      final markLength = (i % 3 == 0) ? _radius * 0.2 : _radius * 0.15;\n      _marksPath.moveTo(\n        _radius + _radius * sin(angle),\n        _radius + _radius * cos(angle),\n      );\n      _marksPath.lineTo(\n        _radius + (_radius - markLength) * sin(angle),\n        _radius + (_radius - markLength) * cos(angle),\n      );\n    }\n\n    arrow = CompassArrow(width: _radius * 0.3, radius: _radius * 0.7)\n      ..position = Vector2(_radius, _radius);\n    rim = CompassRim(radius: _radius, width: _radius * 0.1)\n      ..position = Vector2(_radius, _radius);\n    add(arrow);\n    add(rim);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawCircle(Offset(_radius, _radius), _radius, _bgPaint);\n    canvas.drawPath(_marksPath, _marksPaint);\n  }\n}\n\nclass CompassArrow extends PositionComponent {\n  CompassArrow({required double width, required double radius})\n    : assert(width <= radius, 'The width is larger than the radius'),\n      _radius = radius,\n      _width = width,\n      super(size: Vector2(width, 2 * radius), anchor: Anchor.center);\n\n  final double _radius;\n  final double _width;\n  late final Path _northPath;\n  late final Path _southPath;\n  final _northPaint = Paint()..color = const Color(0xff387fcb);\n  final _southPaint = Paint()..color = const Color(0xffa83636);\n\n  @override\n  Future<void> onLoad() async {\n    _northPath = Path()\n      ..moveTo(0, _radius)\n      ..lineTo(_width / 2, 0)\n      ..lineTo(_width, _radius)\n      ..close();\n    _southPath = Path()\n      ..moveTo(0, _radius)\n      ..lineTo(_width, _radius)\n      ..lineTo(_width / 2, 2 * _radius)\n      ..close();\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawPath(_northPath, _northPaint);\n    canvas.drawPath(_southPath, _southPaint);\n  }\n}\n\nclass CompassRim extends PositionComponent {\n  CompassRim({required double radius, required double width})\n    : assert(radius > width, 'The width is larger than the radius'),\n      _radius = radius,\n      _width = width,\n      super(\n        size: Vector2.all(2 * radius),\n        anchor: Anchor.center,\n      );\n\n  static const int numberOfNotches = 144;\n  final double _radius;\n  final double _width;\n  late final Path _marksPath;\n  final _bgPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..color = const Color(0xffb6a241);\n  final _marksPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..color = const Color(0xff3d3b26);\n\n  @override\n  Future<void> onLoad() async {\n    _bgPaint.strokeWidth = _width;\n    _marksPath = Path();\n    final innerRadius = _radius - _width;\n    final midRadius = _radius - _width / 3;\n    for (var i = 0; i < numberOfNotches; i++) {\n      final angle = tau * (i / numberOfNotches);\n      _marksPath.moveTo(\n        _radius + innerRadius * sin(angle),\n        _radius + innerRadius * cos(angle),\n      );\n      _marksPath.lineTo(\n        _radius + midRadius * sin(angle),\n        _radius + midRadius * cos(angle),\n      );\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawCircle(Offset(_radius, _radius), _radius - _width / 2, _bgPaint);\n    canvas.drawCircle(Offset(_radius, _radius), _radius - _width, _marksPaint);\n    canvas.drawPath(_marksPath, _marksPaint);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/effects/scale_effect_example.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/animation.dart';\n\nclass ScaleEffectExample extends FlameGame with TapCallbacks {\n  static const String description = '''\n    In this example you can tap the screen and the component will scale up or\n    down, depending on its current state.\n    \n    The star pulsates randomly using a RandomEffectController.\n  ''';\n\n  late RectangleComponent square;\n  bool grow = true;\n\n  @override\n  Future<void> onLoad() async {\n    square = RectangleComponent.square(\n      size: 100,\n      position: Vector2.all(200),\n      paint: BasicPalette.white.paint()..style = PaintingStyle.stroke,\n    );\n    final childSquare = RectangleComponent.square(\n      position: Vector2.all(70),\n      size: 20,\n    );\n    square.add(childSquare);\n    add(square);\n\n    add(\n      Star()\n        ..position = Vector2(200, 100)\n        ..add(\n          ScaleEffect.to(\n            Vector2.all(1.2),\n            InfiniteEffectController(\n              SequenceEffectController([\n                LinearEffectController(0.1),\n                ReverseLinearEffectController(0.1),\n                RandomEffectController.exponential(\n                  PauseEffectController(1, progress: 0),\n                  beta: 1,\n                ),\n              ]),\n            ),\n          ),\n        ),\n    );\n  }\n\n  @override\n  void onTapDown(_) {\n    final s = grow ? 3.0 : 1.0;\n\n    grow = !grow;\n\n    square.add(\n      ScaleEffect.to(\n        Vector2.all(s),\n        EffectController(\n          duration: 1.5,\n          curve: Curves.bounceInOut,\n        ),\n      ),\n    );\n  }\n}\n\nclass Star extends PositionComponent {\n  Star() {\n    const smallR = 15.0;\n    const bigR = 30.0;\n    shape = Path()..moveTo(bigR, 0);\n    for (var i = 1; i < 10; i++) {\n      final r = i.isEven ? bigR : smallR;\n      final a = i / 10 * tau;\n      shape.lineTo(r * cos(a), r * sin(a));\n    }\n    shape.close();\n  }\n\n  late final Path shape;\n  late final Paint paint = Paint()..color = const Color(0xFFFFF127);\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawPath(shape, paint);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/effects/sequence_effect_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\n\nclass SequenceEffectExample extends FlameGame {\n  static const String description = '''\n    Sequence of effects, consisting of a move effect, a rotate effect, another\n    move effect, a scale effect, and then one more move effect. The sequence\n    then runs in the opposite order (alternate = true) and loops infinitely\n    (infinite = true).\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    EffectController duration(double x) => EffectController(duration: x);\n    add(\n      Player()\n        ..position = Vector2(200, 300)\n        ..add(\n          SequenceEffect(\n            [\n              MoveEffect.to(Vector2(400, 300), duration(0.7)),\n              RotateEffect.by(tau / 4, duration(0.5)),\n              MoveEffect.to(Vector2(400, 400), duration(0.7)),\n              ScaleEffect.by(Vector2.all(1.5), duration(0.7)),\n              MoveEffect.to(Vector2(400, 500), duration(0.7)),\n            ],\n            alternate: true,\n            infinite: true,\n          ),\n        ),\n    );\n  }\n}\n\nclass Player extends PositionComponent {\n  Player()\n    : path = Path()\n        ..lineTo(40, 20)\n        ..lineTo(0, 40)\n        ..quadraticBezierTo(8, 20, 0, 0)\n        ..close(),\n      bodyPaint = Paint()..color = const Color(0x887F99B3),\n      borderPaint = Paint()\n        ..style = PaintingStyle.stroke\n        ..strokeWidth = 3\n        ..color = const Color(0xFFFFFD9A),\n      super(anchor: Anchor.center, size: Vector2(40, 40));\n\n  final Path path;\n  final Paint borderPaint;\n  final Paint bodyPaint;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawPath(path, bodyPaint);\n    canvas.drawPath(path, borderPaint);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/effects/size_effect_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/animation.dart';\n\nclass SizeEffectExample extends FlameGame with TapCallbacks {\n  static const String description = '''\n    The `SizeEffect` changes the size of the component, the sizes of the\n    children will stay the same.\n    In this example you can tap the screen and the component will size up or\n    down, depending on its current state.\n  ''';\n\n  late Component shape;\n  bool grow = true;\n\n  @override\n  Future<void> onLoad() async {\n    shape = CircleComponent(\n      radius: 100,\n      position: Vector2.all(200),\n      paint: BasicPalette.white.paint()..style = PaintingStyle.stroke,\n      children: [\n        RectangleComponent.square(position: Vector2.all(70), size: 20),\n      ],\n    )..addToParent(this);\n  }\n\n  @override\n  void onTapDown(_) {\n    shape.add(\n      SizeEffect.to(\n        Vector2.all(grow ? 300.0 : 100.0),\n        EffectController(duration: 1.5, curve: Curves.bounceInOut),\n      ),\n    );\n    grow = !grow;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/experimental/experimental.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/experimental/layout_component_example_1.dart';\nimport 'package:examples/stories/experimental/layout_component_example_2.dart';\nimport 'package:examples/stories/experimental/layout_component_example_3.dart';\nimport 'package:examples/stories/experimental/layout_component_example_size.dart';\nimport 'package:examples/stories/experimental/shapes.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/rendering.dart';\n\nvoid addExperimentalStories(Dashbook dashbook) {\n  dashbook\n      .storiesOf('Experimental')\n      .add(\n        'Shapes',\n        (_) => GameWidget(game: ShapesExample()),\n        codeLink: baseLink('experimental/shapes.dart'),\n        info: ShapesExample.description,\n      )\n      .add(\n        'Layout Components 1',\n        (DashbookContext context) {\n          return GameWidget(\n            game: LayoutComponentExample1(\n              mainAxisAlignment: context.listProperty(\n                'MainAxisAlignment',\n                MainAxisAlignment.values.first,\n                MainAxisAlignment.values,\n              ),\n              crossAxisAlignment: context.listProperty(\n                'CrossAxisAlignment',\n                CrossAxisAlignment.values.first,\n                CrossAxisAlignment.values,\n              ),\n              direction: context.listProperty(\n                'Direction',\n                Direction.values.first,\n                Direction.values,\n              ),\n              gap: context.numberProperty('Gap', 0),\n              demoSize: context.optionsProperty<LayoutComponentExampleSize>(\n                'Size',\n                LayoutComponentExampleSize.small,\n                LayoutComponentExampleSize.values.map((exampleSize) {\n                  return PropertyOption(exampleSize.name, exampleSize);\n                }).toList(),\n              ),\n              padding: context.edgeInsetsProperty(\n                'Padding',\n                const EdgeInsets.all(10),\n              ),\n              expandedMode: context.boolProperty(\n                'Wrap with ExpandedComponent',\n                false,\n              ),\n              paddingInflateChild: context.boolProperty(\n                'Padding Component inflates child',\n                false,\n              ),\n            ),\n          );\n        },\n        codeLink: baseLink('experimental/layout_components.dart'),\n        info: LayoutComponentExample1.description,\n      )\n      .add(\n        'Layout Components 2',\n        (DashbookContext context) {\n          return GameWidget(\n            game: LayoutComponentExample2(\n              mainAxisAlignment: context.listProperty(\n                'MainAxisAlignment',\n                MainAxisAlignment.values.first,\n                MainAxisAlignment.values,\n              ),\n              crossAxisAlignment: context.listProperty(\n                'CrossAxisAlignment',\n                CrossAxisAlignment.stretch,\n                CrossAxisAlignment.values,\n              ),\n              direction: context.listProperty(\n                'Direction',\n                Direction.values.first,\n                Direction.values,\n              ),\n              gap: context.numberProperty('Gap', 0),\n              demoSize: context.optionsProperty<LayoutComponentExampleSize>(\n                'Size',\n                LayoutComponentExampleSize.small,\n                LayoutComponentExampleSize.values.map((exampleSize) {\n                  return PropertyOption(exampleSize.name, exampleSize);\n                }).toList(),\n              ),\n            ),\n          );\n        },\n        codeLink: baseLink('experimental/layout_components.dart'),\n        info: LayoutComponentExample2.description,\n      )\n      .add(\n        'Layout Components 3',\n        (DashbookContext context) {\n          return GameWidget(\n            game: LayoutComponentExample3(\n              demoSize: context.optionsProperty<LayoutComponentExampleSize>(\n                'Size',\n                LayoutComponentExampleSize.small,\n                LayoutComponentExampleSize.values.map((exampleSize) {\n                  return PropertyOption(exampleSize.name, exampleSize);\n                }).toList(),\n              ),\n            ),\n          );\n        },\n        codeLink: baseLink('experimental/layout_components.dart'),\n        info: LayoutComponentExample3.description,\n      );\n}\n"
  },
  {
    "path": "examples/lib/stories/experimental/layout_component_example_1.dart",
    "content": "import 'dart:async';\n\nimport 'package:examples/stories/experimental/layout_component_example_size.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass LayoutComponentExample1 extends FlameGame with DragCallbacks {\n  LayoutComponentExample1({\n    required this.direction,\n    required this.mainAxisAlignment,\n    required this.crossAxisAlignment,\n    required this.gap,\n    required this.demoSize,\n    required this.padding,\n    required this.expandedMode,\n    required this.paddingInflateChild,\n  });\n\n  static const String description = '''\nThis example demonstrates the various behaviors of LayoutComponents.\nPress the pen button on the floating group of icons on the upper right to see\nthe various ways you can change this layout.\n  ''';\n\n  final Direction direction;\n  final MainAxisAlignment mainAxisAlignment;\n  final CrossAxisAlignment crossAxisAlignment;\n  final double gap;\n  final LayoutComponentExampleSize demoSize;\n  final EdgeInsets padding;\n  final bool expandedMode;\n  final bool paddingInflateChild;\n\n  @override\n  FutureOr<void> onLoad() {\n    camera.viewfinder.anchor = Anchor.topLeft;\n\n    final rootColumnComponent = ColumnComponent(\n      position: Vector2(48, 48),\n      gap: 24,\n      children: [\n        TextComponent(\n          text:\n              'Because this example deals with sizes a lot, we have made it '\n              'draggable. Note that the blue square has a PaddingComponent '\n              'around it.',\n        ),\n        LayoutDemo1(\n          direction: direction,\n          crossAxisAlignment: crossAxisAlignment,\n          mainAxisAlignment: mainAxisAlignment,\n          gap: gap,\n          position: Vector2.zero(),\n          padding: padding,\n          expandedMode: expandedMode,\n          paddingInflateChild: paddingInflateChild,\n          size: demoSize.toVector2(),\n        ),\n      ],\n    );\n    world.add(\n      rootColumnComponent,\n    );\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    camera.viewfinder.position -= event.localDelta;\n  }\n\n  @override\n  // This is intentional for this example, so the user can see the bounding\n  // boxes without us having to render RectangleComponents.\n  bool get debugMode => true;\n}\n\nclass LayoutDemo1 extends LinearLayoutComponent {\n  LayoutDemo1({\n    required super.direction,\n    required super.crossAxisAlignment,\n    required super.mainAxisAlignment,\n    required super.gap,\n    required super.position,\n    required EdgeInsets padding,\n    required bool expandedMode,\n    required this.paddingInflateChild,\n    super.size,\n    super.key,\n  }) : _padding = padding,\n       _expandedMode = expandedMode,\n       super(anchor: Anchor.topLeft, priority: 0, children: []);\n\n  bool _expandedMode = false;\n\n  bool get expandedMode => _expandedMode;\n\n  set expandedMode(bool value) {\n    _expandedMode = value;\n    removeAll(children.toList());\n    addAll(\n      createComponentList(\n        expandedMode: expandedMode,\n        padding: padding,\n        inflateChild: paddingInflateChild,\n      ),\n    );\n  }\n\n  EdgeInsets _padding = EdgeInsets.zero;\n\n  EdgeInsets get padding => _padding;\n\n  set padding(EdgeInsets value) {\n    _padding = value;\n    paddingComponent?.padding = padding;\n  }\n\n  final bool paddingInflateChild;\n\n  @override\n  FutureOr<void> onLoad() {\n    super.onLoad();\n    addAll(\n      createComponentList(\n        expandedMode: expandedMode,\n        padding: padding,\n        inflateChild: paddingInflateChild,\n      ),\n    );\n  }\n\n  PaddingComponent? get paddingComponent {\n    return descendants().whereType<PaddingComponent>().firstOrNull;\n  }\n\n  /// This needs to be a method rather than a static list\n  /// because each of these components needs to be recreated.\n  /// Otherwise, they'll be operated on by reference and re-parented.\n  static List<Component> createComponentList({\n    required bool expandedMode,\n    required EdgeInsets padding,\n    required bool inflateChild,\n  }) {\n    return [\n      TextComponent(text: 'Some short text'),\n      if (expandedMode)\n        ExpandedComponent(\n          child: RectangleComponent(\n            size: Vector2(100, 70),\n            paint: Paint()..color = Colors.amber,\n          ),\n        )\n      else\n        RectangleComponent(\n          size: Vector2(100, 70),\n          paint: Paint()..color = Colors.amber,\n        ),\n      if (expandedMode)\n        ExpandedComponent(\n          child: PaddingComponent(\n            padding: padding,\n            inflateChild: inflateChild,\n            child: RectangleComponent(\n              size: Vector2(96, 96),\n              paint: Paint()..color = Colors.blue,\n            ),\n          ),\n        )\n      else\n        PaddingComponent(\n          padding: padding,\n          inflateChild: inflateChild,\n          child: RectangleComponent(\n            size: Vector2(96, 96),\n            paint: Paint()..color = Colors.blue,\n          ),\n        ),\n    ];\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/experimental/layout_component_example_2.dart",
    "content": "import 'dart:async';\n\nimport 'package:examples/stories/experimental/layout_component_example_size.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass LayoutComponentExample2 extends FlameGame with DragCallbacks {\n  LayoutComponentExample2({\n    required this.direction,\n    required this.mainAxisAlignment,\n    required this.crossAxisAlignment,\n    required this.gap,\n    required this.demoSize,\n  });\n\n  static const String description = '''\nThis example demonstrates the various behaviors of LayoutComponents.\nPress the pen button on the floating group of icons on the upper right to see\nthe various ways you can change this layout.\n  ''';\n\n  final Direction direction;\n  final MainAxisAlignment mainAxisAlignment;\n  final CrossAxisAlignment crossAxisAlignment;\n  final double gap;\n  final LayoutComponentExampleSize demoSize;\n\n  @override\n  FutureOr<void> onLoad() {\n    camera.viewfinder.anchor = Anchor.topLeft;\n\n    final rootColumnComponent = ColumnComponent(\n      position: Vector2(48, 48),\n      gap: 24,\n      children: [\n        TextComponent(\n          text:\n              'Because this example deals with sizes a lot, we have made it '\n              'draggable.',\n        ),\n        LayoutDemo2(\n          direction: direction,\n          crossAxisAlignment: crossAxisAlignment,\n          mainAxisAlignment: mainAxisAlignment,\n          gap: gap,\n          position: Vector2.zero(),\n          size: demoSize.toVector2(),\n        ),\n      ],\n    );\n    world.add(\n      rootColumnComponent,\n    );\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    camera.viewfinder.position -= event.localDelta;\n  }\n\n  @override\n  // This is intentional for this example, so the user can see the bounding\n  // boxes without us having to render RectangleComponents.\n  bool get debugMode => true;\n}\n\nclass LayoutDemo2 extends LinearLayoutComponent {\n  LayoutDemo2({\n    required super.direction,\n    required super.crossAxisAlignment,\n    required super.mainAxisAlignment,\n    required super.gap,\n    required super.position,\n    super.size,\n    super.key,\n  }) : super(anchor: Anchor.topLeft, priority: 0, children: []);\n\n  @override\n  FutureOr<void> onLoad() {\n    super.onLoad();\n    addAll(\n      createComponentList(\n        direction: direction,\n      ),\n    );\n  }\n\n  PaddingComponent? get paddingComponent {\n    return descendants().whereType<PaddingComponent>().firstOrNull;\n  }\n\n  /// This needs to be a method rather than a static list\n  /// because each of these components needs to be recreated.\n  /// Otherwise, they'll be operated on by reference and re-parented.\n  static List<Component> createComponentList({\n    required Direction direction,\n  }) {\n    return [\n      ExpandedComponent(\n        child: RectangleComponent(\n          paint: Paint()..color = Colors.purple,\n        ),\n      ),\n      LinearLayoutComponent.fromDirection(\n        // Basically adopt the opposite direction\n        direction == Direction.horizontal\n            ? Direction.vertical\n            : Direction.horizontal,\n        mainAxisAlignment: MainAxisAlignment.end,\n        children: [\n          TextComponent(text: 'test'),\n        ],\n      ),\n    ];\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/experimental/layout_component_example_3.dart",
    "content": "import 'dart:async';\n\nimport 'package:examples/stories/experimental/layout_component_example_size.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass LayoutComponentExample3 extends FlameGame with DragCallbacks {\n  LayoutComponentExample3({\n    required this.demoSize,\n  });\n\n  static const String description = '''\nWhen ColumnComponent has a TextBoxComponent as a child, and its \ncrossAxisAlignment is stretch, it will also set the maxWidth of the\nTextBoxComponent.\n  ''';\n\n  final LayoutComponentExampleSize demoSize;\n\n  @override\n  FutureOr<void> onLoad() {\n    camera.viewfinder.anchor = Anchor.topLeft;\n\n    final rootColumnComponent = ColumnComponent(\n      position: Vector2(48, 48),\n      gap: 24,\n      children: [\n        TextComponent(\n          text:\n              'Because this example deals with sizes a lot, we have made it '\n              'draggable.',\n        ),\n        LayoutDemo3(\n          position: Vector2.zero(),\n          size: demoSize.toVector2(),\n        ),\n      ],\n    );\n    world.add(\n      rootColumnComponent,\n    );\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    camera.viewfinder.position -= event.localDelta;\n  }\n\n  @override\n  // This is intentional for this example, so the user can see the bounding\n  // boxes without us having to render RectangleComponents.\n  bool get debugMode => true;\n}\n\nclass LayoutDemo3 extends ColumnComponent {\n  LayoutDemo3({\n    required super.position,\n    super.size,\n    super.key,\n  }) : super(\n         anchor: Anchor.topLeft,\n         priority: 0,\n         children: [],\n         gap: 16,\n         mainAxisAlignment: MainAxisAlignment.start,\n         crossAxisAlignment: CrossAxisAlignment.stretch,\n       );\n\n  @override\n  FutureOr<void> onLoad() {\n    super.onLoad();\n    addAll([\n      TextBoxComponent(\n        text: sampleText,\n      ),\n      TextBoxComponent(\n        text: sampleText,\n      ),\n    ]);\n  }\n\n  PaddingComponent? get paddingComponent {\n    return descendants().whereType<PaddingComponent>().firstOrNull;\n  }\n\n  static const sampleText =\n      'In a bustling city, a small team of developers set out to create '\n      'a mobile game using the Flame engine for Flutter. Their goal was '\n      'simple: to create an engaging, easy-to-play game that could reach '\n      'a wide audience on both iOS and Android platforms. '\n      'After weeks of brainstorming, they decided on a concept: '\n      'a fast-paced, endless runner game set in a whimsical, '\n      'ever-changing world. They named it \"Swift Dash.\" '\n      \"Using Flutter's versatility and the Flame engine's \"\n      'capabilities, the team crafted a game with vibrant graphics, '\n      'smooth animations, and responsive controls. '\n      'The game featured a character dashing through various landscapes, '\n      'dodging obstacles, and collecting points. '\n      'As they launched \"Swift Dash,\" the team was anxious but hopeful. '\n      'To their delight, the game was well-received. Players loved its '\n      'simplicity and charm, and the game quickly gained popularity.';\n}\n"
  },
  {
    "path": "examples/lib/stories/experimental/layout_component_example_size.dart",
    "content": "import 'package:flame/components.dart';\n\nenum LayoutComponentExampleSize {\n  shrinkWrap(null, null),\n  small(640, 480),\n  large(1080, 720)\n  ;\n\n  const LayoutComponentExampleSize(\n    this.x,\n    this.y,\n  );\n  final double? x;\n  final double? y;\n\n  Vector2? toVector2() {\n    final x = this.x;\n    final y = this.y;\n    if (x == null || y == null) {\n      return null;\n    } else {\n      return Vector2(x, y);\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/experimental/shapes.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/game.dart';\n\nclass ShapesExample extends FlameGame {\n  static const description = '''\n    This example shows multiple raw `Shape`s, and random points whose color\n    should match the colors of the shapes that they fall in. Points that are\n    outside of any shape should be grey.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final shapes = [\n      Circle(Vector2(50, 30), 20),\n      Circle(Vector2(700, 500), 50),\n      Rectangle.fromLTRB(100, 30, 260, 100),\n      RoundedRectangle.fromLTRBR(40, 300, 120, 550, 30),\n      Polygon([Vector2(10, 70), Vector2(180, 200), Vector2(220, 150)]),\n      Polygon([\n        Vector2(400, 160),\n        Vector2(550, 400),\n        Vector2(710, 350),\n        Vector2(540, 170),\n        Vector2(710, 100),\n        Vector2(710, 320),\n        Vector2(730, 315),\n        Vector2(750, 60),\n        Vector2(590, 30),\n      ]),\n    ];\n    const colors = [\n      Color(0xFFFFFF88),\n      Color(0xFFff88FF),\n      Color(0xFF88FFFF),\n      Color(0xFF88FF88),\n      Color(0xFFaaaaFF),\n      Color(0xFFFF8888),\n    ];\n    add(ShapesComponent(shapes, colors));\n    add(DotsComponent(shapes, colors));\n  }\n}\n\nclass ShapesComponent extends Component {\n  ShapesComponent(this.shapes, List<Color> colors)\n    : assert(\n        shapes.length == colors.length,\n        'The shapes and colors lists have to be of the same length',\n      ),\n      paints = colors\n          .map(\n            (color) => Paint()\n              ..style = PaintingStyle.stroke\n              ..strokeWidth = 1\n              ..color = color,\n          )\n          .toList();\n\n  final List<Shape> shapes;\n  final List<Paint> paints;\n\n  @override\n  void render(Canvas canvas) {\n    for (var i = 0; i < shapes.length; i++) {\n      canvas.drawPath(shapes[i].asPath(), paints[i]);\n    }\n  }\n}\n\nclass DotsComponent extends Component {\n  DotsComponent(this.shapes, this.shapeColors)\n    : assert(\n        shapes.length == shapeColors.length,\n        'The shapes and shapeColors lists have to be of the same length',\n      );\n\n  final List<Shape> shapes;\n  final List<Color> shapeColors;\n\n  final Random random = Random();\n  final List<Vector2> points = [];\n  final List<Color> pointColors = [];\n  static const pointSize = 3;\n\n  @override\n  void update(double dt) {\n    generatePoint();\n  }\n\n  void generatePoint() {\n    final point = Vector2(\n      random.nextDouble() * 800,\n      random.nextDouble() * 600,\n    );\n    points.add(point);\n    pointColors.add(const Color(0xff444444));\n    for (var i = 0; i < shapes.length; i++) {\n      if (shapes[i].containsPoint(point)) {\n        pointColors.last = shapeColors[i];\n        break;\n      }\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    const d = pointSize / 2;\n    final paint = Paint();\n    for (var i = 0; i < points.length; i++) {\n      final x = points[i].x;\n      final y = points[i].y;\n      paint.color = pointColors[i];\n      canvas.drawRect(Rect.fromLTRB(x - d, y - d, x + d, y + d), paint);\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/games/games.dart",
    "content": "import 'package:crystal_ball/crystal_ball.dart';\nimport 'package:dashbook/dashbook.dart';\nimport 'package:padracing/padracing_game.dart';\nimport 'package:padracing/padracing_widget.dart';\nimport 'package:rogue_shooter/rogue_shooter_game.dart';\nimport 'package:rogue_shooter/rogue_shooter_widget.dart';\nimport 'package:trex_game/trex_game.dart';\nimport 'package:trex_game/trex_widget.dart';\n\nString gamesLink(String game) =>\n    'https://github.com/flame-engine/flame/blob/main/examples/games/$game';\n\nvoid addGameStories(Dashbook dashbook) {\n  dashbook.storiesOf('Sample Games')\n    ..add(\n      'Crystal Ball',\n      (_) => const CrystalBallWidget(),\n      codeLink: gamesLink('crystal_ball'),\n      info: CrystalBallWidget.description,\n    )\n    ..add(\n      'Padracing',\n      (_) => const PadracingWidget(),\n      codeLink: gamesLink('padracing'),\n      info: PadRacingGame.description,\n    )\n    ..add(\n      'Rogue Shooter',\n      (_) => const RogueShooterWidget(),\n      codeLink: gamesLink('rogue_shooter'),\n      info: RogueShooterGame.description,\n    )\n    ..add(\n      'T-Rex',\n      (_) => const TRexWidget(),\n      codeLink: gamesLink('trex'),\n      info: TRexGame.description,\n    );\n}\n"
  },
  {
    "path": "examples/lib/stories/image/brighten.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\n\nclass ImageBrightnessExample extends FlameGame {\n  ImageBrightnessExample({\n    required this.brightness,\n  });\n\n  final double brightness;\n\n  static const String description = '''\n     Shows how a dart:ui `Image` can be brightened using Flame Image extensions.\n     Use the properties on the side to change the brightness of the image.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final image = await images.load('flame.png');\n    final brightenedImage = await image.brighten(brightness / 100);\n\n    add(\n      SpriteComponent(\n        sprite: Sprite(image),\n        position: (size / 2) - Vector2(0, image.height / 2),\n        size: image.size,\n        anchor: Anchor.center,\n      ),\n    );\n\n    add(\n      SpriteComponent(\n        sprite: Sprite(brightenedImage),\n        position: (size / 2) + Vector2(0, brightenedImage.height / 2),\n        size: image.size,\n        anchor: Anchor.center,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/image/darken.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\n\nclass ImageDarknessExample extends FlameGame {\n  ImageDarknessExample({\n    required this.darkness,\n  });\n\n  final double darkness;\n\n  static const String description = '''\n     Shows how a dart:ui `Image` can be darkened using Flame Image extensions.\n     Use the properties on the side to change the darkness of the image.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final image = await images.load('flame.png');\n    final darkenedImage = await image.darken(darkness / 100);\n\n    add(\n      SpriteComponent(\n        sprite: Sprite(image),\n        position: (size / 2) - Vector2(0, image.height / 2),\n        size: image.size,\n        anchor: Anchor.center,\n      ),\n    );\n\n    add(\n      SpriteComponent(\n        sprite: Sprite(darkenedImage),\n        position: (size / 2) + Vector2(0, darkenedImage.height / 2),\n        size: image.size,\n        anchor: Anchor.center,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/image/image.dart",
    "content": "import 'package:dashbook/dashbook.dart';\n\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/image/brighten.dart';\nimport 'package:examples/stories/image/darken.dart';\nimport 'package:examples/stories/image/resize.dart';\nimport 'package:flame/game.dart';\n\nvoid addImageStories(Dashbook dashbook) {\n  dashbook.storiesOf('Image')\n    ..decorator(CenterDecorator())\n    ..add(\n      'resize',\n      (context) => GameWidget(\n        game: ImageResizeExample(\n          Vector2(\n            context.numberProperty('width', 200),\n            context.numberProperty('height', 300),\n          ),\n        ),\n      ),\n      codeLink: baseLink('image/resize.dart'),\n      info: ImageResizeExample.description,\n    )\n    ..add(\n      'brightness',\n      (context) => GameWidget(\n        game: ImageBrightnessExample(\n          brightness: context.numberProperty('brightness', 80),\n        ),\n      ),\n      codeLink: baseLink('image/brighten.dart'),\n      info: ImageBrightnessExample.description,\n    )\n    ..add(\n      'darkness',\n      (context) => GameWidget(\n        game: ImageDarknessExample(\n          darkness: context.numberProperty('darkness', 80),\n        ),\n      ),\n      codeLink: baseLink('image/darkness.dart'),\n      info: ImageDarknessExample.description,\n    );\n}\n"
  },
  {
    "path": "examples/lib/stories/image/resize.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\n\nclass ImageResizeExample extends FlameGame {\n  ImageResizeExample(this.sizeTarget);\n\n  static const String description = '''\n     Shows how a dart:ui `Image` can be resized using Flame Image extensions.\n     Use the properties on the side to change the size of the image.\n  ''';\n\n  final Vector2 sizeTarget;\n\n  @override\n  Future<void> onLoad() async {\n    final image = await images.load('flame.png');\n\n    final resized = await image.resize(sizeTarget);\n    add(\n      SpriteComponent(\n        sprite: Sprite(resized),\n        position: size / 2,\n        size: resized.size,\n        anchor: Anchor.center,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/advanced_button_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/painting.dart';\n\nclass AdvancedButtonExample extends FlameGame {\n  static const String description =\n      '''This example shows how you can use a button with different states''';\n\n  @override\n  Future<void> onLoad() async {\n    final defaultButton = DefaultButton();\n    defaultButton.position = Vector2(50, 50);\n    defaultButton.size = Vector2(250, 50);\n    add(defaultButton);\n\n    final disableButton = DisableButton();\n    disableButton.isDisabled = true;\n    disableButton.position = Vector2(400, 50);\n    disableButton.size = defaultButton.size;\n    add(disableButton);\n\n    final toggleButton = ToggleButton();\n    toggleButton.position = Vector2(50, 150);\n    toggleButton.size = defaultButton.size;\n    add(toggleButton);\n  }\n}\n\nclass ToggleButton extends ToggleButtonComponent {\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n\n    defaultLabel = TextComponent(\n      text: 'Toggle button',\n      textRenderer: TextPaint(\n        style: TextStyle(\n          fontSize: 24,\n          color: BasicPalette.white.color,\n        ),\n      ),\n    );\n\n    defaultSelectedLabel = TextComponent(\n      text: 'Toggle button',\n      textRenderer: TextPaint(\n        style: TextStyle(\n          fontSize: 24,\n          color: BasicPalette.red.color,\n        ),\n      ),\n    );\n\n    defaultSkin = RoundedRectComponent()\n      ..setColor(const Color.fromRGBO(0, 200, 0, 1));\n\n    hoverSkin = RoundedRectComponent()\n      ..setColor(const Color.fromRGBO(0, 180, 0, 1));\n\n    downSkin = RoundedRectComponent()\n      ..setColor(const Color.fromRGBO(0, 100, 0, 1));\n\n    defaultSelectedSkin = RoundedRectComponent()\n      ..setColor(const Color.fromRGBO(0, 0, 200, 1));\n\n    hoverAndSelectedSkin = RoundedRectComponent()\n      ..setColor(const Color.fromRGBO(0, 0, 180, 1));\n\n    downAndSelectedSkin = RoundedRectComponent()\n      ..setColor(const Color.fromRGBO(0, 0, 100, 1));\n  }\n}\n\nclass DefaultButton extends AdvancedButtonComponent {\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n\n    defaultLabel = TextComponent(text: 'Default button');\n\n    defaultSkin = RoundedRectComponent()\n      ..setColor(const Color.fromRGBO(0, 200, 0, 1));\n\n    hoverSkin = RoundedRectComponent()\n      ..setColor(const Color.fromRGBO(0, 180, 0, 1));\n\n    downSkin = RoundedRectComponent()\n      ..setColor(const Color.fromRGBO(0, 100, 0, 1));\n  }\n}\n\nclass DisableButton extends AdvancedButtonComponent {\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n\n    disabledLabel = TextComponent(text: 'Disabled button');\n\n    defaultSkin = RoundedRectComponent()\n      ..setColor(const Color.fromRGBO(0, 255, 0, 1));\n\n    disabledSkin = RoundedRectComponent()\n      ..setColor(const Color.fromRGBO(100, 100, 100, 1));\n  }\n}\n\nclass RoundedRectComponent extends PositionComponent with HasPaint {\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(\n      RRect.fromLTRBAndCorners(\n        0,\n        0,\n        width,\n        height,\n        topLeft: Radius.circular(height),\n        topRight: Radius.circular(height),\n        bottomRight: Radius.circular(height),\n        bottomLeft: Radius.circular(height),\n      ),\n      paint,\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/double_tap_callbacks_example.dart",
    "content": "import 'package:examples/commons/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass DoubleTapCallbacksExample extends FlameGame with DoubleTapCallbacks {\n  static const String description = '''\n  In this example, we show how you can use the `DoubleTapCallbacks` mixin on\n  a `Component`. Double tap Ember and see her color changing.\n  The example also adds white circles when double-tapping on the game area.\n''';\n\n  @override\n  Future<void> onLoad() async {\n    children.register<DoubleTappableEmber>();\n  }\n\n  @override\n  void onGameResize(Vector2 size) {\n    children.query<DoubleTappableEmber>().forEach(\n      (element) => element.removeFromParent(),\n    );\n    add(DoubleTappableEmber(position: size / 2));\n\n    super.onGameResize(size);\n  }\n\n  @override\n  void onDoubleTapDown(DoubleTapDownEvent event) {\n    add(\n      CircleComponent(\n        radius: 30,\n        position: event.localPosition,\n        anchor: Anchor.center,\n      ),\n    );\n  }\n}\n\nclass DoubleTappableEmber extends Ember with DoubleTapCallbacks {\n  @override\n  bool debugMode = true;\n\n  DoubleTappableEmber({Vector2? position})\n    : super(\n        position: position ?? Vector2.all(100),\n        size: Vector2.all(100),\n      );\n\n  @override\n  void onDoubleTapUp(DoubleTapEvent event) {\n    debugColor = Colors.greenAccent;\n  }\n\n  @override\n  void onDoubleTapCancel(DoubleTapCancelEvent event) {\n    debugColor = Colors.red;\n  }\n\n  @override\n  void onDoubleTapDown(DoubleTapDownEvent event) {\n    debugColor = Colors.blue;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/drag_callbacks_example.dart",
    "content": "import 'package:examples/commons/ember.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart' show Colors;\n\nclass DragCallbacksExample extends FlameGame {\n  static const String description = '''\n    In this example we show you can use the `DragCallbacks` mixin on\n    `PositionComponent`s. Drag around the Embers and see their position\n    changing.\n  ''';\n\n  DragCallbacksExample({required this.zoom});\n\n  final double zoom;\n  late final DraggableEmber square;\n\n  @override\n  Future<void> onLoad() async {\n    camera.viewfinder.zoom = zoom;\n    world.add(square = DraggableEmber());\n    world.add(DraggableEmber()..y = 350);\n  }\n}\n\n// Note: this component does not consider the possibility of multiple\n// simultaneous drags with different pointerIds.\nclass DraggableEmber extends Ember with DragCallbacks {\n  @override\n  bool debugMode = true;\n\n  DraggableEmber({super.position}) : super(size: Vector2.all(100));\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    debugColor = isDragged ? Colors.greenAccent : Colors.purple;\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    position += event.localDelta;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/gesture_hitboxes_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\n\nenum Shapes { circle, rectangle, polygon }\n\nclass GestureHitboxesExample extends FlameGame {\n  static const description = '''\n    Tap to create a PositionComponent with a randomly shaped hitbox.\n    You can then hover over to shapes to see that they receive the hover events\n    only when the cursor is within the shape. If you tap/click within the shape\n    it is removed.\n  ''';\n\n  GestureHitboxesExample() : super(world: _GestureHitboxesWorld());\n}\n\nclass _GestureHitboxesWorld extends World with TapCallbacks {\n  final _rng = Random();\n\n  PositionComponent randomShape(Vector2 position) {\n    final shapeType = Shapes.values[_rng.nextInt(Shapes.values.length)];\n    final shapeSize =\n        Vector2.all(100) + Vector2.all(50.0).scaled(_rng.nextDouble());\n    final shapeAngle = _rng.nextDouble() * 6;\n    final hitbox = switch (shapeType) {\n      Shapes.circle => CircleHitbox(),\n      Shapes.rectangle => RectangleHitbox(),\n      Shapes.polygon => PolygonHitbox.relative(\n        [\n          -Vector2.random(_rng),\n          Vector2.random(_rng)..x *= -1,\n          Vector2.random(_rng),\n          Vector2.random(_rng)..y *= -1,\n        ],\n        parentSize: shapeSize,\n      ),\n    };\n    return MyShapeComponent(\n      hitbox: hitbox,\n      position: position,\n      size: shapeSize,\n      angle: shapeAngle,\n    );\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    add(randomShape(event.localPosition));\n  }\n}\n\nclass MyShapeComponent extends PositionComponent\n    with TapCallbacks, HoverCallbacks, GestureHitboxes {\n  final ShapeHitbox hitbox;\n  late final Color baseColor;\n  late final Color hoverColor;\n\n  MyShapeComponent({\n    required this.hitbox,\n    super.position,\n    super.size,\n    super.angle,\n  }) : super(anchor: Anchor.center);\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    baseColor = ColorExtension.random(withAlpha: 0.8, base: 100);\n    hitbox.paint.color = baseColor;\n    hitbox.renderShape = true;\n    add(hitbox);\n  }\n\n  @override\n  void onTapDown(TapDownEvent _) {\n    removeFromParent();\n  }\n\n  @override\n  void onHoverEnter() {\n    hitbox.paint.color = hitbox.paint.color.darken(0.5);\n  }\n\n  @override\n  void onHoverExit() {\n    hitbox.paint.color = baseColor;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/hardware_keyboard_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/text.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:flutter/services.dart';\n\nclass HardwareKeyboardExample extends FlameGame {\n  static const String description = '''\n    This example uses the HardwareKeyboardDetector mixin in order to keep\n    track of which keys on a keyboard are currently pressed.\n\n    Tap as many keys on the keyboard at once as you want, and see whether the\n    system can detect them or not.\n  ''';\n\n  /// The list of [KeyboardKey] components currently shown on the screen. This\n  /// list is re-generated on every KeyEvent. These components are also\n  /// attached as children.\n  List<KeyboardKey> _keyComponents = [];\n\n  void replaceKeyComponents(List<KeyboardKey> newComponents) {\n    for (final key in _keyComponents) {\n      key.visible = false;\n      key.removeFromParent();\n    }\n    _keyComponents = newComponents;\n    addAll(_keyComponents);\n  }\n\n  @override\n  void onLoad() {\n    add(MyKeyboardDetector());\n    add(\n      TextComponent(\n        text: 'Press any key(s)',\n        textRenderer: TextPaint(\n          style: const TextStyle(\n            fontSize: 12,\n            color: Color(0x77ffffff),\n          ),\n        ),\n      )..position = Vector2(80, 60),\n    );\n  }\n}\n\nclass MyKeyboardDetector extends HardwareKeyboardDetector\n    with HasGameReference<HardwareKeyboardExample> {\n  @override\n  void onKeyEvent(KeyEvent event) {\n    final newComponents = <KeyboardKey>[];\n    var x0 = 80.0;\n    const y0 = 100.0;\n    for (final key in physicalKeysPressed) {\n      final keyComponent = KeyboardKey(\n        text: keyNames[key] ?? '[${key.usbHidUsage} ${key.debugName}]',\n        position: Vector2(x0, y0),\n      );\n      newComponents.add(keyComponent);\n      x0 += keyComponent.width + 10;\n    }\n    game.replaceKeyComponents(newComponents);\n  }\n\n  /// The names of keyboard keys (at least the most important ones). We can't\n  /// rely on `key.debugName` because this property is not available in release\n  /// builds.\n  static final Map<PhysicalKeyboardKey, String> keyNames = {\n    PhysicalKeyboardKey.hyper: 'Hyper',\n    PhysicalKeyboardKey.superKey: 'Super',\n    PhysicalKeyboardKey.fn: 'Fn',\n    PhysicalKeyboardKey.fnLock: 'FnLock',\n    PhysicalKeyboardKey.gameButton1: 'Game 1',\n    PhysicalKeyboardKey.gameButton2: 'Game 2 ',\n    PhysicalKeyboardKey.gameButton3: 'Game 3',\n    PhysicalKeyboardKey.gameButton4: 'Game 4',\n    PhysicalKeyboardKey.gameButton5: 'Game 5',\n    PhysicalKeyboardKey.gameButton6: 'Game 6',\n    PhysicalKeyboardKey.gameButton7: 'Game 7',\n    PhysicalKeyboardKey.gameButton8: 'Game 8',\n    PhysicalKeyboardKey.gameButtonA: 'Game A',\n    PhysicalKeyboardKey.gameButtonB: 'Game B',\n    PhysicalKeyboardKey.gameButtonC: 'Game C',\n    PhysicalKeyboardKey.gameButtonLeft1: 'Game L1',\n    PhysicalKeyboardKey.gameButtonLeft2: 'Game L2',\n    PhysicalKeyboardKey.gameButtonMode: 'Game Mode',\n    PhysicalKeyboardKey.gameButtonRight1: 'Game R1',\n    PhysicalKeyboardKey.gameButtonRight2: 'Game R2',\n    PhysicalKeyboardKey.gameButtonSelect: 'Game Select',\n    PhysicalKeyboardKey.gameButtonStart: 'Game Start',\n    PhysicalKeyboardKey.gameButtonThumbLeft: 'Game LThumb',\n    PhysicalKeyboardKey.gameButtonThumbRight: 'Game RThumb',\n    PhysicalKeyboardKey.gameButtonX: 'Game X',\n    PhysicalKeyboardKey.gameButtonY: 'Game Y',\n    PhysicalKeyboardKey.gameButtonZ: 'Game Z',\n    PhysicalKeyboardKey.keyA: 'A',\n    PhysicalKeyboardKey.keyB: 'B',\n    PhysicalKeyboardKey.keyC: 'C',\n    PhysicalKeyboardKey.keyD: 'D',\n    PhysicalKeyboardKey.keyE: 'E',\n    PhysicalKeyboardKey.keyF: 'F',\n    PhysicalKeyboardKey.keyG: 'G',\n    PhysicalKeyboardKey.keyH: 'H',\n    PhysicalKeyboardKey.keyI: 'I',\n    PhysicalKeyboardKey.keyJ: 'J',\n    PhysicalKeyboardKey.keyK: 'K',\n    PhysicalKeyboardKey.keyL: 'L',\n    PhysicalKeyboardKey.keyM: 'M',\n    PhysicalKeyboardKey.keyN: 'N',\n    PhysicalKeyboardKey.keyO: 'O',\n    PhysicalKeyboardKey.keyP: 'P',\n    PhysicalKeyboardKey.keyQ: 'Q',\n    PhysicalKeyboardKey.keyR: 'R',\n    PhysicalKeyboardKey.keyS: 'S',\n    PhysicalKeyboardKey.keyT: 'T',\n    PhysicalKeyboardKey.keyU: 'U',\n    PhysicalKeyboardKey.keyV: 'V',\n    PhysicalKeyboardKey.keyW: 'W',\n    PhysicalKeyboardKey.keyX: 'X',\n    PhysicalKeyboardKey.keyY: 'Y',\n    PhysicalKeyboardKey.keyZ: 'Z',\n    PhysicalKeyboardKey.digit1: '1',\n    PhysicalKeyboardKey.digit2: '2',\n    PhysicalKeyboardKey.digit3: '3',\n    PhysicalKeyboardKey.digit4: '4',\n    PhysicalKeyboardKey.digit5: '5',\n    PhysicalKeyboardKey.digit6: '6',\n    PhysicalKeyboardKey.digit7: '7',\n    PhysicalKeyboardKey.digit8: '8',\n    PhysicalKeyboardKey.digit9: '9',\n    PhysicalKeyboardKey.digit0: '0',\n    PhysicalKeyboardKey.enter: 'Enter',\n    PhysicalKeyboardKey.escape: 'Esc',\n    PhysicalKeyboardKey.backspace: 'Backspace',\n    PhysicalKeyboardKey.tab: 'Tab',\n    PhysicalKeyboardKey.space: 'Space',\n    PhysicalKeyboardKey.minus: '-',\n    PhysicalKeyboardKey.equal: '=',\n    PhysicalKeyboardKey.bracketLeft: '[',\n    PhysicalKeyboardKey.bracketRight: ']',\n    PhysicalKeyboardKey.backslash: r'\\',\n    PhysicalKeyboardKey.semicolon: ';',\n    PhysicalKeyboardKey.quote: \"'\",\n    PhysicalKeyboardKey.backquote: '`',\n    PhysicalKeyboardKey.comma: ',',\n    PhysicalKeyboardKey.period: '.',\n    PhysicalKeyboardKey.slash: '/',\n    PhysicalKeyboardKey.capsLock: 'CapsLock',\n    PhysicalKeyboardKey.f1: 'F1',\n    PhysicalKeyboardKey.f2: 'F2',\n    PhysicalKeyboardKey.f3: 'F3',\n    PhysicalKeyboardKey.f4: 'F4',\n    PhysicalKeyboardKey.f5: 'F5',\n    PhysicalKeyboardKey.f6: 'F6',\n    PhysicalKeyboardKey.f7: 'F7',\n    PhysicalKeyboardKey.f8: 'F8',\n    PhysicalKeyboardKey.f9: 'F9',\n    PhysicalKeyboardKey.f10: 'F10',\n    PhysicalKeyboardKey.f11: 'F11',\n    PhysicalKeyboardKey.f12: 'F12',\n    PhysicalKeyboardKey.f13: 'F13',\n    PhysicalKeyboardKey.f14: 'F14',\n    PhysicalKeyboardKey.f15: 'F15',\n    PhysicalKeyboardKey.f16: 'F16',\n    PhysicalKeyboardKey.printScreen: 'PrintScreen',\n    PhysicalKeyboardKey.scrollLock: 'ScrollLock',\n    PhysicalKeyboardKey.pause: 'Pause',\n    PhysicalKeyboardKey.insert: 'Insert',\n    PhysicalKeyboardKey.home: 'Home',\n    PhysicalKeyboardKey.pageUp: 'PageUp',\n    PhysicalKeyboardKey.delete: 'Delete',\n    PhysicalKeyboardKey.end: 'End',\n    PhysicalKeyboardKey.pageDown: 'PageDown',\n    PhysicalKeyboardKey.arrowRight: 'ArrowRight',\n    PhysicalKeyboardKey.arrowLeft: 'ArrowLeft',\n    PhysicalKeyboardKey.arrowDown: 'ArrowDown',\n    PhysicalKeyboardKey.arrowUp: 'ArrowUp',\n    PhysicalKeyboardKey.numLock: 'NumLock',\n    PhysicalKeyboardKey.numpadDivide: 'Num /',\n    PhysicalKeyboardKey.numpadMultiply: 'Num *',\n    PhysicalKeyboardKey.numpadSubtract: 'Num -',\n    PhysicalKeyboardKey.numpadAdd: 'Num +',\n    PhysicalKeyboardKey.numpadEnter: 'Num Enter',\n    PhysicalKeyboardKey.numpad1: 'Num 1',\n    PhysicalKeyboardKey.numpad2: 'Num 2',\n    PhysicalKeyboardKey.numpad3: 'Num 3',\n    PhysicalKeyboardKey.numpad4: 'Num 4',\n    PhysicalKeyboardKey.numpad5: 'Num 5',\n    PhysicalKeyboardKey.numpad6: 'Num 6',\n    PhysicalKeyboardKey.numpad7: 'Num 7',\n    PhysicalKeyboardKey.numpad8: 'Num 8',\n    PhysicalKeyboardKey.numpad9: 'Num 9',\n    PhysicalKeyboardKey.numpad0: 'Num 0',\n    PhysicalKeyboardKey.numpadDecimal: 'Num .',\n    PhysicalKeyboardKey.contextMenu: 'ContextMenu',\n    PhysicalKeyboardKey.controlLeft: 'LControl',\n    PhysicalKeyboardKey.shiftLeft: 'LShift',\n    PhysicalKeyboardKey.altLeft: 'LAlt',\n    PhysicalKeyboardKey.metaLeft: 'LMeta',\n    PhysicalKeyboardKey.controlRight: 'RControl',\n    PhysicalKeyboardKey.shiftRight: 'RShift',\n    PhysicalKeyboardKey.altRight: 'RAlt',\n    PhysicalKeyboardKey.metaRight: 'RMeta',\n  };\n}\n\nclass KeyboardKey extends PositionComponent {\n  KeyboardKey({required this.text, super.position}) {\n    textElement = textRenderer.format(text);\n    width = textElement.metrics.width + padding.x;\n    height = textElement.metrics.height + padding.y;\n    textElement.translate(\n      padding.x / 2,\n      padding.y / 2 + textElement.metrics.ascent,\n    );\n    rect = RRect.fromLTRBR(0, 0, width, height, const Radius.circular(8));\n  }\n\n  final String text;\n  late final InlineTextElement textElement;\n  late final RRect rect;\n\n  /// The KeyEvents may occur very fast, and out of sync with the game loop.\n  /// On each such event we remove old KeyboardKey components, and add new ones.\n  /// However, since multiple KeyEvents may occur within a single game tick,\n  /// we end up adding/removing components many times within that tick, and for\n  /// a brief moment there could be a situation that the old components still\n  /// haven't been removed while the new ones were already added. In order to\n  /// prevent this from happening, we mark all components that are about to be\n  /// removed as \"not visible\", which prevents them from being rendered while\n  /// they are waiting to be removed.\n  bool visible = true;\n\n  static final Vector2 padding = Vector2(24, 12);\n  static final Paint borderPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 3\n    ..color = const Color(0xffb5ffd0);\n  static final TextPaint textRenderer = TextPaint(\n    style: const TextStyle(\n      fontSize: 20,\n      fontWeight: FontWeight.bold,\n      color: Color(0xffb5ffd0),\n    ),\n  );\n\n  @override\n  void render(Canvas canvas) {\n    if (visible) {\n      canvas.drawRRect(rect, borderPaint);\n      textElement.draw(canvas);\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/hover_callbacks_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass HoverCallbacksExample extends FlameGame {\n  static const String description = '''\n    This example shows how to use `HoverCallbacks`s.\\n\\n\n    Add more squares by clicking and hover them to change their color.\n  ''';\n\n  HoverCallbacksExample() : super(world: HoverCallbacksWorld());\n\n  @override\n  Future<void> onLoad() async {\n    camera.viewfinder.anchor = Anchor.topLeft;\n    camera.viewfinder.zoom = 1.5;\n  }\n}\n\nclass HoverCallbacksWorld extends World with TapCallbacks {\n  @override\n  Future<void> onLoad() async {\n    add(HoverSquare(Vector2(200, 500)));\n    add(HoverSquare(Vector2(700, 300)));\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    add(HoverSquare(event.localPosition));\n  }\n}\n\nclass HoverSquare extends RectangleComponent with HoverCallbacks {\n  static final Paint _white = Paint()..color = const Color(0xFFFFFFFF);\n  static final Paint _grey = Paint()..color = const Color(0xFFA5A5A5);\n\n  HoverSquare(Vector2 position)\n    : super(\n        position: position,\n        size: Vector2.all(100),\n        anchor: Anchor.center,\n      );\n\n  @override\n  Paint get paint => isHovered ? _grey : _white;\n}\n"
  },
  {
    "path": "examples/lib/stories/input/input.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/input/advanced_button_example.dart';\nimport 'package:examples/stories/input/double_tap_callbacks_example.dart';\nimport 'package:examples/stories/input/drag_callbacks_example.dart';\nimport 'package:examples/stories/input/gesture_hitboxes_example.dart';\nimport 'package:examples/stories/input/hardware_keyboard_example.dart';\nimport 'package:examples/stories/input/hover_callbacks_example.dart';\nimport 'package:examples/stories/input/joystick_advanced_example.dart';\nimport 'package:examples/stories/input/joystick_example.dart';\nimport 'package:examples/stories/input/keyboard_example.dart';\nimport 'package:examples/stories/input/keyboard_listener_component_example.dart';\nimport 'package:examples/stories/input/mouse_cursor_example.dart';\nimport 'package:examples/stories/input/mouse_movement_example.dart';\nimport 'package:examples/stories/input/multitap_advanced_example.dart';\nimport 'package:examples/stories/input/multitap_example.dart';\nimport 'package:examples/stories/input/overlapping_tap_callbacks_example.dart';\nimport 'package:examples/stories/input/scroll_example.dart';\nimport 'package:examples/stories/input/secondary_tap_callbacks_example.dart';\nimport 'package:examples/stories/input/tap_callbacks_example.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nvoid addInputStories(Dashbook dashbook) {\n  dashbook.storiesOf('Input')\n    ..add(\n      'TapCallbacks',\n      (_) => GameWidget(game: TapCallbacksExample()),\n      codeLink: baseLink('input/tap_callbacks_example.dart'),\n      info: TapCallbacksExample.description,\n    )\n    ..add(\n      'SecondaryTapCallbacks',\n      (_) => GameWidget(game: SecondaryTapCallbacksExample()),\n      codeLink: baseLink('input/secondary_tap_callbacks_example.dart'),\n      info: SecondaryTapCallbacksExample.description,\n    )\n    ..add(\n      'DragCallbacks',\n      (context) {\n        return GameWidget(\n          game: DragCallbacksExample(\n            zoom: context.listProperty('zoom', 1, [0.5, 1, 1.5]),\n          ),\n        );\n      },\n      codeLink: baseLink('input/drag_callbacks_example.dart'),\n      info: DragCallbacksExample.description,\n    )\n    ..add(\n      'Double Tap (Component)',\n      (context) {\n        return GameWidget(\n          game: DoubleTapCallbacksExample(),\n        );\n      },\n      codeLink: baseLink('input/double_tap_callbacks_example.dart'),\n      info: DoubleTapCallbacksExample.description,\n    )\n    ..add(\n      'HoverCallbacks',\n      (_) => GameWidget(game: HoverCallbacksExample()),\n      codeLink: baseLink('input/hover_callbacks_example.dart'),\n      info: HoverCallbacksExample.description,\n    )\n    ..add(\n      'Keyboard',\n      (_) => GameWidget(game: KeyboardExample()),\n      codeLink: baseLink('input/keyboard_example.dart'),\n      info: KeyboardExample.description,\n    )\n    ..add(\n      'Keyboard (Component)',\n      (_) => GameWidget(game: KeyboardListenerComponentExample()),\n      codeLink: baseLink('input/keyboard_listener_component_example.dart'),\n      info: KeyboardListenerComponentExample.description,\n    )\n    ..add(\n      'Hardware Keyboard',\n      (_) => GameWidget(game: HardwareKeyboardExample()),\n      codeLink: baseLink('input/hardware_keyboard_example.dart'),\n      info: HardwareKeyboardExample.description,\n    )\n    ..add(\n      'Mouse Movement',\n      (_) => GameWidget(game: MouseMovementExample()),\n      codeLink: baseLink('input/mouse_movement_example.dart'),\n      info: MouseMovementExample.description,\n    )\n    ..add(\n      'Mouse Cursor',\n      (_) => GameWidget(\n        game: MouseCursorExample(),\n        mouseCursor: SystemMouseCursors.move,\n      ),\n      codeLink: baseLink('input/mouse_cursor_example.dart'),\n      info: MouseCursorExample.description,\n    )\n    ..add(\n      'Scroll',\n      (_) => GameWidget(game: ScrollExample()),\n      codeLink: baseLink('input/scroll_example.dart'),\n      info: ScrollExample.description,\n    )\n    ..add(\n      'Multitap',\n      (_) => GameWidget(game: MultitapExample()),\n      codeLink: baseLink('input/multitap_example.dart'),\n      info: MultitapExample.description,\n    )\n    ..add(\n      'Multitap Advanced',\n      (_) => GameWidget(game: MultitapAdvancedExample()),\n      codeLink: baseLink('input/multitap_advanced_example.dart'),\n      info: MultitapAdvancedExample.description,\n    )\n    ..add(\n      'Overlapping TapCallbacks',\n      (_) => GameWidget(game: OverlappingTapCallbacksExample()),\n      codeLink: baseLink('input/overlapping_tap_callbacks_example.dart'),\n      info: OverlappingTapCallbacksExample.description,\n    )\n    ..add(\n      'Gesture Hitboxes',\n      (_) => GameWidget(game: GestureHitboxesExample()),\n      codeLink: baseLink('input/gesture_hitboxes_example.dart'),\n      info: GestureHitboxesExample.description,\n    )\n    ..add(\n      'Joystick',\n      (_) => GameWidget(game: JoystickExample()),\n      codeLink: baseLink('input/joystick_example.dart'),\n      info: JoystickExample.description,\n    )\n    ..add(\n      'Joystick Advanced',\n      (_) => GameWidget(game: JoystickAdvancedExample()),\n      codeLink: baseLink('input/joystick_advanced_example.dart'),\n      info: JoystickAdvancedExample.description,\n    )\n    ..add(\n      'Advanced Button',\n      (_) => GameWidget(game: AdvancedButtonExample()),\n      codeLink: baseLink('input/advanced_button_example.dart'),\n      info: AdvancedButtonExample.description,\n    );\n}\n"
  },
  {
    "path": "examples/lib/stories/input/joystick_advanced_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:examples/stories/input/joystick_player.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame/sprite.dart';\nimport 'package:flutter/material.dart';\n\nclass JoystickAdvancedExample extends FlameGame with HasCollisionDetection {\n  static const String description = '''\n    In this example we showcase how to use the joystick by creating \n    `SpriteComponent`s that serve as the joystick's knob and background.\n    We also showcase the `HudButtonComponent` which is a button that has its\n    position defined by margins to the edges, which can be useful when making\n    controller buttons.\\n\\n\n    Steer the player by using the joystick and flip and rotate it by pressing\n    the buttons.\n  ''';\n\n  JoystickAdvancedExample()\n    : super(\n        camera: CameraComponent.withFixedResolution(width: 1200, height: 800),\n      );\n\n  late final JoystickPlayer player;\n  late final JoystickComponent joystick;\n  late final TextComponent speedText;\n  late final TextComponent directionText;\n\n  @override\n  Future<void> onLoad() async {\n    final image = await images.load('joystick.png');\n    final sheet = SpriteSheet.fromColumnsAndRows(\n      image: image,\n      columns: 6,\n      rows: 1,\n    );\n    world.add(ScreenHitbox());\n    joystick = JoystickComponent(\n      knob: SpriteComponent(\n        sprite: sheet.getSpriteById(1),\n        size: Vector2.all(100),\n      ),\n      background: SpriteComponent(\n        sprite: sheet.getSpriteById(0),\n        size: Vector2.all(150),\n      ),\n      margin: const EdgeInsets.only(left: 40, bottom: 40),\n    );\n    player = JoystickPlayer(joystick);\n\n    final buttonSize = Vector2.all(80);\n    // A button with margin from the edge of the viewport that flips the\n    // rendering of the player on the X-axis.\n    final flipButton = HudButtonComponent(\n      button: SpriteComponent(\n        sprite: sheet.getSpriteById(2),\n        size: buttonSize,\n      ),\n      buttonDown: SpriteComponent(\n        sprite: sheet.getSpriteById(4),\n        size: buttonSize,\n      ),\n      margin: const EdgeInsets.only(\n        right: 80,\n        bottom: 60,\n      ),\n      onPressed: player.flipHorizontally,\n    );\n\n    // A button with margin from the edge of the viewport that flips the\n    // rendering of the player on the Y-axis.\n    final flopButton = HudButtonComponent(\n      button: SpriteComponent(\n        sprite: sheet.getSpriteById(3),\n        size: buttonSize,\n      ),\n      buttonDown: SpriteComponent(\n        sprite: sheet.getSpriteById(5),\n        size: buttonSize,\n      ),\n      margin: const EdgeInsets.only(\n        right: 160,\n        bottom: 60,\n      ),\n      onPressed: player.flipVertically,\n    );\n\n    final rng = Random();\n    // A button, created from a shape, that adds a rotation effect to the player\n    // when it is pressed.\n    final shapeButton = HudButtonComponent(\n      button: CircleComponent(radius: 35),\n      buttonDown: RectangleComponent(\n        size: buttonSize,\n        paint: BasicPalette.blue.paint(),\n      ),\n      margin: const EdgeInsets.only(\n        right: 85,\n        bottom: 150,\n      ),\n      onPressed: () => player.add(\n        RotateEffect.by(\n          8 * rng.nextDouble(),\n          EffectController(\n            duration: 1,\n            reverseDuration: 1,\n            curve: Curves.bounceOut,\n          ),\n        ),\n      ),\n    );\n\n    // A button, created from a shape, that adds a scale effect to the player\n    // when it is pressed.\n    final buttonComponent = ButtonComponent(\n      button: RectangleComponent(\n        size: Vector2(185, 50),\n        paint: Paint()\n          ..color = Colors.orange\n          ..style = PaintingStyle.stroke,\n      ),\n      buttonDown: RectangleComponent(\n        size: Vector2(185, 50),\n        paint: BasicPalette.magenta.paint(),\n      ),\n      position: Vector2(20, size.y - 280),\n      onPressed: () => player.add(\n        ScaleEffect.by(\n          Vector2.all(1.5),\n          EffectController(duration: 1.0, reverseDuration: 1.0),\n        ),\n      ),\n    );\n\n    final buttonSprites = await images.load('buttons.png');\n    final buttonSheet = SpriteSheet.fromColumnsAndRows(\n      image: buttonSprites,\n      columns: 1,\n      rows: 2,\n    );\n\n    // A sprite button, created from a shape, that adds a opacity effect to the\n    // player when it is pressed.\n    final spriteButtonComponent = SpriteButtonComponent(\n      button: buttonSheet.getSpriteById(0),\n      buttonDown: buttonSheet.getSpriteById(1),\n      position: Vector2(20, size.y - 360),\n      size: Vector2(185, 50),\n      onPressed: () => player.add(\n        OpacityEffect.fadeOut(\n          EffectController(duration: 0.5, reverseDuration: 0.5),\n        ),\n      ),\n    );\n\n    final regular = TextPaint(\n      style: TextStyle(color: BasicPalette.white.color),\n    );\n    speedText = TextComponent(\n      text: 'Speed: 0',\n      textRenderer: regular,\n    );\n    directionText = TextComponent(\n      text: 'Direction: idle',\n      textRenderer: regular,\n    );\n\n    final speedWithMargin = HudMarginComponent(\n      margin: const EdgeInsets.only(\n        top: 80,\n        left: 80,\n      ),\n    )..add(speedText);\n\n    final directionWithMargin = HudMarginComponent(\n      margin: const EdgeInsets.only(\n        top: 110,\n        left: 80,\n      ),\n    )..add(directionText);\n\n    world.add(player);\n    camera.viewport.addAll([\n      joystick,\n      flipButton,\n      flopButton,\n      buttonComponent,\n      spriteButtonComponent,\n      shapeButton,\n      speedWithMargin,\n      directionWithMargin,\n    ]);\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    speedText.text = 'Speed: ${(joystick.intensity * player.maxSpeed).round()}';\n    final direction = joystick.direction.toString().replaceAll(\n      'JoystickDirection.',\n      '',\n    );\n    directionText.text = 'Direction: $direction';\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/joystick_example.dart",
    "content": "import 'package:examples/stories/input/joystick_player.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/painting.dart';\n\nclass JoystickExample extends FlameGame {\n  static const String description = '''\n    In this example we showcase how to use the joystick by creating simple\n    `CircleComponent`s that serve as the joystick's knob and background.\n    Steer the player by using the joystick.\n  ''';\n\n  late final JoystickPlayer player;\n  late final JoystickComponent joystick;\n\n  @override\n  Future<void> onLoad() async {\n    final knobPaint = BasicPalette.blue.withAlpha(200).paint();\n    final backgroundPaint = BasicPalette.blue.withAlpha(100).paint();\n    joystick = JoystickComponent(\n      knob: CircleComponent(radius: 30, paint: knobPaint),\n      background: CircleComponent(radius: 100, paint: backgroundPaint),\n      margin: const EdgeInsets.only(left: 40, bottom: 40),\n    );\n    player = JoystickPlayer(joystick);\n\n    world.add(player);\n    camera.viewport.add(joystick);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/joystick_player.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\n\nclass JoystickPlayer extends SpriteComponent\n    with HasGameReference, CollisionCallbacks {\n  /// Pixels/s\n  double maxSpeed = 300.0;\n  late final Vector2 _lastSize = size.clone();\n  late final Transform2D _lastTransform = transform.clone();\n\n  final JoystickComponent joystick;\n\n  JoystickPlayer(this.joystick)\n    : super(size: Vector2.all(100.0), anchor: Anchor.center);\n\n  @override\n  Future<void> onLoad() async {\n    sprite = await game.loadSprite('layers/player.png');\n    add(RectangleHitbox());\n  }\n\n  @override\n  void update(double dt) {\n    if (!joystick.delta.isZero() && activeCollisions.isEmpty) {\n      _lastSize.setFrom(size);\n      _lastTransform.setFrom(transform);\n      position.add(joystick.relativeDelta * maxSpeed * dt);\n      angle = joystick.delta.screenAngle();\n    }\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n    transform.setFrom(_lastTransform);\n    size.setFrom(_lastSize);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/keyboard_example.dart",
    "content": "import 'package:examples/commons/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter/widgets.dart';\n\nclass KeyboardExample extends FlameGame with KeyboardEvents {\n  static const String description = '''\n    Example showcasing how to act on keyboard events.\n    It also briefly showcases how to create a game without the FlameGame.\n    Usage: Use WASD to steer Ember.\n  ''';\n\n  // Speed at which amber moves.\n  static const double _speed = 200;\n\n  // Direction in which amber is moving.\n  final Vector2 _direction = Vector2.zero();\n\n  @override\n  Future<void> onLoad() async {\n    add(\n      Ember(\n        key: ComponentKey.named('ember'),\n        position: size / 2,\n        size: Vector2.all(100),\n      ),\n    );\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    final ember = findByKeyName<Ember>('ember');\n    final displacement = _direction.normalized() * _speed * dt;\n    ember?.position.add(displacement);\n  }\n\n  @override\n  KeyEventResult onKeyEvent(\n    KeyEvent event,\n    Set<LogicalKeyboardKey> keysPressed,\n  ) {\n    final isKeyDown = event is KeyDownEvent;\n\n    // Avoiding repeat event as we are interested only in\n    // key up and key down event.\n    if (key is! KeyRepeatEvent) {\n      if (event.logicalKey == LogicalKeyboardKey.keyA) {\n        _direction.x += isKeyDown ? -1 : 1;\n      } else if (event.logicalKey == LogicalKeyboardKey.keyD) {\n        _direction.x += isKeyDown ? 1 : -1;\n      } else if (event.logicalKey == LogicalKeyboardKey.keyW) {\n        _direction.y += isKeyDown ? -1 : 1;\n      } else if (event.logicalKey == LogicalKeyboardKey.keyS) {\n        _direction.y += isKeyDown ? 1 : -1;\n      }\n    }\n\n    return super.onKeyEvent(event, keysPressed);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/keyboard_listener_component_example.dart",
    "content": "import 'package:examples/commons/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flutter/services.dart';\n\nclass KeyboardListenerComponentExample extends FlameGame\n    with HasKeyboardHandlerComponents {\n  static const String description = '''\n    Similar to the default Keyboard example, but shows a different\n    implementation approach, which uses Flame's\n    KeyboardListenerComponent to handle input.\n    Usage: Use WASD to steer Ember.\n  ''';\n\n  static const int _speed = 200;\n\n  late final Ember _ember;\n  final Vector2 _direction = Vector2.zero();\n\n  final Map<LogicalKeyboardKey, double> _keyWeights = {\n    LogicalKeyboardKey.keyW: 0,\n    LogicalKeyboardKey.keyA: 0,\n    LogicalKeyboardKey.keyS: 0,\n    LogicalKeyboardKey.keyD: 0,\n  };\n\n  @override\n  Future<void> onLoad() async {\n    _ember = Ember(position: size / 2, size: Vector2.all(100));\n    add(_ember);\n\n    add(\n      KeyboardListenerComponent(\n        keyUp: {\n          LogicalKeyboardKey.keyA: (keys) =>\n              _handleKey(LogicalKeyboardKey.keyA, false),\n          LogicalKeyboardKey.keyD: (keys) =>\n              _handleKey(LogicalKeyboardKey.keyD, false),\n          LogicalKeyboardKey.keyW: (keys) =>\n              _handleKey(LogicalKeyboardKey.keyW, false),\n          LogicalKeyboardKey.keyS: (keys) =>\n              _handleKey(LogicalKeyboardKey.keyS, false),\n        },\n        keyDown: {\n          LogicalKeyboardKey.keyA: (keys) =>\n              _handleKey(LogicalKeyboardKey.keyA, true),\n          LogicalKeyboardKey.keyD: (keys) =>\n              _handleKey(LogicalKeyboardKey.keyD, true),\n          LogicalKeyboardKey.keyW: (keys) =>\n              _handleKey(LogicalKeyboardKey.keyW, true),\n          LogicalKeyboardKey.keyS: (keys) =>\n              _handleKey(LogicalKeyboardKey.keyS, true),\n        },\n      ),\n    );\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    _direction\n      ..setValues(xInput, yInput)\n      ..normalize();\n\n    final displacement = _direction * (_speed * dt);\n    _ember.position.add(displacement);\n  }\n\n  bool _handleKey(LogicalKeyboardKey key, bool isDown) {\n    _keyWeights[key] = isDown ? 1 : 0;\n    return true;\n  }\n\n  double get xInput =>\n      _keyWeights[LogicalKeyboardKey.keyD]! -\n      _keyWeights[LogicalKeyboardKey.keyA]!;\n\n  double get yInput =>\n      _keyWeights[LogicalKeyboardKey.keyS]! -\n      _keyWeights[LogicalKeyboardKey.keyW]!;\n}\n"
  },
  {
    "path": "examples/lib/stories/input/mouse_cursor_example.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/widgets.dart';\n\nclass MouseCursorExample extends FlameGame with MouseMovementDetector {\n  static const String description = '''\n    Example showcasing the ability to change the game cursor in runtime\n    hover the little square to see the cursor changing\n  ''';\n\n  static const speed = 200;\n  static final Paint _blue = BasicPalette.blue.paint();\n  static final Paint _white = BasicPalette.white.paint();\n  static final Vector2 objSize = Vector2.all(150);\n\n  Vector2 position = Vector2(100, 100);\n  Vector2? target;\n\n  bool onTarget = false;\n\n  @override\n  void onMouseMove(PointerHoverInfo info) {\n    target = info.eventPosition.widget;\n  }\n\n  Rect _toRect() => position.toPositionedRect(objSize);\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    canvas.drawRect(\n      _toRect(),\n      onTarget ? _blue : _white,\n    );\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    final target = this.target;\n    if (target != null) {\n      final hovering = _toRect().contains(target.toOffset());\n      if (hovering) {\n        if (!onTarget) {\n          //Entered\n          mouseCursor = SystemMouseCursors.grab;\n        }\n      } else {\n        if (onTarget) {\n          // Exited\n          mouseCursor = SystemMouseCursors.move;\n        }\n      }\n      onTarget = hovering;\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/mouse_movement_example.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/material.dart';\n\nclass MouseMovementExample extends FlameGame with MouseMovementDetector {\n  static const String description = '''\n    In this example we show how you can use `MouseMovementDetector`.\\n\\n\n    Move around the mouse on the canvas and the white square will follow it and\n    turn into blue if it reaches the mouse, or the edge of the canvas.\n  ''';\n\n  static const speed = 200;\n  static final Paint _blue = BasicPalette.blue.paint();\n  static final Paint _white = BasicPalette.white.paint();\n  static final Vector2 objSize = Vector2.all(50);\n\n  Vector2 position = Vector2(0, 0);\n  Vector2? target;\n\n  bool onTarget = false;\n\n  @override\n  void onMouseMove(PointerHoverInfo info) {\n    target = info.eventPosition.widget;\n  }\n\n  Rect _toRect() => position.toPositionedRect(objSize);\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    canvas.drawRect(\n      _toRect(),\n      onTarget ? _blue : _white,\n    );\n  }\n\n  @override\n  void update(double dt) {\n    final target = this.target;\n    super.update(dt);\n    if (target != null) {\n      onTarget = _toRect().contains(target.toOffset());\n\n      if (!onTarget) {\n        final dir = (target - position).normalized();\n        position += dir * (speed * dt);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/multitap_advanced_example.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/palette.dart';\n\n/// Showcases how to mix two advanced detectors\nclass MultitapAdvancedExample extends FlameGame\n    with MultiTouchTapDetector, MultiTouchDragDetector {\n  static const String description = '''\n    This showcases the use of both `MultiTouchTapDetector` and\n    `MultiTouchDragDetector` simultaneously. Drag multiple fingers on the screen\n    to see rectangles of different sizes being drawn.\n  ''';\n\n  static final whitePaint = BasicPalette.white.paint();\n  static final tapSize = Vector2.all(50);\n\n  final Map<int, Rect> taps = {};\n\n  Vector2? start;\n  Vector2? end;\n  Rect? panRect;\n\n  @override\n  void onTapDown(int pointerId, TapDownInfo info) {\n    taps[pointerId] = info.eventPosition.widget.toPositionedRect(tapSize);\n  }\n\n  @override\n  void onTapUp(int pointerId, _) {\n    taps.remove(pointerId);\n  }\n\n  @override\n  void onTapCancel(int pointerId) {\n    taps.remove(pointerId);\n  }\n\n  @override\n  void onDragCancel(int pointerId) {\n    end = null;\n    start = null;\n    panRect = null;\n  }\n\n  @override\n  void onDragStart(int pointerId, DragStartInfo info) {\n    end = null;\n    start = info.eventPosition.widget;\n  }\n\n  @override\n  void onDragUpdate(int pointerId, DragUpdateInfo info) {\n    end = info.eventPosition.widget;\n  }\n\n  @override\n  void onDragEnd(int pointerId, _) {\n    final start = this.start;\n    final end = this.end;\n    if (start != null && end != null) {\n      panRect = start.toPositionedRect(end - start);\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    final panRect = this.panRect;\n    super.render(canvas);\n    taps.values.forEach((rect) {\n      canvas.drawRect(rect, whitePaint);\n    });\n\n    if (panRect != null) {\n      canvas.drawRect(panRect, whitePaint);\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/multitap_example.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/palette.dart';\n\n/// Includes an example including advanced detectors\nclass MultitapExample extends FlameGame with MultiTouchTapDetector {\n  static const String description = '''\n    In this example we showcase the multi touch capabilities\n    Touch multiple places on the screen and you will see multiple squares drawn,\n    one under each finger.\n  ''';\n\n  static final whitePaint = BasicPalette.white.paint();\n  static final tapSize = Vector2.all(50);\n\n  final Map<int, Rect> taps = {};\n\n  @override\n  void onTapDown(int pointerId, TapDownInfo info) {\n    taps[pointerId] = info.eventPosition.widget.toPositionedRect(tapSize);\n  }\n\n  @override\n  void onTapUp(int pointerId, _) {\n    taps.remove(pointerId);\n  }\n\n  @override\n  void onTapCancel(int pointerId) {\n    taps.remove(pointerId);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    taps.values.forEach((rect) {\n      canvas.drawRect(rect, whitePaint);\n    });\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/overlapping_tap_callbacks_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass OverlappingTapCallbacksExample extends FlameGame {\n  static const String description = '''\n    In this example we show you that events can choose to continue propagating\n    to underlying components. The middle green square continue to propagate the\n    events, meanwhile the others do not.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    add(TapCallbacksSquare(position: Vector2(100, 100)));\n    add(\n      TapCallbacksSquare(\n        position: Vector2(150, 150),\n        continuePropagation: true,\n      ),\n    );\n    add(TapCallbacksSquare(position: Vector2(100, 200)));\n  }\n}\n\nclass TapCallbacksSquare extends RectangleComponent with TapCallbacks {\n  TapCallbacksSquare({Vector2? position, this.continuePropagation = false})\n    : super(\n        position: position ?? Vector2.all(100),\n        size: Vector2.all(100),\n        paint: continuePropagation\n            ? (Paint()..color = Colors.green.withValues(alpha: 0.9))\n            : PaintExtension.random(withAlpha: 0.9, base: 100),\n      );\n\n  final bool continuePropagation;\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    event.continuePropagation = continuePropagation;\n    angle += 1.0;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/scale_example.dart",
    "content": "import 'dart:math';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(GameWidget(game: ScaleExample()));\n}\n\nclass ScaleExample extends FlameGame {\n  late RectangleComponent rect;\n  late TextComponent debugText;\n\n  late InteractiveRectangle interactiveRectangle;\n  Vector2 zoomCenter = Vector2.zero();\n  double startingZoom = 1;\n\n  final bool addScaleOnlyRectangle = true;\n  final bool addDragOnlyRectangle = true;\n  final bool addScaleDragRectangle = true;\n  final bool addZoom = false;\n  final bool addCameraRotation = false;\n\n  @override\n  Future<void> onLoad() async {\n    camera.viewfinder.zoom = 1;\n\n    debugText = TextComponent(\n      text: 'hello',\n      textRenderer: TextPaint(\n        style: const TextStyle(fontSize: 25, color: Colors.white),\n      ),\n      position: Vector2(50, 50),\n    );\n\n    if (addScaleOnlyRectangle) {\n      final scaleOnlyRectangle = ScaleOnlyRectangle(\n        position: Vector2(0, 0),\n        size: Vector2.all(150),\n      );\n      world.add(scaleOnlyRectangle);\n    }\n    if (addDragOnlyRectangle) {\n      final dragOnlyRectangle = DragOnlyRectangle(\n        position: Vector2(-200, -200),\n        size: Vector2.all(150),\n        color: Colors.green,\n      );\n      world.add(dragOnlyRectangle);\n    }\n\n    if (addScaleDragRectangle) {\n      interactiveRectangle = InteractiveRectangle(\n        position: Vector2(200, 200),\n        size: Vector2.all(150),\n        color: Colors.red,\n      );\n      world.add(interactiveRectangle);\n    }\n\n    camera.viewport.add(debugText);\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    if (addCameraRotation) {\n      camera.viewfinder.angle += 0.1 * dt;\n    }\n    if (addZoom) {\n      debugText.text = '${camera.viewfinder.zoom}';\n      camera.viewfinder.zoom += 0.1 * dt;\n    }\n  }\n}\n\n/// A rectangle component that can respond to both drag and scale gestures.\nclass InteractiveRectangle extends RectangleComponent\n    with ScaleCallbacks, DragCallbacks, HasGameReference<FlameGame> {\n  InteractiveRectangle({\n    required Vector2 position,\n    required Vector2 size,\n    Color color = Colors.blue,\n    Anchor anchor = Anchor.center,\n  }) : super(\n         position: position,\n         size: size,\n         anchor: anchor,\n         paint: Paint()..color = color,\n       );\n\n  bool isDoingScaling = false;\n  double initialAngle = 0;\n  Vector2 initialScale = Vector2.all(1);\n  double lastScale = 1.0;\n\n  @override\n  Future<void> onLoad() async {\n    final text = TextComponent(\n      text: 'drag + scale',\n      textRenderer: TextPaint(\n        style: const TextStyle(fontSize: 25, color: Colors.white),\n      ),\n      position: size / 2,\n      anchor: Anchor.center,\n    );\n    add(text);\n  }\n\n  /// DragCallbacks overrides\n  @override\n  void onDragStart(DragStartEvent event) {\n    super.onDragStart(event);\n    debugPrint('Drag started at ${event.devicePosition}');\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    super.onDragUpdate(event);\n    if (isScaling) {\n      return;\n    }\n    final rotated = event.canvasDelta.clone()\n      ..rotate(game.camera.viewfinder.angle);\n    position.add(rotated);\n  }\n\n  @override\n  void onDragEnd(DragEndEvent event) {\n    super.onDragEnd(event);\n    debugPrint('Drag ended with velocity ${event.velocity}');\n  }\n\n  /// ScaleCallbacks overrides\n  @override\n  void onScaleStart(ScaleStartEvent event) {\n    super.onScaleStart(event);\n    isDoingScaling = true;\n    initialAngle = angle;\n    initialScale = scale;\n    lastScale = 1.0;\n    debugPrint('Scale started at ${event.devicePosition}');\n  }\n\n  @override\n  void onScaleUpdate(ScaleUpdateEvent event) {\n    super.onScaleUpdate(event);\n    // scale rectangle size by pinch\n    angle = initialAngle + event.rotation;\n\n    // delta scale since last frame\n    if (lastScale == 0) {\n      return;\n    }\n    final scaleDelta = event.scale / lastScale;\n    lastScale = event.scale; // update for next frame\n\n    // apply delta gently\n    scale *= sqrt(scaleDelta);\n\n    // clamp\n    scale.clamp(Vector2.all(0.8), Vector2.all(3));\n  }\n\n  @override\n  void onScaleEnd(ScaleEndEvent event) {\n    super.onScaleEnd(event);\n    isDoingScaling = false;\n    debugPrint('Scale ended with velocity ${event.velocity}');\n  }\n}\n\n/// A rectangle that only responds to drag\nclass DragOnlyRectangle extends RectangleComponent\n    with DragCallbacks, HasGameReference<FlameGame> {\n  DragOnlyRectangle({\n    required Vector2 position,\n    required Vector2 size,\n    Color color = Colors.blue,\n    Anchor anchor = Anchor.center,\n  }) : super(\n         position: position,\n         size: size,\n         anchor: anchor,\n         paint: Paint()..color = color,\n       );\n\n  @override\n  Future<void> onLoad() async {\n    final text = TextComponent(\n      text: 'drag',\n      textRenderer: TextPaint(\n        style: const TextStyle(fontSize: 25, color: Colors.white),\n      ),\n      position: size / 2,\n      anchor: Anchor.center,\n    );\n    add(text);\n  }\n\n  /// DragCallbacks overrides\n  @override\n  void onDragStart(DragStartEvent event) {\n    super.onDragStart(event);\n    debugPrint('Drag started at ${event.devicePosition}');\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    super.onDragUpdate(event);\n    debugPrint('On Drag update');\n    final rotated = event.canvasDelta.clone()\n      ..rotate(game.camera.viewfinder.angle);\n    position.add(rotated);\n  }\n\n  @override\n  void onDragEnd(DragEndEvent event) {\n    super.onDragEnd(event);\n    debugPrint('Drag ended with velocity ${event.velocity}');\n  }\n}\n\n/// A rectangle that only responds to scale\nclass ScaleOnlyRectangle extends RectangleComponent with ScaleCallbacks {\n  ScaleOnlyRectangle({\n    required Vector2 position,\n    required Vector2 size,\n    Color color = Colors.blue,\n    Anchor anchor = Anchor.center,\n  }) : super(\n         position: position,\n         size: size,\n         anchor: anchor,\n         paint: Paint()..color = color,\n       );\n\n  @override\n  Future<void> onLoad() async {\n    final text = TextComponent(\n      text: 'scale',\n      textRenderer: TextPaint(\n        style: const TextStyle(fontSize: 25, color: Colors.white),\n      ),\n      position: size / 2,\n      anchor: Anchor.center,\n    );\n    add(text);\n  }\n\n  bool isDoingScaling = false;\n  double initialAngle = 0;\n  Vector2 initialScale = Vector2.all(1);\n  double lastScale = 1.0;\n\n  /// ScaleCallbacks overrides\n  @override\n  void onScaleStart(ScaleStartEvent event) {\n    super.onScaleStart(event);\n    isDoingScaling = true;\n    initialAngle = angle;\n    initialScale = scale;\n    lastScale = 1.0;\n    debugPrint('Scale started at ${event.devicePosition}');\n  }\n\n  @override\n  void onScaleUpdate(ScaleUpdateEvent event) {\n    super.onScaleUpdate(event);\n    // scale rectangle size by pinch\n    angle = initialAngle + event.rotation;\n    // delta scale since last frame\n    if (lastScale == 0) {\n      return;\n    }\n    final scaleDelta = event.scale / lastScale;\n    lastScale = event.scale; // update for next frame\n\n    // apply delta gently\n    scale *= sqrt(scaleDelta);\n\n    // clamp\n    scale.clamp(Vector2.all(0.8), Vector2.all(3));\n  }\n\n  @override\n  void onScaleEnd(ScaleEndEvent event) {\n    super.onScaleEnd(event);\n    isDoingScaling = false;\n    debugPrint('Scale ended with velocity ${event.velocity}');\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/scroll_example.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/palette.dart';\n\nclass ScrollExample extends FlameGame with ScrollDetector {\n  static const String description = '''\n    In this example we show how to use the `ScrollDetector`.\\n\\n\n    Scroll within the canvas (both horizontally and vertically) and the white\n    square will move around.\n  ''';\n\n  static const speed = 2000.0;\n  final _size = Vector2.all(50);\n  final _paint = BasicPalette.white.paint();\n\n  Vector2 position = Vector2.all(100);\n  Vector2? target;\n\n  @override\n  void onScroll(PointerScrollInfo info) {\n    target = position + info.scrollDelta.global * 5;\n  }\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    canvas.drawRect(position.toPositionedRect(_size), _paint);\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    final target = this.target;\n    final ds = speed * dt;\n    if (target != null) {\n      if (position != target) {\n        final diff = target - position;\n        if (diff.length < ds) {\n          position.setFrom(target);\n        } else {\n          diff.scaleTo(ds);\n          position.setFrom(position + diff);\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/secondary_tap_callbacks_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/material.dart';\n\nclass SecondaryTapCallbacksExample extends FlameGame {\n  static const String description = '''\n    In this example we show how to listen to both primary (left) and\n    secondary (right) tap events using the `TapCallbacks` and\n    the `SecondaryTapCallbacks` mixin to any `PositionComponent`.\\n\\n\n    The squares will change color depending on which button was used to tap them.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    world.add(TappableSquare()..anchor = Anchor.center);\n    world.add(TappableSquare()..y = 350);\n  }\n}\n\nclass TappableSquare extends RectangleComponent\n    with TapCallbacks, SecondaryTapCallbacks {\n  static final Paint _red = BasicPalette.red.paint();\n  static final Paint _blue = BasicPalette.blue.paint();\n  static final TextPaint _text = TextPaint(\n    style: TextStyle(color: BasicPalette.white.color, fontSize: 24),\n  );\n\n  int counter = 0;\n\n  TappableSquare({Vector2? position})\n    : super(\n        position: position ?? Vector2.all(100),\n        size: Vector2.all(100),\n        paint: _red,\n        anchor: Anchor.center,\n      );\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    _text.render(\n      canvas,\n      '$counter',\n      size / 2,\n      anchor: Anchor.center,\n    );\n  }\n\n  @override\n  void onTapDown(_) {\n    paint = _red;\n    counter++;\n  }\n\n  @override\n  void onSecondaryTapDown(_) {\n    paint = _blue;\n    counter++;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/input/tap_callbacks_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass TapCallbacksExample extends FlameGame {\n  static const String description = '''\n    In this example we show the `TapCallbacks` mixin functionality. You can add\n    the `TapCallbacks` mixin to any `PositionComponent`.\\n\\n\n    Tap the squares to see them change their angle around their anchor.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    world.add(TappableSquare()..anchor = Anchor.center);\n    world.add(TappableSquare()..y = 350);\n  }\n}\n\nclass TappableSquare extends RectangleComponent with TapCallbacks {\n  static final Paint _white = Paint()..color = const Color(0xFFFFFFFF);\n  static final Paint _grey = Paint()..color = const Color(0xFFA5A5A5);\n\n  bool _beenPressed = false;\n\n  TappableSquare({Vector2? position})\n    : super(\n        position: position ?? Vector2.all(100),\n        size: Vector2.all(100),\n      );\n\n  @override\n  Paint get paint => _beenPressed ? _grey : _white;\n\n  @override\n  void onTapUp(_) {\n    _beenPressed = false;\n  }\n\n  @override\n  void onTapDown(_) {\n    _beenPressed = true;\n    angle += 1.0;\n  }\n\n  @override\n  void onTapCancel(_) {\n    _beenPressed = false;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/layout/align_component.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/layout.dart';\n\nclass AlignComponentExample extends FlameGame {\n  static const String description = '''\n    In this example the AlignComponent is used to arrange the circles\n    so that there is one in the middle and 8 more surrounding it in\n    the shape of a diamond.\n    \n    The arrangement will remain intact if you change the window size.\n  ''';\n\n  @override\n  void onLoad() {\n    addAll([\n      AlignComponent(\n        child: CircleComponent(\n          radius: 40,\n          children: [\n            SizeEffect.by(\n              Vector2.all(25),\n              EffectController(\n                infinite: true,\n                duration: 0.75,\n                reverseDuration: 0.5,\n              ),\n            ),\n            AlignComponent(\n              alignment: Anchor.topCenter,\n              child: CircleComponent(\n                radius: 10,\n                anchor: Anchor.bottomCenter,\n              ),\n              keepChildAnchor: true,\n            ),\n            AlignComponent(\n              alignment: Anchor.bottomCenter,\n              child: CircleComponent(\n                radius: 10,\n                anchor: Anchor.topCenter,\n              ),\n              keepChildAnchor: true,\n            ),\n            AlignComponent(\n              alignment: Anchor.centerLeft,\n              child: CircleComponent(\n                radius: 10,\n                anchor: Anchor.centerRight,\n              ),\n              keepChildAnchor: true,\n            ),\n            AlignComponent(\n              alignment: Anchor.centerRight,\n              child: CircleComponent(\n                radius: 10,\n                anchor: Anchor.centerLeft,\n              ),\n              keepChildAnchor: true,\n            ),\n          ],\n        ),\n        alignment: Anchor.center,\n      ),\n      AlignComponent(\n        child: CircleComponent(radius: 30),\n        alignment: Anchor.topCenter,\n      ),\n      AlignComponent(\n        child: CircleComponent(radius: 30),\n        alignment: Anchor.bottomCenter,\n      ),\n      AlignComponent(\n        child: CircleComponent(radius: 30),\n        alignment: Anchor.centerLeft,\n      ),\n      AlignComponent(\n        child: CircleComponent(radius: 30),\n        alignment: Anchor.centerRight,\n      ),\n      AlignComponent(\n        child: CircleComponent(radius: 10),\n        alignment: const Anchor(0.25, 0.25),\n      ),\n      AlignComponent(\n        child: CircleComponent(radius: 10),\n        alignment: const Anchor(0.25, 0.75),\n      ),\n      AlignComponent(\n        child: CircleComponent(radius: 10),\n        alignment: const Anchor(0.75, 0.25),\n      ),\n      AlignComponent(\n        child: CircleComponent(radius: 10),\n        alignment: const Anchor(0.75, 0.75),\n      ),\n    ]);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/layout/layout.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/layout/align_component.dart';\nimport 'package:flame/game.dart';\n\nvoid addLayoutStories(Dashbook dashbook) {\n  dashbook\n      .storiesOf('Layout')\n      .add(\n        'AlignComponent',\n        (_) => GameWidget(game: AlignComponentExample()),\n        codeLink: baseLink('layout/align_component.dart'),\n        info: AlignComponentExample.description,\n      );\n}\n"
  },
  {
    "path": "examples/lib/stories/parallax/advanced_parallax_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/parallax.dart';\n\nclass AdvancedParallaxExample extends FlameGame {\n  static const String description = '''\n    Shows how to create a parallax with different velocity deltas on each layer.\n  ''';\n\n  final _layersMeta = {\n    'parallax/bg.png': 1.0,\n    'parallax/mountain-far.png': 1.5,\n    'parallax/mountains.png': 2.3,\n    'parallax/trees.png': 3.8,\n    'parallax/foreground-trees.png': 6.6,\n  };\n\n  @override\n  Future<void> onLoad() async {\n    final layers = _layersMeta.entries.map(\n      (e) => loadParallaxLayer(\n        ParallaxImageData(e.key),\n        velocityMultiplier: Vector2(e.value, 1.0),\n        filterQuality: FilterQuality.none,\n      ),\n    );\n    final parallax = ParallaxComponent(\n      parallax: Parallax(\n        await Future.wait(layers),\n        baseVelocity: Vector2(20, 0),\n      ),\n    );\n    add(parallax);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/parallax/animation_parallax_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/parallax.dart';\nimport 'package:flutter/painting.dart';\n\nclass AnimationParallaxExample extends FlameGame {\n  static const String description = '''\n    Shows how to use animations in a `ParallaxComponent`.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final cityLayer = await loadParallaxLayer(\n      ParallaxImageData('parallax/city.png'),\n      filterQuality: FilterQuality.none,\n    );\n\n    final rainLayer = await loadParallaxLayer(\n      ParallaxAnimationData(\n        'parallax/rain.png',\n        SpriteAnimationData.sequenced(\n          amount: 4,\n          stepTime: 0.3,\n          textureSize: Vector2(80, 160),\n        ),\n      ),\n      velocityMultiplier: Vector2(2, 0),\n      filterQuality: FilterQuality.none,\n    );\n\n    final cloudsLayer = await loadParallaxLayer(\n      ParallaxImageData('parallax/heavy_clouded.png'),\n      velocityMultiplier: Vector2(4, 0),\n      fill: LayerFill.none,\n      alignment: Alignment.topLeft,\n      filterQuality: FilterQuality.none,\n    );\n\n    final parallax = Parallax(\n      [\n        cityLayer,\n        rainLayer,\n        cloudsLayer,\n      ],\n      baseVelocity: Vector2(20, 0),\n    );\n\n    final parallaxComponent = ParallaxComponent(parallax: parallax);\n    add(parallaxComponent);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/parallax/basic_parallax_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/parallax.dart';\n\nclass BasicParallaxExample extends FlameGame {\n  static const String description = '''\n    Shows the simplest way to use a fullscreen `ParallaxComponent`.\n  ''';\n\n  final _imageNames = [\n    ParallaxImageData('parallax/bg.png'),\n    ParallaxImageData('parallax/mountain-far.png'),\n    ParallaxImageData('parallax/mountains.png'),\n    ParallaxImageData('parallax/trees.png'),\n    ParallaxImageData('parallax/foreground-trees.png'),\n  ];\n\n  @override\n  Future<void> onLoad() async {\n    final parallax = await loadParallaxComponent(\n      _imageNames,\n      baseVelocity: Vector2(20, 0),\n      velocityMultiplierDelta: Vector2(1.8, 1.0),\n      filterQuality: FilterQuality.none,\n    );\n    add(parallax);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/parallax/component_parallax_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/camera.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/parallax.dart';\n\nclass ComponentParallaxExample extends FlameGame {\n  static const String description = '''\n    Shows how to do initiation and loading of assets from within an extended\n    `ParallaxComponent`. This example uses a `FixedResolutionViewport` which\n    the `ParallaxComponent` is fullscreen within.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    camera.viewport = FixedResolutionViewport(\n      resolution: Vector2(800, 600),\n    );\n    camera.viewport.add(MyParallaxComponent());\n  }\n}\n\nclass MyParallaxComponent extends ParallaxComponent<ComponentParallaxExample> {\n  @override\n  Future<void> onLoad() async {\n    parallax = await game.loadParallax(\n      [\n        ParallaxImageData('parallax/bg.png'),\n        ParallaxImageData('parallax/mountain-far.png'),\n        ParallaxImageData('parallax/mountains.png'),\n        ParallaxImageData('parallax/trees.png'),\n        ParallaxImageData('parallax/foreground-trees.png'),\n      ],\n      baseVelocity: Vector2(20, 0),\n      velocityMultiplierDelta: Vector2(1.8, 1.0),\n      filterQuality: FilterQuality.none,\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/parallax/no_fcs_parallax_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/parallax.dart';\n\nclass NoFCSParallaxExample extends Game {\n  static const String description = '''\n    This examples serves to test the Parallax feature outside of the Flame\n    Component System (FCS), use the other files in this folder for examples on\n    how to use parallax with FCS.\\n\n    FCS is only used when you extend FlameGame, not when you only use the Game\n    mixin, like we do in this example.\n  ''';\n\n  late Parallax parallax;\n\n  @override\n  Future<void> onLoad() async {\n    parallax = await loadParallax(\n      [\n        ParallaxImageData('parallax/bg.png'),\n        ParallaxImageData('parallax/mountain-far.png'),\n        ParallaxImageData('parallax/mountains.png'),\n        ParallaxImageData('parallax/trees.png'),\n        ParallaxImageData('parallax/foreground-trees.png'),\n      ],\n      size: size,\n      baseVelocity: Vector2(20, 0),\n      velocityMultiplierDelta: Vector2(1.8, 1.0),\n      filterQuality: FilterQuality.none,\n    );\n  }\n\n  @override\n  void update(double dt) {\n    parallax.update(dt);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    parallax.render(canvas);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/parallax/parallax.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/parallax/advanced_parallax_example.dart';\nimport 'package:examples/stories/parallax/animation_parallax_example.dart';\nimport 'package:examples/stories/parallax/basic_parallax_example.dart';\nimport 'package:examples/stories/parallax/component_parallax_example.dart';\nimport 'package:examples/stories/parallax/no_fcs_parallax_example.dart';\nimport 'package:examples/stories/parallax/sandbox_layer_parallax_example.dart';\nimport 'package:examples/stories/parallax/small_parallax_example.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/parallax.dart';\nimport 'package:flutter/painting.dart';\n\nvoid addParallaxStories(Dashbook dashbook) {\n  dashbook.storiesOf('Parallax')\n    ..add(\n      'Basic',\n      (_) => GameWidget(game: BasicParallaxExample()),\n      codeLink: baseLink('parallax/basic_parallax_example.dart'),\n      info: BasicParallaxExample.description,\n    )\n    ..add(\n      'Component',\n      (_) => GameWidget(game: ComponentParallaxExample()),\n      codeLink: baseLink('parallax/component_parallax_example.dart'),\n      info: ComponentParallaxExample.description,\n    )\n    ..add(\n      'Animation',\n      (_) => GameWidget(game: AnimationParallaxExample()),\n      codeLink: baseLink('parallax/animation_parallax_example.dart'),\n      info: AnimationParallaxExample.description,\n    )\n    ..add(\n      'Non-fullscreen',\n      (_) => GameWidget(game: SmallParallaxExample()),\n      codeLink: baseLink('parallax/small_parallax_example.dart'),\n      info: SmallParallaxExample.description,\n    )\n    ..add(\n      'No FCS',\n      (_) => GameWidget(game: NoFCSParallaxExample()),\n      codeLink: baseLink('parallax/no_fcs_parallax_example.dart'),\n      info: NoFCSParallaxExample.description,\n    )\n    ..add(\n      'Advanced',\n      (_) => GameWidget(game: AdvancedParallaxExample()),\n      codeLink: baseLink('parallax/advanced_parallax_example.dart'),\n      info: AdvancedParallaxExample.description,\n    )\n    ..add(\n      'Layer sandbox',\n      (context) {\n        return GameWidget(\n          game: SandboxLayerParallaxExample(\n            planeSpeed: Vector2(\n              context.numberProperty('plane x speed', 0),\n              context.numberProperty('plane y speed', 0),\n            ),\n            planeRepeat: context.listProperty(\n              'plane repeat strategy',\n              ImageRepeat.noRepeat,\n              ImageRepeat.values,\n            ),\n            planeFill: context.listProperty(\n              'plane fill strategy',\n              LayerFill.none,\n              LayerFill.values,\n            ),\n            planeAlignment: context.listProperty(\n              'plane alignment strategy',\n              Alignment.center,\n              [\n                Alignment.topLeft,\n                Alignment.topRight,\n                Alignment.center,\n                Alignment.topCenter,\n                Alignment.centerLeft,\n                Alignment.bottomLeft,\n                Alignment.bottomRight,\n                Alignment.bottomCenter,\n              ],\n            ),\n          ),\n        );\n      },\n      codeLink: baseLink('parallax/sandbox_layer_parallax_example.dart'),\n      info: SandboxLayerParallaxExample.description,\n    );\n}\n"
  },
  {
    "path": "examples/lib/stories/parallax/sandbox_layer_parallax_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/parallax.dart';\nimport 'package:flutter/painting.dart';\n\nclass SandboxLayerParallaxExample extends FlameGame {\n  static const String description = '''\n    In this example, properties of a layer can be changed to preview the\n    different combination of values. You can change the properties by pressing\n    the pen in the upper right corner.\n  ''';\n\n  final Vector2 planeSpeed;\n  final ImageRepeat planeRepeat;\n  final LayerFill planeFill;\n  final Alignment planeAlignment;\n\n  SandboxLayerParallaxExample({\n    required this.planeSpeed,\n    required this.planeRepeat,\n    required this.planeFill,\n    required this.planeAlignment,\n  });\n\n  @override\n  Future<void> onLoad() async {\n    final bgLayer = await loadParallaxLayer(\n      ParallaxImageData('parallax/bg.png'),\n      filterQuality: FilterQuality.none,\n    );\n    final mountainFarLayer = await loadParallaxLayer(\n      ParallaxImageData('parallax/mountain-far.png'),\n      velocityMultiplier: Vector2(1.8, 0),\n      filterQuality: FilterQuality.none,\n    );\n    final mountainLayer = await loadParallaxLayer(\n      ParallaxImageData('parallax/mountains.png'),\n      velocityMultiplier: Vector2(2.8, 0),\n      filterQuality: FilterQuality.none,\n    );\n    final treeLayer = await loadParallaxLayer(\n      ParallaxImageData('parallax/trees.png'),\n      velocityMultiplier: Vector2(3.8, 0),\n      filterQuality: FilterQuality.none,\n    );\n    final foregroundTreesLayer = await loadParallaxLayer(\n      ParallaxImageData('parallax/foreground-trees.png'),\n      velocityMultiplier: Vector2(4.8, 0),\n      filterQuality: FilterQuality.none,\n    );\n    final airplaneLayer = await loadParallaxLayer(\n      ParallaxAnimationData(\n        'parallax/airplane.png',\n        SpriteAnimationData.sequenced(\n          amount: 4,\n          stepTime: 0.2,\n          textureSize: Vector2(320, 160),\n        ),\n      ),\n      repeat: planeRepeat,\n      velocityMultiplier: planeSpeed,\n      fill: planeFill,\n      alignment: planeAlignment,\n      filterQuality: FilterQuality.none,\n    );\n\n    final parallax = Parallax(\n      [\n        bgLayer,\n        mountainFarLayer,\n        mountainLayer,\n        treeLayer,\n        foregroundTreesLayer,\n        airplaneLayer,\n      ],\n      baseVelocity: Vector2(20, 0),\n    );\n\n    add(ParallaxComponent(parallax: parallax));\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/parallax/small_parallax_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/parallax.dart';\n\nclass SmallParallaxExample extends FlameGame {\n  static const String description = '''\n    Shows how to create a smaller parallax in the center of the screen.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final component = await loadParallaxComponent(\n      [\n        ParallaxImageData('parallax/bg.png'),\n        ParallaxImageData('parallax/mountain-far.png'),\n        ParallaxImageData('parallax/mountains.png'),\n        ParallaxImageData('parallax/trees.png'),\n        ParallaxImageData('parallax/foreground-trees.png'),\n      ],\n      size: Vector2.all(200),\n      baseVelocity: Vector2(20, 0),\n      velocityMultiplierDelta: Vector2(1.8, 1.0),\n    );\n    component.position = size / 2;\n    add(component);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/rendering/decorator_hue_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:examples/commons/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/rendering.dart';\n\nclass DecoratorHueExample extends FlameGame with TapCallbacks {\n  static const String description = '''\nThis example demonstrates the usage of `HueDecorator` to shift the\ncolors of a component.\n\nBasic `HueDecorator` shifting the hue of an Ember component.\n\nClick to cycle through hue shifts.\n''';\n\n  late final HueDecorator decorator;\n  int step = 0;\n\n  @override\n  Future<void> onLoad() async {\n    decorator = HueDecorator();\n    world.add(\n      PositionComponent(\n        size: Vector2(150, 120),\n        anchor: Anchor.center,\n        children: [\n          Ember(\n            size: Vector2.all(80),\n            position: Vector2(75, 40),\n          ),\n          TextComponent(\n            text: 'HueDecorator',\n            position: Vector2(75, 100),\n            anchor: Anchor.center,\n          ),\n        ],\n      )..decorator.addLast(decorator),\n    );\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    step++;\n    final hues = [0.0, pi / 4, pi / 2, pi, 0.0];\n    final hue = hues[step % hues.length];\n    decorator.hue = hue;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/rendering/decorator_vs_effect_example.dart",
    "content": "import 'package:examples/commons/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/rendering.dart';\nimport 'package:flutter/widgets.dart';\n\nclass DecoratorVsEffectExample extends FlameGame {\n  static const String description = '''\nThis example demonstrates the difference between using an `Effect` and a\n`Decorator` for group transparency.\n\n1. Top (OpacityEffect):\nOpacity is applied to EACH child individually.\nNote the \"double-exposure\" where the sprites overlap.\n\n2. Bottom (Decorator):\nThe entire group is flattened into a layer first using `saveLayer`,\nand then transparency is applied to the whole layer.\nNote how the overlapping area is uniform.\n''';\n\n  @override\n  Future<void> onLoad() async {\n    final groupA = _buildItem(\n      'OpacityEffect (Individual Blend)',\n      _buildCompositeObject()\n        ..children.forEach((child) {\n          if (child is OpacityProvider) {\n            child.add(OpacityEffect.to(0.5, EffectController(duration: 0)));\n          }\n        }),\n    );\n\n    final groupB = _buildItem(\n      'Decorator (Group Blend)',\n      _buildCompositeObject(),\n    )..decorator.addLast(_GroupOpacityDecorator(0.5));\n\n    world.add(\n      ColumnComponent(\n        gap: 150,\n        anchor: Anchor.center,\n        children: [groupA, groupB],\n      ),\n    );\n  }\n\n  PositionComponent _buildItem(String title, Component object) {\n    return PositionComponent(\n      size: Vector2(150, 120),\n      children: [\n        object,\n        TextComponent(\n          text: title,\n          position: Vector2(0, 80),\n          anchor: Anchor.center,\n        ),\n      ],\n    );\n  }\n\n  /// Builds an object consisting of two overlapping Embers.\n  Component _buildCompositeObject() {\n    return PositionComponent(\n      children: [\n        Ember(\n          size: Vector2.all(100),\n          position: Vector2(-25, 0),\n        ),\n        Ember(\n          size: Vector2.all(100),\n          position: Vector2(25, 0),\n        ),\n      ],\n    );\n  }\n}\n\n/// A simple decorator that applies opacity to the entire decorated subtree.\nclass _GroupOpacityDecorator extends Decorator {\n  _GroupOpacityDecorator(double opacity)\n    : _paint = Paint()..color = Color.fromRGBO(255, 255, 255, opacity);\n\n  final Paint _paint;\n\n  @override\n  void apply(void Function(Canvas) draw, Canvas canvas) {\n    canvas.saveLayer(null, _paint);\n    draw(canvas);\n    canvas.restore();\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/rendering/decorators.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/rendering/decorator_hue_example.dart';\nimport 'package:examples/stories/rendering/decorator_vs_effect_example.dart';\nimport 'package:flame/game.dart';\n\nvoid addDecoratorStories(Dashbook dashbook) {\n  dashbook.storiesOf('Decorators')\n    ..add(\n      'Decorator Hue',\n      (_) => GameWidget(game: DecoratorHueExample()),\n      codeLink: baseLink('rendering/decorator_hue_example.dart'),\n      info: DecoratorHueExample.description,\n    )\n    ..add(\n      'Decorators vs Effects',\n      (_) => GameWidget(game: DecoratorVsEffectExample()),\n      codeLink: baseLink('rendering/decorator_vs_effect_example.dart'),\n      info: DecoratorVsEffectExample.description,\n    );\n}\n"
  },
  {
    "path": "examples/lib/stories/rendering/flip_sprite_example.dart",
    "content": "import 'package:examples/commons/ember.dart';\nimport 'package:flame/game.dart';\n\nclass FlipSpriteExample extends FlameGame {\n  static const String description = '''\n    In this example we show how you can flip components horizontally and\n    vertically.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final regular = Ember(position: Vector2(size.x / 2 - 100, 200));\n    add(regular);\n\n    final flipX = Ember(position: Vector2(size.x / 2 - 100, 400));\n    flipX.flipHorizontally();\n    add(flipX);\n\n    final flipY = Ember(position: Vector2(size.x / 2 + 100, 200));\n    flipY.flipVertically();\n    add(flipY);\n\n    final flipWithRotation = Ember(position: Vector2(size.x / 2 + 100, 400))\n      ..angle = 2;\n    flipWithRotation.flipVertically();\n    add(flipWithRotation);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/rendering/isometric_tile_map_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/sprite.dart';\n\nclass IsometricTileMapExample extends FlameGame with MouseMovementDetector {\n  static const String description = '''\n    Shows an example of how to use the `IsometricTileMapComponent`.\\n\\n\n    Move the mouse over the board to see a selector appearing on the tiles.\n  ''';\n\n  final topLeft = Vector2.all(500);\n\n  static const scale = 2.0;\n  static const srcTileSize = 32.0;\n  static const destTileSize = scale * srcTileSize;\n\n  final originColor = Paint()..color = const Color(0xFFFF00FF);\n  final originColor2 = Paint()..color = const Color(0xFFAA55FF);\n\n  final bool halfSize;\n  late final tileHeight = scale * (halfSize ? 8.0 : 16.0);\n  late final suffix = halfSize ? '-short' : '';\n\n  late IsometricTileMapComponent base;\n  late Selector selector;\n\n  IsometricTileMapExample({required this.halfSize});\n\n  @override\n  Future<void> onLoad() async {\n    final tilesetImage = await images.load('tile_maps/tiles$suffix.png');\n    final tileset = SpriteSheet(\n      image: tilesetImage,\n      srcSize: Vector2.all(srcTileSize),\n    );\n    final matrix = [\n      [3, 1, 1, 1, 0, 0],\n      [-1, 1, 2, 1, 0, 0],\n      [-1, 0, 1, 1, 0, 0],\n      [-1, 1, 1, 1, 0, 0],\n      [1, 1, 1, 1, 0, 2],\n      [1, 3, 3, 3, 0, 2],\n    ];\n    add(\n      base = IsometricTileMapComponent(\n        tileset,\n        matrix,\n        destTileSize: Vector2.all(destTileSize),\n        tileHeight: tileHeight,\n        position: topLeft,\n      ),\n    );\n\n    final selectorImage = await images.load('tile_maps/selector$suffix.png');\n    add(selector = Selector(destTileSize, selectorImage));\n  }\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    canvas.renderPoint(topLeft, size: 5, paint: originColor);\n    canvas.renderPoint(\n      base.position + base.getBlockCenterPosition(const Block(0, 0)),\n      size: 5,\n      paint: originColor2,\n    );\n  }\n\n  @override\n  void onMouseMove(PointerHoverInfo info) {\n    final screenPosition = info.eventPosition.widget;\n    final block = base.getBlock(screenPosition);\n    selector.show = base.containsBlock(block);\n    selector.position.setFrom(topLeft + base.getBlockRenderPosition(block));\n  }\n}\n\nclass Selector extends SpriteComponent {\n  bool show = true;\n\n  Selector(double s, Image image)\n    : super(\n        sprite: Sprite(image, srcSize: Vector2.all(32.0)),\n        size: Vector2.all(s),\n      );\n\n  @override\n  void render(Canvas canvas) {\n    if (!show) {\n      return;\n    }\n\n    super.render(canvas);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/rendering/layers_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/layers.dart';\n\nclass LayerExample extends FlameGame {\n  static const String description = '''\n    In this example we show how layers can be used to produce a shadow effect.\n  ''';\n\n  late Layer gameLayer;\n  late Layer backgroundLayer;\n\n  @override\n  Future<void> onLoad() async {\n    final playerSprite = Sprite(await images.load('layers/player.png'));\n    final enemySprite = Sprite(await images.load('layers/enemy.png'));\n    final backgroundSprite = Sprite(await images.load('layers/background.png'));\n\n    gameLayer = GameLayer(playerSprite, enemySprite);\n    backgroundLayer = BackgroundLayer(backgroundSprite);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    gameLayer.render(canvas);\n    backgroundLayer.render(canvas);\n  }\n\n  @override\n  Color backgroundColor() => const Color(0xFF38607C);\n}\n\nclass GameLayer extends DynamicLayer {\n  final Sprite playerSprite;\n  final Sprite enemySprite;\n\n  GameLayer(this.playerSprite, this.enemySprite) {\n    preProcessors.add(ShadowProcessor());\n  }\n\n  @override\n  void drawLayer() {\n    playerSprite.render(\n      canvas,\n      position: Vector2.all(50),\n      size: Vector2.all(150),\n    );\n    enemySprite.render(\n      canvas,\n      position: Vector2(250, 150),\n      size: Vector2(100, 50),\n    );\n  }\n}\n\nclass BackgroundLayer extends PreRenderedLayer {\n  final Sprite sprite;\n\n  BackgroundLayer(this.sprite) {\n    preProcessors.add(ShadowProcessor());\n  }\n\n  @override\n  void drawLayer() {\n    sprite.render(\n      canvas,\n      position: Vector2(50, 200),\n      size: Vector2(300, 150),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/rendering/nine_tile_box_custom_grid_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\n\nclass NineTileBoxCustomGridExample extends FlameGame\n    with TapCallbacks, DoubleTapDetector {\n  static const String description = '''\n    If you want to create a background for something that can stretch you can\n    use the `NineTileBox` which is showcased here. In this example a custom\n    grid is used.\\n\\n\n    Tap to make the box bigger and double tap to make it smaller.\n  ''';\n\n  late NineTileBoxComponent nineTileBoxComponent;\n\n  @override\n  Future<void> onLoad() async {\n    final sprite = Sprite(await images.load('speech-bubble.png'));\n    final boxSize = Vector2.all(300);\n    final nineTileBox = NineTileBox.withGrid(\n      sprite,\n      leftWidth: 31,\n      rightWidth: 5,\n      topHeight: 5,\n      bottomHeight: 21,\n    );\n    add(\n      nineTileBoxComponent = NineTileBoxComponent(\n        nineTileBox: nineTileBox,\n        position: size / 2,\n        size: boxSize,\n        anchor: Anchor.center,\n      ),\n    );\n  }\n\n  @override\n  void onTapDown(_) {\n    nineTileBoxComponent.scale.scale(1.2);\n  }\n\n  @override\n  void onDoubleTap() {\n    nineTileBoxComponent.scale.scale(0.8);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/rendering/nine_tile_box_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\n\nclass NineTileBoxExample extends FlameGame\n    with TapCallbacks, DoubleTapDetector {\n  static const String description = '''\n    If you want to create a background for something that can stretch you can\n    use the `NineTileBox` which is showcased here.\\n\\n\n    Tap to make the box bigger and double tap to make it smaller.\n  ''';\n\n  late NineTileBoxComponent nineTileBoxComponent;\n\n  @override\n  Future<void> onLoad() async {\n    final sprite = Sprite(await images.load('nine-box.png'));\n    final boxSize = Vector2.all(300);\n    final nineTileBox = NineTileBox(sprite, destTileSize: 148);\n    add(\n      nineTileBoxComponent = NineTileBoxComponent(\n        nineTileBox: nineTileBox,\n        position: size / 2,\n        size: boxSize,\n        anchor: Anchor.center,\n      ),\n    );\n  }\n\n  @override\n  void onTapDown(_) {\n    nineTileBoxComponent.scale.scale(1.2);\n  }\n\n  @override\n  void onDoubleTap() {\n    nineTileBoxComponent.scale.scale(0.8);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/rendering/particles_example.dart",
    "content": "import 'dart:async';\nimport 'dart:math';\n\nimport 'package:flame/components.dart' hide Timer;\nimport 'package:flame/game.dart';\nimport 'package:flame/particles.dart';\nimport 'package:flame/sprite.dart';\nimport 'package:flame/timer.dart' as flame_timer;\nimport 'package:flutter/material.dart' hide Image;\n\nclass ParticlesExample extends FlameGame {\n  static const String description = '''\n    In this example we show how to render a lot of different particles.\n  ''';\n\n  /// Defines dimensions of the sample\n  /// grid to be displayed on the screen,\n  /// 5x5 in this particular case\n  static const gridSize = 5.0;\n  static const steps = 5;\n\n  /// Miscellaneous values used\n  /// by examples below\n  final Random rnd = Random();\n  Timer? spawnTimer;\n  final StepTween steppedTween = StepTween(begin: 0, end: 5);\n  final trafficLight = TrafficLightComponent();\n\n  /// Defines the lifespan of all the particles in these examples\n  final sceneDuration = const Duration(seconds: 1);\n\n  Vector2 get cellSize => size / gridSize;\n  Vector2 get halfCellSize => cellSize / 2;\n\n  @override\n  Future<void> onLoad() async {\n    await images.load('zap.png');\n    await images.load('boom.png');\n  }\n\n  @override\n  void onMount() {\n    spawnParticles();\n    // Spawn new particles every second\n    spawnTimer = Timer.periodic(sceneDuration, (_) {\n      spawnParticles();\n    });\n  }\n\n  @override\n  void onRemove() {\n    super.onRemove();\n    spawnTimer?.cancel();\n  }\n\n  /// Showcases various different uses of [Particle]\n  /// and its derivatives\n  void spawnParticles() {\n    // Contains sample particles, in order by complexity\n    // and amount of used features. Jump to source for more explanation on each\n    final particles = <Particle>[\n      circle(),\n      smallWhiteCircle(),\n      movingParticle(),\n      randomMovingParticle(),\n      alignedMovingParticles(),\n      easedMovingParticle(),\n      intervalMovingParticle(),\n      computedParticle(),\n      chainingBehaviors(),\n      steppedComputedParticle(),\n      reuseParticles(),\n      imageParticle(),\n      reuseImageParticle(),\n      rotatingImage(),\n      acceleratedParticles(),\n      paintParticle(),\n      spriteParticle(),\n      animationParticle(),\n      fireworkParticle(),\n      componentParticle(),\n    ];\n\n    // Place all the [Particle] instances\n    // defined above in a grid on the screen\n    // as per defined grid parameters\n    do {\n      final particle = particles.removeLast();\n      final col = particles.length % gridSize;\n      final row = (particles.length ~/ gridSize).toDouble();\n      final cellCenter = (cellSize..multiply(Vector2(col, row))) + halfCellSize;\n\n      add(\n        // Bind all the particles to a [Component] update\n        // lifecycle from the [FlameGame].\n        ParticleSystemComponent(\n          particle: TranslatedParticle(\n            lifespan: 1,\n            offset: cellCenter,\n            child: particle,\n          ),\n        ),\n      );\n    } while (particles.isNotEmpty);\n  }\n\n  /// Simple static circle, doesn't move or\n  /// change any of its attributes\n  Particle circle() {\n    return CircleParticle(\n      paint: Paint()..color = Colors.white10,\n    );\n  }\n\n  /// This one will is a bit smaller,\n  /// and a bit less transparent\n  Particle smallWhiteCircle() {\n    return CircleParticle(\n      radius: 5.0,\n      paint: Paint()..color = Colors.white,\n    );\n  }\n\n  /// Particle which is moving from\n  /// one predefined position to another one\n  Particle movingParticle() {\n    return MovingParticle(\n      /// This parameter is optional, will default to [Vector2.zero]\n      from: Vector2(-20, -20),\n      to: Vector2(20, 20),\n      child: CircleParticle(paint: Paint()..color = Colors.amber),\n    );\n  }\n\n  /// [Particle] which is moving to a random direction\n  /// within each cell each time created\n  Particle randomMovingParticle() {\n    return MovingParticle(\n      to: randomCellVector2(),\n      child: CircleParticle(\n        radius: 5 + rnd.nextDouble() * 5,\n        paint: Paint()..color = Colors.red,\n      ),\n    );\n  }\n\n  /// Generates 5 particles, each moving\n  /// symmetrically within grid cell\n  Particle alignedMovingParticles() {\n    return Particle.generate(\n      count: 5,\n      generator: (i) {\n        final currentColumn = (cellSize.x / 5) * i - halfCellSize.x;\n        return MovingParticle(\n          from: Vector2(currentColumn, -halfCellSize.y),\n          to: Vector2(currentColumn, halfCellSize.y),\n          child: CircleParticle(\n            radius: 2.0,\n            paint: Paint()..color = Colors.blue,\n          ),\n        );\n      },\n    );\n  }\n\n  /// Burst of 5 particles each moving\n  /// to a random direction within the cell\n  Particle randomMovingParticles() {\n    return Particle.generate(\n      count: 5,\n      generator: (i) => MovingParticle(\n        to: randomCellVector2()..scale(0.5),\n        child: CircleParticle(\n          radius: 5 + rnd.nextDouble() * 5,\n          paint: Paint()..color = Colors.deepOrange,\n        ),\n      ),\n    );\n  }\n\n  /// Same example as above, but with easing, utilizing [CurvedParticle]\n  /// extension.\n  Particle easedMovingParticle() {\n    return Particle.generate(\n      count: 5,\n      generator: (i) => MovingParticle(\n        curve: Curves.easeOutQuad,\n        to: randomCellVector2()..scale(0.5),\n        child: CircleParticle(\n          radius: 5 + rnd.nextDouble() * 5,\n          paint: Paint()..color = Colors.deepPurple,\n        ),\n      ),\n    );\n  }\n\n  /// Same example as above, but using awesome [Interval]\n  /// curve, which \"schedules\" transition to happen between\n  /// certain values of progress. In this example, circles will\n  /// move from their initial to their final position\n  /// when progress is changing from 0.2 to 0.6 respectively.\n  Particle intervalMovingParticle() {\n    return Particle.generate(\n      count: 5,\n      generator: (i) => MovingParticle(\n        curve: const Interval(0.2, 0.6, curve: Curves.easeInOutCubic),\n        to: randomCellVector2()..scale(0.5),\n        child: CircleParticle(\n          radius: 5 + rnd.nextDouble() * 5,\n          paint: Paint()..color = Colors.greenAccent,\n        ),\n      ),\n    );\n  }\n\n  /// A [ComputedParticle] completely delegates all the rendering\n  /// to an external function, hence It's very flexible, as you can implement\n  /// any currently missing behavior with it.\n  /// Also, it allows to optimize complex behaviors by avoiding nesting too\n  /// many [Particle] together and having all the computations in place.\n  Particle computedParticle() {\n    return ComputedParticle(\n      renderer: (canvas, particle) => canvas.drawCircle(\n        Offset.zero,\n        particle.progress * halfCellSize.x,\n        Paint()\n          ..color = Color.lerp(\n            Colors.red,\n            Colors.blue,\n            particle.progress,\n          )!,\n      ),\n    );\n  }\n\n  /// Using [ComputedParticle] to use custom tweening\n  /// In reality, you would like to keep as much of renderer state\n  /// defined outside and reused between each call\n  Particle steppedComputedParticle() {\n    return ComputedParticle(\n      lifespan: 2,\n      renderer: (canvas, particle) {\n        const steps = 5;\n        final steppedProgress =\n            steppedTween.transform(particle.progress) / steps;\n\n        canvas.drawCircle(\n          Offset.zero,\n          (1 - steppedProgress) * halfCellSize.x,\n          Paint()\n            ..color = Color.lerp(\n              Colors.red,\n              Colors.blue,\n              steppedProgress,\n            )!,\n        );\n      },\n    );\n  }\n\n  /// Particle which is used in example below\n  Particle? reusableParticle;\n\n  /// A burst of white circles which actually using a single circle\n  /// as a form of optimization. Look for reusing parts of particle effects\n  /// whenever possible, as there are limits which are relatively easy to reach.\n  Particle reuseParticles() {\n    reusableParticle ??= circle();\n\n    return Particle.generate(\n      generator: (i) => MovingParticle(\n        curve: Interval(rnd.nextDouble() * 0.1, rnd.nextDouble() * 0.8 + 0.1),\n        to: randomCellVector2()..scale(0.5),\n        child: reusableParticle!,\n      ),\n    );\n  }\n\n  /// Simple static image particle which doesn't do much.\n  /// Images are great examples of where assets should\n  /// be reused across particles. See example below for more details.\n  Particle imageParticle() {\n    return ImageParticle(\n      size: Vector2.all(24),\n      image: images.fromCache('zap.png'),\n    );\n  }\n\n  /// Particle which is used in example below\n  Particle? reusableImageParticle;\n\n  /// A single [imageParticle] is drawn 9 times\n  /// in a grid within grid cell. Looks as 9 particles\n  /// to user, saves us 8 particle objects.\n  Particle reuseImageParticle() {\n    const count = 9;\n    const perLine = 3;\n    const imageSize = 24.0;\n    final colWidth = cellSize.x / perLine;\n    final rowHeight = cellSize.y / perLine;\n\n    reusableImageParticle ??= imageParticle();\n\n    return Particle.generate(\n      count: count,\n      generator: (i) => TranslatedParticle(\n        offset: Vector2(\n          (i % perLine) * colWidth - halfCellSize.x + imageSize,\n          (i ~/ perLine) * rowHeight - halfCellSize.y + imageSize,\n        ),\n        child: reusableImageParticle!,\n      ),\n    );\n  }\n\n  /// [RotatingParticle] is a simple container which rotates\n  /// a child particle passed to it.\n  /// As you can see, we're reusing [imageParticle] from example above.\n  /// Such a composability is one of the main implementation features.\n  Particle rotatingImage({double initialAngle = 0}) {\n    return RotatingParticle(from: initialAngle, child: imageParticle());\n  }\n\n  /// [AcceleratedParticle] is a very basic acceleration physics container,\n  /// which could help implementing such behaviors as gravity, or adding\n  /// some non-linearity to something like [MovingParticle]\n  Particle acceleratedParticles() {\n    return Particle.generate(\n      generator: (i) => AcceleratedParticle(\n        speed:\n            Vector2(\n              rnd.nextDouble() * 600 - 300,\n              -rnd.nextDouble() * 600,\n            ) *\n            0.2,\n        acceleration: Vector2(0, 200),\n        child: rotatingImage(initialAngle: rnd.nextDouble() * pi),\n      ),\n    );\n  }\n\n  /// [PaintParticle] allows to perform basic composite operations\n  /// by specifying custom [Paint].\n  /// Be aware that it's very easy to get *really* bad performance\n  /// misusing composites.\n  Particle paintParticle() {\n    final colors = [\n      const Color(0xffff0000),\n      const Color(0xff00ff00),\n      const Color(0xff0000ff),\n    ];\n    final positions = [\n      Vector2(-10, 10),\n      Vector2(10, 10),\n      Vector2(0, -14),\n    ];\n\n    return Particle.generate(\n      count: 3,\n      generator: (i) => PaintParticle(\n        paint: Paint()..blendMode = BlendMode.difference,\n        child: MovingParticle(\n          curve: SineCurve(),\n          from: positions[i],\n          to: i == 0 ? positions.last : positions[i - 1],\n          child: CircleParticle(\n            radius: 20.0,\n            paint: Paint()..color = colors[i],\n          ),\n        ),\n      ),\n    );\n  }\n\n  /// [SpriteParticle] allows easily embed\n  /// Flame's [Sprite] into the effect.\n  Particle spriteParticle() {\n    return SpriteParticle(\n      sprite: Sprite(images.fromCache('zap.png')),\n      size: cellSize * 0.5,\n    );\n  }\n\n  /// An [SpriteAnimationParticle] takes a Flame [SpriteAnimation]\n  /// and plays it during the particle lifespan.\n  Particle animationParticle() {\n    return SpriteAnimationParticle(\n      animation: getBoomAnimation(),\n      size: Vector2(128, 128),\n    );\n  }\n\n  /// [ComponentParticle] proxies particle lifecycle hooks\n  /// to its child [Component]. In example below, [Component] is\n  /// reused between particle effects and has internal behavior\n  /// which is independent from the parent [Particle].\n  Particle componentParticle() {\n    return MovingParticle(\n      from: -halfCellSize * 0.2,\n      to: halfCellSize * 0.2,\n      curve: SineCurve(),\n      child: ComponentParticle(component: trafficLight),\n    );\n  }\n\n  /// Not very realistic firework, yet it highlights\n  /// use of [ComputedParticle] within other particles,\n  /// mixing predefined and fully custom behavior.\n  Particle fireworkParticle() {\n    // A palette to paint over the \"sky\"\n    final paints = [\n      Colors.amber,\n      Colors.amberAccent,\n      Colors.red,\n      Colors.redAccent,\n      Colors.yellow,\n      Colors.yellowAccent,\n      // Adds a nice \"lense\" tint\n      // to overall effect\n      Colors.blue,\n    ].map((color) => Paint()..color = color).toList();\n\n    return Particle.generate(\n      generator: (i) {\n        final initialSpeed = randomCellVector2();\n        final deceleration = initialSpeed * -1;\n        final gravity = Vector2(0, 40);\n\n        return AcceleratedParticle(\n          speed: initialSpeed,\n          acceleration: deceleration + gravity,\n          child: ComputedParticle(\n            renderer: (canvas, particle) {\n              final paint = randomElement(paints);\n              // Override the color to dynamically update opacity\n              paint.color = paint.color.withValues(\n                alpha: 1 - particle.progress,\n              );\n\n              canvas.drawCircle(\n                Offset.zero,\n                // Closer to the end of lifespan particles\n                // will turn into larger glaring circles\n                rnd.nextDouble() * particle.progress > 0.6\n                    ? rnd.nextDouble() * (50 * particle.progress)\n                    : 2 + (3 * particle.progress),\n                paint,\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n\n  /// [Particle] base class exposes a number\n  /// of convenience wrappers to make positioning.\n  ///\n  /// Just remember that the less chaining and nesting - the\n  /// better for performance!\n  Particle chainingBehaviors() {\n    final paint = Paint()..color = randomMaterialColor();\n    final rect = ComputedParticle(\n      renderer: (canvas, _) => canvas.drawRect(\n        Rect.fromCenter(center: Offset.zero, width: 10, height: 10),\n        paint,\n      ),\n    );\n\n    return ComposedParticle(\n      children: [\n        rect\n            .rotating(to: pi / 2)\n            .moving(to: -cellSize)\n            .scaled(2)\n            .accelerated(acceleration: halfCellSize * 5)\n            .translated(halfCellSize),\n        rect\n            .rotating(to: -pi)\n            .moving(to: Vector2(1, -1)..multiply(cellSize))\n            .scaled(2)\n            .translated(Vector2(1, -1)..multiply(halfCellSize))\n            .accelerated(acceleration: Vector2(-5, 5)..multiply(halfCellSize)),\n      ],\n    );\n  }\n\n  /// Returns random [Vector2] within a virtual grid cell\n  Vector2 randomCellVector2() {\n    return (Vector2.random() - Vector2.random())..multiply(cellSize);\n  }\n\n  /// Returns random [Color] from primary swatches\n  /// of material palette\n  Color randomMaterialColor() {\n    return Colors.primaries[rnd.nextInt(Colors.primaries.length)];\n  }\n\n  /// Returns a random element from a given list\n  T randomElement<T>(List<T> list) {\n    return list[rnd.nextInt(list.length)];\n  }\n\n  /// Sample \"explosion\" animation for [SpriteAnimationParticle] example\n  SpriteAnimation getBoomAnimation() {\n    const columns = 8;\n    const rows = 8;\n    const frames = columns * rows;\n    final spriteImage = images.fromCache('boom.png');\n    final spriteSheet = SpriteSheet.fromColumnsAndRows(\n      image: spriteImage,\n      columns: columns,\n      rows: rows,\n    );\n    final sprites = List<Sprite>.generate(frames, spriteSheet.getSpriteById);\n    return SpriteAnimation.spriteList(sprites, stepTime: 0.1);\n  }\n}\n\nFuture<FlameGame> loadGame() async {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  return ParticlesExample();\n}\n\n/// A curve which maps sinus output (-1..1,0..pi)\n/// to an oscillating (0..1..0,0..1), essentially \"ease-in-out and back\"\nclass SineCurve extends Curve {\n  @override\n  double transformInternal(double t) {\n    return (sin(pi * (t * 2 - 1 / 2)) + 1) / 2;\n  }\n}\n\n/// Sample for [ComponentParticle], changes its colors\n/// each 2s of registered lifetime.\nclass TrafficLightComponent extends Component {\n  final Rect rect = Rect.fromCenter(center: Offset.zero, height: 32, width: 32);\n  final flame_timer.Timer colorChangeTimer = flame_timer.Timer(2, repeat: true);\n  final colors = <Color>[\n    Colors.green,\n    Colors.orange,\n    Colors.red,\n  ];\n  final Paint _paint = Paint();\n\n  @override\n  void onMount() {\n    colorChangeTimer.start();\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(rect, _paint..color = currentColor);\n  }\n\n  @override\n  void update(double dt) {\n    colorChangeTimer.update(dt);\n  }\n\n  Color get currentColor {\n    return colors[(colorChangeTimer.progress * colors.length).toInt()];\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/rendering/particles_interactive_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/particles.dart';\nimport 'package:flutter/material.dart';\n\nclass ParticlesInteractiveExample extends FlameGame with PanDetector {\n  static const description =\n      'An example which shows how '\n      'ParticleSystemComponent can be added in runtime '\n      'following an event, in this example, the mouse '\n      'dragging';\n\n  final random = Random();\n  final Tween<double> noise = Tween(begin: -1, end: 1);\n  final ColorTween colorTween;\n\n  ParticlesInteractiveExample({\n    required Color from,\n    required Color to,\n    required double zoom,\n  }) : colorTween = ColorTween(begin: from, end: to),\n       super(\n         camera: CameraComponent.withFixedResolution(\n           width: 400,\n           height: 600,\n         )..viewfinder.zoom = zoom,\n       );\n\n  @override\n  void onPanUpdate(DragUpdateInfo info) {\n    add(\n      ParticleSystemComponent(\n        position: info.eventPosition.widget,\n        particle: Particle.generate(\n          count: 40,\n          generator: (i) {\n            return AcceleratedParticle(\n              lifespan: 2,\n              speed:\n                  Vector2(\n                    noise.transform(random.nextDouble()),\n                    noise.transform(random.nextDouble()),\n                  ) *\n                  i.toDouble(),\n              child: CircleParticle(\n                radius: 2,\n                paint: Paint()\n                  ..color = colorTween.transform(random.nextDouble())!,\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/rendering/rendering.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/rendering/flip_sprite_example.dart';\nimport 'package:examples/stories/rendering/isometric_tile_map_example.dart';\nimport 'package:examples/stories/rendering/layers_example.dart';\nimport 'package:examples/stories/rendering/nine_tile_box_example.dart';\nimport 'package:examples/stories/rendering/particles_example.dart';\nimport 'package:examples/stories/rendering/particles_interactive_example.dart';\nimport 'package:examples/stories/rendering/rich_text_example.dart';\nimport 'package:examples/stories/rendering/text_box_example.dart';\nimport 'package:examples/stories/rendering/text_example.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nvoid addRenderingStories(Dashbook dashbook) {\n  dashbook.storiesOf('Rendering')\n    ..add(\n      'Text',\n      (_) => GameWidget(game: TextExample()),\n      codeLink: baseLink('rendering/text_example.dart'),\n      info: TextExample.description,\n    )\n    ..add(\n      'Isometric Tile Map',\n      (context) => GameWidget(\n        game: IsometricTileMapExample(\n          halfSize: context.boolProperty('Half size', true),\n        ),\n      ),\n      codeLink: baseLink('rendering/isometric_tile_map_example.dart'),\n      info: IsometricTileMapExample.description,\n    )\n    ..add(\n      'Nine Tile Box',\n      (_) => GameWidget(game: NineTileBoxExample()),\n      codeLink: baseLink('rendering/nine_tile_box_example.dart'),\n      info: NineTileBoxExample.description,\n    )\n    ..add(\n      'Flip Sprite',\n      (_) => GameWidget(game: FlipSpriteExample()),\n      codeLink: baseLink('rendering/flip_sprite_example.dart'),\n      info: FlipSpriteExample.description,\n    )\n    ..add(\n      'Layers',\n      (_) => GameWidget(game: LayerExample()),\n      codeLink: baseLink('rendering/layers_example.dart'),\n      info: LayerExample.description,\n    )\n    ..add(\n      'Particles',\n      (_) => GameWidget(game: ParticlesExample()),\n      codeLink: baseLink('rendering/particles_example.dart'),\n      info: ParticlesExample.description,\n    )\n    ..add(\n      'Particles (Interactive)',\n      (context) => GameWidget(\n        game: ParticlesInteractiveExample(\n          from: context.colorProperty('From color', Colors.pink),\n          to: context.colorProperty('To color', Colors.blue),\n          zoom: context.numberProperty('Zoom', 1),\n        ),\n      ),\n      codeLink: baseLink('rendering/particles_interactive_example.dart'),\n      info: ParticlesInteractiveExample.description,\n    )\n    ..add(\n      'Rich Text',\n      (context) => GameWidget(\n        game: RichTextExample(\n          textAlign: context.listProperty(\n            'Text align',\n            TextAlign.left,\n            TextAlign.values,\n          ),\n        ),\n      ),\n      codeLink: baseLink('rendering/rich_text_example.dart'),\n      info: RichTextExample.description,\n    )\n    ..add(\n      'TextBoxComponent',\n      (context) {\n        return GameWidget(\n          game: TextBoxExample(),\n        );\n      },\n      codeLink: baseLink('rendering/text_box_example.dart'),\n      info: TextBoxExample.description,\n    );\n}\n"
  },
  {
    "path": "examples/lib/stories/rendering/rich_text_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/text.dart';\nimport 'package:flutter/painting.dart';\n\nclass RichTextExample extends FlameGame {\n  final TextAlign textAlign;\n\n  RichTextExample({this.textAlign = TextAlign.left});\n\n  static const String description =\n      'A non-interactive example of how to render rich text in Flame.';\n\n  @override\n  Color backgroundColor() => const Color(0xFF888888);\n\n  @override\n  Future<void> onLoad() async {\n    final style = DocumentStyle(\n      width: 400,\n      height: 200,\n      padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),\n      background: BackgroundStyle(\n        color: const Color(0xFF4E322E),\n        borderColor: const Color(0xFF000000),\n        borderWidth: 2.0,\n      ),\n      paragraph: BlockStyle(\n        padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),\n        textAlign: textAlign,\n        background: BackgroundStyle(\n          color: const Color(0xFF004D40),\n          borderColor: const Color(0xFFAAAAAA),\n        ),\n      ),\n      header1: BlockStyle(\n        textAlign: textAlign,\n      ),\n    );\n    final document = DocumentRoot([\n      HeaderNode.simple('1984', level: 1),\n      ParagraphNode.simple(\n        'Anything could be true. The so-called laws of nature were nonsense.',\n      ),\n      ParagraphNode.simple(\n        'The law of gravity was nonsense. \"If I wished,\" O\\'Brien had said, '\n        '\"I could float off this floor like a soap bubble.\" Winston worked it '\n        'out. \"If he thinks he floats off the floor, and I simultaneously '\n        'think I can see him do it, then the thing happens.\"',\n      ),\n      ParagraphNode.group([\n        PlainTextNode(\n          'Suddenly, like a lump of submerged wreckage breaking the surface '\n          'of water, the thought burst into his mind: ',\n        ),\n        ItalicTextNode.group([\n          PlainTextNode('\"It doesn\\'t really happen. We imagine it. It is '),\n          BoldTextNode.simple('hallucination'),\n          PlainTextNode('.\"'),\n        ]),\n      ]),\n      ParagraphNode.group([\n        PlainTextNode(\n          'He pushed the thought under instantly. The fallacy was obvious. It '\n          'presupposed that somewhere or other, outside oneself, there was a '\n          '\"',\n        ),\n        CodeTextNode.simple('real'),\n        PlainTextNode(\n          '\" world where \"',\n        ),\n        CodeTextNode.simple('real'),\n        PlainTextNode(\n          '\" things happened. But how could there be '\n          'such a world? What knowledge have we of anything, save through our '\n          'own minds? All happenings are in the mind. Whatever happens in all '\n          'minds, truly happens.',\n        ),\n      ]),\n    ]);\n\n    add(\n      TextElementComponent.fromDocument(\n        document: document,\n        style: style,\n        position: Vector2(100, 50),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/rendering/text_box_example.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\n\nenum TextBoxConfigMaxWidth {\n  small(200),\n  large(640)\n  ;\n\n  const TextBoxConfigMaxWidth(this.value);\n\n  final double value;\n}\n\nclass TextBoxExample extends FlameGame {\n  static const String description =\n      'TextBoxComponent reflows text when boxConfig.maxWidth is changed';\n\n  final textBoxComponent = TextBoxComponent(\n    text: sampleText,\n  )..debugMode = true;\n\n  TextBoxConfigMaxWidth currentWidth = TextBoxConfigMaxWidth.small;\n\n  @override\n  FutureOr<void> onLoad() {\n    add(\n      ColumnComponent(\n        position: Vector2(0, 48),\n        children: [\n          TextComponent(text: 'TextBoxComponent changes'),\n          ButtonComponent(\n            button: TextComponent(\n              text: '[Toggle Between Sizes]',\n            ),\n            onReleased: () {\n              currentWidth = currentWidth == TextBoxConfigMaxWidth.small\n                  ? TextBoxConfigMaxWidth.large\n                  : TextBoxConfigMaxWidth.small;\n              textBoxComponent.boxConfig = textBoxComponent.boxConfig.copyWith(\n                maxWidth: currentWidth.value,\n              );\n              textBoxComponent.redraw();\n            },\n          ),\n          textBoxComponent,\n        ],\n      ),\n    );\n  }\n\n  static const sampleText =\n      'In a bustling city, a small team of developers set out to create '\n      'a mobile game using the Flame engine for Flutter. Their goal was '\n      'simple: to create an engaging, easy-to-play game that could reach '\n      'a wide audience on both iOS and Android platforms. '\n      'After weeks of brainstorming, they decided on a concept: '\n      'a fast-paced, endless runner game set in a whimsical, '\n      'ever-changing world. They named it \"Swift Dash.\" '\n      \"Using Flutter's versatility and the Flame engine's \"\n      'capabilities, the team crafted a game with vibrant graphics, '\n      'smooth animations, and responsive controls. '\n      'The game featured a character dashing through various landscapes, '\n      'dodging obstacles, and collecting points. '\n      'As they launched \"Swift Dash,\" the team was anxious but hopeful. '\n      'To their delight, the game was well-received. Players loved its '\n      'simplicity and charm, and the game quickly gained popularity.';\n}\n"
  },
  {
    "path": "examples/lib/stories/rendering/text_example.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame/text.dart';\nimport 'package:flutter/material.dart';\n\nclass TextExample extends FlameGame {\n  static const String description = '''\n    In this example we show different ways of rendering text.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    addAll(\n      [\n        TextComponent(text: 'Hello, Flame', textRenderer: _regular)\n          ..anchor = Anchor.topCenter\n          ..x = size.x / 2\n          ..y = 32.0,\n        TextComponent(text: 'Text with shade', textRenderer: _shaded)\n          ..anchor = Anchor.topRight\n          ..position = size - Vector2.all(100),\n        TextComponent(text: 'center', textRenderer: _tiny)\n          ..anchor = Anchor.center\n          ..position.setFrom(size / 2),\n        TextComponent(text: 'bottomRight', textRenderer: _tiny)\n          ..anchor = Anchor.bottomRight\n          ..position.setFrom(size),\n        MyTextBox(\n            '\"This is our world now. The world of the electron and the switch; '\n            'the beauty of the baud. We exist without nationality, skin color, '\n            'or religious bias. You wage wars, murder, cheat, lie to us and '\n            \"try to make us believe it's for our own good, yet we're the \"\n            'criminals. Yes, I am a criminal. My crime is that of curiosity.\"',\n          )\n          ..anchor = Anchor.bottomLeft\n          ..y = size.y,\n        MyTextBox(\n          'Let A be a finitely generated torsion-free abelian group. Then '\n          'A is free.',\n          align: Anchor.center,\n          size: Vector2(300, 200),\n          timePerChar: 0,\n          margins: 10,\n        )..position = Vector2(10, 50),\n        MyTextBox(\n          'Let A be a torsion abelian group. Then A is the direct sum of its '\n          'subgroups A(p) for all primes p such that A(p) ≠ 0.',\n          align: Anchor.bottomRight,\n          size: Vector2(300, 200),\n          timePerChar: 0,\n          margins: 10,\n        )..position = Vector2(10, 260),\n        TextComponent(\n          text: 'Scroll me when finished:',\n          position: Vector2(size.x / 2, size.y / 2 + 100),\n          anchor: Anchor.bottomCenter,\n        ),\n        MyScrollTextBox(\n          'In a bustling city, a small team of developers set out to create '\n          'a mobile game using the Flame engine for Flutter. Their goal was '\n          'simple: to create an engaging, easy-to-play game that could reach '\n          'a wide audience on both iOS and Android platforms. '\n          'After weeks of brainstorming, they decided on a concept: '\n          'a fast-paced, endless runner game set in a whimsical, '\n          'ever-changing world. They named it \"Swift Dash.\" '\n          \"Using Flutter's versatility and the Flame engine's \"\n          'capabilities, the team crafted a game with vibrant graphics, '\n          'smooth animations, and responsive controls. '\n          'The game featured a character dashing through various landscapes, '\n          'dodging obstacles, and collecting points. '\n          'As they launched \"Swift Dash,\" the team was anxious but hopeful. '\n          'To their delight, the game was well-received. Players loved its '\n          'simplicity and charm, and the game quickly gained popularity.',\n          size: Vector2(200, 150),\n          position: Vector2(size.x / 2, size.y / 2 + 100),\n          anchor: Anchor.topCenter,\n          boxConfig: const TextBoxConfig(\n            timePerChar: 0.005,\n            margins: EdgeInsets.fromLTRB(10, 10, 10, 10),\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nfinal _regularTextStyle = TextStyle(\n  fontSize: 18,\n  color: BasicPalette.white.color,\n);\nfinal _regular = TextPaint(\n  style: _regularTextStyle,\n);\nfinal _tiny = TextPaint(style: _regularTextStyle.copyWith(fontSize: 14.0));\nfinal _box = _regular.copyWith(\n  (style) => style.copyWith(\n    color: Colors.lightGreenAccent,\n    fontFamily: 'monospace',\n    letterSpacing: 2.0,\n  ),\n);\nfinal _shaded = TextPaint(\n  style: TextStyle(\n    color: BasicPalette.white.color,\n    fontSize: 40.0,\n    shadows: const [\n      Shadow(color: Colors.red, offset: Offset(2, 2), blurRadius: 2),\n      Shadow(color: Colors.yellow, offset: Offset(4, 4), blurRadius: 4),\n    ],\n  ),\n);\n\nclass MyTextBox extends TextBoxComponent {\n  late Paint paint;\n  late Rect bgRect;\n\n  MyTextBox(\n    String text, {\n    super.align,\n    super.size,\n    double? timePerChar,\n    double? margins,\n  }) : super(\n         text: text,\n         textRenderer: _box,\n         boxConfig: TextBoxConfig(\n           maxWidth: 400,\n           timePerChar: timePerChar ?? 0.05,\n           growingBox: true,\n           margins: EdgeInsets.all(margins ?? 25),\n         ),\n       );\n\n  @override\n  Future<void> onLoad() {\n    paint = Paint();\n    bgRect = Rect.fromLTWH(0, 0, width, height);\n    size.addListener(() {\n      bgRect = Rect.fromLTWH(0, 0, width, height);\n    });\n\n    paint.color = Colors.white10;\n    return super.onLoad();\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(bgRect, paint);\n    super.render(canvas);\n  }\n}\n\nclass MyScrollTextBox extends ScrollTextBoxComponent {\n  late Paint paint;\n  late Rect backgroundRect;\n\n  MyScrollTextBox(\n    String text, {\n    required super.size,\n    super.boxConfig,\n    super.position,\n    super.anchor,\n  }) : super(text: text, textRenderer: _box);\n\n  @override\n  FutureOr<void> onLoad() {\n    paint = Paint();\n    backgroundRect = Rect.fromLTWH(0, 0, width, height);\n\n    paint.color = Colors.white10;\n    return super.onLoad();\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(backgroundRect, paint);\n    super.render(canvas);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/router/router.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/router/router_world_example.dart';\nimport 'package:flame/game.dart';\n\nvoid addRouterStories(Dashbook dashbook) {\n  dashbook\n      .storiesOf('Router')\n      .add(\n        'Router with multiple worlds',\n        (_) => GameWidget(game: RouterWorldExample()),\n        codeLink: baseLink('router/router_world_example.dart'),\n        info: RouterWorldExample.description,\n      );\n}\n"
  },
  {
    "path": "examples/lib/stories/router/router_world_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/rendering.dart';\nimport 'package:flutter/material.dart' show Colors;\nimport 'package:flutter/rendering.dart';\n\nclass RouterWorldExample extends FlameGame {\n  static const description = '''\nThis example shows how to use the RouterComponent to navigate between\ndifferent worlds and pages.\n''';\n\n  late final RouterComponent router;\n\n  @override\n  Future<void> onLoad() async {\n    add(\n      router = RouterComponent(\n        routes: {\n          'home': Route(StartPage.new),\n          'level1': WorldRoute(Level1Page.new),\n          'level2': WorldRoute(Level2Page.new, maintainState: false),\n          'pause': PauseRoute(),\n        },\n        initialRoute: 'home',\n      ),\n    );\n  }\n}\n\nclass StartPage extends Component with HasGameReference<RouterWorldExample> {\n  StartPage() {\n    addAll([\n      _logo = TextComponent(\n        text: 'Your Game',\n        textRenderer: TextPaint(\n          style: const TextStyle(\n            fontSize: 64,\n            color: Color(0xFFC8FFF5),\n            fontWeight: FontWeight.w800,\n          ),\n        ),\n        anchor: Anchor.center,\n      ),\n      _button1 = RoundedButton(\n        text: 'Level 1',\n        action: () => game.router.pushNamed('level1'),\n        color: const Color(0xffadde6c),\n        borderColor: const Color(0xffedffab),\n      ),\n      _button2 = RoundedButton(\n        text: 'Level 2',\n        action: () => game.router.pushNamed('level2'),\n        color: const Color(0xffdebe6c),\n        borderColor: const Color(0xfffff4c7),\n      ),\n    ]);\n  }\n\n  late final TextComponent _logo;\n  late final RoundedButton _button1;\n  late final RoundedButton _button2;\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    _logo.position = Vector2(size.x / 2, size.y / 3);\n    _button1.position = Vector2(size.x / 2, _logo.y + 80);\n    _button2.position = Vector2(size.x / 2, _logo.y + 140);\n  }\n}\n\nclass Background extends Component {\n  Background(this.color);\n  final Color color;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawColor(color, BlendMode.srcATop);\n  }\n}\n\nclass RoundedButton extends PositionComponent with TapCallbacks {\n  RoundedButton({\n    required this.text,\n    required this.action,\n    required Color color,\n    required Color borderColor,\n    super.position,\n    super.anchor = Anchor.center,\n  }) : _textDrawable = TextPaint(\n         style: const TextStyle(\n           fontSize: 20,\n           color: Color(0xFF000000),\n           fontWeight: FontWeight.w800,\n         ),\n       ).toTextPainter(text) {\n    size = Vector2(150, 40);\n    _textOffset = Offset(\n      (size.x - _textDrawable.width) / 2,\n      (size.y - _textDrawable.height) / 2,\n    );\n    _rrect = RRect.fromLTRBR(0, 0, size.x, size.y, Radius.circular(size.y / 2));\n    _bgPaint = Paint()..color = color;\n    _borderPaint = Paint()\n      ..style = PaintingStyle.stroke\n      ..strokeWidth = 2\n      ..color = borderColor;\n  }\n\n  final String text;\n  final void Function() action;\n  final TextPainter _textDrawable;\n  late final Offset _textOffset;\n  late final RRect _rrect;\n  late final Paint _borderPaint;\n  late final Paint _bgPaint;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(_rrect, _bgPaint);\n    canvas.drawRRect(_rrect, _borderPaint);\n    _textDrawable.paint(canvas, _textOffset);\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    scale = Vector2.all(1.05);\n  }\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    scale = Vector2.all(1.0);\n    action();\n  }\n\n  @override\n  void onTapCancel(TapCancelEvent event) {\n    scale = Vector2.all(1.0);\n  }\n}\n\nabstract class SimpleButton extends PositionComponent with TapCallbacks {\n  SimpleButton(this._iconPath, {super.position}) : super(size: Vector2.all(40));\n\n  final Paint _borderPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..color = const Color(0x66ffffff);\n  final Paint _iconPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..color = const Color(0xffaaaaaa)\n    ..strokeWidth = 7;\n  final Path _iconPath;\n\n  void action();\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(\n      RRect.fromRectAndRadius(size.toRect(), const Radius.circular(8)),\n      _borderPaint,\n    );\n    canvas.drawPath(_iconPath, _iconPaint);\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    _iconPaint.color = const Color(0xffffffff);\n  }\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    _iconPaint.color = const Color(0xffaaaaaa);\n    action();\n  }\n\n  @override\n  void onTapCancel(TapCancelEvent event) {\n    _iconPaint.color = const Color(0xffaaaaaa);\n  }\n}\n\nclass BackButton extends SimpleButton\n    with HasGameReference<RouterWorldExample> {\n  BackButton()\n    : super(\n        Path()\n          ..moveTo(22, 8)\n          ..lineTo(10, 20)\n          ..lineTo(22, 32)\n          ..moveTo(12, 20)\n          ..lineTo(34, 20),\n        position: Vector2.all(10),\n      );\n\n  @override\n  void action() => game.router.pop();\n}\n\nclass PauseButton extends SimpleButton\n    with HasGameReference<RouterWorldExample> {\n  PauseButton()\n    : super(\n        Path()\n          ..moveTo(14, 10)\n          ..lineTo(14, 30)\n          ..moveTo(26, 10)\n          ..lineTo(26, 30),\n        position: Vector2(60, 10),\n      );\n\n  bool isPaused = false;\n\n  @override\n  void action() {\n    if (isPaused) {\n      game.router.pop();\n    } else {\n      game.router.pushNamed('pause');\n    }\n    isPaused = !isPaused;\n  }\n}\n\nclass Level1Page extends DecoratedWorld with HasGameReference {\n  @override\n  Future<void> onLoad() async {\n    addAll([\n      Background(const Color(0xbb2a074f)),\n      Planet(\n        radius: 25,\n        color: const Color(0xfffff188),\n        children: [\n          Orbit(\n            radius: 110,\n            revolutionPeriod: 6,\n            planet: Planet(\n              radius: 10,\n              color: const Color(0xff54d7b1),\n              children: [\n                Orbit(\n                  radius: 25,\n                  revolutionPeriod: 5,\n                  planet: Planet(radius: 3, color: const Color(0xFFcccccc)),\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    ]);\n  }\n\n  final hudComponents = <Component>[];\n\n  @override\n  void onMount() {\n    hudComponents.addAll([\n      BackButton(),\n      PauseButton(),\n    ]);\n    game.camera.viewport.addAll(hudComponents);\n  }\n\n  @override\n  void onRemove() {\n    game.camera.viewport.removeAll(hudComponents);\n    super.onRemove();\n  }\n}\n\nclass Level2Page extends DecoratedWorld with HasGameReference {\n  @override\n  Future<void> onLoad() async {\n    addAll([\n      Background(const Color(0xff052b44)),\n      Planet(\n        radius: 30,\n        color: const Color(0xFFFFFFff),\n        children: [\n          Orbit(\n            radius: 60,\n            revolutionPeriod: 5,\n            planet: Planet(radius: 10, color: const Color(0xffc9ce0d)),\n          ),\n          Orbit(\n            radius: 110,\n            revolutionPeriod: 10,\n            planet: Planet(\n              radius: 14,\n              color: const Color(0xfff32727),\n              children: [\n                Orbit(\n                  radius: 26,\n                  revolutionPeriod: 3,\n                  planet: Planet(radius: 5, color: const Color(0xffffdb00)),\n                ),\n                Orbit(\n                  radius: 35,\n                  revolutionPeriod: 4,\n                  planet: Planet(radius: 3, color: const Color(0xffdc00ff)),\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    ]);\n  }\n\n  final hudComponents = <Component>[];\n\n  @override\n  void onMount() {\n    hudComponents.addAll([\n      BackButton(),\n      PauseButton(),\n    ]);\n    game.camera.viewport.addAll(hudComponents);\n  }\n\n  @override\n  void onRemove() {\n    game.camera.viewport.removeAll(hudComponents);\n    super.onRemove();\n  }\n}\n\nclass Planet extends CircleComponent\n    with TapCallbacks, HasGameReference<RouterWorldExample> {\n  Planet({\n    required super.radius,\n    required Color color,\n    super.children,\n  }) : super(paint: Paint()..color = color, anchor: Anchor.center);\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    game.router.pushAndWait(YesNoDialog()).then((shouldRemove) {\n      if (shouldRemove) {\n        removeFromParent();\n      }\n    });\n  }\n}\n\nclass Orbit extends CircleComponent {\n  Orbit({\n    required super.radius,\n    required this.planet,\n    required this.revolutionPeriod,\n  }) : super(\n         children: [planet],\n         anchor: Anchor.center,\n         paint: Paint()\n           ..style = PaintingStyle.stroke\n           ..color = const Color(0x888888aa),\n       );\n\n  final double revolutionPeriod;\n  final Planet planet;\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    if (parent is Planet) {\n      position = Vector2.all((parent! as Planet).radius);\n    }\n    planet.position.x = size.x;\n    planet.position.y = size.y / 2;\n    planet.add(\n      RotateAroundEffect(\n        tau,\n        EffectController(duration: revolutionPeriod, infinite: true),\n        center: size / 2,\n      ),\n    );\n  }\n}\n\nclass PauseRoute extends Route {\n  PauseRoute() : super(PausePage.new, transparent: true);\n\n  @override\n  void onPush(Route? previousRoute) {\n    if (previousRoute is WorldRoute && previousRoute.world is DecoratedWorld) {\n      (previousRoute.world! as DecoratedWorld).timeScale = 0;\n      (previousRoute.world! as DecoratedWorld).decorator =\n          PaintDecorator.grayscale(opacity: 0.5)..addBlur(3.0);\n    }\n  }\n\n  @override\n  void onPop(Route nextRoute) {\n    if (nextRoute is WorldRoute && nextRoute.world is DecoratedWorld) {\n      (nextRoute.world! as DecoratedWorld).timeScale = 1;\n      (nextRoute.world! as DecoratedWorld).decorator = null;\n    }\n  }\n}\n\nclass PausePage extends Component\n    with TapCallbacks, HasGameReference<RouterWorldExample> {\n  @override\n  Future<void> onLoad() async {\n    final game = findGame()!;\n    addAll([\n      TextComponent(\n        text: 'PAUSED',\n        position: game.canvasSize / 2,\n        anchor: Anchor.center,\n        children: [\n          ScaleEffect.to(\n            Vector2.all(1.1),\n            EffectController(\n              duration: 0.3,\n              alternate: true,\n              infinite: true,\n            ),\n          ),\n        ],\n      ),\n    ]);\n  }\n\n  @override\n  bool containsLocalPoint(Vector2 point) => true;\n\n  @override\n  void onTapUp(TapUpEvent event) => game.router.pop();\n}\n\nclass DecoratedWorld extends World with HasTimeScale {\n  PaintDecorator? decorator;\n\n  @override\n  void renderFromCamera(Canvas canvas) {\n    if (decorator == null) {\n      super.renderFromCamera(canvas);\n    } else {\n      decorator!.applyChain(super.renderFromCamera, canvas);\n    }\n  }\n}\n\nclass YesNoDialog extends ValueRoute<bool> {\n  YesNoDialog() : super(value: false);\n\n  @override\n  Component build() {\n    final gameSize = findGame()!.size;\n    const margin = 10.0;\n    final boxSize = Vector2(350, 100);\n    return PositionComponent(\n      position: Vector2(gameSize.x / 2, margin),\n      size: boxSize,\n      anchor: Anchor.topCenter,\n      children: [\n        RectangleComponent(\n          size: boxSize,\n          paint: Paint()..color = const Color(0xFFAA0000),\n        ),\n        TextComponent(\n          position: Vector2.all(margin),\n          text: 'Remove the planet?',\n        ),\n        RoundedButton(\n          text: 'Yes',\n          action: () => completeWith(true),\n          color: Colors.green,\n          borderColor: Colors.white,\n          position: Vector2(boxSize.x / 4, boxSize.y - 30),\n        ),\n        RoundedButton(\n          text: 'No',\n          action: () => completeWith(false),\n          color: Colors.red,\n          borderColor: Colors.white,\n          position: Vector2(boxSize.x * 0.75, boxSize.y - 30),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/sprites/base64_sprite_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\n\nclass Base64SpriteExample extends FlameGame {\n  static const String description = '''\n    In this example we load a sprite from the a base64 string and put it into a\n    `SpriteComponent`.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    const exampleUrl =\n        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/'\n        '9hAAAAxElEQVQ4jYWTMQ7DIAxFIeoNuAGK1K1ISL0DMwOHzNC5p6iUPeoNOEM7GZ'\n        'nPJ/EUbP7Lx7KtIfH91B/L++gs5m5M9NreTN/dEZiVghatwbXvY68UlksyPjprRa'\n        'xFGAJZg+uAuSSzzC7rEDirDYAz2wg0RjWRFa/EUwdnQnQ37QFe1Odjrw04AKTTaB'\n        'XPAlx8dDaXdNk4rMsc0B7ge/UcYLTZxoFizxCQ/L0DMAhaX4Mzj/uzW6phu3AvtH'\n        'UUU4BAWJ6t8x9N/HHcruXjwQAAAABJRU5ErkJggg==';\n    final image = await images.fromBase64('shield.png', exampleUrl);\n    add(\n      SpriteComponent.fromImage(\n        image,\n        position: size / 2,\n        size: Vector2.all(100),\n        anchor: Anchor.center,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/sprites/basic_sprite_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\n\nclass BasicSpriteExample extends FlameGame {\n  static const String description = '''\n    In this example we load a sprite from the assets folder and put it into a\n    `SpriteComponent`.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final sprite = await loadSprite('flame.png');\n    add(\n      SpriteComponent(\n        sprite: sprite,\n        position: size / 2,\n        size: sprite.srcSize * 2,\n        anchor: Anchor.center,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/sprites/sprite_batch_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/sprite.dart';\nimport 'package:flutter/material.dart';\n\nclass SpriteBatchExample extends FlameGame {\n  static const String description = '''\n    In this example we show how to render many sprites in a batch for\n    efficiency, this is done with `SpriteBatch` and the `SpriteBatchComponent`.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final spriteBatch = await SpriteBatch.load('boom.png');\n\n    spriteBatch.add(\n      source: const Rect.fromLTWH(128 * 4.0, 128 * 4.0, 64, 128),\n      offset: Vector2.all(200),\n      color: Colors.greenAccent,\n      scale: 2,\n      rotation: pi / 9.0,\n      anchor: Vector2.all(64),\n    );\n\n    spriteBatch.addTransform(\n      source: const Rect.fromLTWH(128 * 4.0, 128 * 4.0, 64, 128),\n      color: Colors.redAccent,\n    );\n\n    const num = 100;\n    final r = Random();\n    for (var i = 0; i < num; ++i) {\n      final sx = r.nextInt(8) * 128.0;\n      final sy = r.nextInt(8) * 128.0;\n      final x = r.nextInt(size.x.toInt()).toDouble();\n      final y = r.nextInt(size.y ~/ 2).toDouble() + size.y / 2.0;\n      spriteBatch.add(\n        source: Rect.fromLTWH(sx, sy, 128, 128),\n        offset: Vector2(x - 64, y - 64),\n      );\n    }\n\n    add(\n      SpriteBatchComponent(\n        spriteBatch: spriteBatch,\n        blendMode: BlendMode.srcOver,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/sprites/sprite_batch_load_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/sprite.dart';\nimport 'package:flutter/material.dart';\n\nclass SpriteBatchLoadExample extends FlameGame {\n  static const String description = '''\n    In this example we do the same thing as in the normal sprite batch example,\n    but in this example the logic and loading is moved into a component that\n    extends `SpriteBatchComponent`.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    add(MySpriteBatchComponent());\n  }\n}\n\nclass MySpriteBatchComponent extends SpriteBatchComponent\n    with HasGameReference<SpriteBatchLoadExample> {\n  MySpriteBatchComponent()\n    : super(\n        blendMode: BlendMode.srcOver,\n      );\n\n  @override\n  Future<void> onLoad() async {\n    final spriteBatch = await game.loadSpriteBatch('boom.png');\n    this.spriteBatch = spriteBatch;\n\n    spriteBatch.add(\n      source: const Rect.fromLTWH(128 * 4.0, 128 * 4.0, 64, 128),\n      offset: Vector2.all(200),\n      color: Colors.greenAccent,\n      scale: 2,\n      rotation: pi / 9.0,\n      anchor: Vector2.all(64),\n    );\n\n    spriteBatch.addTransform(\n      source: const Rect.fromLTWH(128 * 4.0, 128 * 4.0, 64, 128),\n      color: Colors.redAccent,\n    );\n\n    final size = game.size;\n    const num = 100;\n    final r = Random();\n    for (var i = 0; i < num; ++i) {\n      final sx = r.nextInt(8) * 128.0;\n      final sy = r.nextInt(8) * 128.0;\n      final x = r.nextInt(size.x.toInt()).toDouble();\n      final y = r.nextInt(size.y ~/ 2).toDouble() + size.y / 2.0;\n      spriteBatch.add(\n        source: Rect.fromLTWH(sx, sy, 128, 128),\n        offset: Vector2(x - 64, y - 64),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/sprites/sprite_group_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\n\nenum ButtonState { unpressed, pressed }\n\nclass SpriteGroupExample extends FlameGame {\n  static const String description = '''\n    In this example we show how a `SpriteGroupComponent` can be used to create\n    a button which displays different sprites depending on whether it is pressed\n    or not.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    add(\n      ButtonComponent()\n        ..position = size / 2\n        ..size = Vector2(200, 50)\n        ..anchor = Anchor.center,\n    );\n  }\n}\n\nclass ButtonComponent extends SpriteGroupComponent<ButtonState>\n    with HasGameReference<SpriteGroupExample>, TapCallbacks {\n  @override\n  Future<void> onLoad() async {\n    final pressedSprite = await game.loadSprite(\n      'buttons.png',\n      srcPosition: Vector2(0, 20),\n      srcSize: Vector2(60, 20),\n    );\n    final unpressedSprite = await game.loadSprite(\n      'buttons.png',\n      srcSize: Vector2(60, 20),\n    );\n\n    sprites = {\n      ButtonState.pressed: pressedSprite,\n      ButtonState.unpressed: unpressedSprite,\n    };\n\n    current = ButtonState.unpressed;\n  }\n\n  @override\n  void onTapDown(_) {\n    current = ButtonState.pressed;\n  }\n\n  @override\n  void onTapUp(_) {\n    current = ButtonState.unpressed;\n  }\n\n  @override\n  void onTapCancel(_) {\n    current = ButtonState.unpressed;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/sprites/sprite_sheet_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/sprite.dart';\n\nclass SpriteSheetExample extends FlameGame {\n  static const String description = '''\n    In this example we show how to load images and how to create animations from\n    sprite sheets.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final spriteSheet = SpriteSheet(\n      image: await images.load('sprite_sheet.png'),\n      srcSize: Vector2(16.0, 18.0),\n    );\n\n    final vampireAnimation = spriteSheet.createAnimation(\n      row: 0,\n      stepTime: 0.1,\n      to: 7,\n    );\n\n    final ghostAnimation = spriteSheet.createAnimation(\n      row: 1,\n      stepTime: 0.1,\n      to: 7,\n    );\n\n    final ghostAnimationVariableStepTimes = spriteSheet\n        .createAnimationWithVariableStepTimes(\n          row: 1,\n          to: 7,\n          stepTimes: [0.1, 0.1, 0.3, 0.3, 0.5, 0.3, 0.1],\n        );\n\n    final customVampireAnimation = SpriteAnimation.fromFrameData(\n      spriteSheet.image,\n      SpriteAnimationData([\n        spriteSheet.createFrameData(0, 0, stepTime: 0.1),\n        spriteSheet.createFrameData(0, 1, stepTime: 0.1),\n        spriteSheet.createFrameData(0, 2, stepTime: 0.3),\n        spriteSheet.createFrameDataFromId(4, stepTime: 0.3),\n        spriteSheet.createFrameDataFromId(5, stepTime: 0.5),\n        spriteSheet.createFrameDataFromId(6, stepTime: 0.3),\n        spriteSheet.createFrameDataFromId(7, stepTime: 0.1),\n      ]),\n    );\n\n    final spriteSize = Vector2(80.0, 90.0);\n\n    final vampireComponent = SpriteAnimationComponent(\n      animation: vampireAnimation,\n      position: Vector2(150, 100),\n      size: spriteSize,\n    );\n\n    final ghostComponent = SpriteAnimationComponent(\n      animation: ghostAnimation,\n      position: Vector2(150, 220),\n      size: spriteSize,\n    );\n\n    final ghostAnimationVariableStepTimesComponent = SpriteAnimationComponent(\n      animation: ghostAnimationVariableStepTimes,\n      position: Vector2(250, 220),\n      size: spriteSize,\n    );\n\n    final customVampireComponent = SpriteAnimationComponent(\n      animation: customVampireAnimation,\n      position: Vector2(250, 100),\n      size: spriteSize,\n    );\n\n    add(vampireComponent);\n    add(ghostComponent);\n    add(ghostAnimationVariableStepTimesComponent);\n    add(customVampireComponent);\n\n    // Some plain sprites\n    final vampireSpriteComponent = SpriteComponent(\n      sprite: spriteSheet.getSprite(0, 0),\n      position: Vector2(50, 100),\n      size: spriteSize,\n    );\n\n    final ghostSpriteComponent = SpriteComponent(\n      sprite: spriteSheet.getSprite(1, 0),\n      size: spriteSize,\n      position: Vector2(50, 220),\n    );\n\n    add(vampireSpriteComponent);\n    add(ghostSpriteComponent);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/sprites/sprites.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/sprites/base64_sprite_example.dart';\nimport 'package:examples/stories/sprites/basic_sprite_example.dart';\nimport 'package:examples/stories/sprites/sprite_batch_example.dart';\nimport 'package:examples/stories/sprites/sprite_batch_load_example.dart';\nimport 'package:examples/stories/sprites/sprite_group_example.dart';\nimport 'package:examples/stories/sprites/sprite_sheet_example.dart';\nimport 'package:flame/game.dart';\n\nvoid addSpritesStories(Dashbook dashbook) {\n  dashbook.storiesOf('Sprites')\n    ..add(\n      'Basic Sprite',\n      (_) => GameWidget(game: BasicSpriteExample()),\n      codeLink: baseLink('sprites/basic_sprite_example.dart'),\n      info: BasicSpriteExample.description,\n    )\n    ..add(\n      'Base64 Sprite',\n      (_) => GameWidget(game: Base64SpriteExample()),\n      codeLink: baseLink('sprites/base64_sprite_example.dart'),\n      info: Base64SpriteExample.description,\n    )\n    ..add(\n      'SpriteSheet',\n      (_) => GameWidget(game: SpriteSheetExample()),\n      codeLink: baseLink('sprites/sprite_sheet_example.dart'),\n      info: SpriteSheetExample.description,\n    )\n    ..add(\n      'SpriteBatch',\n      (_) => GameWidget(game: SpriteBatchExample()),\n      codeLink: baseLink('sprites/sprite_batch_example.dart'),\n      info: SpriteBatchExample.description,\n    )\n    ..add(\n      'SpriteBatch Auto Load',\n      (_) => GameWidget(game: SpriteBatchLoadExample()),\n      codeLink: baseLink('sprites/sprite_batch_load_example.dart'),\n      info: SpriteBatchLoadExample.description,\n    )\n    ..add(\n      'SpriteGroup',\n      (_) => GameWidget(game: SpriteGroupExample()),\n      codeLink: baseLink('sprites/sprite_group_example.dart'),\n      info: SpriteGroupExample.description,\n    );\n}\n"
  },
  {
    "path": "examples/lib/stories/structure/levels.dart",
    "content": "import 'package:examples/commons/ember.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flutter/material.dart';\n\nclass LevelsExample extends FlameGame {\n  static const String description = '''\n    In this example we showcase how you can utilize World components as levels.\n    Press the different buttons in the bottom to change levels and press in the\n    center to add new Ember's. You can see how level 1-3 keeps their state,\n    meanwhile the one called Resettable always resets.\n  ''';\n\n  LevelsExample() : super(world: ResettableLevel());\n\n  late final TextComponent header;\n\n  @override\n  Future<void> onLoad() async {\n    header = TextComponent(\n      text: 'test',\n      position: Vector2(size.x / 2, 50),\n      anchor: Anchor.center,\n    );\n    // If you have a lot of HUDs you could also create separate viewports for\n    // each level and then just change them from within the world's onLoad with:\n    // game.cameraComponent.viewport = Level1Viewport();\n    final viewport = camera.viewport;\n    viewport.add(header);\n    final levels = [Level1(), Level2(), Level3()];\n    viewport.addAll(\n      [\n        LevelButton(\n          'Level 1',\n          onPressed: () => world = levels[0],\n          position: Vector2(size.x / 2 - 210, size.y - 50),\n        ),\n        LevelButton(\n          'Level 2',\n          onPressed: () => world = levels[1],\n          position: Vector2(size.x / 2 - 70, size.y - 50),\n        ),\n        LevelButton(\n          'Level 3',\n          onPressed: () => world = levels[2],\n          position: Vector2(size.x / 2 + 70, size.y - 50),\n        ),\n        LevelButton(\n          'Resettable',\n          onPressed: () => world = ResettableLevel(),\n          position: Vector2(size.x / 2 + 210, size.y - 50),\n        ),\n      ],\n    );\n  }\n}\n\nclass ResettableLevel extends Level {\n  @override\n  Future<void> onLoad() async {\n    add(\n      Ember()..add(\n        ScaleEffect.by(\n          Vector2.all(3),\n          EffectController(duration: 1, alternate: true, infinite: true),\n        ),\n      ),\n    );\n    game.header.text = 'Resettable';\n  }\n}\n\nclass Level1 extends Level {\n  @override\n  Future<void> onLoad() async {\n    add(Ember());\n    game.header.text = 'Level 1';\n  }\n}\n\nclass Level2 extends Level {\n  @override\n  Future<void> onLoad() async {\n    add(Ember(position: Vector2(-100, 0)));\n    add(Ember(position: Vector2(100, 0)));\n    game.header.text = 'Level 2';\n  }\n}\n\nclass Level3 extends Level {\n  @override\n  Future<void> onLoad() async {\n    add(Ember(position: Vector2(-100, -50)));\n    add(Ember(position: Vector2(100, -50)));\n    add(Ember(position: Vector2(0, 50)));\n    game.header.text = 'Level 3';\n  }\n}\n\nclass Level extends World with HasGameReference<LevelsExample>, TapCallbacks {\n  @override\n  void onTapDown(TapDownEvent event) {\n    add(Ember(position: event.localPosition));\n  }\n}\n\nclass LevelButton extends ButtonComponent {\n  LevelButton(String text, {super.onPressed, super.position})\n    : super(\n        button: ButtonBackground(Colors.white),\n        buttonDown: ButtonBackground(Colors.orangeAccent),\n        children: [\n          TextComponent(\n            text: text,\n            position: Vector2(60, 20),\n            anchor: Anchor.center,\n          ),\n        ],\n        size: Vector2(120, 40),\n        anchor: Anchor.center,\n      );\n}\n\nclass ButtonBackground extends PositionComponent with HasAncestor<LevelButton> {\n  ButtonBackground(Color color) {\n    _paint.color = color;\n  }\n\n  @override\n  void onMount() {\n    super.onMount();\n    size = ancestor.size;\n  }\n\n  late final _background = RRect.fromRectAndRadius(\n    size.toRect(),\n    const Radius.circular(5),\n  );\n  final _paint = Paint()..style = PaintingStyle.stroke;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(_background, _paint);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/structure/structure.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/structure/levels.dart';\nimport 'package:flame/game.dart';\n\nvoid addStructureStories(Dashbook dashbook) {\n  dashbook\n      .storiesOf('Structure')\n      .add(\n        'Levels',\n        (_) => GameWidget(game: LevelsExample()),\n        info: LevelsExample.description,\n        codeLink: baseLink('structure/levels.dart'),\n      );\n}\n"
  },
  {
    "path": "examples/lib/stories/svg/svg.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/svg/svg_component.dart';\nimport 'package:flame/game.dart';\n\nvoid addSvgStories(Dashbook dashbook) {\n  dashbook\n      .storiesOf('Svg')\n      .add(\n        'Svg Component',\n        (_) => GameWidget(game: SvgComponentExample()),\n        codeLink: baseLink('svg/svg_component.dart'),\n        info: SvgComponentExample.description,\n      );\n}\n"
  },
  {
    "path": "examples/lib/stories/svg/svg_component.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_svg/flame_svg.dart';\n\nclass Player extends SvgComponent with HasGameReference<SvgComponentExample> {\n  Player() : super(priority: 3, size: Vector2(106, 146), anchor: Anchor.center);\n\n  Vector2? destination;\n\n  @override\n  Future<void>? onLoad() async {\n    await super.onLoad();\n\n    svg = await game.loadSvg('svgs/happy_player.svg');\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    if (destination != null) {\n      final difference = destination! - position;\n      if (difference.length < 2) {\n        destination = null;\n      } else {\n        final direction = difference.normalized();\n        position += direction * 200 * dt;\n      }\n    }\n  }\n}\n\nclass Background extends SvgComponent\n    with HasGameReference<SvgComponentExample> {\n  Background()\n    : super(\n        priority: 1,\n        size: Vector2(745, 415),\n        anchor: Anchor.center,\n      );\n\n  @override\n  Future<void>? onLoad() async {\n    await super.onLoad();\n\n    svg = await game.loadSvg('svgs/checkerboard.svg');\n  }\n}\n\nclass Balloons extends SvgComponent with HasGameReference<SvgComponentExample> {\n  Balloons({super.position})\n    : super(\n        priority: 2,\n        size: Vector2(75, 125),\n        anchor: Anchor.center,\n      );\n\n  @override\n  Future<void>? onLoad() async {\n    await super.onLoad();\n\n    final color = Random().nextBool() ? 'red' : 'green';\n\n    svg = await game.loadSvg('svgs/${color}_balloons.svg');\n  }\n}\n\nclass SvgComponentExample extends FlameGame {\n  static const description = '''\n      Simple game showcasing how to use SVGs inside a flame game. This game \n      uses several SVGs for its graphics. Click or touch the screen to make the \n      player move, and double click/tap to add a new set of balloons at the \n      clicked position.\n  ''';\n\n  SvgComponentExample()\n    : super(\n        camera: CameraComponent.withFixedResolution(\n          width: 400,\n          height: 600,\n        ),\n        world: _SvgComponentWorld(),\n      );\n}\n\nclass _SvgComponentWorld extends World with TapCallbacks, DoubleTapCallbacks {\n  late Player player;\n\n  @override\n  Future<void>? onLoad() async {\n    await super.onLoad();\n\n    add(player = Player());\n    add(Background());\n\n    addAll([\n      Balloons(position: Vector2(-10, -20)),\n      Balloons(position: Vector2(-100, -150)),\n      Balloons(position: Vector2(-200, -140)),\n      Balloons(position: Vector2(100, 130)),\n      Balloons(position: Vector2(50, -130)),\n    ]);\n  }\n\n  @override\n  void onTapUp(TapUpEvent info) {\n    player.destination = info.localPosition;\n  }\n\n  @override\n  void onDoubleTapDown(DoubleTapDownEvent info) {\n    add(Balloons()..position = info.localPosition);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/system/overlays_example.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass OverlaysExample extends FlameGame with TapCallbacks {\n  static const String description = '''\n    In this example we show how the overlays system can be used.\\n\\n\n    If you tap the canvas the game will start and if you tap it again it will\n    pause.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final animation = await loadSpriteAnimation(\n      'animations/chopper.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        textureSize: Vector2.all(48),\n        stepTime: 0.15,\n      ),\n    );\n\n    add(\n      SpriteAnimationComponent(\n          animation: animation,\n        )\n        ..position.y = size.y / 2\n        ..position.x = 100\n        ..anchor = Anchor.center\n        ..size = Vector2.all(100),\n    );\n\n    // 'SecondaryMenu' will be displayed above 'PauseMenu'\n    overlays.add('SecondaryMenu', priority: 1);\n  }\n\n  @override\n  void onTapDown(_) {\n    toggleMenu();\n  }\n\n  void toggleMenu() {\n    if (overlays.isActive('PauseMenu')) {\n      overlays.remove('PauseMenu');\n      resumeEngine();\n    } else {\n      overlays.add('PauseMenu');\n      pauseEngine();\n    }\n  }\n}\n\nWidget _pauseMenuBuilder(\n  BuildContext buildContext,\n  OverlaysExample game,\n  GestureTapCallback? onTap,\n) {\n  return Center(\n    child: GestureDetector(\n      onTap: onTap,\n      child: Container(\n        width: 100,\n        height: 100,\n        color: Colors.orange,\n        child: const Center(\n          child: Text('Paused'),\n        ),\n      ),\n    ),\n  );\n}\n\nWidget _secondaryMenuBuilder(BuildContext buildContext, OverlaysExample game) {\n  return Align(\n    alignment: Alignment.bottomRight,\n    child: Padding(\n      padding: const EdgeInsets.all(8.0),\n      child: Container(\n        width: 100,\n        height: 50,\n        alignment: Alignment.center,\n        color: Colors.red,\n        child: const Row(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            Icon(Icons.music_off_rounded),\n            Icon(Icons.info),\n            Icon(Icons.star),\n          ],\n        ),\n      ),\n    ),\n  );\n}\n\nWidget overlayBuilder(DashbookContext ctx) {\n  return GameWidget<OverlaysExample>(\n    game: OverlaysExample()..paused = true,\n    overlayBuilderMap: {\n      'PauseMenu': (context, game) => _pauseMenuBuilder(\n        context,\n        game,\n        () => game.toggleMenu(),\n      ),\n      'SecondaryMenu': _secondaryMenuBuilder,\n    },\n    initialActiveOverlays: const ['PauseMenu'],\n  );\n}\n"
  },
  {
    "path": "examples/lib/stories/system/pause_resume_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\n\nclass PauseResumeExample extends FlameGame\n    with TapCallbacks, DoubleTapDetector {\n  static const description = '''\n    Demonstrate how to use the pause and resume engine methods and paused\n    attribute.\n\n    Tap on the screen to toggle the execution of the engine using the\n    `resumeEngine` and `pauseEngine`.\n\n    Double Tap on the screen to toggle the execution of the engine using the\n    `paused` attribute.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    final animation = await loadSpriteAnimation(\n      'animations/chopper.png',\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        textureSize: Vector2.all(48),\n        stepTime: 0.15,\n      ),\n    );\n\n    add(\n      SpriteAnimationComponent(\n          animation: animation,\n        )\n        ..position = size / 2\n        ..anchor = Anchor.center\n        ..size = Vector2.all(100),\n    );\n  }\n\n  @override\n  void onTapDown(_) {\n    if (paused) {\n      resumeEngine();\n    } else {\n      pauseEngine();\n    }\n  }\n\n  @override\n  void onDoubleTap() {\n    paused = !paused;\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/system/resize_example.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\n\nclass ResizingRectangle extends RectangleComponent {\n  ResizingRectangle()\n    : super(\n        paint: Paint()..color = const Color(0xFFFE4813),\n      );\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n\n    this.size = size * 0.4;\n  }\n}\n\nclass ResizeExampleGame extends FlameGame {\n  ResizeExampleGame() : super(children: [ResizingRectangle()]);\n\n  static const description = '''\n    This example shows how to react to the game being resized.\n\n    The rectangle will always be 40% of the screen size.\n\n    Try resizing the window and see the rectangle change its size.\n  ''';\n}\n"
  },
  {
    "path": "examples/lib/stories/system/step_engine_example.dart",
    "content": "import 'dart:async';\nimport 'dart:math';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nclass StepEngineExample extends FlameGame\n    with HasCollisionDetection, HasKeyboardHandlerComponents {\n  static const description = '''\n    This example demonstrates how the game can be advanced frame by frame using\n    stepEngine method.\n\n    To pause and un-pause the game anytime press the `P` key. Once paused, use\n    the `S` key to step by one frame.\n\n    Up arrow and down arrow can be used to increase or decrease the step time.\n  ''';\n\n  // Fixed resolution of the game.\n  static final Vector2 _visibleSize = Vector2(320, 180);\n  double _stepTimeMultiplier = 1;\n  static const _stepTime = 1 / 60;\n\n  @override\n  Color backgroundColor() => BasicPalette.darkGreen.color;\n\n  @override\n  Future<void> onLoad() async {\n    final carSprite = await Sprite.load('Car.png');\n    final car = SpriteComponent(\n      sprite: carSprite,\n      anchor: Anchor.center,\n      angle: -pi / 10,\n      position: Vector2(0, _visibleSize.y / 3),\n      children: [CircleHitbox()],\n    );\n\n    final world = World(\n      children: [\n        ..._createCircularDetectors(),\n        PositionComponent(children: [car, _rotateEffect]),\n      ],\n    );\n\n    final cameraComponent = CameraComponent.withFixedResolution(\n      world: world,\n      width: _visibleSize.x,\n      height: _visibleSize.y,\n      hudComponents: [_controlsText],\n    );\n\n    await addAll([world, cameraComponent]);\n  }\n\n  @override\n  KeyEventResult onKeyEvent(\n    KeyEvent event,\n    Set<LogicalKeyboardKey> keysPressed,\n  ) {\n    if (keysPressed.contains(LogicalKeyboardKey.keyP)) {\n      paused = !paused;\n    } else if (keysPressed.contains(LogicalKeyboardKey.keyS)) {\n      stepEngine(stepTime: _stepTime * _stepTimeMultiplier);\n    } else if (keysPressed.contains(LogicalKeyboardKey.arrowUp)) {\n      _stepTimeMultiplier += 1;\n      _controlsText.text = _text;\n    } else if (keysPressed.contains(LogicalKeyboardKey.arrowDown)) {\n      _stepTimeMultiplier -= 1;\n      _controlsText.text = _text;\n    }\n    return super.onKeyEvent(event, keysPressed);\n  }\n\n  // Creates the circle detectors.\n  List<Component> _createCircularDetectors() {\n    final componentsToAdd = <Component>[];\n    final offsetVec = Vector2(0, -_visibleSize.y / 2.5);\n    for (var i = 0; i < 12; ++i) {\n      offsetVec.rotate(2 * pi / 12);\n      componentsToAdd.add(\n        _DetectorComponents(\n          radius: 5,\n          position: offsetVec,\n          anchor: Anchor.center,\n          children: [CircleHitbox()],\n        ),\n      );\n    }\n    return componentsToAdd;\n  }\n\n  final _rotateEffect = RotateEffect.by(\n    2 * pi,\n    InfiniteEffectController(\n      SpeedEffectController(\n        LinearEffectController(1),\n        speed: 1,\n      ),\n    ),\n  );\n\n  String get _text =>\n      'P: Pause/Unpause\\nS: Step x$_stepTimeMultiplier\\nUp: Increase step\\nDown: Decrease step';\n\n  late final _controlsText = TextBoxComponent(\n    text: _text,\n    textRenderer: TextPaint(\n      style: TextStyle(\n        color: BasicPalette.white.color,\n        fontSize: 20.0,\n        shadows: const [\n          Shadow(offset: Offset(1, 1), blurRadius: 1),\n        ],\n      ),\n    ),\n  );\n}\n\nclass _DetectorComponents extends CircleComponent with CollisionCallbacks {\n  _DetectorComponents({\n    super.radius,\n    super.position,\n    super.anchor,\n    super.children,\n  });\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    paint.color = BasicPalette.black.color;\n    super.onCollisionStart(intersectionPoints, other);\n  }\n\n  @override\n  void onCollisionEnd(PositionComponent other) {\n    paint.color = BasicPalette.white.color;\n    super.onCollisionEnd(other);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/system/system.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/system/overlays_example.dart';\nimport 'package:examples/stories/system/pause_resume_example.dart';\nimport 'package:examples/stories/system/resize_example.dart';\nimport 'package:examples/stories/system/step_engine_example.dart';\nimport 'package:examples/stories/system/without_flame_game_example.dart';\nimport 'package:flame/game.dart';\n\nvoid addSystemStories(Dashbook dashbook) {\n  dashbook.storiesOf('System')\n    ..add(\n      'Pause/resume engine',\n      (_) => GameWidget(game: PauseResumeExample()),\n      codeLink: baseLink('system/pause_resume_example.dart'),\n      info: PauseResumeExample.description,\n    )\n    ..add(\n      'Overlay',\n      overlayBuilder,\n      codeLink: baseLink('system/overlays_example.dart'),\n      info: OverlaysExample.description,\n    )\n    ..add(\n      'Without FlameGame',\n      (_) => GameWidget(game: NoFlameGameExample()),\n      codeLink: baseLink('system/without_flame_game_example.dart'),\n      info: NoFlameGameExample.description,\n    )\n    ..add(\n      'Step Game',\n      (_) => GameWidget(game: StepEngineExample()),\n      codeLink: baseLink('system/step_engine_game.dart'),\n      info: StepEngineExample.description,\n    )\n    ..add(\n      'On Game Resize',\n      (_) => GameWidget(game: ResizeExampleGame()),\n      codeLink: baseLink('system/resize_example.dart'),\n      info: ResizeExampleGame.description,\n    );\n}\n"
  },
  {
    "path": "examples/lib/stories/system/without_flame_game_example.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter/widgets.dart';\n\nclass NoFlameGameExample extends Game with KeyboardEvents {\n  static const String description = '''\n    This example showcases how to create a game without the FlameGame.\n    It also briefly showcases how to act on keyboard events.\n    Usage: Use W A S D to steer the rectangle.\n  ''';\n\n  static final Paint white = BasicPalette.white.paint();\n  static const int speed = 200;\n\n  Rect rect = const Rect.fromLTWH(0, 100, 100, 100);\n  final Vector2 velocity = Vector2(0, 0);\n\n  @override\n  void update(double dt) {\n    final displacement = velocity * (speed * dt);\n    rect = rect.translate(displacement.x, displacement.y);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(rect, white);\n  }\n\n  @override\n  KeyEventResult onKeyEvent(\n    KeyEvent event,\n    Set<LogicalKeyboardKey> keysPressed,\n  ) {\n    final isKeyDown = event is KeyDownEvent;\n\n    if (event.logicalKey == LogicalKeyboardKey.keyA) {\n      velocity.x = isKeyDown ? -1 : 0;\n    } else if (event.logicalKey == LogicalKeyboardKey.keyD) {\n      velocity.x = isKeyDown ? 1 : 0;\n    } else if (event.logicalKey == LogicalKeyboardKey.keyW) {\n      velocity.y = isKeyDown ? -1 : 0;\n    } else if (event.logicalKey == LogicalKeyboardKey.keyS) {\n      velocity.y = isKeyDown ? 1 : 0;\n    }\n\n    return super.onKeyEvent(event, keysPressed);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/tiled/flame_tiled_animation_example.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame_tiled/flame_tiled.dart';\n\nclass FlameTiledAnimationExample extends FlameGame {\n  static const String description = '''\n    Loads and displays an animated Tiled map.\n  ''';\n\n  late final TiledComponent map;\n\n  @override\n  Future<void> onLoad() async {\n    map = await TiledComponent.load('dungeon.tmx', Vector2.all(32));\n    add(map);\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/tiled/tiled.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/tiled/flame_tiled_animation_example.dart';\n\nimport 'package:flame/game.dart';\n\nvoid addTiledStories(Dashbook dashbook) {\n  dashbook\n      .storiesOf('Tiled')\n      .add(\n        'Flame Tiled Animation',\n        (_) => GameWidget(game: FlameTiledAnimationExample()),\n        codeLink: baseLink('tiled/flame_tiled_animation_example.dart'),\n        info: FlameTiledAnimationExample.description,\n      );\n}\n"
  },
  {
    "path": "examples/lib/stories/utils/timer_component_example.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass TimerComponentExample extends FlameGame\n    with TapCallbacks, DoubleTapDetector {\n  static const String description = '''\n    This examples showcases the `TimerComponent`.\\n\\n\n    Tap to start a timer that lives for one second and double tap to start\n    another timer that lives for 5 seconds.\n  ''';\n\n  RenderedTimeComponent? tapComponent;\n  RenderedTimeComponent? doubleTapComponent;\n\n  @override\n  void onTapDown(_) {\n    tapComponent?.removeFromParent();\n    tapComponent = RenderedTimeComponent(1);\n    add(tapComponent!);\n  }\n\n  @override\n  void onDoubleTap() {\n    doubleTapComponent?.removeFromParent();\n    doubleTapComponent = RenderedTimeComponent(5, yOffset: 180);\n    add(doubleTapComponent!);\n  }\n}\n\nclass RenderedTimeComponent extends TimerComponent {\n  final TextPaint textPaint = TextPaint(\n    style: const TextStyle(color: Colors.white, fontSize: 20),\n  );\n\n  final double yOffset;\n\n  RenderedTimeComponent(double period, {this.yOffset = 150})\n    : super(\n        period: period,\n        removeOnFinish: true,\n      );\n\n  @override\n  void render(Canvas canvas) {\n    textPaint.render(\n      canvas,\n      'Elapsed time: ${timer.current.toStringAsFixed(3)}',\n      Vector2(30, yOffset),\n    );\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/utils/timer_example.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/timer.dart';\nimport 'package:flutter/material.dart';\n\nclass TimerExample extends FlameGame with TapCallbacks {\n  static const String description = '''\n    This example shows how to use the `Timer`.\\n\\n\n    Tap down to start the countdown timer, it will then count to 5 and then stop\n    until you tap the canvas again and it restarts.\n  ''';\n\n  final TextPaint textConfig = TextPaint(\n    style: const TextStyle(color: Colors.white, fontSize: 20),\n  );\n  late Timer countdown;\n  late Timer interval;\n\n  int elapsedSecs = 0;\n\n  @override\n  Future<void> onLoad() async {\n    countdown = Timer(5);\n    interval = Timer(\n      1,\n      onTick: () => elapsedSecs += 1,\n      repeat: true,\n    );\n    interval.start();\n  }\n\n  @override\n  void onTapDown(_) {\n    countdown.start();\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    countdown.update(dt);\n    interval.update(dt);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    textConfig.render(\n      canvas,\n      'Countdown: ${countdown.current.toStringAsPrecision(3)}',\n      Vector2(30, 100),\n    );\n    textConfig.render(canvas, 'Elapsed time: $elapsedSecs', Vector2(30, 130));\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/utils/utils.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/utils/timer_component_example.dart';\nimport 'package:examples/stories/utils/timer_example.dart';\nimport 'package:flame/game.dart';\n\nvoid addUtilsStories(Dashbook dashbook) {\n  dashbook.storiesOf('Utils')\n    ..add(\n      'Timer',\n      (_) => GameWidget(game: TimerExample()),\n      codeLink: baseLink('utils/timer_example.dart'),\n      info: TimerExample.description,\n    )\n    ..add(\n      'Timer Component',\n      (_) => GameWidget(game: TimerComponentExample()),\n      codeLink: baseLink('utils/timer_component_example.dart'),\n      info: TimerComponentExample.description,\n    );\n}\n"
  },
  {
    "path": "examples/lib/stories/widgets/custom_painter_example.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass CustomPainterExample extends FlameGame with TapCallbacks {\n  static const description = '''\n    Example demonstration of how to use the CustomPainterComponent.\n\n    On the screen you can see a component using a custom painter being\n    rendered on a FlameGame, and if you tap, that same painter is used to\n    show a smiley on a widget overlay.\n  ''';\n\n  @override\n  Future<void> onLoad() async {\n    add(Player());\n  }\n\n  @override\n  void onTapDown(_) {\n    if (overlays.isActive('Smiley')) {\n      overlays.remove('Smiley');\n    } else {\n      overlays.add('Smiley');\n    }\n  }\n}\n\nWidget customPainterBuilder(DashbookContext ctx) {\n  return GameWidget(\n    game: CustomPainterExample(),\n    overlayBuilderMap: {\n      'Smiley': (context, game) {\n        return Center(\n          child: Container(\n            color: Colors.transparent,\n            width: 200,\n            height: 200,\n            child: Column(\n              children: [\n                const Text(\n                  'Hey, I can be a widget too!',\n                  style: TextStyle(\n                    color: Colors.white70,\n                  ),\n                ),\n                const SizedBox(height: 32),\n                SizedBox(\n                  height: 132,\n                  width: 132,\n                  child: CustomPaint(painter: PlayerCustomPainter()),\n                ),\n              ],\n            ),\n          ),\n        );\n      },\n    },\n  );\n}\n\nclass PlayerCustomPainter extends CustomPainter {\n  late final facePaint = Paint()..color = Colors.yellow;\n\n  late final eyesPaint = Paint()..color = Colors.black;\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    final faceRadius = size.height / 2;\n\n    canvas.drawCircle(\n      Offset(\n        faceRadius,\n        faceRadius,\n      ),\n      faceRadius,\n      facePaint,\n    );\n\n    final eyeSize = faceRadius * 0.15;\n\n    canvas.drawCircle(\n      Offset(\n        faceRadius - (eyeSize * 2),\n        faceRadius - eyeSize,\n      ),\n      eyeSize,\n      eyesPaint,\n    );\n\n    canvas.drawCircle(\n      Offset(\n        faceRadius + (eyeSize * 2),\n        faceRadius - eyeSize,\n      ),\n      eyeSize,\n      eyesPaint,\n    );\n  }\n\n  @override\n  bool shouldRepaint(CustomPainter oldDelegate) {\n    return false;\n  }\n}\n\nclass Player extends CustomPainterComponent\n    with HasGameReference<CustomPainterExample> {\n  static const speed = 150;\n\n  int direction = 1;\n\n  @override\n  Future<void> onLoad() async {\n    painter = PlayerCustomPainter();\n    size = Vector2.all(100);\n\n    y = 200;\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    x += speed * direction * dt;\n\n    if ((x + width >= game.size.x && direction > 0) ||\n        (x <= 0 && direction < 0)) {\n      direction *= -1;\n    }\n  }\n}\n"
  },
  {
    "path": "examples/lib/stories/widgets/nine_tile_box_example.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:flame/widgets.dart';\nimport 'package:flutter/widgets.dart';\n\nWidget nineTileBoxBuilder(DashbookContext ctx) {\n  return Container(\n    width: ctx.numberProperty('width', 200),\n    height: ctx.numberProperty('height', 200),\n    child: NineTileBoxWidget.asset(\n      path: 'nine-box.png',\n      tileSize: 22,\n      destTileSize: 50,\n      child: const Center(\n        child: Text(\n          'Cool label',\n          style: TextStyle(\n            color: Color(0xFF000000),\n          ),\n        ),\n      ),\n    ),\n  );\n}\n"
  },
  {
    "path": "examples/lib/stories/widgets/nine_tile_box_example_with_animation.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:flame/widgets.dart';\nimport 'package:flutter/material.dart';\n\nvar _opacity = 1.0;\n\nWidget nineTileBoxBuilderWithAnimation(DashbookContext ctx) {\n  return StatefulBuilder(\n    builder: (context, setState) {\n      return Column(\n        children: [\n          const SizedBox(height: 8),\n          ElevatedButton(\n            onPressed: () {\n              setState(() {\n                _opacity = _opacity == 1.0 ? 0.0 : 1.0;\n              });\n            },\n            child: const Text('Toggle'),\n          ),\n          const SizedBox(height: 8),\n          AnimatedOpacity(\n            duration: const Duration(seconds: 2),\n            opacity: _opacity,\n            child: NineTileBoxWidget.asset(\n              width: 400,\n              height: 400,\n              path: 'nine-box.png',\n              tileSize: 22,\n              destTileSize: 50,\n              child: const Center(\n                child: Text(\n                  'Cool label',\n                  style: TextStyle(\n                    color: Color(0xFF000000),\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ],\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "examples/lib/stories/widgets/paints.dart",
    "content": "import 'dart:ui';\n\nfinal paintChoices = [\n  'none',\n  'transparent',\n  'red tinted',\n  'green tinted',\n  'blue tinted',\n];\nfinal paintList = [\n  null,\n  Paint()..color = const Color(0x22FFFFFF),\n  Paint()\n    ..colorFilter = const ColorFilter.mode(\n      Color(0x88FF0000),\n      BlendMode.srcATop,\n    ),\n  Paint()\n    ..colorFilter = const ColorFilter.mode(\n      Color(0x8800FF00),\n      BlendMode.srcATop,\n    ),\n  Paint()\n    ..colorFilter = const ColorFilter.mode(\n      Color(0x880000FF),\n      BlendMode.srcATop,\n    ),\n];\n"
  },
  {
    "path": "examples/lib/stories/widgets/partial_sprite_widget_example.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/widgets.dart';\nimport 'package:flutter/material.dart';\n\nfinal anchorOptions = Anchor.values.map((e) => e.name).toList();\n\nWidget partialSpriteWidgetBuilder(DashbookContext ctx) {\n  return Container(\n    width: ctx.numberProperty('container width', 400),\n    height: ctx.numberProperty('container height', 200),\n    decoration: BoxDecoration(border: Border.all(color: Colors.amber)),\n    child: SpriteWidget.asset(\n      path: 'bomb_ptero.png',\n      srcPosition: Vector2(\n        ctx.numberProperty('srcPosition.x', 48),\n        ctx.numberProperty('srcPosition.y', 0),\n      ),\n      srcSize: Vector2(\n        ctx.numberProperty('srcSize.x', 48),\n        ctx.numberProperty('srcSize.y', 32),\n      ),\n      anchor: Anchor.valueOf(\n        ctx.listProperty('anchor', 'center', anchorOptions),\n      ),\n    ),\n  );\n}\n"
  },
  {
    "path": "examples/lib/stories/widgets/sprite_animation_widget_example.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:examples/stories/widgets/paints.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/widgets.dart';\nimport 'package:flutter/widgets.dart';\n\nfinal anchorOptions = Anchor.values.map((e) => e.name).toList();\n\nWidget spriteAnimationWidgetBuilder(DashbookContext ctx) {\n  return Container(\n    width: ctx.numberProperty('container width', 400),\n    height: ctx.numberProperty('container height', 200),\n    child: SpriteAnimationWidget.asset(\n      path: 'bomb_ptero.png',\n      data: SpriteAnimationData.sequenced(\n        amount: 4,\n        stepTime: 0.2,\n        textureSize: Vector2(48, 32),\n      ),\n      playing: ctx.boolProperty('playing', true),\n      anchor: Anchor.valueOf(\n        ctx.listProperty('anchor', 'center', anchorOptions),\n      ),\n      paint:\n          paintList[paintChoices.indexOf(\n            ctx.listProperty(\n              'paint',\n              'none',\n              paintChoices,\n            ),\n          )],\n    ),\n  );\n}\n"
  },
  {
    "path": "examples/lib/stories/widgets/sprite_button_example.dart",
    "content": "import 'package:dashbook/dashbook.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/widgets.dart';\nimport 'package:flutter/widgets.dart';\n\nWidget spriteButtonBuilder(DashbookContext ctx) {\n  return Container(\n    padding: const EdgeInsets.all(20),\n    child: SpriteButton.asset(\n      path: 'buttons.png',\n      pressedPath: 'buttons.png',\n      srcPosition: Vector2(0, 0),\n      srcSize: Vector2(60, 20),\n      pressedSrcPosition: Vector2(0, 20),\n      pressedSrcSize: Vector2(60, 20),\n      onPressed: () {\n        // Do something\n      },\n      label: const Text(\n        'Sprite Button',\n        style: TextStyle(color: Color(0xFF5D275D)),\n      ),\n      width: ctx.numberProperty('width', 250),\n      height: ctx.numberProperty('height', 75),\n    ),\n  );\n}\n"
  },
  {
    "path": "examples/lib/stories/widgets/sprite_widget_example.dart",
    "content": "import 'dart:math';\n\nimport 'package:dashbook/dashbook.dart';\nimport 'package:examples/stories/widgets/paints.dart';\nimport 'package:flame/widgets.dart';\nimport 'package:flutter/material.dart';\n\nfinal anchorOptions = Anchor.values.map((e) => e.name).toList();\n\nWidget spriteWidgetBuilder(DashbookContext ctx) {\n  return Container(\n    width: ctx.numberProperty('container width', 400),\n    height: ctx.numberProperty('container height', 200),\n    decoration: BoxDecoration(border: Border.all(color: Colors.amber)),\n    child: SpriteWidget.asset(\n      path: 'shield.png',\n      angle: pi / 180 * ctx.numberProperty('angle (deg)', 0),\n      anchor: Anchor.valueOf(\n        ctx.listProperty('anchor', 'center', anchorOptions),\n      ),\n      paint:\n          paintList[paintChoices.indexOf(\n            ctx.listProperty(\n              'paint',\n              'none',\n              paintChoices,\n            ),\n          )],\n    ),\n  );\n}\n"
  },
  {
    "path": "examples/lib/stories/widgets/widgets.dart",
    "content": "import 'package:dashbook/dashbook.dart';\n\nimport 'package:examples/commons/commons.dart';\nimport 'package:examples/stories/widgets/custom_painter_example.dart';\nimport 'package:examples/stories/widgets/nine_tile_box_example.dart';\nimport 'package:examples/stories/widgets/nine_tile_box_example_with_animation.dart';\nimport 'package:examples/stories/widgets/partial_sprite_widget_example.dart';\nimport 'package:examples/stories/widgets/sprite_animation_widget_example.dart';\nimport 'package:examples/stories/widgets/sprite_button_example.dart';\nimport 'package:examples/stories/widgets/sprite_widget_example.dart';\n\nvoid addWidgetsStories(Dashbook dashbook) {\n  dashbook.storiesOf('Widgets')\n    ..decorator(CenterDecorator())\n    ..add(\n      'Nine Tile Box',\n      nineTileBoxBuilder,\n      codeLink: baseLink('widgets/nine_tile_box_example.dart'),\n      info: '''\n        If you want to create a background for something that can stretch you\n        can use the `NineTileBox` which is showcased here, don't forget to check\n        out the settings on the pen icon.\n      ''',\n    )\n    ..add(\n      'Nine Tile Box (With animation widgets)',\n      nineTileBoxBuilderWithAnimation,\n      codeLink: baseLink('widgets/nine_tile_box_example_with_animation.dart'),\n      info: '''\n        Similar to the Nine Tile Box example, but here a NineTileBoxWidget is composed\n        with Flutter's AnimatedOpacity.\n      ''',\n    )\n    ..add(\n      'Sprite Button',\n      spriteButtonBuilder,\n      codeLink: baseLink('widgets/sprite_button_example.dart'),\n      info: '''\n        If you want to use sprites as a buttons within the flutter widget tree\n        you can create a `SpriteButton`, don't forget to check out the settings\n        on the pen icon.\n      ''',\n    )\n    ..add(\n      'Sprite Widget (full image)',\n      spriteWidgetBuilder,\n      codeLink: baseLink('widgets/sprite_widget_example.dart'),\n      info: '''\n        If you want to use a sprite within the flutter widget tree\n        you can create a `SpriteWidget`, don't forget to check out the settings\n        on the pen icon.\n      ''',\n    )\n    ..add(\n      'Sprite Widget (section of image)',\n      partialSpriteWidgetBuilder,\n      codeLink: baseLink('widgets/partial_sprite_widget_example.dart'),\n      info: '''\n        In this example we show how you can render only parts of a sprite within\n        a `SpriteWidget`, don't forget to check out the settings on the pen\n        icon.\n      ''',\n    )\n    ..add(\n      'Sprite Animation Widget',\n      spriteAnimationWidgetBuilder,\n      codeLink: baseLink('widgets/sprite_animation_widget_example.dart'),\n      info: '''\n        If you want to use a sprite animation directly on the flutter widget\n        tree you can create a `SpriteAnimationWidget`, don't forget to check out\n        the settings on the pen icon.\n      ''',\n    )\n    ..add(\n      'CustomPainterComponent',\n      customPainterBuilder,\n      codeLink: baseLink('widgets/custom_painter_example.dart'),\n      info: CustomPainterExample.description,\n    );\n}\n"
  },
  {
    "path": "examples/pubspec.yaml",
    "content": "name: examples\nresolution: workspace\ndescription: A set of small examples showcasing each feature provided by the Flame Engine.\nhomepage: https://github.com/flame-engine/flame/tree/main/examples\npublish_to: \"none\"\n\nversion: 0.1.0\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  crystal_ball: ^0.1.0\n  dashbook: ^0.1.16\n  flame: ^1.36.0\n  flame_audio: ^2.12.0\n  flame_forge2d: ^0.19.2+5\n  flame_isolate: ^0.6.2+21\n  flame_lottie: ^0.4.2+21\n  flame_noise: ^0.3.2+21\n  flame_spine: ^0.3.0+4\n  flame_svg: ^1.12.0\n  flame_tiled: ^3.1.0\n  flutter:\n    sdk: flutter\n  google_fonts: ^8.0.2\n  jenny: ^1.5.1\n  meta: ^1.12.0\n  padracing: ^1.0.0\n  provider: ^6.1.2\n  rogue_shooter: ^0.1.0\n  trex_game: ^0.1.0\n  web: ^1.1.0\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  test: any\n\nflutter:\n  uses-material-design: true\n\n  assets:\n    - assets/images/animations/\n    - assets/images/\n    - assets/images/tile_maps/\n    - assets/images/layers/\n    - assets/images/parallax/\n    - assets/images/parallax/\n    - assets/images/rogue_shooter/\n    - assets/spine/\n    - assets/svgs/\n    - assets/tiles/\n    - assets/audio/music/\n    - assets/audio/sfx/\n    - assets/yarn/\n    \n"
  },
  {
    "path": "examples/web/CNAME",
    "content": "examples.flame-engine.org\n"
  },
  {
    "path": "examples/web/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"UTF-8\">\n  <meta content=\"IE=Edge\" http-equiv=\"X-UA-Compatible\">\n  <meta name=\"description\" content=\"A set of small game examples showcasing each feature provided by the Flame Engine\">\n  <base href=\"$FLUTTER_BASE_HREF\">\n\n  <!-- iOS meta tags & icons -->\n  <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n  <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\">\n  <meta name=\"apple-mobile-web-app-title\" content=\"Flame Examples\">\n  <link rel=\"apple-touch-icon\" href=\"icons/Icon-192.png\">\n\n  <!-- Favicon -->\n  <link rel=\"icon\" type=\"image/png\" href=\"favicon.png\"/>\n\n  <title>Flame Examples</title>\n  <link rel=\"manifest\" href=\"manifest.json\">\n</head>\n<body>\n  <script src=\"flutter_bootstrap.js\" async></script>\n</body>\n</html>\n"
  },
  {
    "path": "examples/web/manifest.json",
    "content": "{\n    \"name\": \"flame_example\",\n    \"short_name\": \"flame_example\",\n    \"start_url\": \".\",\n    \"display\": \"standalone\",\n    \"background_color\": \"#0175C2\",\n    \"theme_color\": \"#0175C2\",\n    \"description\": \"A set of small game examples showcasing each feature provided by the Flame Engine.\",\n    \"orientation\": \"portrait-primary\",\n    \"prefer_related_applications\": false,\n    \"icons\": [\n        {\n            \"src\": \"icons/Icon-192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"icons/Icon-512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\"\n        }\n    ]\n}\n"
  },
  {
    "path": "packages/README.md",
    "content": "# Flame repository structure\n\nThe Flame repository is a monorepo which contains both the main Flame package and other so-called\nbridge packages.\n\n\n## Bridge packages\n\nBridge packages are packages which:\n\n- Provides Flame with an interface to another external package (e.g. `flame_audio` which is\n  Flame + Audioplayers, `flame_tiled`, which is Flame + tiled).\n- Packages with features that are somehow very context specific and doesn't have a place inside the\n  core package (e.g. `flame_splash_screen`).\n\nWhile most of the officially supported bridge packages live in this monorepo, a few can be found\nunder Flame Engine's GitHub organization. There are also many bridge packages developed and maintained\nby other members of the community.\n"
  },
  {
    "path": "packages/flame/.min_coverage",
    "content": "78.0\n"
  },
  {
    "path": "packages/flame/.pubignore",
    "content": "!extension/**/*\n"
  },
  {
    "path": "packages/flame/CHANGELOG.md",
    "content": "## 1.36.0\n\n - **FIX**: Ray direction normalization drift issues ([#3841](https://github.com/flame-engine/flame/issues/3841)). ([b8e2bab5](https://github.com/flame-engine/flame/commit/b8e2bab58fbfb1b817dd294db3d2389097d31a2d))\n - **FIX**: Initialize center offset in `CircleComponent` ctor ([#3842](https://github.com/flame-engine/flame/issues/3842)). ([a0d2a5f3](https://github.com/flame-engine/flame/commit/a0d2a5f3a2db6ca2b0c25c93ca48fe12a4bdbc11))\n - **FIX**: Resolve `fontPackage` in `IconComponent` (`_rasterizeIcon`) ([#3838](https://github.com/flame-engine/flame/issues/3838)). ([cdb2a0dd](https://github.com/flame-engine/flame/commit/cdb2a0ddf8ec6db733c904696a17ceeaed2e7b5f))\n - **FIX**: Hitboxes now correctly account for parent scale and rotation ([#3834](https://github.com/flame-engine/flame/issues/3834)). ([57adcd60](https://github.com/flame-engine/flame/commit/57adcd60970efb2dbf4f52e396a4e78a51e47be8))\n - **FIX**: Make `buildContext` available during `onLoad` and `onMount` ([#3833](https://github.com/flame-engine/flame/issues/3833)). ([60bfcb30](https://github.com/flame-engine/flame/commit/60bfcb30e7751ee65caa6425d79bbd7bb82ae3ed))\n - **FIX**: Add CJK wrapping for `TextBoxComponent` ([#3830](https://github.com/flame-engine/flame/issues/3830)). ([7f41c261](https://github.com/flame-engine/flame/commit/7f41c2614e1669398bb08858df30270d75dced68))\n - **FIX**: Prevent removed children from being re-added when parent is moved ([#3824](https://github.com/flame-engine/flame/issues/3824)). ([8e77bc2d](https://github.com/flame-engine/flame/commit/8e77bc2d87300a52c03177fb187c4e35b810acc4))\n - **FIX**: Make `removeAll(children)` work in `onRemove` ([#3823](https://github.com/flame-engine/flame/issues/3823)). ([ff760230](https://github.com/flame-engine/flame/commit/ff760230881f2a27f9d3a9462fc831c460d7ffc3))\n - **FIX**: End active collisions in ShapeHitbox.onRemove to fix inconsistent isColliding ([#3821](https://github.com/flame-engine/flame/issues/3821)). ([bc81e7fd](https://github.com/flame-engine/flame/commit/bc81e7fd96c1ec5ffad210d6d027894f6faef77b))\n - **FIX**: Use scaledRadius for CircleHitbox collision detection ([#3808](https://github.com/flame-engine/flame/issues/3808)). ([3498c1e5](https://github.com/flame-engine/flame/commit/3498c1e565985ce7d12212e6a625f538da98c362))\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n - **FEAT**: Add `clampDouble` to `Vector2Extension` and its tests ([#3840](https://github.com/flame-engine/flame/issues/3840)). ([6c6ccf31](https://github.com/flame-engine/flame/commit/6c6ccf317f3f397edf30a61c90ed00d087a96e9a))\n - **FEAT**: Add `transformMatrix` setter to `Transform2D` ([#3836](https://github.com/flame-engine/flame/issues/3836)). ([957ff2e0](https://github.com/flame-engine/flame/commit/957ff2e085f03ab43f40f4213d94730b52e2cfc6))\n - **FEAT**: Support package argument in asset loading methods and widgets ([#3835](https://github.com/flame-engine/flame/issues/3835)). ([3f6f95b0](https://github.com/flame-engine/flame/commit/3f6f95b0225df9e4035c74de6cfad501e8c45167))\n - **FEAT**: Propagate Flutter hot reload through the component tree ([#3828](https://github.com/flame-engine/flame/issues/3828)). ([c44643f1](https://github.com/flame-engine/flame/commit/c44643f1afdf9890d9890d6653f7429366e7f03b))\n - **FEAT**: Add an IconComponent that renders IconData ([#3820](https://github.com/flame-engine/flame/issues/3820)). ([97931a59](https://github.com/flame-engine/flame/commit/97931a59361c51c433d366fecb30b7867813c521))\n - **FEAT**: Add `dispose()` method to `FlameGame` ([#3825](https://github.com/flame-engine/flame/issues/3825)). ([aa5a27b7](https://github.com/flame-engine/flame/commit/aa5a27b791c4bd5a3ab64ee73b34feefedb8b210))\n - **FEAT**: Add object pooling support with `ComponentPool` ([#3816](https://github.com/flame-engine/flame/issues/3816)). ([46802fab](https://github.com/flame-engine/flame/commit/46802fab4705ae8d1ff2a61660b2d80450f6075e))\n - **FEAT**: Add opposite method to Anchor ([#3817](https://github.com/flame-engine/flame/issues/3817)). ([1ffd59f0](https://github.com/flame-engine/flame/commit/1ffd59f09f90d17980b70e494ce585d676888b57))\n - **FEAT**: HitTestBehavior for GameWidget ([#3815](https://github.com/flame-engine/flame/issues/3815)). ([b888d4e2](https://github.com/flame-engine/flame/commit/b888d4e2d7ea95694a0c5f83e2fa68f8cee67069))\n - **DOCS**: Document FlameGame<W extends World> generic type parameter ([#3822](https://github.com/flame-engine/flame/issues/3822)). ([00a66123](https://github.com/flame-engine/flame/commit/00a66123d84d305e66953f1a85e66c46c8e0c4fc))\n\n## 1.35.1\n\n - **FIX**: Cancel taps that start inside the component and end outside ([#3805](https://github.com/flame-engine/flame/issues/3805)). ([ebcdb81c](https://github.com/flame-engine/flame/commit/ebcdb81c83b0aa23906d1c652bad2bfadd5add71))\n\n## 1.35.0\n\n - **FIX**: Loading page should always be possible to add to route ([#3800](https://github.com/flame-engine/flame/issues/3800)). ([a2f5df11](https://github.com/flame-engine/flame/commit/a2f5df113293525d3c5cc6626c5fea05a02350c2))\n - **FIX**: Reimplement setLayoutSize to only notify once ([#3796](https://github.com/flame-engine/flame/issues/3796)). ([97f8bebe](https://github.com/flame-engine/flame/commit/97f8bebecaf5bb9a8018c85220609b2d9d67524a))\n - **FEAT**: Use a Free List Strategy on BatchItem indexes within SpriteBatch and return index from .add() ([#3650](https://github.com/flame-engine/flame/issues/3650)). ([8d77c84e](https://github.com/flame-engine/flame/commit/8d77c84e0c05d0b5b6ca57187bd0ee39e94c752f))\n - **FEAT**: Add TextBoxComponent.resetAnimation ([#3787](https://github.com/flame-engine/flame/issues/3787)). ([33fb10c0](https://github.com/flame-engine/flame/commit/33fb10c02a83354030fe0a278c16869f5940941a))\n - **FEAT**: Implement padding component inflateChild ([#3785](https://github.com/flame-engine/flame/issues/3785)). ([9ac53a69](https://github.com/flame-engine/flame/commit/9ac53a69e468a3e3ff073db94c1c5df57997c4f1))\n\n## 1.34.0\n\n - **FEAT**: Add scaling gesture for components ([#3770](https://github.com/flame-engine/flame/issues/3770)). ([f413eddb](https://github.com/flame-engine/flame/commit/f413eddbf1581f30087ba53f9516e22e035bda7a))\n - **FEAT**: Linear layout component text box component ([#3779](https://github.com/flame-engine/flame/issues/3779)). ([476680d7](https://github.com/flame-engine/flame/commit/476680d7b699be7ffd4e3c3421e6f27659a66420))\n - **FEAT**: TextBoxComponent updateBounds on set boxConfig ([#3777](https://github.com/flame-engine/flame/issues/3777)). ([eefb2f9e](https://github.com/flame-engine/flame/commit/eefb2f9e966a4ead5bcf0d3985314609166609f1))\n - **FEAT**: Add the `CombinedEffect` that bundles many effects together in one ([#3776](https://github.com/flame-engine/flame/issues/3776)). ([77869f57](https://github.com/flame-engine/flame/commit/77869f573df8b0eb1ce02de8f2b896286768bc6f))\n - **DOCS**: Fix documentation typos ([#3769](https://github.com/flame-engine/flame/issues/3769)). ([db74da15](https://github.com/flame-engine/flame/commit/db74da154b309e7544f5242c49d56717e385c5fc))\n\n## 1.33.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Re-organize internal event imports ([#3742](https://github.com/flame-engine/flame/issues/3742)). ([7523e014](https://github.com/flame-engine/flame/commit/7523e014706a2a368eebf4843379d470c5924c68))\n - **PERF**: `addAll` shouldn't create unnecessary growing lists ([#3737](https://github.com/flame-engine/flame/issues/3737)). ([d1fa9d0d](https://github.com/flame-engine/flame/commit/d1fa9d0d5491264fbf4bb2b7e0c731597e8c0fb5))\n - **FIX**: Store json maps directly in AssetsCache ([#3746](https://github.com/flame-engine/flame/issues/3746)). ([8a9f493f](https://github.com/flame-engine/flame/commit/8a9f493fdddf68ba2889f19be71eb9e91071190f))\n - **FIX**: Unique `ComponentKey` toString ([#3739](https://github.com/flame-engine/flame/issues/3739)). ([9a4a8f20](https://github.com/flame-engine/flame/commit/9a4a8f20ff0ad20625ae7db7aca0ca683e4db417))\n - **FIX**: Depreciated AssetManifest.json switched to AssetManifest API ([#3734](https://github.com/flame-engine/flame/issues/3734)). ([a2bd9827](https://github.com/flame-engine/flame/commit/a2bd982764b8e59830e86ba8a07239cedc1bad1c))\n - **FEAT**: Dummy commit. ([8f7437d3](https://github.com/flame-engine/flame/commit/8f7437d3a468a9708f47da8863454e843bbbf72c))\n - **FEAT**: Added fromCache method to AssetsCache ([#3740](https://github.com/flame-engine/flame/issues/3740)). ([33a7123f](https://github.com/flame-engine/flame/commit/33a7123ff9538f222534379a35d9a1074102a3fd))\n - **FEAT**: Add a \"raw\" field to access the underlying Flutter event in the new event system ([#3731](https://github.com/flame-engine/flame/issues/3731)). ([36eb3929](https://github.com/flame-engine/flame/commit/36eb3929aa44451c1d6aa986a305c436cfa93349))\n - **DOCS**: Layout components ([#3752](https://github.com/flame-engine/flame/issues/3752)). ([0aa145bb](https://github.com/flame-engine/flame/commit/0aa145bbbd6decfa121080b1cf223e9e799c1ac4))\n - **DOCS**: Deprecate TapDetector in favour of TapCallbacks ([#2886](https://github.com/flame-engine/flame/issues/2886)). ([b173697b](https://github.com/flame-engine/flame/commit/b173697bfb7ea61287251c43cd3c9d2fdb448fe3))\n - **BREAKING** **FEAT**: Implements ExpandedComponent ([#3662](https://github.com/flame-engine/flame/issues/3662)). ([212ed354](https://github.com/flame-engine/flame/commit/212ed354d7704915a7585424e216ca83300c9530))\n - **BREAKING** **FEAT**: Support secondary taps (right click) on new callbacks system ([#3741](https://github.com/flame-engine/flame/issues/3741)). ([46bd3856](https://github.com/flame-engine/flame/commit/46bd385675ae781c4614d997e4792f53fc43271d))\n\n## 1.32.0\n\n - **REFACTOR**: Move MutableRSTransform out of flame_tiled package and into flame package ([#3695](https://github.com/flame-engine/flame/issues/3695)). ([7d644dd8](https://github.com/flame-engine/flame/commit/7d644dd84ce27e292b53f7310967393cf4c60618))\n - **FEAT**: Add renderLine helper to canvas extensions ([#3697](https://github.com/flame-engine/flame/issues/3697)). ([7ede916f](https://github.com/flame-engine/flame/commit/7ede916f77f20d8c1b0c89627800214dba9facec))\n\n## 1.31.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Resume engine on mount if paused by backgrounding ([#3631](https://github.com/flame-engine/flame/issues/3631)) ([#3637](https://github.com/flame-engine/flame/issues/3637)). ([b556dc35](https://github.com/flame-engine/flame/commit/b556dc3557d4b655d605c8e2b3744cafd0635841))\n - **FIX**: Export `ComponentRenderContext` ([#3669](https://github.com/flame-engine/flame/issues/3669)). ([086096ca](https://github.com/flame-engine/flame/commit/086096ca73236aaea79a2651cb9e3fa8b6211d50))\n - **FIX**: The `ParallaxComponent` should respect the `virtualSize` ([#3666](https://github.com/flame-engine/flame/issues/3666)). ([9f29c785](https://github.com/flame-engine/flame/commit/9f29c785a1e17428d3a59965b2bf484267c4b2a8))\n - **FIX**: Attach layout listeners to new children ([#3648](https://github.com/flame-engine/flame/issues/3648)). ([4821ec2c](https://github.com/flame-engine/flame/commit/4821ec2ca9cccbf8017d0b539373f599d168c45c))\n - **FEAT**: Add support for model parsing and rendering in flame_3d, including skeletal animations ([#3675](https://github.com/flame-engine/flame/issues/3675)). ([cc58aef5](https://github.com/flame-engine/flame/commit/cc58aef5b53f208fb1cbb116bfb9f9af9a351e8e))\n - **FEAT**: Add Random extensions ([#3672](https://github.com/flame-engine/flame/issues/3672)). ([50e5f296](https://github.com/flame-engine/flame/commit/50e5f29610e9bcc8d939d1e86b5c8bc398516eb1))\n - **FEAT**: Padding component ([#3661](https://github.com/flame-engine/flame/issues/3661)). ([6c953a28](https://github.com/flame-engine/flame/commit/6c953a2862b46c66b91785c5d481385567596adb))\n - **FEAT**: Add canPop to RouterComponent ([#3659](https://github.com/flame-engine/flame/issues/3659)). ([6bd3b48f](https://github.com/flame-engine/flame/commit/6bd3b48ff34c92b221b8a66ac951238d7e6176e0))\n - **FEAT**: Add children and priority to SpriteBatchComponent ([#3649](https://github.com/flame-engine/flame/issues/3649)). ([97b9ba83](https://github.com/flame-engine/flame/commit/97b9ba837e094d79f9e8d8c1ed413717b9d11663))\n - **BREAKING** **REFACTOR**: Remove shrinkwrap ([#3660](https://github.com/flame-engine/flame/issues/3660)). ([e8860f62](https://github.com/flame-engine/flame/commit/e8860f622acaf7df97ca8fcfbdf94fdae26d5921))\n\n## 1.30.1\n\n - **FIX**: Hitboxes with vertically flipped ancestor should not reflect angle for vertices ([#3642](https://github.com/flame-engine/flame/issues/3642)). ([7e8d3a98](https://github.com/flame-engine/flame/commit/7e8d3a9885f77da12456b148cd1f425395a00f71))\n - **FIX**: Remove unnecessary breaks ([#3638](https://github.com/flame-engine/flame/issues/3638)). ([ea29929c](https://github.com/flame-engine/flame/commit/ea29929cd86ed00407f2d2aa69dcf6f34ffc5bbd))\n - **FEAT**: Adding priority to layout components ([#3639](https://github.com/flame-engine/flame/issues/3639)). ([2eff267d](https://github.com/flame-engine/flame/commit/2eff267d795fbfbf9f5b3215b6dca4a2da9864e1))\n\n## 1.30.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: `angleTo` and `lookAt` should consider parental transformations ([#3629](https://github.com/flame-engine/flame/issues/3629)). ([e6f3d105](https://github.com/flame-engine/flame/commit/e6f3d105577cd346d377aaaed42d4ceb93aec077))\n - **FIX**: `angleTo`, `absoluteAngle` and the `angle` setter now returns normalized angles between `[-pi, pi]` ([#3629](https://github.com/flame-engine/flame/issues/3629)). ([e6f3d105](https://github.com/flame-engine/flame/commit/e6f3d105577cd346d377aaaed42d4ceb93aec077))\n - **FIX**: Delay should work with SpeedEffectControllers ([#3618](https://github.com/flame-engine/flame/issues/3618)). ([bfbb49f5](https://github.com/flame-engine/flame/commit/bfbb49f5b6aac4f69c8602cd20a457e95fe02973))\n - **FIX**: Pass in intended parent to remove ([#3626](https://github.com/flame-engine/flame/issues/3626)). ([7a05f74d](https://github.com/flame-engine/flame/commit/7a05f74dff7c3dbac96d8c8eb52ad7f0625266a1))\n - **FIX**: Call `super.onDispose` last and check `mounted` before `setState` ([#3623](https://github.com/flame-engine/flame/issues/3623)). ([3d2716c1](https://github.com/flame-engine/flame/commit/3d2716c1fb2b64d363dbc8e9aea852723e909710))\n - **FIX**: Angled line intersections should work with 32-bit vectors ([#3617](https://github.com/flame-engine/flame/issues/3617)). ([e32bff45](https://github.com/flame-engine/flame/commit/e32bff455c0f5715c1a7018f865b44b2410ed7db))\n - **FIX**: PostProcessComponent should size dynamically ([#3611](https://github.com/flame-engine/flame/issues/3611)). ([baecb861](https://github.com/flame-engine/flame/commit/baecb86186a1bff7f21d804e7867f894d2f9d23c))\n - **FEAT**: Add `target` argument to `SpawnComponent` ([#3635](https://github.com/flame-engine/flame/issues/3635)). ([3747e1e8](https://github.com/flame-engine/flame/commit/3747e1e8bd1f4bde3c6b64fff0f336690f9da6c8))\n - **FEAT**: Add `spawnCount` to `SpawnComponent` ([#3634](https://github.com/flame-engine/flame/issues/3634)). ([f377d7e7](https://github.com/flame-engine/flame/commit/f377d7e702892836a5fded1c8d4f648682e69e50))\n - **FEAT**: Adding RasterSpriteComponent.fromImage constructor ([#3627](https://github.com/flame-engine/flame/issues/3627)). ([74a84ba7](https://github.com/flame-engine/flame/commit/74a84ba7c159631296961eec994179e227ccd1d3))\n - **FEAT**: Implement measure to fix ghost lines and graphical artifacts in Sprites ([#3590](https://github.com/flame-engine/flame/issues/3590)). ([6fd36bc1](https://github.com/flame-engine/flame/commit/6fd36bc1d883d61621806fba54a792dc6924c4e8))\n - **BREAKING** **FEAT**: Pass `WidgetTester` for `testGolden` prepare function ([#3624](https://github.com/flame-engine/flame/issues/3624)). ([10509326](https://github.com/flame-engine/flame/commit/105093266431408db0f9e74042e03e2234d9b22e))\n\n## 1.29.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Only expose `ReadOnlyOrderedset` from `component.children` ([#3606](https://github.com/flame-engine/flame/issues/3606)). ([79351d19](https://github.com/flame-engine/flame/commit/79351d195ea968b8016129e79a489ef113a0fdf3))\n - **FIX**: Dispose picture is postprocess  ([#3604](https://github.com/flame-engine/flame/issues/3604)). ([3b24cdac](https://github.com/flame-engine/flame/commit/3b24cdac18ec6d846dbc4d08905fbcb897f90be8))\n - **FIX**: Materialize post process list before removing items ([#3591](https://github.com/flame-engine/flame/issues/3591)). ([e858cc1f](https://github.com/flame-engine/flame/commit/e858cc1fc74814769fc11f49014190d37bda5cbe))\n - **DOCS**: Update structure and add RowComponent + ColumnComponent docs ([#3599](https://github.com/flame-engine/flame/issues/3599)). ([d04843a4](https://github.com/flame-engine/flame/commit/d04843a44c9987825cc927a2ec8952395b423ba4))\n - **BREAKING** **FEAT**: Children should retain `parent` after parent is remove from tree ([#3602](https://github.com/flame-engine/flame/issues/3602)). ([008829af](https://github.com/flame-engine/flame/commit/008829af67e3556a92b926db6b6368acf10e249b))\n\n## 1.28.1\n\n - **REFACTOR**: Replace dart:io usage with defaultTargetPlatform ([#3567](https://github.com/flame-engine/flame/issues/3567)). ([77925eb8](https://github.com/flame-engine/flame/commit/77925eb84e3ab23c301d504ccc85cc84a91cb3e4))\n - **FIX**: Add fragment shader extension from flutter_shaders ([#3578](https://github.com/flame-engine/flame/issues/3578)). ([27115729](https://github.com/flame-engine/flame/commit/271157295209cc3f147d38582c7c9447e2e84844))\n - **FIX**: Use `virtualSize` when calling `onParentResize` on children of `Viewport` ([#3577](https://github.com/flame-engine/flame/issues/3577)). ([245fb3f5](https://github.com/flame-engine/flame/commit/245fb3f5cf286b19076e758b8fea75a410680ffe))\n - **FEAT**: Add method to toggle overlays ([#3581](https://github.com/flame-engine/flame/issues/3581)). ([ad7c37e1](https://github.com/flame-engine/flame/commit/ad7c37e16b20b71c8049d68fd57414b174fd9492))\n\n## 1.28.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Priority change should be reflected directly ([#3564](https://github.com/flame-engine/flame/issues/3564)). ([ab2fd639](https://github.com/flame-engine/flame/commit/ab2fd639f73896c0859dd133337ec2adc7adf832))\n - **FIX**: Deprecate `HasGameRef` in favor of `HasGameReference` ([#3559](https://github.com/flame-engine/flame/issues/3559)). ([a882261b](https://github.com/flame-engine/flame/commit/a882261b63ef21e29dde041d99b2eaf94264d7ad))\n - **FIX**: The SpriteButton label should be nullable ([#3557](https://github.com/flame-engine/flame/issues/3557)). ([80a598cd](https://github.com/flame-engine/flame/commit/80a598cd006f2cf90b1b799bbb51c0c073a94743))\n - **FIX**: Update ordered_set, remove ComponentSet ([#3554](https://github.com/flame-engine/flame/issues/3554)). ([728e13f8](https://github.com/flame-engine/flame/commit/728e13f855d988e8f8e24976557b213b98221603))\n - **FEAT**: Post Process API ([#3404](https://github.com/flame-engine/flame/issues/3404)). ([c3316ae4](https://github.com/flame-engine/flame/commit/c3316ae4a50230e6d9720cb4653a8e3e309f3234))\n - **FEAT**: Add helper methods on LineSegment (translate, inflate, deflate, spread) ([#3561](https://github.com/flame-engine/flame/issues/3561)). ([6d388870](https://github.com/flame-engine/flame/commit/6d388870138b9e02e120647d241d7cf9093795f6))\n - **FEAT**: Support for disabled state for `SpriteButton` ([#3560](https://github.com/flame-engine/flame/issues/3560)). ([eaa2b442](https://github.com/flame-engine/flame/commit/eaa2b442b717ae086cac2d715a322ffa7c7a1116))\n - **FEAT**: Add a Render Context API ([#3409](https://github.com/flame-engine/flame/issues/3409)). ([532f0191](https://github.com/flame-engine/flame/commit/532f0191f658e767fde4c200cf1902cbe36d6e7d))\n - **FEAT**: Adding tickCount to TimerComponent ([#3552](https://github.com/flame-engine/flame/issues/3552)). ([dcd694e8](https://github.com/flame-engine/flame/commit/dcd694e8554c59b4b92f6d05928320c175d433f0))\n - **FEAT**: Ability to reset SpriteAnimation on removal ([#3553](https://github.com/flame-engine/flame/issues/3553)). ([59ae5831](https://github.com/flame-engine/flame/commit/59ae58318eba93e3909bdb2deaa13f6aa7b7d35e))\n - **BREAKING** **FIX**: Use 32bit Vector2 in Flame to be compatible with shaders and forge2d ([#3515](https://github.com/flame-engine/flame/issues/3515)). ([19dcecf5](https://github.com/flame-engine/flame/commit/19dcecf5df85345fd4fac168e1f360cee4665658))\n\n## 1.27.0\n\n - **FIX**: Remove outdated deprecations ([#3541](https://github.com/flame-engine/flame/issues/3541)). ([b918e40d](https://github.com/flame-engine/flame/commit/b918e40d0ce14b89ba9b5c82aed8ff51d6f549c3))\n - **FEAT**: Bump to new lint package ([#3545](https://github.com/flame-engine/flame/issues/3545)). ([bf6ee518](https://github.com/flame-engine/flame/commit/bf6ee51897591b7ad6e5f9da2193b1eeeaf026f4))\n - **FEAT**: The `FunctionEffect`, run any function as an `Effect` ([#3537](https://github.com/flame-engine/flame/issues/3537)). ([f4ac1ec6](https://github.com/flame-engine/flame/commit/f4ac1ec63a22b7a7d0c17d7119787f3ce2acadc1))\n\n## 1.26.1\n\n - **FIX**: Fix priority rebalancing causing concurrent mutation of component ordered_set ([#3530](https://github.com/flame-engine/flame/issues/3530)). ([c2afe11f](https://github.com/flame-engine/flame/commit/c2afe11f2ce736791a35e77afa5e1ddef0ae7cbb))\n - **FIX**: Expose event dispatcher classes ([#3532](https://github.com/flame-engine/flame/issues/3532)). ([db8e0b20](https://github.com/flame-engine/flame/commit/db8e0b20746dc96a221dc4e85b09f5a35ecc7339))\n\n## 1.26.0\n\n - **FIX**: RouterComponent should be on top ([#3524](https://github.com/flame-engine/flame/issues/3524)). ([aa52a2a5](https://github.com/flame-engine/flame/commit/aa52a2a58de9661557113c3d6ae5cc760842b1e7))\n - **FEAT**: Support custom attributes syntax to allow for multiple styles in the text rendering pipeline ([#3519](https://github.com/flame-engine/flame/issues/3519)). ([fbc58053](https://github.com/flame-engine/flame/commit/fbc58053dd12e6dc62b09cb14e4b438ef7b7f1b2))\n - **FEAT**: Layout shrinkwrap ([#3513](https://github.com/flame-engine/flame/issues/3513)). ([b3fbdd9d](https://github.com/flame-engine/flame/commit/b3fbdd9d3fd031083ecf7c53a28e2381579e846c))\n - **FEAT**: Layout Components ([#3507](https://github.com/flame-engine/flame/issues/3507)). ([678cf057](https://github.com/flame-engine/flame/commit/678cf05780580dd2cb61dde5e40c0efb1f3bc928))\n - **FEAT**: Add `RotateAroundEffect` ([#3499](https://github.com/flame-engine/flame/issues/3499)). ([0688f410](https://github.com/flame-engine/flame/commit/0688f41093cd451269366a2c2001a0d88bc6e532))\n - **DOCS**: Fix missing reference on documentation for InlineTextNode ([#3520](https://github.com/flame-engine/flame/issues/3520)). ([e3aa78b2](https://github.com/flame-engine/flame/commit/e3aa78b28206150eb85621e2a788fc31f218ff1d))\n - **DOCS**: Make onRemove() behavior more clear in API doc ([#3502](https://github.com/flame-engine/flame/issues/3502)). ([f387ad76](https://github.com/flame-engine/flame/commit/f387ad7604fca4b652d3c7521004a5d420137634))\n\n## 1.25.0\n\n - **FEAT**: Add a setter for TextBoxComponent.boxConfig and add a convenience method to skip per-char buildup ([#3490](https://github.com/flame-engine/flame/issues/3490)). ([d1777b7a](https://github.com/flame-engine/flame/commit/d1777b7a9efcf065c4474b7c6702c45d37bf710c))\n\n## 1.24.0\n\n - **PERF**: Switch from forEach to regular for-loops for about 30% improvement in raw update performance (#3472).\n - **FIX**: SpawnComponent.periodRange should change range each iteration (#3464).\n - **FIX**: Don't use a future when assets for SpriteButton is already loaded (#3456).\n - **FIX**: Darkness increases with higher values (#3448).\n - **FEAT**: NineTileBoxComponent with HasPaint to enable effects (#3459).\n - **FEAT**: Devtools overlay navigation (#3449).\n - **FEAT**: Add direction and length getters and constructor to LineSegment (#3446).\n - **FEAT**: Add multiFactory to SpawnComponent (#3440).\n\n## 1.23.0\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n - **FIX**: Take into consideration when child is added to parent that is removed in the same tick ([#3428](https://github.com/flame-engine/flame/issues/3428)). ([9a5c54be](https://github.com/flame-engine/flame/commit/9a5c54bea858fc8e9e84878f3ac0a0f7bc190b46))\n - **FIX**: Add missing export of GroupTextElement to text.dart ([#3424](https://github.com/flame-engine/flame/issues/3424)). ([c9c0f691](https://github.com/flame-engine/flame/commit/c9c0f691412bb026c1d766ec7b424a468f8929f7))\n - **FIX**: Add missing export of GroupElement to text.dart ([#3423](https://github.com/flame-engine/flame/issues/3423)). ([c0c4bb02](https://github.com/flame-engine/flame/commit/c0c4bb02a32306120a8770122116631f55c1c700))\n - **FIX**: Fix brighten and darken alpha issue ([#3414](https://github.com/flame-engine/flame/issues/3414)). ([de8e3bce](https://github.com/flame-engine/flame/commit/de8e3bcea2c2c2fa5e01dd288176c8f5623d21fb))\n - **FIX**: Set button size in onMount if not set ([#3413](https://github.com/flame-engine/flame/issues/3413)). ([916aa5ce](https://github.com/flame-engine/flame/commit/916aa5ce2ad3851b3044e043d2be7cbe923f2c40))\n - **FIX**: Fix bug preventing removeAll(children) from be called before mount ([#3408](https://github.com/flame-engine/flame/issues/3408)). ([726cb8b6](https://github.com/flame-engine/flame/commit/726cb8b6390c839f9cbab959b2268a7b45fa691c))\n - **FEAT**: Add support for strike-through text for flame_markdown ([#3426](https://github.com/flame-engine/flame/issues/3426)). ([1f9b0ea9](https://github.com/flame-engine/flame/commit/1f9b0ea9f35a7180725ec7f8f79a561c5f544bb7))\n - **FEAT**: Warning and docs about fullscreen methods outside the mobile platforms ([#3419](https://github.com/flame-engine/flame/issues/3419)). ([994e098b](https://github.com/flame-engine/flame/commit/994e098bd699a30aa13aed65f2bd0ab7254ad779))\n - **FEAT**: Add baseColor to Shadow3DDecorator ([#3375](https://github.com/flame-engine/flame/issues/3375)). ([b5d7ee07](https://github.com/flame-engine/flame/commit/b5d7ee0752ee1f2dddf1da4ac817f138296e1c96))\n\n## 1.22.0\n\n - **FIX**: Remove extra `implements SizeProvider`s ([#3358](https://github.com/flame-engine/flame/issues/3358)). ([47ba0d87](https://github.com/flame-engine/flame/commit/47ba0d8738b101ed59781f8ba384cf05a16d65f1))\n - **FEAT**: Add WorldRoute to enable swapping worlds from the RouterComponent ([#3372](https://github.com/flame-engine/flame/issues/3372)). ([497f128f](https://github.com/flame-engine/flame/commit/497f128f8c32758f94d8d4752e9166fd3b625608))\n - **FEAT**(overlays): Added the 'priority' parameter for overlays ([#3349](https://github.com/flame-engine/flame/issues/3349)). ([e591ebf8](https://github.com/flame-engine/flame/commit/e591ebf8a320ff3d55b9ae9e50390bf2ab5a8919))\n\n## 1.21.0\n\n - **FIX**: Widgets flickering ([#3343](https://github.com/flame-engine/flame/issues/3343)). ([ff170dc5](https://github.com/flame-engine/flame/commit/ff170dc5c2acc41190249b48e61767ea459fabb4))\n - **FIX**: Ray should not be able to escape `CircleHitbox` ([#3341](https://github.com/flame-engine/flame/issues/3341)). ([7311d034](https://github.com/flame-engine/flame/commit/7311d034d4c3b43592b49472384fe8576809e6a5))\n - **FIX**: Fix SpriteBatch to comply with new drawAtlas requirement ([#3338](https://github.com/flame-engine/flame/issues/3338)). ([a17fe4cd](https://github.com/flame-engine/flame/commit/a17fe4cdfaafa071cfd2ab8ef8279b26b79f00a7))\n - **FIX**: Set SpriteButtonComponent sprites in `onMount` ([#3327](https://github.com/flame-engine/flame/issues/3327)). ([f36533e7](https://github.com/flame-engine/flame/commit/f36533e78c7634866680ab5fb202a3e230529943))\n - **FIX**: Export TapConfig to make visible ([#3323](https://github.com/flame-engine/flame/issues/3323)). ([8e00115c](https://github.com/flame-engine/flame/commit/8e00115cd299423564dfce4b9d1674c9257a3c42))\n - **FIX**: Clarify `SpriteGroupComponent.updateSprite` assertion ([#3317](https://github.com/flame-engine/flame/issues/3317)). ([d976ee8c](https://github.com/flame-engine/flame/commit/d976ee8c7e4fbbca08e549412ca8b5af6928d4f4))\n - **FEAT**: Adding spawnWhenLoaded flag on SpawnComponent ([#3334](https://github.com/flame-engine/flame/issues/3334)). ([51a7e26b](https://github.com/flame-engine/flame/commit/51a7e26b1ab0ef2a2d040548c74aef84b164272d))\n - **FEAT**: Add a getter for images cache keys ([#3324](https://github.com/flame-engine/flame/issues/3324)). ([7746f2f8](https://github.com/flame-engine/flame/commit/7746f2f867092c19222a40aec2b66dc80558dccb))\n\n## 1.20.0\n\n - **FIX**: SpriteButtonComponent to initialize sprites in `onLoad` ([#3302](https://github.com/flame-engine/flame/issues/3302)). ([1204216c](https://github.com/flame-engine/flame/commit/1204216cb227d3831b546a54818075065fa7beec))\n - **FIX**: ViewportAwareBounds component and lifecycle issues ([#3276](https://github.com/flame-engine/flame/issues/3276)). ([026bf41f](https://github.com/flame-engine/flame/commit/026bf41f020de66ae9adfcdda9209bfbb75cf60c))\n - **FEAT**: Add ComponentTreeRoot.lifecycleEventsProcessed future ([#3308](https://github.com/flame-engine/flame/issues/3308)). ([ebc47418](https://github.com/flame-engine/flame/commit/ebc474189ceb587bcdebef7d3645ed2f3b3dba6f))\n - **FEAT**: Adding paint attribute to SpriteWidget and SpriteAnimationWidget ([#3298](https://github.com/flame-engine/flame/issues/3298)). ([a5338d0c](https://github.com/flame-engine/flame/commit/a5338d0c20d01bbe461c6d7fed5951d11e1c76f0))\n - **FEAT**: Adding tickOnLoad to TimerComponent ([#3285](https://github.com/flame-engine/flame/issues/3285)). ([0113aa37](https://github.com/flame-engine/flame/commit/0113aa376145109079a89bd310b9e528631ce9d4))\n - **DOCS**: Include information about the Flame DevTools extension in example readme ([#3288](https://github.com/flame-engine/flame/issues/3288)). ([76a9abaf](https://github.com/flame-engine/flame/commit/76a9abaf3c70659323e02bf7b6531b4fbba1f7a2))\n\n## 1.19.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Use a temp vector for delta calculations of `FollowBehavior` ([#3230](https://github.com/flame-engine/flame/issues/3230)). ([524793d4](https://github.com/flame-engine/flame/commit/524793d4a0dbe384d42fb9f844685b85abb05574))\n - **FIX**: Add assertion when trying to set \"current\" that doesn't exist ([#3258](https://github.com/flame-engine/flame/issues/3258)). ([267d6801](https://github.com/flame-engine/flame/commit/267d6801cb7e6cbbaa450e24e38aaa7d8fcfc03f))\n - **FIX**: Update version of lints to comply with new pub requirements ([#3223](https://github.com/flame-engine/flame/issues/3223)). ([1b0bee72](https://github.com/flame-engine/flame/commit/1b0bee726b5937f73d4be5e304bc8780aa3ca6f0))\n - **FIX**: Replace CurvedParticle inheritance with Particle in ScaledParticle ([#3221](https://github.com/flame-engine/flame/issues/3221)). ([8cd054d0](https://github.com/flame-engine/flame/commit/8cd054d02b614d1ee35a71f32dcbacf0952c9780))\n - **FIX**: Fix text rendering issue where spaces are missing ([#3192](https://github.com/flame-engine/flame/issues/3192)). ([28fd2a0f](https://github.com/flame-engine/flame/commit/28fd2a0f0f1ea04872d0c4e8b674c8ce7bca69ee))\n - **FIX**: Add nativeAngle to constructors where it makes sense ([#3197](https://github.com/flame-engine/flame/issues/3197)). ([e8704934](https://github.com/flame-engine/flame/commit/e8704934b19d9ed1982d35ce62819f01ac3de189))\n - **FIX**: Wire in background and foreground colors in TextPaint ([#3191](https://github.com/flame-engine/flame/issues/3191)). ([983cfab6](https://github.com/flame-engine/flame/commit/983cfab6def86dbf68455fb021281caaf0135793))\n - **FIX**: Disallow mutatation of `SpriteGroupComponent.sprites` ([#3185](https://github.com/flame-engine/flame/issues/3185)). ([7c40034d](https://github.com/flame-engine/flame/commit/7c40034d20ed26114b14fd262130d11cf226fb6a))\n - **FIX**: Disallow mutatation of `SpriteAnimationGroupComponent.animations` ([#3183](https://github.com/flame-engine/flame/issues/3183)). ([52773407](https://github.com/flame-engine/flame/commit/527734071b030ec7dbe0f3c017108db0dfda3ced))\n - **FEAT**: Adding scale and angle to devtools attributes ([#3267](https://github.com/flame-engine/flame/issues/3267)). ([b2a5e658](https://github.com/flame-engine/flame/commit/b2a5e6581ebaebc8044d65504efc58309f8a2b9b))\n - **FEAT**: Adding x,y,width and height inputs to position components on Dev Tools ([#3263](https://github.com/flame-engine/flame/issues/3263)). ([003ec3a1](https://github.com/flame-engine/flame/commit/003ec3a17beed2bad5540b968a0f5602c19ada79))\n - **FEAT**: Adding component snapshot to Dev tools ([#3261](https://github.com/flame-engine/flame/issues/3261)). ([1a574917](https://github.com/flame-engine/flame/commit/1a574917cd5311aea2576942d5cf4ea579218aaf))\n - **FEAT**: Fixing tests on flutter 3.24.0 ([#3259](https://github.com/flame-engine/flame/issues/3259)). ([bf9a2481](https://github.com/flame-engine/flame/commit/bf9a2481fbeb77413a26ae96b57843ca51411f9f))\n - **FEAT**: Loading builder for Route ([#3113](https://github.com/flame-engine/flame/issues/3113)). ([1e62b342](https://github.com/flame-engine/flame/commit/1e62b3424578150718514aa762f184485dba024a))\n - **FEAT**: Take in super.curve in ScalingParticle ([#3220](https://github.com/flame-engine/flame/issues/3220)). ([0fbc73cc](https://github.com/flame-engine/flame/commit/0fbc73ccdf36938a20f2eb8ae544881a8dbeae1e))\n - **FEAT**: Add `pause` and `resume` to `HasTimeScale` mixin ([#3216](https://github.com/flame-engine/flame/issues/3216)). ([9a86e7b5](https://github.com/flame-engine/flame/commit/9a86e7b54b55047ec9c63997015f71b7308dec27))\n - **FEAT**: Add missing background and foreground properties to InlineTextStyle ([#3187](https://github.com/flame-engine/flame/issues/3187)). ([34dde50f](https://github.com/flame-engine/flame/commit/34dde50f978f810df89fb1c051d13aee9214b307))\n - **FEAT**: Support inline code blocks on markdown rich text ([#3186](https://github.com/flame-engine/flame/issues/3186)). ([67e069c0](https://github.com/flame-engine/flame/commit/67e069c00dcb32c258231a326b0918739c6f80e6))\n - **DOCS**: Remove `PositionType` from the docs ([#3198](https://github.com/flame-engine/flame/issues/3198)). ([b0ff5c41](https://github.com/flame-engine/flame/commit/b0ff5c41c572da4dfa4221bef89b93b6f6be74c6))\n - **DOCS**: Add dartdocs to inline text node classes ([#3189](https://github.com/flame-engine/flame/issues/3189)). ([84c1ee87](https://github.com/flame-engine/flame/commit/84c1ee87f827a85c7accd92e061077ef291cb433))\n - **BREAKING** **REFACTOR**: Make query() result an Iterable ([#3209](https://github.com/flame-engine/flame/issues/3209)). ([c094caa7](https://github.com/flame-engine/flame/commit/c094caa77b17b1d69856396e27c88db8515bb44a))\n\n## 1.18.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n - **FIX**: Add key parameters to the rest of the components ([#3170](https://github.com/flame-engine/flame/issues/3170)). ([2477ea0f](https://github.com/flame-engine/flame/commit/2477ea0fcee99e71597983146f4af2dffd866971))\n - **FIX**: Invoke `setToStart` on child effect controller of wrapping effect controllers ([#3168](https://github.com/flame-engine/flame/issues/3168)). ([217c95f0](https://github.com/flame-engine/flame/commit/217c95f0a53fd5a7933bfa57833f951cc0037878))\n - **FIX**: Fix cascading and fallback propagation of text styles ([#3129](https://github.com/flame-engine/flame/issues/3129)). ([7b706d5f](https://github.com/flame-engine/flame/commit/7b706d5f63207aaf82d12a4b26233bc476771b1e))\n - **FEAT**: Add `onReleased` action to `AdvancedButtonComponent` which will be called within `onTapUp` ([#3152](https://github.com/flame-engine/flame/issues/3152)). ([2269732e](https://github.com/flame-engine/flame/commit/2269732e64a2acef2451d283c85b03e1101229ec))\n - **FEAT**: Support text align on new text rendering pipeline ([#3147](https://github.com/flame-engine/flame/issues/3147)). ([194d5536](https://github.com/flame-engine/flame/commit/194d5536560e464644bff8d5582a8ca8996539f5))\n - **FEAT**: Add missing parameters to InlineTextStyle ([#3146](https://github.com/flame-engine/flame/issues/3146)). ([ce9392ab](https://github.com/flame-engine/flame/commit/ce9392abd85fe5fd3ae6f766c3a2957275c6fb8c))\n - **FEAT**: Expand flame_lint to respect required pub.dev checks ([#3139](https://github.com/flame-engine/flame/issues/3139)). ([6e80bf5e](https://github.com/flame-engine/flame/commit/6e80bf5e679d1cdeeb9362d4103690b0b381161d))\n - **FEAT**: Add accessor to determine a TextElement size ([#3130](https://github.com/flame-engine/flame/issues/3130)). ([8a63a07a](https://github.com/flame-engine/flame/commit/8a63a07ae3b569c316eafa23f0378e00180e0963))\n - **FEAT**: Add ability to convert between TextPaint and InlineTextStyle ([#3128](https://github.com/flame-engine/flame/issues/3128)). ([6b63a57a](https://github.com/flame-engine/flame/commit/6b63a57a4888211b284f3a074c17519cb31341e0))\n - **FEAT**: Add completed future for effects ([#3123](https://github.com/flame-engine/flame/issues/3123)). ([5e967deb](https://github.com/flame-engine/flame/commit/5e967deb876ed39fa4ee6839471bbfbcd3b72463))\n - **FEAT**: Add custom long tap delay ([#3110](https://github.com/flame-engine/flame/issues/3110)). ([a95d7df6](https://github.com/flame-engine/flame/commit/a95d7df606bd2119423cc8a7ae51cacfb7b4dbed))\n - **DOCS**: Update the dartdocs for `FixedResolutionViewport` ([#3132](https://github.com/flame-engine/flame/issues/3132)). ([db4b6fd6](https://github.com/flame-engine/flame/commit/db4b6fd6fa5968462d3f89238a92edbb93e4898d))\n - **BREAKING** **FIX**: Update IsometricTileMapComponent to have better defined position and size ([#3142](https://github.com/flame-engine/flame/issues/3142)). ([9a7bdc74](https://github.com/flame-engine/flame/commit/9a7bdc7439322a26a388e3ac1b9c1a7c43742222))\n\n## 1.17.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Change the ClipComponent factory Constructor to redirect Constructor ([#3089](https://github.com/flame-engine/flame/issues/3089)). ([cc035fb4](https://github.com/flame-engine/flame/commit/cc035fb4a3e123473d4e5e0db1fa0253e533bc61))\n - **FIX**: Call `render` properly from nested `FlameGame`s ([#3106](https://github.com/flame-engine/flame/issues/3106)). ([cb1e3701](https://github.com/flame-engine/flame/commit/cb1e37014bac7bb68b647234b718a37e26ad7559))\n - **FIX**: CircleHitbox should properly detect when ray is outside ([#3100](https://github.com/flame-engine/flame/issues/3100)). ([8cd9e123](https://github.com/flame-engine/flame/commit/8cd9e12319c0715def655fcf42f3976fa5f45e11))\n - **FIX**: Clamp opacity set by the `ColorEffect` to 1.0 ([#3069](https://github.com/flame-engine/flame/issues/3069)). ([9282cc38](https://github.com/flame-engine/flame/commit/9282cc38f06cd6c87ed3a1880d28d5c9f290cc04))\n - **FIX**: FutureOr return type of ComponentViewportMargin.onLoad ([#3059](https://github.com/flame-engine/flame/issues/3059)). ([72678c67](https://github.com/flame-engine/flame/commit/72678c676020480beae1d944ee687fd73eac9cf7))\n - **FIX**: Size for `SpriteComponent.fromImage` should be nullable ([#3054](https://github.com/flame-engine/flame/issues/3054)). ([2ed71a3c](https://github.com/flame-engine/flame/commit/2ed71a3c89b3c2182828f2812d8515811483f4d5))\n - **FIX**: Check for removing state while adding a child ([#3050](https://github.com/flame-engine/flame/issues/3050)). ([3a24a51d](https://github.com/flame-engine/flame/commit/3a24a51d108b1138ac3dd735956f4276f16b2974))\n - **FEAT**: Add onFinished callback to ScrollTextBoxComponent ([#3105](https://github.com/flame-engine/flame/issues/3105)). ([233cc94c](https://github.com/flame-engine/flame/commit/233cc94c557e0af2fcf7599943ddf75180abf801))\n - **FEAT**: Add `copyWith` method on the `TextBoxConfig` ([#3099](https://github.com/flame-engine/flame/issues/3099)). ([b946ba70](https://github.com/flame-engine/flame/commit/b946ba70cbfb5793a8d4d7c61d6ba029fbc303ab))\n - **FEAT**: Component tree for the devtools extension tab ([#3094](https://github.com/flame-engine/flame/issues/3094)). ([bf5d68e9](https://github.com/flame-engine/flame/commit/bf5d68e9b5147dd5e7c10d72bf9c2f705733d688))\n - **FEAT**: Add PositionComponent.toString ([#3095](https://github.com/flame-engine/flame/issues/3095)). ([b1f01986](https://github.com/flame-engine/flame/commit/b1f01986b440ac18bb35b0d76963b2c66f49ea33))\n - **FEAT**: Add SpriteBatch.replace to allow the replacement of the batch information ([#3079](https://github.com/flame-engine/flame/issues/3079)). ([bf3c282d](https://github.com/flame-engine/flame/commit/bf3c282dd669e9a32a550b86770dba7fb8472afa))\n - **FEAT**: Initial functionality of flame_devtools ([#3061](https://github.com/flame-engine/flame/issues/3061)). ([c92910c6](https://github.com/flame-engine/flame/commit/c92910c688f5dc4463e129132759102e7ebf2e36))\n - **FEAT**: Add `HasPerformanceTracker` mixin on `Game` ([#3043](https://github.com/flame-engine/flame/issues/3043)). ([6270353a](https://github.com/flame-engine/flame/commit/6270353af9a6ec58ee9275ddfa6a8b26276a2c20))\n - **BREAKING** **REFACTOR**: Use HasTimeScale for Route ([#3064](https://github.com/flame-engine/flame/issues/3064)). ([30fde805](https://github.com/flame-engine/flame/commit/30fde805b4650cc802f9908f9a1149dae19669d4))\n - **BREAKING** **FIX**: Removed unused parameters from SpriteWidget ([#3074](https://github.com/flame-engine/flame/issues/3074)). ([f49d24c0](https://github.com/flame-engine/flame/commit/f49d24c02dd0d9b781926908bad1fb6dfcbda5f2))\n\n## 1.16.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Fix unrelated types reported by DCM ([#3023](https://github.com/flame-engine/flame/issues/3023)). ([1d020a52](https://github.com/flame-engine/flame/commit/1d020a525b81df1cb45345d3e36a9c4e9caf701e))\n - **FIX**: Vertices in `PolygonComponent` should subtract vertices positioning ([#3040](https://github.com/flame-engine/flame/issues/3040)). ([4f053ed7](https://github.com/flame-engine/flame/commit/4f053ed74c09d4e19a53694130b5d5c0d3e23aa6))\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 1.15.0\n\n - **REFACTOR**: Minimize `Vector2` creation in `IsometricTileMapComponent` ([#3018](https://github.com/flame-engine/flame/issues/3018)). ([5d3be313](https://github.com/flame-engine/flame/commit/5d3be3137a177c8900158fce10cffc01f729ed7a))\n - **FIX**: Set margins of `JoystickComponent` properly ([#3019](https://github.com/flame-engine/flame/issues/3019)). ([e27818d8](https://github.com/flame-engine/flame/commit/e27818d8721c577507411fca085859335206391f))\n - **FIX**: Properly update sprites in SpriteButtonComponent ([#3013](https://github.com/flame-engine/flame/issues/3013)). ([23cf8b9d](https://github.com/flame-engine/flame/commit/23cf8b9de81cade9ce90b8401c39432bc70f9d0d))\n - **FIX**: Lifecycle completers to be called for FlameGame ([#3007](https://github.com/flame-engine/flame/issues/3007)). ([3804f524](https://github.com/flame-engine/flame/commit/3804f52434cf1bcaf28b501bf96858ecd3636164))\n - **FIX**: CameraComponent no longer throws Concurrent modification on stop ([#2997](https://github.com/flame-engine/flame/issues/2997)). ([6a1059b0](https://github.com/flame-engine/flame/commit/6a1059b0a6e381020cdaa7a96ceecbcaa45b9a42))\n - **FIX**: Updated PolygonComponent.containsPoint to account for concave polygons ([#2979](https://github.com/flame-engine/flame/issues/2979)). ([a6fe62a2](https://github.com/flame-engine/flame/commit/a6fe62a2c3b74d9b4781531e0c53470b6d3242ea))\n - **FIX**: Add missing generic to `ComponentViewportMargin` ([#2983](https://github.com/flame-engine/flame/issues/2983)). ([1d9fe613](https://github.com/flame-engine/flame/commit/1d9fe6139287b984a05e0056c279b9c5d277e026))\n - **FEAT**: Add support for base64 encoded images to be manually added to Images cache. ([#3008](https://github.com/flame-engine/flame/issues/3008)). ([1e56293c](https://github.com/flame-engine/flame/commit/1e56293c1f89e7636c97b0ed518642bd493d7a40))\n - **FEAT**: Make `Component.key` public ([#2988](https://github.com/flame-engine/flame/issues/2988)). ([7fbd5af9](https://github.com/flame-engine/flame/commit/7fbd5af935211264822f89bc1beb4062d3efdf7a))\n - **FEAT**: Add a hitboxFilter argument to raycast() ([#2968](https://github.com/flame-engine/flame/issues/2968)). ([d7c53e23](https://github.com/flame-engine/flame/commit/d7c53e230f32b4b224e23483d99b0b276d14686f))\n\n## 1.14.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Set hitbox `debugColor` to yellow ([#2958](https://github.com/flame-engine/flame/issues/2958)). ([6858eae0](https://github.com/flame-engine/flame/commit/6858eae0766225bb7c940c2aa453459063f7d514))\n - **FIX**: Consider displaced hitboxes in GestureHitboxes mixin ([#2957](https://github.com/flame-engine/flame/issues/2957)). ([1085518f](https://github.com/flame-engine/flame/commit/1085518fe279e674bef9a7b938d59926472511f3))\n - **FIX**: PolygonComponent.containsLocalPoint to use anchor ([#2953](https://github.com/flame-engine/flame/issues/2953)). ([7969321e](https://github.com/flame-engine/flame/commit/7969321e8662515aa9efe305831ff36d51dd43cb))\n - **FEAT**: Notifier for changing current sprite/animation in group components ([#2956](https://github.com/flame-engine/flame/issues/2956)). ([75cf2390](https://github.com/flame-engine/flame/commit/75cf23908e5d509a25cd794d6810162f22b978cb))\n - **BREAKING** **REFACTOR**: Remove the Projector interface that is no longer used for coordinate transformations ([#2955](https://github.com/flame-engine/flame/issues/2955)). ([0979dc97](https://github.com/flame-engine/flame/commit/0979dc97f54af1b71b200ced609d874d390c1ca6))\n\n## 1.13.1\n\n - **FIX**: The `visibleGameSize` should be based on `viewport.virtualSize` ([#2945](https://github.com/flame-engine/flame/issues/2945)). ([bd130b71](https://github.com/flame-engine/flame/commit/bd130b711b5cb486b8f05225711c6e6c3fe574e6))\n - **FEAT**: Adding ability for a SpawnComponent to not auto start ([#2947](https://github.com/flame-engine/flame/issues/2947)). ([37c7a075](https://github.com/flame-engine/flame/commit/37c7a075a37cfc7c298f02542715b18e87f4cf99))\n\n## 1.13.0\n\n - **FIX**: Logic error in MemoryCache.setValue() ([#2931](https://github.com/flame-engine/flame/issues/2931)). ([8cee80c3](https://github.com/flame-engine/flame/commit/8cee80c35ca676ad25a25c771f0aade88b58150b))\n - **FIX**: Export `ScalingParticle` ([#2928](https://github.com/flame-engine/flame/issues/2928)). ([3730cb1d](https://github.com/flame-engine/flame/commit/3730cb1d834c73c87dc3597554039fd0f0a32bae))\n - **FIX**: Misalignment of the hittest area of PolygonHitbox ([#2930](https://github.com/flame-engine/flame/issues/2930)). ([dbdb1379](https://github.com/flame-engine/flame/commit/dbdb1379d0bc1b6ac02b3ee27f62263bd1be3fc3))\n - **FIX**: Allow setting `bounds` while `BoundedPositionBehavior`'s target is null ([#2926](https://github.com/flame-engine/flame/issues/2926)). ([bab9be6e](https://github.com/flame-engine/flame/commit/bab9be6e7051b7be6c84fc9760c7347692dbf140))\n - **FEAT**: Ability to use `selfPositioning` in `SpawnComponent` ([#2927](https://github.com/flame-engine/flame/issues/2927)). ([b526aa14](https://github.com/flame-engine/flame/commit/b526aa1488c0f891edb356007ebc2c5c2de596b5))\n - **FEAT**: Add `margin` and `spacing` properties to `SpriteSheet` ([#2925](https://github.com/flame-engine/flame/issues/2925)). ([67f7c126](https://github.com/flame-engine/flame/commit/67f7c126b4c8052df99ffa8c657a90cc7fb6f867))\n - **FEAT**: Add `children` to `SpriteAnimationComponent.fromFrameData` ([#2914](https://github.com/flame-engine/flame/issues/2914)). ([caf2b909](https://github.com/flame-engine/flame/commit/caf2b90930ca500c85b9f9f63e7d3d7a5d82c18e))\n - **DOCS**: Remove references to Tappable and Draggable ([#2912](https://github.com/flame-engine/flame/issues/2912)). ([d12e4544](https://github.com/flame-engine/flame/commit/d12e45444e49bbe0b24a7acbd24f0cda20a13755))\n\n## 1.12.0\n\n - **FIX**: SpriteAnimationWidget was resetting the ticker even when the playing didn't changed ([#2891](https://github.com/flame-engine/flame/issues/2891)). ([9aed8b4d](https://github.com/flame-engine/flame/commit/9aed8b4dea3074c9ca708ad991cdc90b12707fbe))\n - **FEAT**: Scrollable TextBoxComponent ([#2901](https://github.com/flame-engine/flame/issues/2901)). ([8c3cb725](https://github.com/flame-engine/flame/commit/8c3cb725413c46089854713f6ecc4617e1f15600))\n - **FEAT**: Add collision completed listener ([#2896](https://github.com/flame-engine/flame/issues/2896)). ([957db3c1](https://github.com/flame-engine/flame/commit/957db3c1ed476b22f7cc62d4df22595449f8756c))\n - **FEAT**: Adding autoResetTicker option to SpriteAnimationGroupComponent ([#2899](https://github.com/flame-engine/flame/issues/2899)). ([001c870d](https://github.com/flame-engine/flame/commit/001c870d61be6e7e7aae798df0dc33e5321ed882))\n - **FEAT**: Add clearSnapshot function ([#2897](https://github.com/flame-engine/flame/issues/2897)). ([d4decd21](https://github.com/flame-engine/flame/commit/d4decd21eb7506ffd6d84ab5a3ebf1f2067083b6))\n\n## 1.11.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n - **FIX**: Properly resize ScreenHitbox when needed ([#2826](https://github.com/flame-engine/flame/issues/2826)). ([24fed757](https://github.com/flame-engine/flame/commit/24fed757ac313453639ddf122ba84b1012a4b606))\n - **FIX**: Setting world on FlameGame camera setter ([#2831](https://github.com/flame-engine/flame/issues/2831)). ([3a8e2464](https://github.com/flame-engine/flame/commit/3a8e2464420f2b513f4f0d99cd7d64ab0eda9826))\n - **FIX**: Allow null passthrough parent ([#2821](https://github.com/flame-engine/flame/issues/2821)). ([c4d2f86e](https://github.com/flame-engine/flame/commit/c4d2f86e1214e9895ff858c511fa3c686313f204))\n - **FIX**: Do not scale debug texts with zoom ([#2818](https://github.com/flame-engine/flame/issues/2818)). ([c2f3f040](https://github.com/flame-engine/flame/commit/c2f3f040c6128d8fd3340d8f7622a2d4c2f22819))\n - **FIX**(flame): Export `FixedResolutionViewport` and make `withFixedResolution` a redirect constructor ([#2817](https://github.com/flame-engine/flame/issues/2817)). ([3420d0e6](https://github.com/flame-engine/flame/commit/3420d0e6f8af6f2dd8695ea61231aa93944c602b))\n - **FEAT**: Using viewport scale on debug mode text paint ([#2883](https://github.com/flame-engine/flame/issues/2883)). ([07ef46ca](https://github.com/flame-engine/flame/commit/07ef46cab01ae08749e678211245896572bb1081))\n - **FEAT**: Make Viewfinder and Viewport comply with CoordinateTransform interface ([#2872](https://github.com/flame-engine/flame/issues/2872)). ([685e1d95](https://github.com/flame-engine/flame/commit/685e1d9529df90f203e7827950ed5d9261b2ce42))\n - **FEAT**: Allow sequence effect to be extended ([#2864](https://github.com/flame-engine/flame/issues/2864)). ([ee11aae9](https://github.com/flame-engine/flame/commit/ee11aae9f519fdb967eb384aaffdb5a6f87a808f))\n - **FEAT**: Adding children argument to all constructors in the shape components ([#2862](https://github.com/flame-engine/flame/issues/2862)). ([082743d3](https://github.com/flame-engine/flame/commit/082743d3ba0860a87a58377a7b5a9cd6b5ae7c70))\n - **FEAT**: Optimization in sprite batch ([#2861](https://github.com/flame-engine/flame/issues/2861)). ([208d7897](https://github.com/flame-engine/flame/commit/208d7897f1e9e512f0bc235233e41e1953a8d546))\n - **FEAT**: Add TimeTrackComponent and ChildCounterComponent ([#2846](https://github.com/flame-engine/flame/issues/2846)). ([6269551a](https://github.com/flame-engine/flame/commit/6269551a77cfbc27094e262c131dec09e489e583))\n - **FEAT**: MoveAlongPathEffect should maintain initial angle of the component ([#2835](https://github.com/flame-engine/flame/issues/2835)). ([e6e78c0d](https://github.com/flame-engine/flame/commit/e6e78c0d66bc958dbe1c2295a7cc946dc5852455))\n - **FEAT**: Add a method to adapt the camera bounds to the world ([#2769](https://github.com/flame-engine/flame/issues/2769)). ([87b69df6](https://github.com/flame-engine/flame/commit/87b69df6a1d29261a514a7ee7d28d2d1f730920e))\n - **FEAT**: Scaling particle feature ([#2830](https://github.com/flame-engine/flame/issues/2830)). ([9faae8a2](https://github.com/flame-engine/flame/commit/9faae8a2371efdcbdf03cad70bded05470d4719a))\n - **BREAKING** **REFACTOR**: Replace `Offset` with `opacityFrom` and `opacityTo` in ColorEffect ([#2876](https://github.com/flame-engine/flame/issues/2876)). ([0fd2662d](https://github.com/flame-engine/flame/commit/0fd2662d4b1187285ee168271a38e1576b6e444a))\n - **BREAKING** **FIX**: Add DisplacementEvent to fix delta coordinate transformations for drag events ([#2871](https://github.com/flame-engine/flame/issues/2871)). ([63994ebc](https://github.com/flame-engine/flame/commit/63994ebcd8e850f68622f4a89ea17224574a8214))\n\n### Migration instructions\n\nTo specify start and end opacities for ColorEffect use the optional named\nparameters opacityFrom and opacityTo. So offset.dx should be set as opacityFrom\nand offset.dy should be set as opacityTo.\n\n - If you are using DragUpdateEvent events, the devicePosition, canvasPosition,\n   localPosition, and delta are deprecated as they are unclear.\n - Use xStartPosition to get the position at the start of the drag event (\"from\").\n - Use xEndPosition to get the position at the end of the drag event (\"to\").\n - If you want the delta, use localDelta. it now already considers the camera\n   zoom. No need to manually account for that.\n - Now you keep receiving drag events for the same component even if the\n   drag event leaves the component (breaking).\n\n## 1.10.1\n\n - **FIX**: Properly resize ScreenHitbox when needed ([#2826](https://github.com/flame-engine/flame/issues/2826)). ([24fed757](https://github.com/flame-engine/flame/commit/24fed757ac313453639ddf122ba84b1012a4b606))\n - **FIX**: Setting world on FlameGame camera setter ([#2831](https://github.com/flame-engine/flame/issues/2831)). ([3a8e2464](https://github.com/flame-engine/flame/commit/3a8e2464420f2b513f4f0d99cd7d64ab0eda9826))\n - **FIX**: Allow null passthrough parent ([#2821](https://github.com/flame-engine/flame/issues/2821)). ([c4d2f86e](https://github.com/flame-engine/flame/commit/c4d2f86e1214e9895ff858c511fa3c686313f204))\n - **FIX**: Do not scale debug texts with zoom ([#2818](https://github.com/flame-engine/flame/issues/2818)). ([c2f3f040](https://github.com/flame-engine/flame/commit/c2f3f040c6128d8fd3340d8f7622a2d4c2f22819))\n - **FIX**(flame): Export `FixedResolutionViewport` and make `withFixedResolution` a redirect constructor ([#2817](https://github.com/flame-engine/flame/issues/2817)). ([3420d0e6](https://github.com/flame-engine/flame/commit/3420d0e6f8af6f2dd8695ea61231aa93944c602b))\n - **FEAT**: Scaling particle feature ([#2830](https://github.com/flame-engine/flame/issues/2830)). ([9faae8a2](https://github.com/flame-engine/flame/commit/9faae8a2371efdcbdf03cad70bded05470d4719a))\n\n## 1.10.0\n\n - **REFACTOR**: Remove unnecessary 'async' keyword across the codebase [DCM] ([#2803](https://github.com/flame-engine/flame/issues/2803)). ([2dfe0e5a](https://github.com/flame-engine/flame/commit/2dfe0e5a431213c7148ab6389e3e8c8dc49fbf3d))\n - **REFACTOR**: Avoid nested conditional expressions whenever possible [DCM] ([#2784](https://github.com/flame-engine/flame/issues/2784)). ([7b6a5712](https://github.com/flame-engine/flame/commit/7b6a571263ece3aa1a8267644d9118230a850830))\n - **REFACTOR**: Mark semantically final variables as final (or const) proper [DCM] ([#2783](https://github.com/flame-engine/flame/issues/2783)). ([71f7b475](https://github.com/flame-engine/flame/commit/71f7b475e33dd6fa7224c4a3ab29ffdb89702c34))\n - **FIX**: Remove deprecations for 1.10.0 ([#2809](https://github.com/flame-engine/flame/issues/2809)). ([5b67b8f1](https://github.com/flame-engine/flame/commit/5b67b8f14ad4fdb38a249d0a41ecba49ba2fcc44))\n - **FIX**: Un-register component keys down the component tree ([#2792](https://github.com/flame-engine/flame/issues/2792)). ([0f679b3f](https://github.com/flame-engine/flame/commit/0f679b3f3d4a643ff4c29569388941c459e35021))\n - **FIX**: AlignComponent set child (remove compare) ([#2774](https://github.com/flame-engine/flame/issues/2774)). ([20aaf656](https://github.com/flame-engine/flame/commit/20aaf656617cef6538b49c067a562f9daf0a5972))\n - **FIX**: Hardcode initCurrentGame lifecycle state as resumed ([#2775](https://github.com/flame-engine/flame/issues/2775)). ([0cd5037c](https://github.com/flame-engine/flame/commit/0cd5037c6a837706891d5f1b85a91715cf85ebb1))\n - **FIX**: Fix TextBoxComponent alignment bug ([#2781](https://github.com/flame-engine/flame/issues/2781)). ([0fb53efb](https://github.com/flame-engine/flame/commit/0fb53efb661ae2a3b4a39407655efb69e92dced0))\n - **FIX**(flame): The `component.removeFromParent` method should use `parent.remove` internally ([#2779](https://github.com/flame-engine/flame/issues/2779)). ([bdb1c79a](https://github.com/flame-engine/flame/commit/bdb1c79a0524801ab425982dae206915c691e4b2))\n - **FIX**: Take unmounted adds into consideration ([#2770](https://github.com/flame-engine/flame/issues/2770)). ([be28a440](https://github.com/flame-engine/flame/commit/be28a4405f4024a3066b764d6dbad0713665665d))\n - **FEAT**: Add `IgnoreEvents` mixin to ignore events for the whole subtree ([#2811](https://github.com/flame-engine/flame/issues/2811)). ([313411c3](https://github.com/flame-engine/flame/commit/313411c311a6a3c2d36e12abf16bdd27ae801f29))\n - **FEAT**: Add advanced button component ([#2742](https://github.com/flame-engine/flame/issues/2742)). ([97fff0ed](https://github.com/flame-engine/flame/commit/97fff0ed2baab53f2780eca29a9d08ea5d90426a))\n - **FEAT**: Introduce the `FixedResolutionViewport` ([#2796](https://github.com/flame-engine/flame/issues/2796)). ([4c762f94](https://github.com/flame-engine/flame/commit/4c762f94d40d200bb2b8a102867b0859a345dbf0))\n - **FEAT**: AssetsBundle can be customized in Images and AssetsCache. ([#2807](https://github.com/flame-engine/flame/issues/2807)). ([a23f80e9](https://github.com/flame-engine/flame/commit/a23f80e94a5d935fc8ba232956fe02e001d5a8f9))\n - **FEAT**: Backdrop (static backgrounds) component for CameraComponent ([#2787](https://github.com/flame-engine/flame/issues/2787)). ([ab329f71](https://github.com/flame-engine/flame/commit/ab329f718a581b8331749fed6f942b6ade0da5ac))\n - **FEAT**: Align component refactoring ([#2767](https://github.com/flame-engine/flame/issues/2767)). ([bde34efe](https://github.com/flame-engine/flame/commit/bde34efef7264c91f49b237b589c74ba80a1554e))\n - **DOCS**: Remove last broad cSpell bypass regex and fix all violations ([#2802](https://github.com/flame-engine/flame/issues/2802)). ([9b16b178](https://github.com/flame-engine/flame/commit/9b16b178048eb19b6596273fcf4daec83277c3b5))\n\n## 1.9.1\n\n - **FIX**: Add necessary generics on mixins on FlameGame ([#2763](https://github.com/flame-engine/flame/issues/2763)). ([b1f5ff26](https://github.com/flame-engine/flame/commit/b1f5ff269441d55b09ce12d5ce99656f2d88a978))\n - **FIX**: Correctly refreshes the widget after new mouse detector ([#2765](https://github.com/flame-engine/flame/issues/2765)). ([64330022](https://github.com/flame-engine/flame/commit/643300222f8bf0545abdd1d8608202f388f8693f))\n - **FIX**: Allow moving to a new parent in the same tick ([#2762](https://github.com/flame-engine/flame/issues/2762)). ([313650ea](https://github.com/flame-engine/flame/commit/313650eafadca4427421ddd355fa5b373966b8d1))\n\n## 1.9.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **REFACTOR**: Fix lint issues across the codebase - Part 2 ([#2677](https://github.com/flame-engine/flame/issues/2677)). ([10e4109c](https://github.com/flame-engine/flame/commit/10e4109c81b266147ec35744e484c2ec7ea15acd))\n - **FIX**: Prevent `onRemove`/`onDetach` being called for initial Gesture Detector addition ([#2653](https://github.com/flame-engine/flame/issues/2653)). ([d1721464](https://github.com/flame-engine/flame/commit/d17214640548eba26f10ba0d55e70545d58cb1b9))\n - **FIX**: Use root game for gestures ([#2756](https://github.com/flame-engine/flame/issues/2756)). ([f5d0cb38](https://github.com/flame-engine/flame/commit/f5d0cb3856e5b397b11a1c4bb2dc9c49afa51de0))\n - **FIX**: Add possibility to remove a child and add it to the same parent ([#2755](https://github.com/flame-engine/flame/issues/2755)). ([285d31ab](https://github.com/flame-engine/flame/commit/285d31ab9894a8c24b995a68fc29329f142d0d09))\n - **FIX**: Adding scale parameter to RectangleComponent constructors ([#2730](https://github.com/flame-engine/flame/issues/2730)). ([173908d9](https://github.com/flame-engine/flame/commit/173908d9f26c5555ffa69d1557bf346c0ab5fbee))\n - **FIX**: Set `CameraComponent.priority` to max ([#2732](https://github.com/flame-engine/flame/issues/2732)). ([820ece1c](https://github.com/flame-engine/flame/commit/820ece1c9aba9d770326adcd2224c951ef54f6f7))\n - **FIX**: Change to `FilterQuality.medium` instead of `high` ([#2733](https://github.com/flame-engine/flame/issues/2733)). ([fc19890c](https://github.com/flame-engine/flame/commit/fc19890c87a78599ea49ee0dfb52a04ea6b09a99))\n - **FIX**: Avoid creating new `Vector2` in `globalToLocal` and `localToGlobal` ([#2727](https://github.com/flame-engine/flame/issues/2727)). ([9fb3bf8d](https://github.com/flame-engine/flame/commit/9fb3bf8dbd71bc981b00d3b4dabbe997d50030bb))\n - **FIX**: Ambiguation is not needed in render box anymore ([#2711](https://github.com/flame-engine/flame/issues/2711)). ([b3d78f58](https://github.com/flame-engine/flame/commit/b3d78f58831ec72f40f721f96f8d659111f25a88))\n - **FIX**: HasGameReference should default to FlameGame ([#2710](https://github.com/flame-engine/flame/issues/2710)). ([93dcb3a1](https://github.com/flame-engine/flame/commit/93dcb3a117c365767e3f20569b2d82abc8a7b152))\n - **FIX**: Make `debugCoordinatesPrecision` into a variable instead of a getter ([#2713](https://github.com/flame-engine/flame/issues/2713)). ([9918c051](https://github.com/flame-engine/flame/commit/9918c0515ae88c2f1bfb7423a2993c983dec16c2))\n - **FIX**: Absolute angle takes into account BodyComponent ancestors too ([#2678](https://github.com/flame-engine/flame/issues/2678)). ([75aee767](https://github.com/flame-engine/flame/commit/75aee767811ef440841956d9e467be157c4ab880))\n - **FEAT**: SpawnComponent ([#2709](https://github.com/flame-engine/flame/issues/2709)). ([83f5ea45](https://github.com/flame-engine/flame/commit/83f5ea45dcc024c3bfd3fe9002533daaf1a2be4e))\n - **FEAT**: Add globalToLocal and localToGlobal methods to viewport, viewfinder and camera ([#2720](https://github.com/flame-engine/flame/issues/2720)). ([00185a3b](https://github.com/flame-engine/flame/commit/00185a3b6b1e0e6b06e67dc724a26d4e9651e1a2))\n - **FEAT**: Add HoverCallbacks ([#2706](https://github.com/flame-engine/flame/issues/2706)). ([d460b846](https://github.com/flame-engine/flame/commit/d460b846c23fb1f67041469c99c81e4c78b89c2e))\n - **FEAT**: Add `onDispose` to `game.dart` called from `game_widget.dart` ([#2659](https://github.com/flame-engine/flame/issues/2659)). ([2f44e483](https://github.com/flame-engine/flame/commit/2f44e4832f0a9a8edf9c002783501610aa051370))\n - **FEAT**(flame): Add helper methods to create frame data on `SpriteSheet` ([#2754](https://github.com/flame-engine/flame/issues/2754)). ([47722199](https://github.com/flame-engine/flame/commit/477221998a272bf659cd86d2bf145adf0f277e65))\n - **FEAT**: Implement Snapshot mixin on PositionComponent ([#2695](https://github.com/flame-engine/flame/issues/2695)). ([c1ee24a2](https://github.com/flame-engine/flame/commit/c1ee24a2894eaffa1f6e206313cda4087a02f0a4))\n - **FEAT**: Add TextElementComponent ([#2694](https://github.com/flame-engine/flame/issues/2694)). ([10fb65f6](https://github.com/flame-engine/flame/commit/10fb65f66ca1f1dbac04a138ef4a28b1ed5e5a23))\n - **FEAT**: Component visibility (HasVisibility mixin) ([#2681](https://github.com/flame-engine/flame/issues/2681)). ([76405daf](https://github.com/flame-engine/flame/commit/76405daf48b2efd59241329d4d1fb4b451d254c0))\n - **FEAT**: Add `HasWorldReference` mixin ([#2746](https://github.com/flame-engine/flame/issues/2746)). ([9105411d](https://github.com/flame-engine/flame/commit/9105411d46e097d4b5bf84ee8921c146dcf5a6cd))\n - **FEAT**: Add `pause` and `isPaused` to SpriteAnimationTicker ([#2660](https://github.com/flame-engine/flame/issues/2660)). ([37271f5c](https://github.com/flame-engine/flame/commit/37271f5c52e75e6b086520a35361a03e0d784586))\n - **DOCS**: Improve documentation around SpriteFontTextFormatter ([#2661](https://github.com/flame-engine/flame/issues/2661)). ([8401c569](https://github.com/flame-engine/flame/commit/8401c569bfbc92a13ce5cee18cd817da06bd0bd8))\n - **DOCS**: Improved spellchecking ([#2722](https://github.com/flame-engine/flame/issues/2722)). ([2f973abe](https://github.com/flame-engine/flame/commit/2f973abe8b298a4f6f1164065783de560953d789))\n - **DOCS**: Enable CSpell on tests ([#2723](https://github.com/flame-engine/flame/issues/2723)). ([e051298c](https://github.com/flame-engine/flame/commit/e051298cba76550229780438b1a589557c7b488d))\n - **DOCS**: Improve comments and documentation for text-rendering Nodes ([#2662](https://github.com/flame-engine/flame/issues/2662)). ([96978e24](https://github.com/flame-engine/flame/commit/96978e2496ffe29dbbf19b8e7e70c2d63309b115))\n - **DOCS**: Fix examples for v1.9.0 ([#2757](https://github.com/flame-engine/flame/issues/2757)). ([152fbb61](https://github.com/flame-engine/flame/commit/152fbb61db1986632f60f3bf98c93aa2e4fbfc86))\n - **BREAKING** **REFACTOR**: Rename (Text) Elements, Nodes and Styles for clarity, add docs ([#2700](https://github.com/flame-engine/flame/issues/2700)). ([4b420b79](https://github.com/flame-engine/flame/commit/4b420b7952ab8d675140b9d8d132015ff2780f92))\n - **BREAKING** **REFACTOR**: Extract TextRendererFactory ([#2680](https://github.com/flame-engine/flame/issues/2680)). ([eeb6749f](https://github.com/flame-engine/flame/commit/eeb6749fd2baa825c7e8267a546ec8bf405a63ae))\n - **BREAKING** **REFACTOR**: Make TextElement more usable on its own ([#2679](https://github.com/flame-engine/flame/issues/2679)). ([1a64443c](https://github.com/flame-engine/flame/commit/1a64443ccaae32e71fe7d016ad1e8f18a75c93da))\n - **BREAKING** **REFACTOR**: Simplify text rendering pipeline ([#2663](https://github.com/flame-engine/flame/issues/2663)). ([34f69b95](https://github.com/flame-engine/flame/commit/34f69b953c137fbf0168aebec3860c6abc888594))\n - **BREAKING** **REFACTOR**: Kill TextRenderer, Long Live TextRenderer ([#2683](https://github.com/flame-engine/flame/issues/2683)). ([a1cb9a06](https://github.com/flame-engine/flame/commit/a1cb9a06ada6f87bf22bc20e3c190ccd53517389))\n - **BREAKING** **FIX**: Update should be called before render in first tick ([#2714](https://github.com/flame-engine/flame/issues/2714)). ([51932c09](https://github.com/flame-engine/flame/commit/51932c09c1e934ec30ffa04eda6c050440f85548))\n - **BREAKING** **FEAT**: Move `Forge2DGame` to use `CameraComponent` ([#2728](https://github.com/flame-engine/flame/issues/2728)). ([7a3d5126](https://github.com/flame-engine/flame/commit/7a3d5126a54d23cdebde20953772a53ba1a53204))\n - **BREAKING** **FEAT**: Pause game when backgrounded ([#2642](https://github.com/flame-engine/flame/issues/2642)). ([521e56b6](https://github.com/flame-engine/flame/commit/521e56b6d20c1c5b24a2818d73be58a6e6523f6b))\n - **BREAKING** **FEAT**: Add CameraComponent to FlameGame ([#2740](https://github.com/flame-engine/flame/issues/2740)). ([7c2f4000](https://github.com/flame-engine/flame/commit/7c2f4000761580dbabb5d73b27f64d5819b34e8d))\n\n### Migration instructions\n\nIn this release the old camera system was renamed to `oldCamera` and replaced by a default\n`World` and a `CameraComponent` which is now named `camera`.\n\nTo migrate from the old system:\n\n1. Instead of adding the components directly to your game with `add`,\n   add them to the world, this will make the `CameraComponent` responsible\n   for rendering them instead of the game directly.\n\n   ```dart\n   world.add(yourComponent);\n   ```\n\n2. (Optional) If you want to add a HUD component, instead of using\n   PositionType, add the component as a child of the viewport.\n\n   ```dart\n   cameraComponent.viewport.add(yourHudComponent);\n   ```\n\n3. (Optional) If you want to have a specificly set up `CameraComponent` in your\n   game from the start you can pass in a camera to the constructor.\n\n   ```dart\n   class MyGame extends FlameGame {\n     MyGame() \n         : super(\n             camera: CameraComponent.withFixedResolution(\n               width: 800,\n               height: 600,\n             ),\n           );\n\n   ```\n\n4. (Optional) A lot of the time it is no longer necessary to extend `FlameGame`,\n   you can instead build your game or level by extending the `World` and pass it\n   in to the `FlameGame`.\n\n   ```dart\n   runApp(\n     GameWidget(game: FlameGame(world: MyWorld()),\n   );\n\n   class MyWorld extends World with TapCallbacks {\n     @override\n     Future<void> onLoad() async {\n       // Load your components\n     }\n\n     @override\n     void onTapDown(TapDownEvent event) {\n       print(event.localPosition); // Position of the tap in the world\n     }\n   }\n   ```\n\n## 1.8.2\n\n> Note: This release has breaking changes.\n\n - **PERF**: Improve performance of raycasts ([#2617](https://github.com/flame-engine/flame/issues/2617)). ([8e0a7879](https://github.com/flame-engine/flame/commit/8e0a7879d7669e09efcbcee28d9f2038fe9014c0))\n - **FIX**: Reset _completeCompleter in ticker ([#2636](https://github.com/flame-engine/flame/issues/2636)). ([a35d3a10](https://github.com/flame-engine/flame/commit/a35d3a10abfe9e5caab1a646e0980d03fbf585d1))\n - **FIX**: Viewport should recieve events before the world  ([#2630](https://github.com/flame-engine/flame/issues/2630)). ([e852064e](https://github.com/flame-engine/flame/commit/e852064e494e58ea2be19a5b035e09ed2e465608))\n - **FIX**: Use `ComponentKey`s to keep track of dispatchers ([#2629](https://github.com/flame-engine/flame/issues/2629)). ([ff59aa15](https://github.com/flame-engine/flame/commit/ff59aa152c5a2e0b360f980c78a8b3cc4fad7507))\n - **FIX**: FlameGame onRemove fix to prevent memory leak ([#2602](https://github.com/flame-engine/flame/issues/2602)). ([dac2ebbf](https://github.com/flame-engine/flame/commit/dac2ebbf506ff48ca8f34d872bbc47cba3ad6c7b))\n - **FIX**: Only use pre-set ReadonlySizeProvider for sizing in HudMarginComponent ([#2611](https://github.com/flame-engine/flame/issues/2611)). ([832c0510](https://github.com/flame-engine/flame/commit/832c051085e0fade8a7e4b262bf9941d279baef4))\n - **FIX**: TextBoxConfig dismissDelay to not be ignored ([#2607](https://github.com/flame-engine/flame/issues/2607)). ([1567b389](https://github.com/flame-engine/flame/commit/1567b3891057e4ce168d76c920bd40403febd82a))\n - **FEAT**: Adding key argument to shape components ([#2632](https://github.com/flame-engine/flame/issues/2632)). ([c542d3c3](https://github.com/flame-engine/flame/commit/c542d3c34bf911cec8332dcdeb65d0017e6cb576))\n - **FEAT**: Add optional world input to `CameraComponent.canSee` ([#2616](https://github.com/flame-engine/flame/issues/2616)). ([1cad0b23](https://github.com/flame-engine/flame/commit/1cad0b23e18db8f352da5790c8ea5ec6053936da))\n - **FEAT**: Add a Circle.fromPoints utility method ([#2603](https://github.com/flame-engine/flame/issues/2603)). ([a83f2815](https://github.com/flame-engine/flame/commit/a83f2815bbdaf9c176a34a325485a96b5a323575))\n - **FEAT**: Add a midpoint getter to LineSegment ([#2605](https://github.com/flame-engine/flame/issues/2605)). ([1f9f3509](https://github.com/flame-engine/flame/commit/1f9f35093b3b90113e32a36e1103b87246212fa4))\n - **FEAT**: Add Rectangle.fromLTWH and Rect.toFlameRectangle utility methods ([#2604](https://github.com/flame-engine/flame/issues/2604)). ([76271cee](https://github.com/flame-engine/flame/commit/76271ceef04264ec8fa5c39a23f43d638d731694))\n - **DOCS**: Add more guidance to collision detection algorithm choices ([#2624](https://github.com/flame-engine/flame/issues/2624)). ([781e8983](https://github.com/flame-engine/flame/commit/781e898315a0162117a83bf62e2650ce7244503d))\n - **BREAKING** **PERF**: Pool `CollisionProspect`s and remove some list creations from the collision detection ([#2625](https://github.com/flame-engine/flame/issues/2625)). ([e430b6cd](https://github.com/flame-engine/flame/commit/e430b6cdf2e6be52bf384efb3428bcb41ae13d30))\n - **BREAKING** **FEAT**: Make world nullable in `CameraComponent` ([#2615](https://github.com/flame-engine/flame/issues/2615)). ([14f51635](https://github.com/flame-engine/flame/commit/14f51635421b8b30049ea287b7c472e54a269250))\n\n## 1.8.1\n\n> Note: This release has breaking changes.\n\n - **FIX**: Adds a check to confirm the component is not loaded ([#2579](https://github.com/flame-engine/flame/issues/2579)). ([985400f2](https://github.com/flame-engine/flame/commit/985400f2955f6bed14066660711d53c5b302ab09))\n - **FIX**: Animation ticker readability improvements ([#2578](https://github.com/flame-engine/flame/issues/2578)). ([667a1698](https://github.com/flame-engine/flame/commit/667a1698115ed69cc11b2e5a598371e136c7e7f0))\n - **FIX**: Remove `mustCallSuper` from `onComponentTypeCheck` ([#2561](https://github.com/flame-engine/flame/issues/2561)). ([bcae760c](https://github.com/flame-engine/flame/commit/bcae760c7138839fee203a1693e02fade753292c))\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Reduce the Vector2 creations in Anchor ([#2550](https://github.com/flame-engine/flame/issues/2550)). ([5a9434b0](https://github.com/flame-engine/flame/commit/5a9434b09a6fbe2c86db2d8192cd2d7ae0d5868c))\n - **FIX**: Fix disappearing text on TextBoxComponent for larger pixelRatios ([#2540](https://github.com/flame-engine/flame/issues/2540)). ([6e1d5466](https://github.com/flame-engine/flame/commit/6e1d5466aadc59f90475b1a9e7658bb78ed60340))\n - **FEAT**: Option to prevent propagating collision events from ShapeHitbox to _hitboxParent ([#2594](https://github.com/flame-engine/flame/issues/2594)). ([a58d7436](https://github.com/flame-engine/flame/commit/a58d7436c9b71a2358edc6c3732aeda56d980f64))\n - **FEAT**: Adding filterQuality arguments to Parallax load methods ([#2596](https://github.com/flame-engine/flame/issues/2596)). ([ff3d9107](https://github.com/flame-engine/flame/commit/ff3d91075c49df8efb6130f8e8ac9b711a1a8a14))\n - **FEAT**: Option to use toImageSync in ImageComposition class ([#2593](https://github.com/flame-engine/flame/issues/2593)). ([66d5f97d](https://github.com/flame-engine/flame/commit/66d5f97d303aa1712673b8ca7e1a889cf5e7270e))\n - **FEAT**: ComponentKey API ([#2566](https://github.com/flame-engine/flame/issues/2566)). ([b3efb612](https://github.com/flame-engine/flame/commit/b3efb612cb3cb77f69bc030e9ba71516348035d2))\n - **FEAT**(flame): Set a default negative priority on the world for general use ([#2572](https://github.com/flame-engine/flame/issues/2572)). ([390e9700](https://github.com/flame-engine/flame/commit/390e9700b4293e12b7d4212ce04f6b3d967a24e1))\n - **FEAT**: Add useful methods to Rectangle class ([#2562](https://github.com/flame-engine/flame/issues/2562)). ([4710530b](https://github.com/flame-engine/flame/commit/4710530b420469794602bf4d8cfea98078e0d973))\n - **BREAKING** **FIX**: Convert PositionEvent.canvasPosition to local coordinates ([#2598](https://github.com/flame-engine/flame/issues/2598)). ([87139c85](https://github.com/flame-engine/flame/commit/87139c854534782638fe1b0c24d2dc92f98a3e59))\n\n## 1.8.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Reduce the Vector2 creations in Anchor ([#2550](https://github.com/flame-engine/flame/issues/2550)). ([5a9434b0](https://github.com/flame-engine/flame/commit/5a9434b09a6fbe2c86db2d8192cd2d7ae0d5868c))\n - **FIX**: Fix disappearing text on TextBoxComponent for larger pixelRatios ([#2540](https://github.com/flame-engine/flame/issues/2540)). ([6e1d5466](https://github.com/flame-engine/flame/commit/6e1d5466aadc59f90475b1a9e7658bb78ed60340))\n - **FIX**: Avoid the creation of Vector2 objects in Parallax update ([#2536](https://github.com/flame-engine/flame/issues/2536)). ([3849f07d](https://github.com/flame-engine/flame/commit/3849f07d50870e4364caf9e115e869d8fed6aaed))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n - **FIX**: Move `errorBuilder` and `loadingBuilder` to constructors ([#2526](https://github.com/flame-engine/flame/issues/2526)). ([55ec0bc3](https://github.com/flame-engine/flame/commit/55ec0bc3cbebc0106dba2e0d4f3fd7693b9bc6d6))\n - **FEAT**: Add onComplete callback to `AnimationWidget` ([#2515](https://github.com/flame-engine/flame/issues/2515)). ([0b68be8a](https://github.com/flame-engine/flame/commit/0b68be8a6f306b0102b3be980dec661909d2c1e0))\n - **FEAT**: Add `stepEngine` to `Game` ([#2516](https://github.com/flame-engine/flame/issues/2516)). ([1ed2c5a2](https://github.com/flame-engine/flame/commit/1ed2c5a2974876a32f620d9dc9cb385e4e928c50))\n - **FEAT**: Customise grid of NineTileBox ([#2495](https://github.com/flame-engine/flame/issues/2495)). ([a25b0a03](https://github.com/flame-engine/flame/commit/a25b0a03a56975e1de2e15747bc3e527ac232545))\n - **FEAT**: Accept `CollisionType` in hitbox constructor ([#2509](https://github.com/flame-engine/flame/issues/2509)). ([89926227](https://github.com/flame-engine/flame/commit/89926227c5132455b971dece6ed313634d7ac873))\n - **DOCS**: Update content types of sphinx code snippets ([#2519](https://github.com/flame-engine/flame/issues/2519)). ([306ad320](https://github.com/flame-engine/flame/commit/306ad32052cfba9c6b3ab38ebb7d0604742d2993))\n - **BREAKING** **REFACTOR**: Move `CameraComponent` and events out of experimental ([#2505](https://github.com/flame-engine/flame/issues/2505)). ([87b8a067](https://github.com/flame-engine/flame/commit/87b8a067f3e0096cebff3db4f5767e68616928fd))\n - **BREAKING** **FEAT**: Add `SpriteAnimationTicker` ([#2457](https://github.com/flame-engine/flame/issues/2457)). ([a50c80cf](https://github.com/flame-engine/flame/commit/a50c80cfa34c08463ab29efe4a1f546fb47da34e))\n\n\n### Migration instructions\n\nIn the future (maybe as early as v1.9.0) this camera will be removed,\nplease use the CameraComponent instead.\n\nThis is the simplest way of using the CameraComponent:\n1. Add variables for a CameraComponent and a World to your game class\n\n   ```dart\n   final world = World();\n   late final CameraComponent cameraComponent;\n   ```\n\n2. In your `onLoad` method, initialize the cameraComponent and add the world\n   to it.\n\n   ```dart\n   @override\n   void onLoad() {\n     cameraComponent = CameraComponent(world: world);\n     addAll([cameraComponent, world]);\n   }\n   ```\n\n3. Instead of adding the root components directly to your game with `add`,\n   add them to the world.\n\n   ```dart\n   world.add(yourComponent);\n   ```\n\n4. (Optional) If you want to add a HUD component, instead of using\n   PositionType, add the component as a child of the viewport.\n\n   ```dart\n   cameraComponent.viewport.add(yourHudComponent);\n   ```\n\n\n## 1.7.3\n\n - **REFACTOR**: Make atlas status to be more readable ([#2502](https://github.com/flame-engine/flame/issues/2502)). ([643793d0](https://github.com/flame-engine/flame/commit/643793d06e1c9264ce8fd557552ad8405bc65ec1))\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n - **FIX**: Reverse invalid polygon definitions ([#2503](https://github.com/flame-engine/flame/issues/2503)). ([c4c516eb](https://github.com/flame-engine/flame/commit/c4c516ebf8fe6b8eaf82a3e49454b64faf6a7cd2))\n - **FIX**: Fill in mount implementation in `HasTappables` ([#2496](https://github.com/flame-engine/flame/issues/2496)). ([d51a612f](https://github.com/flame-engine/flame/commit/d51a612f8bed2a7a294444e5f11402394dfbc3cd))\n - **FIX**: Modify size only if changed while auto-resizing ([#2498](https://github.com/flame-engine/flame/issues/2498)). ([aa8d49da](https://github.com/flame-engine/flame/commit/aa8d49da9eb77c47d252ac3cc46d268eb10a2f20))\n - **FIX**: RecycleQueue cannot extends and implements Iterable at the same time ([#2497](https://github.com/flame-engine/flame/issues/2497)). ([3e5be3d6](https://github.com/flame-engine/flame/commit/3e5be3d6c23bfc61237befa5d17311474c6d4234))\n - **FIX**: Remove memory leak when creating the image from PictureRecorder ([#2493](https://github.com/flame-engine/flame/issues/2493)). ([a66f2bc0](https://github.com/flame-engine/flame/commit/a66f2bc0a97415f4f57b6c55174a2930cdf9e61b))\n - **FEAT**: Bump ordered_set version ([#2500](https://github.com/flame-engine/flame/issues/2500)). ([81303ea9](https://github.com/flame-engine/flame/commit/81303ea9d805c04c5d85c8e7c2f40ab8e43ae811))\n - **FEAT**: Deprecate `Component.changeParent` ([#2478](https://github.com/flame-engine/flame/issues/2478)). ([bd3e7886](https://github.com/flame-engine/flame/commit/bd3e7886125e60ad1386ec864a5ef33382f7f7f5))\n\n## 1.7.2\n\n - **FIX**: A mistake in auto-resizing disabling logic ([#2471](https://github.com/flame-engine/flame/issues/2471)). ([e7ebf8e5](https://github.com/flame-engine/flame/commit/e7ebf8e55a0ad7b0f3aaae769c0b8855fb1efd96))\n - **FIX**: It should be possible to re-add `ColorEffect` ([#2469](https://github.com/flame-engine/flame/issues/2469)). ([6fa9e9d5](https://github.com/flame-engine/flame/commit/6fa9e9d5470eaf36c2db5f3b040e708615dbfcf1))\n - **FEAT**: Add `isDragged` in `DragCallbacks` mixin ([#2472](https://github.com/flame-engine/flame/issues/2472)). ([de630a1c](https://github.com/flame-engine/flame/commit/de630a1c3a779cefe49a598b46e105f19aacebfb))\n\n## 1.7.1\n\n - **FIX**: Stop auto-resizing on external size change in sprite based components ([#2467](https://github.com/flame-engine/flame/issues/2467)). ([df236af4](https://github.com/flame-engine/flame/commit/df236af4f0164cc20b664ab973d91b4554b13b62))\n\n## 1.7.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Remove \"items\" variable from core Broadphase class. ([#2284](https://github.com/flame-engine/flame/issues/2284)). ([1819c575](https://github.com/flame-engine/flame/commit/1819c5759060579b8fbbf273befe622e799fef32))\n - **REFACTOR**: Added ComponentTreeRoot ([#2300](https://github.com/flame-engine/flame/issues/2300)). ([619b9b15](https://github.com/flame-engine/flame/commit/619b9b15da5c8992547b38bc88a1378933c20026))\n - **REFACTOR**: Simplify how images.dart decodes images ([#2293](https://github.com/flame-engine/flame/issues/2293)). ([b4925423](https://github.com/flame-engine/flame/commit/b4925423d78f4b152b4808e1aceadf211cc7d2e8))\n - **REFACTOR**: Use variable name on toString in component_test.dart ([#2377](https://github.com/flame-engine/flame/issues/2377)). ([f5c0e5e9](https://github.com/flame-engine/flame/commit/f5c0e5e9d0d20e2c89a57f41f968aeafb3a5a753))\n - **REFACTOR**: Remove unused variable \"tapTimes\" from multi_touch_tap_detector_test.dart ([#2379](https://github.com/flame-engine/flame/issues/2379)). ([cd2b2a10](https://github.com/flame-engine/flame/commit/cd2b2a109707e32a82d9f96b84218d30c03554ab))\n - **REFACTOR**: Component rebalancing is now performed via a global queue ([#2352](https://github.com/flame-engine/flame/issues/2352)). ([1ef51879](https://github.com/flame-engine/flame/commit/1ef518794c8b02995afb1fd0b431a804ef122a4c))\n - **REFACTOR**: Component adoption now handled via ComponentTreeRoot ([#2332](https://github.com/flame-engine/flame/issues/2332)). ([5ceb5dda](https://github.com/flame-engine/flame/commit/5ceb5dda5c6fc27bbad96445f0e99e5e006e5ed3))\n - **FIX**: Auto-resize `SpriteComponent` on sprite change ([#2430](https://github.com/flame-engine/flame/issues/2430)). ([158460d7](https://github.com/flame-engine/flame/commit/158460d7c66c49ffc6ffc99d43a9f547d6ab4e01))\n - **FIX**: Update MoveAlongPathEffect ([#2422](https://github.com/flame-engine/flame/issues/2422)). ([295cd724](https://github.com/flame-engine/flame/commit/295cd72422ee068635057fe9e6684edd5021a9e4))\n - **FIX**: Removed component to be deleted from _broadphaseCheckCache ([#2282](https://github.com/flame-engine/flame/issues/2282)). ([236a74ce](https://github.com/flame-engine/flame/commit/236a74cef160310c1b2d894835fe34157f18178e))\n - **FIX**: TextBoxComponent rendering for new line ([#2413](https://github.com/flame-engine/flame/issues/2413)). ([9008998e](https://github.com/flame-engine/flame/commit/9008998eefe052b145b1d52ef149b99cbf4ddaaa))\n - **FIX**: Buttons in ButtonComponents should not be final ([#2410](https://github.com/flame-engine/flame/issues/2410)). ([55f66add](https://github.com/flame-engine/flame/commit/55f66add6389db212559750d26570d4eeeb54f34))\n - **FIX**: Set size of viewports in `onLoad` ([#2452](https://github.com/flame-engine/flame/issues/2452)). ([d1ac01f5](https://github.com/flame-engine/flame/commit/d1ac01f5754a7ceaf8308ef0561f0bd108e04ba2))\n - **FIX**: Incorrect JoystickComponent position in landscape mode [#2387](https://github.com/flame-engine/flame/issues/2387) ([#2389](https://github.com/flame-engine/flame/issues/2389)). ([f125593a](https://github.com/flame-engine/flame/commit/f125593aaaaef160395b772180b1514f6be3ac4f))\n - **FIX**: RouterComponent replace methods to correctly handle previous/nextRoute ([#2296](https://github.com/flame-engine/flame/issues/2296)). ([2b1f2266](https://github.com/flame-engine/flame/commit/2b1f226618740542127d53a6fafe8bdba3b80593))\n - **FIX**: Use the hitboxParent instead of the parent in the componentTypeCheck ([#2335](https://github.com/flame-engine/flame/issues/2335)). ([7920e2ba](https://github.com/flame-engine/flame/commit/7920e2ba4d52a2461ae4631ffdaf8c52fbcd9dd3))\n - **FIX**: Materialize list in `Component.removeWhere` ([#2458](https://github.com/flame-engine/flame/issues/2458)). ([13cce4ae](https://github.com/flame-engine/flame/commit/13cce4aed61dfd1edd09dee902b402c2b04718cb))\n - **FIX**: TextBoxComponent's boxConfig timePerChar generates \"Optimized Out\" error [#2143](https://github.com/flame-engine/flame/issues/2143) ([#2328](https://github.com/flame-engine/flame/issues/2328)). ([5874f600](https://github.com/flame-engine/flame/commit/5874f6007ae0c71269bc72da0e420eb7bf8e2173))\n - **FIX**: Camera no longer \"sticks\" to boundary with BoundedPositionBehavior ([#2307](https://github.com/flame-engine/flame/issues/2307)). ([914dc6a7](https://github.com/flame-engine/flame/commit/914dc6a7cdd3131023f4b2f52cc18450664bd0f3))\n - **FEAT**: Add reusable vector to the Vector2 extension ([#2429](https://github.com/flame-engine/flame/issues/2429)). ([03d45df5](https://github.com/flame-engine/flame/commit/03d45df5d665c3cff353bdde66ac6fc7bed4e1fe))\n - **FEAT**: Change `HasCollisionDetection` to be on `Component` ([#2404](https://github.com/flame-engine/flame/issues/2404)). ([637c258b](https://github.com/flame-engine/flame/commit/637c258b252892fe5bd1dcc3692d49d1072b0f1d))\n - **FEAT**: Added AlignComponent layout component ([#2350](https://github.com/flame-engine/flame/issues/2350)). ([4f5e56f0](https://github.com/flame-engine/flame/commit/4f5e56f05fdcd6b9ad04077093a9eeadf503b9b3))\n - **FEAT**: Add `autoResize` for `SpriteAnimationComponent` and `SpriteAnimationGroupComponent` ([#2453](https://github.com/flame-engine/flame/issues/2453)). ([dbeba238](https://github.com/flame-engine/flame/commit/dbeba23846b229af95057fe0e260fd9e2394c261))\n - **FEAT**: Adding ImageExtension.resize ([#2418](https://github.com/flame-engine/flame/issues/2418)). ([a3f1601d](https://github.com/flame-engine/flame/commit/a3f1601db863b5b1a0eebd08311467836a7b789c))\n - **FEAT**: Add position and anchor params for Sprite and SpriteAnimation Particles ([#2370](https://github.com/flame-engine/flame/issues/2370)). ([181e0b59](https://github.com/flame-engine/flame/commit/181e0b59fd83a765392a1f1170bfa1e840629029))\n - **FEAT**: Add `autoResize` for `SpriteGroupComponent` ([#2442](https://github.com/flame-engine/flame/issues/2442)). ([1576bd83](https://github.com/flame-engine/flame/commit/1576bd83a5abfebe206d4e4f93381f216f895208))\n - **FEAT**: Introduce flame_noise, deprecate NoiseEffectController ([#2393](https://github.com/flame-engine/flame/issues/2393)). ([b2fdf06a](https://github.com/flame-engine/flame/commit/b2fdf06a79520c2b556c1c83de0b0f24df80cfd2))\n - **FEAT**: Added HardwareKeyboardDetector ([#2257](https://github.com/flame-engine/flame/issues/2257)). ([95b1fc0f](https://github.com/flame-engine/flame/commit/95b1fc0fbc1c40962350bc27a15849c32bba5326))\n - **FEAT**: Allow people to opt-out on repaint boundary ([#2341](https://github.com/flame-engine/flame/issues/2341)). ([b6aeec24](https://github.com/flame-engine/flame/commit/b6aeec24d7745626359e05ad2f0ac9acc8d09fbf))\n - **FEAT**: Add `HasTimeScale` mixin ([#2431](https://github.com/flame-engine/flame/issues/2431)). ([d2a8fe01](https://github.com/flame-engine/flame/commit/d2a8fe01fae54ffd1c2e4584dfa7fdcfbcf4068d))\n - **FEAT**: Add DoubleTapCallbacks that receives double-tap events. ([#2327](https://github.com/flame-engine/flame/issues/2327)). ([b5f79d1c](https://github.com/flame-engine/flame/commit/b5f79d1ce45276d957d0512353ca9cc890b6fef1))\n - **FEAT**: Add ability to opt-out flip ([#2316](https://github.com/flame-engine/flame/issues/2316)). ([34c3b6bd](https://github.com/flame-engine/flame/commit/34c3b6bdc4c570f4e8641b11b94efe19bdd1ef32))\n - **FEAT**: Make `limit` field mutable in the `Timer` class ([#2358](https://github.com/flame-engine/flame/issues/2358)). ([4e0a8c46](https://github.com/flame-engine/flame/commit/4e0a8c468886d57b718f853e78a25a03f3b335ae))\n - **DOCS**: Rename caveace asset to cave_ace in our examples ([#2304](https://github.com/flame-engine/flame/issues/2304)). ([e2399f91](https://github.com/flame-engine/flame/commit/e2399f91e3ce39da8db9ae2b9622c8a6050b94b9))\n - **DOCS**: Update cspell github action and configuration ([#2325](https://github.com/flame-engine/flame/issues/2325)). ([e0a4c07f](https://github.com/flame-engine/flame/commit/e0a4c07f2ad6e19830bfdd3af4eb9b148771698a))\n - **DOCS**: Fix actual typos that made into our dictionary ([#2305](https://github.com/flame-engine/flame/issues/2305)). ([343b8452](https://github.com/flame-engine/flame/commit/343b84529d8f06c0d020b97a40c082b71f0de770))\n - **DOCS**: Add Flame logo for pub.dev ([#2338](https://github.com/flame-engine/flame/issues/2338)). ([65091f34](https://github.com/flame-engine/flame/commit/65091f34bf1fbaaf5a30eab6c59486bc0bf55812))\n - **DOCS**: Refactor documentation for GameWidget ([#2344](https://github.com/flame-engine/flame/issues/2344)). ([655824fc](https://github.com/flame-engine/flame/commit/655824fc00460ec16efc861046c7290ffc14c5c4))\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix old doc code ([#2322](https://github.com/flame-engine/flame/issues/2322)). ([90321658](https://github.com/flame-engine/flame/commit/90321658c48a9279d4c82d48c6433a818270d03e))\n - **BREAKING** **REFACTOR**: Use ComponentTreeRoot for component removal ([#2317](https://github.com/flame-engine/flame/issues/2317)). ([75446185](https://github.com/flame-engine/flame/commit/754461850f5827e0cb1a4193f72492e6e78fbfa9))\n - **BREAKING** **FEAT**: HasDraggableComponents mixin is no longer needed ([#2312](https://github.com/flame-engine/flame/issues/2312)). ([3faf1149](https://github.com/flame-engine/flame/commit/3faf114994f4c6405a5d1a89559f0976b4e8c911))\n - **BREAKING** **FEAT**: The `HasTappableComponents` mixin is no longer needed ([#2450](https://github.com/flame-engine/flame/issues/2450)). ([b5bdf4ec](https://github.com/flame-engine/flame/commit/b5bdf4ec173e87907a59a9f62fcdf35cc968af2a))\n\n\n### Migration instructions\n\nIf you have components that rely on receiving onGameResize calls before they load, then\nyou can retrieve the game's size in onLoad manually via findGame()!.size.\n\nThe HasDraggableComponents mixin is now empty & deprecated. If your game used this mixin overriding\nits methods onDragStart, onDragUpdate, etc -- then they will no longer work. If you want to receive\ndrag events at the top level of the game, then simply add a DragCallbacks component to the top\nlevel of the game.\n\nThe HasTappableComponents mixin is now empty & deprecated. If your game used this mixin overriding\nits methods onTapDown, onTapUp, etc -- then they will no longer work. If you want to receive tap\nevents at the top level of the game, then simply add a TapCallbacks component to the top level of\nthe game.\n\n\n## 1.6.0\n\n> Note: This release has breaking changes.\n\n - **PERF**: Avoid Vector2 creation in `Sprite.render` ([#2261](https://github.com/flame-engine/flame/issues/2261)). ([736733d9](https://github.com/flame-engine/flame/commit/736733d91398721452edb4c2600a47277bb5abee))\n - **FIX**: Only use initialized game for tests and remove setMount from onGameResize ([#2246](https://github.com/flame-engine/flame/issues/2246)). ([2a0f1d4b](https://github.com/flame-engine/flame/commit/2a0f1d4bdc2688e596481aad39762f94bf1cc8f1))\n - **FIX**: Re-use paint object in ImageParticle ([#2210](https://github.com/flame-engine/flame/issues/2210)). ([7a945d96](https://github.com/flame-engine/flame/commit/7a945d960c9b88fde11bbc480c0429295445cf30))\n - **FIX**: Depend on test: any for flame_test ([#2207](https://github.com/flame-engine/flame/issues/2207)). ([acfd418d](https://github.com/flame-engine/flame/commit/acfd418d882ee6872f3aa9961c39680ec123c2e6))\n - **FEAT**: Add a `canSee` method to the `CameraComponent` ([#2270](https://github.com/flame-engine/flame/issues/2270)). ([2347c8f5](https://github.com/flame-engine/flame/commit/2347c8f567c88f29540ef1d8e1c7c4b65fe31b06))\n - **FEAT**: Add `moveBy` to `CameraComponent` ([#2269](https://github.com/flame-engine/flame/issues/2269)). ([51e54ebe](https://github.com/flame-engine/flame/commit/51e54ebef823258f28f3e1a60a645ba4dd12e337))\n - **FEAT**: Added computed property CameraComponent.visibleWorldRect ([#2267](https://github.com/flame-engine/flame/issues/2267)). ([f4b0e73f](https://github.com/flame-engine/flame/commit/f4b0e73fa1f068b8867177e9761b2c4b01216a31))\n - **DOCS**: Update example to not create Rect objects ([#2254](https://github.com/flame-engine/flame/issues/2254)). ([a306338b](https://github.com/flame-engine/flame/commit/a306338b112955972b56baa9ac6e419b1af43ef1))\n - **DOCS**: Teh -> the ([#2225](https://github.com/flame-engine/flame/issues/2225)). ([ff7f36d0](https://github.com/flame-engine/flame/commit/ff7f36d0f682206c6c666ea2dbdce8a2e1d19601))\n - **BREAKING** **REFACTOR**: The method `onLoad()` now returns `FutureOr<void>` ([#2228](https://github.com/flame-engine/flame/issues/2228)). ([d898b539](https://github.com/flame-engine/flame/commit/d898b539f734d3e14c47990ef0727043a0e32efb))\n - **BREAKING** **FEAT**: Adds new route methods `pushReplacement`, `pushReplacementNamed`, and `pushReplacementOverlay` ([#2249](https://github.com/flame-engine/flame/issues/2249)). ([a2772b4e](https://github.com/flame-engine/flame/commit/a2772b4e0f828ee8475603ffdaf5ff63872a1a33))\n\n## 1.5.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: OpacityEffect now uses opacity instead of alpha internally ([#2064](https://github.com/flame-engine/flame/issues/2064)). ([b3b67301](https://github.com/flame-engine/flame/commit/b3b673011cfafc4a9add55f682e7e1074b4dc64b))\n - **REFACTOR**: Game render box cleanup ([#1691](https://github.com/flame-engine/flame/issues/1691)). ([60a5830d](https://github.com/flame-engine/flame/commit/60a5830d3e596c9c6086f8253eb663b01e4f440b))\n - **FIX**: Event mixins missing `@mustCallSuper` ([#2036](https://github.com/flame-engine/flame/issues/2036)). ([c26d5da3](https://github.com/flame-engine/flame/commit/c26d5da3730d4d38002259fbc0d314ae63c3bdff))\n - **FIX**: SpeedController advance() should execute after its effect's onStart() ([#2173](https://github.com/flame-engine/flame/issues/2173)). ([7a1e2e8b](https://github.com/flame-engine/flame/commit/7a1e2e8b657b6b18dc08afd53f52ba513cecb4d9))\n - **FIX**: Refresh vertices on size change of `RectangleComponent` ([#2167](https://github.com/flame-engine/flame/issues/2167)). ([4020d68b](https://github.com/flame-engine/flame/commit/4020d68b4afcba554f8ee493840d7b74b68f6293))\n - **FIX**: Fix coordinate system calculation in FixedAspectRationViewport ([#2175](https://github.com/flame-engine/flame/issues/2175)). ([c9c9881c](https://github.com/flame-engine/flame/commit/c9c9881ccacbdaf1759c7c85b2edff94aa633427))\n - **FIX**: SpriteButtonComponent missing `@mustCallSuper` added ([#2001](https://github.com/flame-engine/flame/issues/2001)). ([45a9d79b](https://github.com/flame-engine/flame/commit/45a9d79bc477d9d9a772d0c2812d82e8a1962468))\n - **FIX**: Focus handling with a scope on the `GameWidget` ([#1725](https://github.com/flame-engine/flame/issues/1725)). ([d1cd8517](https://github.com/flame-engine/flame/commit/d1cd8517e4f9d4aadeacf7caf3ca91440e6041d7))\n - **FIX**: RemoveEffect should work within SequenceEffect ([#2110](https://github.com/flame-engine/flame/issues/2110)). ([03e1f33d](https://github.com/flame-engine/flame/commit/03e1f33d3de1e0d6a16b1f11a7fe503ece9f5d24))\n - **FIX**: [#1966](https://github.com/flame-engine/flame/issues/1966) unit test for `Particles` ([#2097](https://github.com/flame-engine/flame/issues/2097)). ([59bd7ebb](https://github.com/flame-engine/flame/commit/59bd7ebb9deaea44001edce02b306ffaacf5afc8))\n - **FIX**: OpacityEffect custom paint override ([#2056](https://github.com/flame-engine/flame/issues/2056)). ([fe9d4d9b](https://github.com/flame-engine/flame/commit/fe9d4d9bfb97557434d2844357d70db666b02e49))\n - **FIX**: [#1998](https://github.com/flame-engine/flame/issues/1998) ([#2013](https://github.com/flame-engine/flame/issues/2013)). ([f63711dc](https://github.com/flame-engine/flame/commit/f63711dc56961fc664358b4789de5d78b43ce081))\n - **FIX**: solid circles and polygons intersection ([#2067](https://github.com/flame-engine/flame/issues/2067)). ([62c5c2e1](https://github.com/flame-engine/flame/commit/62c5c2e14479c4cb1b0e5487ab6a96182c0f1338))\n - **FIX**: [#2017](https://github.com/flame-engine/flame/issues/2017) ([#2039](https://github.com/flame-engine/flame/issues/2039)). ([7f546b0f](https://github.com/flame-engine/flame/commit/7f546b0f13306edb92a68a331bf28127a42138ce))\n - **FIX**: Exception when having multiple calls to dispose() function of a Svg instance ([#2085](https://github.com/flame-engine/flame/issues/2085)). ([a287904e](https://github.com/flame-engine/flame/commit/a287904eb5dbbe70128207a6f6a56ff98dfbf579))\n - **FIX**: Add missing hitbox parameters ([#2070](https://github.com/flame-engine/flame/issues/2070)). ([8aacb555](https://github.com/flame-engine/flame/commit/8aacb5557ac299852530c5023a1ddd2bebbad564))\n - **FIX**: Change `Vector2.zero()` to `Vector2(0, -1)` in `Vector2Extensions.fromRadians()` ([#2016](https://github.com/flame-engine/flame/issues/2016)). ([801c683c](https://github.com/flame-engine/flame/commit/801c683c6cf448e6d0ae34231a656bc72bcce00a))\n - **FEAT**: Add children to `World` constructor ([#2093](https://github.com/flame-engine/flame/issues/2093)). ([3af416dc](https://github.com/flame-engine/flame/commit/3af416dc2e61c7f43334d06add651d7c21bb511b))\n - **FEAT**: Add paint layers to HasPaint and associated component renders ([#2073](https://github.com/flame-engine/flame/issues/2073)). ([9e6bf4fb](https://github.com/flame-engine/flame/commit/9e6bf4fbccd13b8e7ef848bc77d4da510680539f))\n - **FEAT**: Add SizeProvider to clip_component and custom_paint_component. ([#2100](https://github.com/flame-engine/flame/issues/2100)). ([bb710646](https://github.com/flame-engine/flame/commit/bb71064647c71ff42be40c34fd1231ad9b1c43f0))\n - **FEAT**: Added HasGameReference mixin ([#1828](https://github.com/flame-engine/flame/issues/1828)). ([12ce270b](https://github.com/flame-engine/flame/commit/12ce270b9b3102b6a9bb1f468369a4fce1e064e6))\n - **FEAT**: Added toString method to all the drags events message handlers ([#2014](https://github.com/flame-engine/flame/issues/2014)). ([a34f1df7](https://github.com/flame-engine/flame/commit/a34f1df7904f0bd54fb8465265b24e21be0f4dc2))\n - **FEAT**: Add `maintainState` property to Route ([#2161](https://github.com/flame-engine/flame/issues/2161)). ([576ceaac](https://github.com/flame-engine/flame/commit/576ceaac178de87a3c0ed54c87373cf83f7bd868))\n - **FEAT**: add onCancelled to ButtonComponent and HudButtonComponent ([#2193](https://github.com/flame-engine/flame/issues/2193)). ([e7f08906](https://github.com/flame-engine/flame/commit/e7f089066620ed5326e94ac8d4b7f5705c3ae3f7))\n - **FEAT**: onComponentTypeCheck support for ShapeHitbox ([#1981](https://github.com/flame-engine/flame/issues/1981)). ([f840210b](https://github.com/flame-engine/flame/commit/f840210bf97f9da406282212db265a976506ebf8))\n - **FEAT**: Added glow effect using maskFilter ([#2129](https://github.com/flame-engine/flame/issues/2129)). ([bcecd3c1](https://github.com/flame-engine/flame/commit/bcecd3c1bd400c155807beb77651ebd2ee6f627c))\n - **FEAT**: Add support for styles propagating through the text node tree ([#1915](https://github.com/flame-engine/flame/issues/1915)). ([b5780d42](https://github.com/flame-engine/flame/commit/b5780d421234636144794e663559cec8987656a4))\n - **FEAT**: Added SpriteFont class ([#1992](https://github.com/flame-engine/flame/issues/1992)). ([a0d7eada](https://github.com/flame-engine/flame/commit/a0d7eadae40d4653ce0f5286e7236bedc17ed8cb))\n - **FEAT**: Added CameraComponent.withFixedResolution() constructor ([#2176](https://github.com/flame-engine/flame/issues/2176)). ([e289f118](https://github.com/flame-engine/flame/commit/e289f118eedebf512899d66e01f6234e3890a0d6))\n - **FEAT**: Add optional maxDistance to raycast ([#2012](https://github.com/flame-engine/flame/issues/2012)). ([6b78b10f](https://github.com/flame-engine/flame/commit/6b78b10fb36a9fed5d9c7b06aea89e088bc4d985))\n - **FEAT**: `clampLength` for `Vector2` extension ([#2190](https://github.com/flame-engine/flame/issues/2190)). ([51a896b2](https://github.com/flame-engine/flame/commit/51a896b2c801089968b630937fd23c12a98dbc40))\n - **FEAT**: Adding onChildrenChanged ([#1976](https://github.com/flame-engine/flame/issues/1976)). ([3d043b86](https://github.com/flame-engine/flame/commit/3d043b86f7382cf54313ac59eb3818a5b2788824))\n - **FEAT**: Adding ComponentNotifier API ([#1889](https://github.com/flame-engine/flame/issues/1889)). ([bd7f51f5](https://github.com/flame-engine/flame/commit/bd7f51f5b63e303b8b7230643dccbd040d2708a5))\n - **FEAT**: `removed` future + `isRemoved` field for `Component` ([#2080](https://github.com/flame-engine/flame/issues/2080)). ([9f322785](https://github.com/flame-engine/flame/commit/9f3227857327a99730fd4d02f099acef7c57ca67))\n - **BREAKING** **FIX**: Correct coordinate system for a circular viewport ([#2174](https://github.com/flame-engine/flame/issues/2174)). ([93dc4325](https://github.com/flame-engine/flame/commit/93dc4325476d4727a4de8dd8f0caf3ee081c0ad6))\n - **BREAKING** **FIX**: PolygonComponent no longer modifies _vertices ([#2061](https://github.com/flame-engine/flame/issues/2061)). ([8cd4793a](https://github.com/flame-engine/flame/commit/8cd4793ac2ecade740e53ad628db3f2f9ca6949a))\n - **BREAKING** **FEAT**: Add OpacityProvider ([#2062](https://github.com/flame-engine/flame/issues/2062)). ([0255cc32](https://github.com/flame-engine/flame/commit/0255cc32f0c77b9507f9ad0eddcbd8c35840c885))\n\n## 1.4.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: move broadphase-related functionality into separate subdirectory ([#1943](https://github.com/flame-engine/flame/issues/1943)). ([f23acd41](https://github.com/flame-engine/flame/commit/f23acd41e0341909200437bfb6487cbe9ca58a53))\n - **REFACTOR**: used simpler and more implicit widgets in GameWidget ([#1862](https://github.com/flame-engine/flame/issues/1862)). ([44d17c64](https://github.com/flame-engine/flame/commit/44d17c64f80601159cc7f579cef8568727d411b3))\n - **PERF**: SpriteAnimationWidget will re-render only when needed ([#1876](https://github.com/flame-engine/flame/issues/1876)). ([bb678301](https://github.com/flame-engine/flame/commit/bb6783010f3c14362dcb4ed9182c4d240080a7f6))\n - **FIX**: Hitbox children of a CompositeHitbox to return correct parent ([#1922](https://github.com/flame-engine/flame/issues/1922)). ([d518705e](https://github.com/flame-engine/flame/commit/d518705e1665bfc3f54256f113f0d2227fab14dd))\n - **FIX**: OpacityEffect rounding error calculation ([#1933](https://github.com/flame-engine/flame/issues/1933)). ([4cfcfa64](https://github.com/flame-engine/flame/commit/4cfcfa644c641e85cad891b07eac956db8534590))\n - **FIX**: Expose hitboxParent from Hitbox ([#1928](https://github.com/flame-engine/flame/issues/1928)). ([3ba93351](https://github.com/flame-engine/flame/commit/3ba933513d9a4dd73c56e0a3f304069f6989c002))\n - **FIX**: Raycast from CircleHitbox's center ([#1918](https://github.com/flame-engine/flame/issues/1918)). ([57ca47c8](https://github.com/flame-engine/flame/commit/57ca47c8ccfdb0b78c541efa833d32ae746e6616))\n - **FEAT**: quad tree broadphase support  ([#1894](https://github.com/flame-engine/flame/issues/1894)). ([e33d5410](https://github.com/flame-engine/flame/commit/e33d5410a3bfdae5fdde8939b55d7ce178a0c5c8))\n - **FEAT**: Make `_ButtonState` public for SpriteButtonComponent ([#1941](https://github.com/flame-engine/flame/issues/1941)). ([e80412c5](https://github.com/flame-engine/flame/commit/e80412c56895a51ec281e90506f0104d0a9ce47e))\n - **FEAT**: Add possibility for solid hitboxes ([#1919](https://github.com/flame-engine/flame/issues/1919)). ([205ac561](https://github.com/flame-engine/flame/commit/205ac561eef4becd90a0d5dca2301b988b15959f))\n - **FEAT**: Adding callbacks for EffectController ([#1926](https://github.com/flame-engine/flame/issues/1926)) ([#1931](https://github.com/flame-engine/flame/issues/1931)). ([8dcdf155](https://github.com/flame-engine/flame/commit/8dcdf1557903a46766c46e6cf0855f0d6b524608))\n - **FEAT**: Added DebugTextFormatter ([#1921](https://github.com/flame-engine/flame/issues/1921)). ([426827d1](https://github.com/flame-engine/flame/commit/426827d19e803158dab271dce1fbf93bd09f07de))\n - **FEAT**: Add lookAt method for PositionComponent ([#1891](https://github.com/flame-engine/flame/issues/1891)). ([720c3566](https://github.com/flame-engine/flame/commit/720c3566b02815d7ca2c4b45861041f2bddca0fc))\n - **FEAT**: add applyLifespanToChildren to Particle generate ([#1911](https://github.com/flame-engine/flame/issues/1911)). ([884d5190](https://github.com/flame-engine/flame/commit/884d5190adbe6ddfc9b7d006cda310cc656d7da1))\n - **FEAT**: Add broadphase generics to CollisionDetection ([#1908](https://github.com/flame-engine/flame/issues/1908)). ([f7714122](https://github.com/flame-engine/flame/commit/f77141229345c24abdd8a09934397dc09c622352))\n - **FEAT**: Adding ClipComponent ([#1769](https://github.com/flame-engine/flame/issues/1769)). ([f34d86db](https://github.com/flame-engine/flame/commit/f34d86db1e459fb5fe36b601631d6c1999fadf8c))\n - **FEAT**: Add support for isometric staggered maps ([#1895](https://github.com/flame-engine/flame/issues/1895)). ([96be8408](https://github.com/flame-engine/flame/commit/96be840899022a024cef1eb853818d8138592000))\n - **FEAT**: Experimental integer viewport ([#1866](https://github.com/flame-engine/flame/issues/1866)). ([63822de3](https://github.com/flame-engine/flame/commit/63822de34c7938232e4048c7bb0e9bb648929ac8))\n - **FEAT**: RecycledQueue now supports modification during iteration ([#1884](https://github.com/flame-engine/flame/issues/1884)). ([01b59493](https://github.com/flame-engine/flame/commit/01b59493024a93d7f3ecbe0627ad0c6a4b2454a1))\n - **FEAT**: Allow children of `ComposedParticle` to have varied lifespan ([#1879](https://github.com/flame-engine/flame/issues/1879)). ([6db519ec](https://github.com/flame-engine/flame/commit/6db519ecd09752e15848d29a616a5152f7269686))\n - **FEAT**: Add `removeWhere` to `Component` ([#1878](https://github.com/flame-engine/flame/issues/1878)). ([abd28f28](https://github.com/flame-engine/flame/commit/abd28f28a627799ea4602026d91f52bc97feb91e))\n - **FEAT**: Added RecycledQueue class ([#1864](https://github.com/flame-engine/flame/issues/1864)). ([9457e38e](https://github.com/flame-engine/flame/commit/9457e38ebc2485e235e3bdc01c7ba43097139db7))\n - **FEAT**: Possibility to ignore hitboxes for ray casting ([#1863](https://github.com/flame-engine/flame/issues/1863)). ([b22bc643](https://github.com/flame-engine/flame/commit/b22bc6438407808e2d4137b4021a2777c3c22afe))\n - **DOCS**: Added Style Guide and Test Writing Guide ([#1897](https://github.com/flame-engine/flame/issues/1897)). ([999caca1](https://github.com/flame-engine/flame/commit/999caca10fbeb834e85461b6cc828b1bce62bbf9))\n\n - **BREAKING** **FIX**: Make all `ComponentSet` modifications internal ([#1877](https://github.com/flame-engine/flame/issues/1877)). ([f26a066d](https://github.com/flame-engine/flame/commit/f26a066d77f1f79915c52c93038bde7d3571e068))\n   Migration instructions:\n   For most methods Component has the corresponding methods directly on it already.\n   For example, instead of using component.children.addAll you should do component.addAll.\n\n - **BREAKING** **CHORE**: Remove functions/classes that were scheduled for removal in v1.3.0 ([#1867](https://github.com/flame-engine/flame/issues/1867)). ([00ab347c](https://github.com/flame-engine/flame/commit/00ab347c57b151c9232c85150e36a8a7781511a3))\n   For each deleted function/method/class, the deprecation comment already specifies what functionality should be used instead.\n\n## 1.3.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Use new \"super\"-constructors in ShapeComponents ([#1752](https://github.com/flame-engine/flame/issues/1752)). ([b69e8d85](https://github.com/flame-engine/flame/commit/b69e8d85c77346081d1fc5a2ee5cbf9c204a9edf))\n - **REFACTOR**: Game is now a class, not a mixin ([#1751](https://github.com/flame-engine/flame/issues/1751)). ([5225a4eb](https://github.com/flame-engine/flame/commit/5225a4ebd55a21f5709ccab9a1e24c728b2747ed))\n - **PERF**: Use TextElements within the TextComponent ([#1802](https://github.com/flame-engine/flame/issues/1802)). ([7b044430](https://github.com/flame-engine/flame/commit/7b04443046978c9bcdcf3eacab4813f3bcb545af))\n - **PERF**: Avoid unnecessary copy in AssetsCache.readBinaryFile ([#1749](https://github.com/flame-engine/flame/issues/1749)). ([7e79638d](https://github.com/flame-engine/flame/commit/7e79638dd577cb07d55402d4e862de08ce832b85))\n - **FIX**: ButtonComponent behavior when the engine is paused ([#1726](https://github.com/flame-engine/flame/issues/1726)). ([197e63d6](https://github.com/flame-engine/flame/commit/197e63d69e2a4c6779e49b918d05a60447ce9462))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FIX**: World component can now be queried with `componentsAtPoint` ([#1739](https://github.com/flame-engine/flame/issues/1739)). ([f750d705](https://github.com/flame-engine/flame/commit/f750d705d14dd0ba95d550b2b8a320201a96584b))\n - **FIX**: Merge basic and advanced gesture detectors ([#1718](https://github.com/flame-engine/flame/issues/1718)). ([f08f8e12](https://github.com/flame-engine/flame/commit/f08f8e12f5322c7bea1491908f06b350e13c14b7))\n - **FIX**: Correct key events in GameWidget.controller ([#1745](https://github.com/flame-engine/flame/issues/1745)). ([01ed2ec9](https://github.com/flame-engine/flame/commit/01ed2ec967ee29c946c967786eec6bf7cc6ec958))\n - **FIX**: Camera incorrect follow with zoom and world boundaries. ([c1756177](https://github.com/flame-engine/flame/commit/c175617714e2f15f4379ed8ea412c7cb8bfa1842))\n - **FIX**: Add missing paint arguments on shapes ([#1727](https://github.com/flame-engine/flame/issues/1727)). ([e59f3428](https://github.com/flame-engine/flame/commit/e59f3428469e4298d812bb665171679df8895daf))\n - **FIX**: Delay camera update ([#1811](https://github.com/flame-engine/flame/issues/1811)). ([a5598a8f](https://github.com/flame-engine/flame/commit/a5598a8fa43552028654a3a4b760b7b375dd81e5))\n - **FIX**: Overlays can now be properly added during onLoad ([#1759](https://github.com/flame-engine/flame/issues/1759)). ([9f35b154](https://github.com/flame-engine/flame/commit/9f35b15420bea9ac5eeeddc245484b854e8eed38))\n - **FIX**: SpriteAnimationWidget can now be update animation safely ([#1738](https://github.com/flame-engine/flame/issues/1738)). ([eb070195](https://github.com/flame-engine/flame/commit/eb0701951c165576fac1f540c8860e560a8961e6))\n - **FIX**: JoystickComponent drags using the delta Viewport ([#1831](https://github.com/flame-engine/flame/issues/1831)). ([54e40de6](https://github.com/flame-engine/flame/commit/54e40de674f628282ea19af4f5ce2173ee48fd6e))\n - **FIX**: Specify size for the SpriteWidget ([#1760](https://github.com/flame-engine/flame/issues/1760)). ([82f75fcb](https://github.com/flame-engine/flame/commit/82f75fcb57c8185a7138ee6ceb9082a418099df8))\n - **FEAT**: New colors to palette.dart ([#1783](https://github.com/flame-engine/flame/issues/1783)). ([85cd60e1](https://github.com/flame-engine/flame/commit/85cd60e16c7b4dafdf1823bf85a7ae8a50fd05f2))\n - **FEAT**: add `children` argument to `SpriteComponent.fromImage` ([#1793](https://github.com/flame-engine/flame/issues/1793)). ([80a63362](https://github.com/flame-engine/flame/commit/80a633622a5784f377ef08515115d66ff200b848))\n - **FEAT**: Added Decorator class and HasDecorator mixin ([#1781](https://github.com/flame-engine/flame/issues/1781)). ([8d00847c](https://github.com/flame-engine/flame/commit/8d00847cfcecb60a96772ccba1bcf3aec56b78ff))\n - **FEAT**: Added TextFormatter classes ([#1720](https://github.com/flame-engine/flame/issues/1720)). ([c44272be](https://github.com/flame-engine/flame/commit/c44272be45eadfabc8f03ef250eb663e59ef2aab))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **FEAT**: Added Rotate3DDecorator ([#1805](https://github.com/flame-engine/flame/issues/1805)). ([f05194c8](https://github.com/flame-engine/flame/commit/f05194c80c4d09d024be486882e5defbb10dd506))\n - **FEAT**: Added Shadow3DDecorator ([#1812](https://github.com/flame-engine/flame/issues/1812)). ([0a41b2da](https://github.com/flame-engine/flame/commit/0a41b2dabe51dbdfcd0b4c9f441bc4cc2e9e1b5e))\n - **FEAT**: Add tertiary tap detector mixin ([#1815](https://github.com/flame-engine/flame/issues/1815)). ([e9e7b0d5](https://github.com/flame-engine/flame/commit/e9e7b0d598dac588daf37010b53221da4aea24be))\n - **FEAT**: Add `Ray2` class to be used in raytracing/casting ([#1788](https://github.com/flame-engine/flame/issues/1788)). ([26196c01](https://github.com/flame-engine/flame/commit/26196c0152911c6d20b3feffe96319df4a625a7f))\n - **FEAT**: Added RouterComponent  ([#1755](https://github.com/flame-engine/flame/issues/1755)). ([24092bd7](https://github.com/flame-engine/flame/commit/24092bd72d2e615c06908d9784f19fecb4d0b8b9))\n - **FEAT**: Structured text and text styles ([#1830](https://github.com/flame-engine/flame/issues/1830)). ([bfdc3a29](https://github.com/flame-engine/flame/commit/bfdc3a291ba08ee0df07a80f0709c8470ed8a739))\n - **FEAT**: Drag events that dispatch using componentsAtPoint ([#1715](https://github.com/flame-engine/flame/issues/1715)). ([10669c12](https://github.com/flame-engine/flame/commit/10669c12702a3a82fcf5be9161107dce4349a79f))\n - **FEAT**: Added routes that can return a value ([#1848](https://github.com/flame-engine/flame/issues/1848)). ([f1b276e0](https://github.com/flame-engine/flame/commit/f1b276e020c6f80a18764e63ffbea21abb52b1f2))\n - **FEAT**: PositionComponent now has a built-in Decorator ([#1846](https://github.com/flame-engine/flame/issues/1846)). ([8dd52c33](https://github.com/flame-engine/flame/commit/8dd52c338bbd66938dd90c068f99107337bae4ea))\n - **FEAT**: add `HasAncestor` mixin ([#1711](https://github.com/flame-engine/flame/issues/1711)). ([987a44f4](https://github.com/flame-engine/flame/commit/987a44f441429534c743388b44e6d84b28e8f5ca))\n - **FEAT**: Added ability to control overlays via the RouterComponent ([#1840](https://github.com/flame-engine/flame/issues/1840)). ([e2de70c9](https://github.com/flame-engine/flame/commit/e2de70c98afabb6e570c3442213b2246a724bdd9))\n - **FEAT**: Add vector projection and inversion ([#1787](https://github.com/flame-engine/flame/issues/1787)). ([d197870f](https://github.com/flame-engine/flame/commit/d197870f529829adc51bbafc28180bde33d6f2cb))\n - **DOCS**: Klondike tutorial, part 4 ([#1740](https://github.com/flame-engine/flame/issues/1740)). ([02d0b71b](https://github.com/flame-engine/flame/commit/02d0b71b2379d12b36b53a76b6bcf5f4018ec9df))\n - **BREAKING** **REFACTOR**: Matcher closeToVector() now accepts Vector2 as an argument ([#1761](https://github.com/flame-engine/flame/issues/1761)). ([c5083501](https://github.com/flame-engine/flame/commit/c5083501d54023f04d3f09e3358bce039ade9a20))\n - **BREAKING** **PERF**: Game.images/assets are now same as Flame.images/assets by default ([#1775](https://github.com/flame-engine/flame/issues/1775)). ([0ccb0e2e](https://github.com/flame-engine/flame/commit/0ccb0e2ef525661830c7b4662662ba64fda830fe))\n - **BREAKING** **FEAT**: Raycasting and raytracing ([#1785](https://github.com/flame-engine/flame/issues/1785)). ([ed452dd1](https://github.com/flame-engine/flame/commit/ed452dd172289d49b6a9fbf02ee5b61b33f84c4c))\n\n## 1.2.1\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Game is now a class, not a mixin ([#1751](https://github.com/flame-engine/flame/issues/1751)). ([5225a4eb](https://github.com/flame-engine/flame/commit/5225a4ebd55a21f5709ccab9a1e24c728b2747ed))\n - **REFACTOR**: Use new \"super\"-constructors in ShapeComponents ([#1752](https://github.com/flame-engine/flame/issues/1752)). ([b69e8d85](https://github.com/flame-engine/flame/commit/b69e8d85c77346081d1fc5a2ee5cbf9c204a9edf))\n - **PERF**: Avoid unnecessary copy in AssetsCache.readBinaryFile ([#1749](https://github.com/flame-engine/flame/issues/1749)). ([7e79638d](https://github.com/flame-engine/flame/commit/7e79638dd577cb07d55402d4e862de08ce832b85))\n - **FIX**: Specify size for the SpriteWidget ([#1760](https://github.com/flame-engine/flame/issues/1760)). ([82f75fcb](https://github.com/flame-engine/flame/commit/82f75fcb57c8185a7138ee6ceb9082a418099df8))\n - **FIX**: SpriteAnimationWidget can now be update animation safely ([#1738](https://github.com/flame-engine/flame/issues/1738)). ([eb070195](https://github.com/flame-engine/flame/commit/eb0701951c165576fac1f540c8860e560a8961e6))\n - **FIX**: Overlays can now be properly added during onLoad ([#1759](https://github.com/flame-engine/flame/issues/1759)). ([9f35b154](https://github.com/flame-engine/flame/commit/9f35b15420bea9ac5eeeddc245484b854e8eed38))\n - **FIX**: Camera incorrect follow with zoom and world boundaries. ([c1756177](https://github.com/flame-engine/flame/commit/c175617714e2f15f4379ed8ea412c7cb8bfa1842))\n - **FIX**: Correct key events in GameWidget.controller ([#1745](https://github.com/flame-engine/flame/issues/1745)). ([01ed2ec9](https://github.com/flame-engine/flame/commit/01ed2ec967ee29c946c967786eec6bf7cc6ec958))\n - **FIX**: World component can now be queried with `componentsAtPoint` ([#1739](https://github.com/flame-engine/flame/issues/1739)). ([f750d705](https://github.com/flame-engine/flame/commit/f750d705d14dd0ba95d550b2b8a320201a96584b))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FIX**: ButtonComponent behavior when the engine is paused ([#1726](https://github.com/flame-engine/flame/issues/1726)). ([197e63d6](https://github.com/flame-engine/flame/commit/197e63d69e2a4c6779e49b918d05a60447ce9462))\n - **FIX**: Add missing paint arguments on shapes ([#1727](https://github.com/flame-engine/flame/issues/1727)). ([e59f3428](https://github.com/flame-engine/flame/commit/e59f3428469e4298d812bb665171679df8895daf))\n - **FIX**: Merge basic and advanced gesture detectors ([#1718](https://github.com/flame-engine/flame/issues/1718)). ([f08f8e12](https://github.com/flame-engine/flame/commit/f08f8e12f5322c7bea1491908f06b350e13c14b7))\n - **FEAT**: New colors to palette.dart ([#1783](https://github.com/flame-engine/flame/issues/1783)). ([85cd60e1](https://github.com/flame-engine/flame/commit/85cd60e16c7b4dafdf1823bf85a7ae8a50fd05f2))\n - **FEAT**: Added TextFormatter classes ([#1720](https://github.com/flame-engine/flame/issues/1720)). ([c44272be](https://github.com/flame-engine/flame/commit/c44272be45eadfabc8f03ef250eb663e59ef2aab))\n - **FEAT**: Drag events that dispatch using componentsAtPoint ([#1715](https://github.com/flame-engine/flame/issues/1715)). ([10669c12](https://github.com/flame-engine/flame/commit/10669c12702a3a82fcf5be9161107dce4349a79f))\n - **FEAT**: add `HasAncestor` mixin ([#1711](https://github.com/flame-engine/flame/issues/1711)). ([987a44f4](https://github.com/flame-engine/flame/commit/987a44f441429534c743388b44e6d84b28e8f5ca))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **DOCS**: Klondike tutorial, part 4 ([#1740](https://github.com/flame-engine/flame/issues/1740)). ([02d0b71b](https://github.com/flame-engine/flame/commit/02d0b71b2379d12b36b53a76b6bcf5f4018ec9df))\n - **BREAKING** **PERF**: Game.images/assets are now same as Flame.images/assets by default ([#1775](https://github.com/flame-engine/flame/issues/1775)). ([0ccb0e2e](https://github.com/flame-engine/flame/commit/0ccb0e2ef525661830c7b4662662ba64fda830fe))\n\n## 1.2.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Organize Component class ([#1608](https://github.com/flame-engine/flame/issues/1608)). ([069294f4](https://github.com/flame-engine/flame/commit/069294f44082a5d4ae6e9eff1d29be9cb06ee4a7))\n - **REFACTOR**: Remove unecessary copy operation on Camera ([#1708](https://github.com/flame-engine/flame/issues/1708)). ([94cc115a](https://github.com/flame-engine/flame/commit/94cc115a9ee6660d1f3a72378e8b35523b83bfad))\n - **REFACTOR**: Update and guarantee consistency on mocktail dev dependency version across repo ([#1701](https://github.com/flame-engine/flame/issues/1701)). ([f4a98878](https://github.com/flame-engine/flame/commit/f4a98878062dbd4fe8238a8b014e6be3e528c5d8))\n - **REFACTOR**: Add onComplete as optional parameter ([#1686](https://github.com/flame-engine/flame/issues/1686)). ([4ca65f8a](https://github.com/flame-engine/flame/commit/4ca65f8a2c330d61527e071434441f2df9deefb4))\n - **REFACTOR**: Added MultiDragListener - common API between HasDraggables and MultiTouchDragDetector ([#1668](https://github.com/flame-engine/flame/issues/1668)). ([801dbba1](https://github.com/flame-engine/flame/commit/801dbba1d8b6fd721d4e2fc752c70f97d4771198))\n - **REFACTOR**: Simplify Component.firstChild, .lastChild, and .findParent ([#1673](https://github.com/flame-engine/flame/issues/1673)). ([84f2f57e](https://github.com/flame-engine/flame/commit/84f2f57e5fddb82572177b2bcd0f8309a891ea4e))\n - **REFACTOR**: Replace some usages of fold<> with .sum ([#1670](https://github.com/flame-engine/flame/issues/1670)). ([dd05ecb6](https://github.com/flame-engine/flame/commit/dd05ecb6b8b105b4d1fc894dc6ce7ca3f8cf793e))\n - **REFACTOR**: Deprecate ComponentSet.createDefault() ([#1676](https://github.com/flame-engine/flame/issues/1676)). ([f37e3a20](https://github.com/flame-engine/flame/commit/f37e3a2028e16143d8bb3218691904c38fb848a4))\n - **REFACTOR**: Simplify HudButtonComponent ([#1647](https://github.com/flame-engine/flame/issues/1647)). ([30d84b7c](https://github.com/flame-engine/flame/commit/30d84b7caea128c7dc579dce170129e462bc03bf))\n - **REFACTOR**: Component's lifecycle futures moved into LifecycleManager ([#1613](https://github.com/flame-engine/flame/issues/1613)). ([39201c40](https://github.com/flame-engine/flame/commit/39201c40fa3eea5dbdbaa823309cdf8856f912a6))\n - **REFACTOR**: TextRenderer and TextPaint moved to separate files ([#1628](https://github.com/flame-engine/flame/issues/1628)). ([5e1f5966](https://github.com/flame-engine/flame/commit/5e1f59663bf7e09a02475979d1eded54dbaaefd7))\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **REFACTOR**: Improve tests ([#1609](https://github.com/flame-engine/flame/issues/1609)). ([f33b3986](https://github.com/flame-engine/flame/commit/f33b3986cd913416ae3955c922d6cc8b0db872e3))\n - **FIX**: Fix tile flips when using canvas.drawAtlas ([#1610](https://github.com/flame-engine/flame/issues/1610)). ([b4ad498f](https://github.com/flame-engine/flame/commit/b4ad498fe5488795deb2c2e098fcde357b448bf0))\n - **FIX**: Expose `CompositeHitbox` ([#1589](https://github.com/flame-engine/flame/issues/1589)). ([78775798](https://github.com/flame-engine/flame/commit/7877579868041f4844ebae885da559097b7aa8a5))\n - **FIX**: Anchor equality operator is now more reliable ([#1560](https://github.com/flame-engine/flame/issues/1560)). ([0d6581ef](https://github.com/flame-engine/flame/commit/0d6581ef1aaff4437b2a84f9e57d7d0e1d093d1f))\n - **FIX**: Deprecate Anchor.translate() ([#1672](https://github.com/flame-engine/flame/issues/1672)). ([80c648fc](https://github.com/flame-engine/flame/commit/80c648fc94dc00e37f2c0876fec39b2628b3128a))\n - **FIX**: Avoid leaks when using PictureRecorders ([#1643](https://github.com/flame-engine/flame/issues/1643)). ([d67065e5](https://github.com/flame-engine/flame/commit/d67065e52db453b0f4f190a7aec1bec6bc389e45))\n - **FIX**: Remove nonVirtual method shouldRemove ([#1707](https://github.com/flame-engine/flame/issues/1707)). ([1efd067e](https://github.com/flame-engine/flame/commit/1efd067e31ad425941e5b83891c7289ba063ec90))\n - **FIX**: Fix flame package example app ([#1709](https://github.com/flame-engine/flame/issues/1709)). ([bd2ef967](https://github.com/flame-engine/flame/commit/bd2ef967e10eb0309e0a468652a657cae3d5e7d5))\n - **FIX**: Subscription for events when game changes in GameWidget ([#1659](https://github.com/flame-engine/flame/issues/1659)). ([04f0d5d1](https://github.com/flame-engine/flame/commit/04f0d5d172ca5065e58e8b9b5536cbce706147d4))\n - **FIX**: performance improvements on `SpriteBatch` APIs ([#1637](https://github.com/flame-engine/flame/issues/1637)). ([4b19a1b2](https://github.com/flame-engine/flame/commit/4b19a1b203c5cfca5bb412b91c795fe6a215506e))\n - **FIX**: Removed warnings using flutter v3 ([#1640](https://github.com/flame-engine/flame/issues/1640)). ([69214827](https://github.com/flame-engine/flame/commit/69214827a0edb563468951256eccecab408f89df))\n - **FIX**: ParallaxComponent.update mustCallSuper ([#1635](https://github.com/flame-engine/flame/issues/1635)). ([9474ce74](https://github.com/flame-engine/flame/commit/9474ce7425ffc18f6b1a1a35c35f59b76f435166))\n - **FIX**: Isometric tile map component uses scale when getting block from position ([#1569](https://github.com/flame-engine/flame/issues/1569)). ([0c430786](https://github.com/flame-engine/flame/commit/0c430786e2774174424a21a13464e93d04c69295))\n - **FIX**: Dispose `TextBoxComponent` image cache properly ([#1579](https://github.com/flame-engine/flame/issues/1579)). ([c0e3257a](https://github.com/flame-engine/flame/commit/c0e3257a0b348885275f2659c351bacbfa5a8732))\n - **FIX**: `ParentIsA` missing `mustCallSuper` ([#1604](https://github.com/flame-engine/flame/issues/1604)). ([72129019](https://github.com/flame-engine/flame/commit/721290198cc7062f8cfb958cb8499e64be7a1e9c))\n - **FIX**: Component can now be removed in any lifecycle stage ([#1601](https://github.com/flame-engine/flame/issues/1601)). ([c0a14156](https://github.com/flame-engine/flame/commit/c0a141563b9e832b1a81bf32d860d4dfb2b359ae))\n - **FIX**: Export NotifyingVector2 ([#1633](https://github.com/flame-engine/flame/issues/1633)). ([aeaf9999](https://github.com/flame-engine/flame/commit/aeaf9999b0b4f69e394063d3af8e18f67dff5ed9))\n - **FIX**: RectangleHitbox should shrink to bounds ([#1596](https://github.com/flame-engine/flame/issues/1596)). ([60df3b9f](https://github.com/flame-engine/flame/commit/60df3b9f60f538fbad7a3d806f5d38262ab6d66c))\n - **FIX**: Components in uninitialized state can now be safely removed ([#1551](https://github.com/flame-engine/flame/issues/1551)). ([ba617790](https://github.com/flame-engine/flame/commit/ba617790e4a7ca4dc03f4a2e29de43d42efd3482))\n - **FIX**: Bug in \"tty\" TextBoxComponent ([#1619](https://github.com/flame-engine/flame/issues/1619)). ([6cc3e827](https://github.com/flame-engine/flame/commit/6cc3e82727509f8877873b095c84eef3543fe01e))\n - **FIX**: Component.loaded future sometimes failed to complete ([#1593](https://github.com/flame-engine/flame/issues/1593)). ([89ee9b98](https://github.com/flame-engine/flame/commit/89ee9b984bfc3784dedde1ada1daa992a9f0dedc))\n - **FIX**: correctly calculating frame length ([#1578](https://github.com/flame-engine/flame/issues/1578)). ([efda45e7](https://github.com/flame-engine/flame/commit/efda45e76c38a2d38a4cd0bb66ece9792f5832df))\n - **FIX**: Optimize AcceleratedParticle and MovingParticle ([#1568](https://github.com/flame-engine/flame/issues/1568)). ([5591c109](https://github.com/flame-engine/flame/commit/5591c109437309907cdac72f0bb479a6a6bfa00a))\n - **FEAT**: Method `componentsAtPoint` now reports the \"stacktrace\" of points ([#1615](https://github.com/flame-engine/flame/issues/1615)). ([e2398966](https://github.com/flame-engine/flame/commit/e239896624f1e2736de83148ff172ca1b0f97dae))\n - **FEAT**: Allow changing parent from null parent ([#1662](https://github.com/flame-engine/flame/issues/1662)). ([53268b5f](https://github.com/flame-engine/flame/commit/53268b5f5fd81f3822bfda9721b97be4e72e48e3))\n - **FEAT**: Callbacks in `HudButtonComponent` constructor and `ViewportMargin` mixin to avoid code duplication ([#1685](https://github.com/flame-engine/flame/issues/1685)). ([f55b2e0d](https://github.com/flame-engine/flame/commit/f55b2e0dc01c98718e4871430c6745472c221821))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Keep stacktrace when rethrowing an error from GameWidget ([#1675](https://github.com/flame-engine/flame/issues/1675)). ([dd28183b](https://github.com/flame-engine/flame/commit/dd28183bc4ebe2ea2f80d1dab3b5ab22d11b8382))\n - **FEAT**: Aligned text in the TextBoxComponent ([#1620](https://github.com/flame-engine/flame/issues/1620)). ([c64aedae](https://github.com/flame-engine/flame/commit/c64aedaeb3fed908722b8872b71e288ff87bc761))\n - **FEAT**: Included `completed` completer in `SpriteAnimation` ([#1564](https://github.com/flame-engine/flame/issues/1564)). ([71999b19](https://github.com/flame-engine/flame/commit/71999b191af0285e8d61583b041da58afd40d8d2))\n - **FEAT**: Optional key for Images.load ([#1624](https://github.com/flame-engine/flame/issues/1624)). ([067c34b5](https://github.com/flame-engine/flame/commit/067c34b5f29e1a9bd51861d872092ae5ee0a551f))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n - **FEAT**: Add `isFirstFrame` and `onStart` event to `SpriteAnimation` ([#1492](https://github.com/flame-engine/flame/issues/1492)). ([701d0706](https://github.com/flame-engine/flame/commit/701d0706af74e6437d71376d468b32bb2537e5b7))\n - **FEAT**: Add `FpsComponent` and `FpsTextComponent` ([#1595](https://github.com/flame-engine/flame/issues/1595)). ([4c68c2b0](https://github.com/flame-engine/flame/commit/4c68c2b0a2660e705b30099234da4ab1eb4616d0))\n - **FEAT**: Added FollowBehavior and ability for the new Camera to follow a component ([#1561](https://github.com/flame-engine/flame/issues/1561)). ([b583388c](https://github.com/flame-engine/flame/commit/b583388ca432f799ad13b92a3a7bf25ddf98ceb0))\n - **FEAT**: Added componentsAtPoint() iterable ([#1518](https://github.com/flame-engine/flame/issues/1518)). ([b99e3512](https://github.com/flame-engine/flame/commit/b99e35120dc4fe81ebfedc89a666286ec489384c))\n - **FEAT**: Added AnchorToEffect and AnchorByEffect ([#1556](https://github.com/flame-engine/flame/issues/1556)). ([eff72794](https://github.com/flame-engine/flame/commit/eff72794afed73bdb1df8e14b17d50f0f446e92b))\n - **FEAT**: Added utility function solveCubic() ([#1696](https://github.com/flame-engine/flame/issues/1696)). ([31784ca0](https://github.com/flame-engine/flame/commit/31784ca0b05082042003f847be2b4004da83edb6))\n - **FEAT**: add FutureOr support on SpriteButton ([#1645](https://github.com/flame-engine/flame/issues/1645)). ([2e82dc95](https://github.com/flame-engine/flame/commit/2e82dc95ecd6d7298239cadad5a746341c37fcd9))\n - **FEAT**: MoveAlongPathEffect can now be applied to any PositionProvider ([#1555](https://github.com/flame-engine/flame/issues/1555)). ([a0ff2d18](https://github.com/flame-engine/flame/commit/a0ff2d18a1efc54f648a277453fa9cf6414ce44c))\n - **FEAT**: adding KeyboardListenerComponent ([#1594](https://github.com/flame-engine/flame/issues/1594)). ([c887c361](https://github.com/flame-engine/flame/commit/c887c3616e9f65209b8e29cb8575a0052db3e2bb))\n - **FEAT**: new flame bloc API ([#1538](https://github.com/flame-engine/flame/issues/1538)). ([f98970a9](https://github.com/flame-engine/flame/commit/f98970a91f91fe70e4a38834d7b69bfcb438d197))\n - **FEAT**: Added the onLongTapDown event ([#1587](https://github.com/flame-engine/flame/issues/1587)). ([ed302d89](https://github.com/flame-engine/flame/commit/ed302d89160cd7391e3aaf66a0038cd8f57ceca9))\n - **FEAT**: Add ability to add/remove multiple overlays at once ([#1657](https://github.com/flame-engine/flame/issues/1657)). ([0ac84c00](https://github.com/flame-engine/flame/commit/0ac84c0024338cbe87fcff264b83e01192aa355b))\n - **FEAT**: Helpers for whether a `PositionComponent` is flipped. ([#1700](https://github.com/flame-engine/flame/issues/1700)). ([cf67147e](https://github.com/flame-engine/flame/commit/cf67147ea37aed8e5f1dd12def442dccbe4576fd))\n - **FEAT**: World bounds for a CameraComponent ([#1605](https://github.com/flame-engine/flame/issues/1605)). ([abb497ab](https://github.com/flame-engine/flame/commit/abb497abe47f6366d27f44d25535924bd7de8a28))\n - **FEAT**: Implement tap events based on `componentsAtPoint` ([#1661](https://github.com/flame-engine/flame/issues/1661)). ([2711ba60](https://github.com/flame-engine/flame/commit/2711ba60c2c700984d8a90d90519e17850038ab4))\n - **FEAT**: add `ParentIsA` to force parent child relations ([#1566](https://github.com/flame-engine/flame/issues/1566)). ([2cdf3868](https://github.com/flame-engine/flame/commit/2cdf3868460f04cee76079e3f81cdd12fb407d3a))\n - **FEAT**: Adding classes for raw geometric shapes ([#1528](https://github.com/flame-engine/flame/issues/1528)). ([666a2b19](https://github.com/flame-engine/flame/commit/666a2b199fc740d02628321bb19511ba98de1700))\n - **FEAT**: Add solveQuadratic() utility function ([#1665](https://github.com/flame-engine/flame/issues/1665)). ([d8bbfc06](https://github.com/flame-engine/flame/commit/d8bbfc067e3885cedd133de47a98134fc15c9c82))\n - **FEAT**: allow external packages to await for game to be loaded ([#1699](https://github.com/flame-engine/flame/issues/1699)). ([a15eda0b](https://github.com/flame-engine/flame/commit/a15eda0b67d6020bcb72162f0186e3c5069674bb))\n - **FEAT**: Children as argument to FlameGame ([#1680](https://github.com/flame-engine/flame/issues/1680)). ([db336c03](https://github.com/flame-engine/flame/commit/db336c03b607b878faf618cb1ab5833cd859d0e6))\n - **FEAT**: Add range constructor on SpriteAnimationData ([#1572](https://github.com/flame-engine/flame/issues/1572)). ([e42b4958](https://github.com/flame-engine/flame/commit/e42b495805efd2e969cfe412b069ffcc6e828ad6))\n - **FEAT**: Add helper function for creating golden tests ([#1623](https://github.com/flame-engine/flame/issues/1623)). ([d0faaada](https://github.com/flame-engine/flame/commit/d0faaada2bb971c2dde5a37dfa20d316c532ea28))\n - **FEAT**: Added ability to render spritesheet-based fonts ([#1634](https://github.com/flame-engine/flame/issues/1634)). ([3f287898](https://github.com/flame-engine/flame/commit/3f2878988195606b90d9e48b981444792af08ebe))\n\n - **BREAKING** **FIX**: `FixedResolutionViewport` noClip -> clip ([#1612](https://github.com/flame-engine/flame/issues/1612)). ([02be4acd](https://github.com/flame-engine/flame/commit/02be4acd8798254eeaf832863d4000e1c5240db1))\n - **BREAKING** **FEAT**: Adding GameWidget.controlled ([#1650](https://github.com/flame-engine/flame/issues/1650)). ([7ef6a51e](https://github.com/flame-engine/flame/commit/7ef6a51ec60a70807a126b6121a1fd4379b8e19b))\n - **BREAKING** **FEAT**: remove `onTimingsCallback` for Flutter 3.0 ([#1626](https://github.com/flame-engine/flame/issues/1626)). ([0761a79d](https://github.com/flame-engine/flame/commit/0761a79df6c88a5a6ba74ec78d4f600983657c06))\n\n - **BREAKING** **FIX**: Game.mouseCursor and Game.overlays can now be safely set during onLoad ([#1498](https://github.com/flame-engine/flame/issues/1498)). ([821d01c3](https://github.com/flame-engine/flame/commit/821d01c3fab3cdd9e80d6ead8d491ea2e8ec0643))\n   Migration instructions:\n   The mouseCursor property is now a plain property with a setter, not a ChangeNotifier.\n   Consequently, instead of writing game.mouseCursor.value = ...,\n   one needs to write game.mouseCursor = ... now.\n\n - **BREAKING** **FEAT**: Added anchor for the Viewport ([#1611](https://github.com/flame-engine/flame/issues/1611)). ([c3bb14b7](https://github.com/flame-engine/flame/commit/c3bb14b7ca9513fc75f51b0a5cbc9d986db48dd6))\n   Migration instructions:\n   The coordinate system of the Viewport now always has the origin in the top-left corner\n   of its bounding box (previously it was in the center) -- similar to the local coordinate\n   system of a PositionComponent.\n\n - **BREAKING** **FEAT**: Add ability to render without loading on image related widgets ([#1674](https://github.com/flame-engine/flame/issues/1674)). ([40a061bc](https://github.com/flame-engine/flame/commit/40a061bcf06b5bf028911964617c1d1e2599460a))\n   Migration instructions:\n   It only breaks SpriteButton when you are using future as a sprite or pressedSprite parameter.\n   You should use SpriteButton.future if previously you are using future as a parameter.\n\n - **BREAKING** **FEAT**: Size effects will now work only on components implementing SizeProvider ([#1571](https://github.com/flame-engine/flame/issues/1571)). ([1bfed571](https://github.com/flame-engine/flame/commit/1bfed57132330fb948962261735a0545eb37e7b9))\n   Migration instructions:\n   It only breaks SpriteButton when you are using future as a sprite or pressedSprite parameter.\n   You should use SpriteButton.future if previously you are using future as a parameter.\n\n## 1.1.1\n\n - **REFACTOR**: Added classes MoveByEffect and MoveToEffect (#1524). ([2171a119](https://github.com/flame-engine/flame/commit/2171a119378855872f6bece37edc95b3d68f28ae))\n - **FIX**: Invalidate polygon cache on resize (#1529). ([11bf75d0](https://github.com/flame-engine/flame/commit/11bf75d074fe9c0d3e043ce43611a1bb1824dd40))\n - **FIX**: Bug with anchor parameter in Sprite.render() (#1508). ([325df46e](https://github.com/flame-engine/flame/commit/325df46e19ebcd5ac13e3192f4360bacb3de1c37))\n - **FIX**: Make CollisionProspect's a, b have unordered equality (#1519). ([5b2471c8](https://github.com/flame-engine/flame/commit/5b2471c8ae29a1313db3b2c21dee6d4654a0132c))\n - **FEAT**: able to clear all overlays (#1536). ([7b15c9a1](https://github.com/flame-engine/flame/commit/7b15c9a1ca58c19265e65899e27c65146d42788c))\n - **FEAT**: Automatic Isometric Grid scaling (#1468). ([cae8c0ce](https://github.com/flame-engine/flame/commit/cae8c0ceb395416ed86fd644c1dd7790eae127ca))\n - **FEAT**: Added children parameter to Component constructor (#1525). ([f0b31fcf](https://github.com/flame-engine/flame/commit/f0b31fcfc0fc6b0f8f96895ef6a68fd5a30a3159))\n - **FEAT**: Camera's Viewfinder can now be affected by rotation effects (#1527). ([f46cae04](https://github.com/flame-engine/flame/commit/f46cae040e34f6037a9e0a7e259bf22b9dff7acb))\n - **FEAT**: Scale (zoom) effects can now be applied to Viewfinder in CameraComponent (#1514). ([403b6e60](https://github.com/flame-engine/flame/commit/403b6e60433f5e059b81298fd4b39a77957932fb))\n - **FEAT**: adding HasGameRef.mockGameRef (#1520). ([4f389f8b](https://github.com/flame-engine/flame/commit/4f389f8b88b181832316bae551e31a3a70907ee7))\n - **FEAT**: flame tests can now generate golden tests (#1501). ([316a0b3b](https://github.com/flame-engine/flame/commit/316a0b3bb0996ed20a3b93175102524b38bfa3e2))\n\n## 1.1.0\n\n - Graduate package to a stable release. See pre-releases prior to this version for changelog entries.\n\n## 1.1.0-releasecandidate.6\n\n - **FIX**: Only end collisions where there was a collision (#1471). ([e1e87fc4](https://github.com/flame-engine/flame/commit/e1e87fc42226c1db2f472377901031277349beb3))\n - **FIX**: `debugMode` should be inherited from parent when mounted (#1469). ([e894d201](https://github.com/flame-engine/flame/commit/e894d20133f6e142c67286c449135e37e892f35b))\n - **FEAT**: Added method that returned descendants (#1461). ([a41f5376](https://github.com/flame-engine/flame/commit/a41f53762ab49bb3d51f1f96c37b934a7ab83844))\n - **FEAT**: Possibility to mark gesture events as handled (#1465). ([4c3960c3](https://github.com/flame-engine/flame/commit/4c3960c3418f8ff4d557c1764c6793468238a8da))\n - **FEAT**: adding loaded future to the component (#1466). ([6434829b](https://github.com/flame-engine/flame/commit/6434829b45cc131719fd950ef2d262d0bfbdff1b))\n - **FEAT**: Deprecating Rect methods (#1455). ([4ddd90aa](https://github.com/flame-engine/flame/commit/4ddd90aafc40a3f5ce3d9b181a66369436de3c9c))\n - **FEAT**: Added .anchor property to CameraComponent.Viewfinder (#1458). ([d51dc5e1](https://github.com/flame-engine/flame/commit/d51dc5e132bc3ba5763be4de36131d3739a6c906))\n - **DOCS**: `Rect` extension docs is out of date (#1451). ([7e505722](https://github.com/flame-engine/flame/commit/7e505722491dd03fea6d2329ff4df2447143d45b))\n\n## 1.1.0-releasecandidate.5\n\n - **FIX**: `@mustCallSuper` missing on components (#1443). ([e01b4b1a](https://github.com/flame-engine/flame/commit/e01b4b1ac3e423037fa313672b4882e7d29210b8))\n - **FEAT**: Add setter to priority (#1444). ([34284686](https://github.com/flame-engine/flame/commit/342846860af36ed73a1fc0a9a76ed9add12cec71))\n\n## 1.1.0-releasecandidate.4\n\n - **FIX**: Setting images.prefix to empty string (#1437). ([694102bd](https://github.com/flame-engine/flame/commit/694102bd0304736ed3bdfbd596d64901d7adf57f))\n\n## 1.1.0-releasecandidate.3\n\n - **REFACTOR**: Parent change and component removal logic (#1385). ([8b9fa352](https://github.com/flame-engine/flame/commit/8b9fa3521cc44f7696c5ce0b396e3007c2ae7e8c))\n - **FIX**: viewfinders behavior under zoom (#1432). ([f3cf85b6](https://github.com/flame-engine/flame/commit/f3cf85b638cc71058e85756498e79971a1942491))\n - **FIX**: change strokeWidth in Component (#1431). ([0e174fe8](https://github.com/flame-engine/flame/commit/0e174fe8e5f1262af41c8659c0fce7ed060e69a9))\n - **FEAT**: allowing changing of the images prefix and allowing empty prefixes (#1433). ([de4d9416](https://github.com/flame-engine/flame/commit/de4d941654710add459cc1c923b92c3923556f15))\n\n## 1.1.0-releasecandidate.2\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Loadable mixin no longer declares onMount and onRemove (#1243). ([b1f6a34c](https://github.com/flame-engine/flame/commit/b1f6a34c198a732d51471bf0b79a71a4f3e60973))\n - **REFACTOR**: Organize tests in the game/ folder (#1403). ([102a27cc](https://github.com/flame-engine/flame/commit/102a27cc75d15e1c0ec867d739c9ce3f7feaff56))\n - **REFACTOR**: Clean up of top-level tests (#1386). ([e50003ed](https://github.com/flame-engine/flame/commit/e50003ed609fabe4268ceaa1e728b6f29f05939e))\n - **REFACTOR**: Resize logic in GameRenderBox (#1308). ([17c45c28](https://github.com/flame-engine/flame/commit/17c45c28291862ba2c7c079fe91e994a71b1d807))\n - **REFACTOR**: Simplify GameWidgetState.loaderFuture (#1232). ([eb30c2e5](https://github.com/flame-engine/flame/commit/eb30c2e5e9b10e388a9d56283b90e3f09c5f9379))\n - **REFACTOR**: Component.ancestors() is now an iterator (#1242). ([ce48d77a](https://github.com/flame-engine/flame/commit/ce48d77ad72a3c5f865c7ec40b753678a2fbebe4))\n - **REFACTOR**: Add a few more rules to flame_lint, including use_key_in_widget_constructors (#1248). ([bac6c8a4](https://github.com/flame-engine/flame/commit/bac6c8a4469f2c5c2926335f2f589eec9b1a5b5b))\n - **REFACTOR**: Removed parameter Component.updateTree({callOwnUpdate}) (#1224). ([ed227e7c](https://github.com/flame-engine/flame/commit/ed227e7c74bae51061e9622fe6852cf020ce6fa6))\n - **REFACTOR**: Remove Loadable, optional onLoads (#1333). ([05f7a4c3](https://github.com/flame-engine/flame/commit/05f7a4c3d6b1e3b67575c4ec920cf270691bbab4))\n - **REFACTOR**: Loadable no longer declares onGameResize (#1329). ([20776e86](https://github.com/flame-engine/flame/commit/20776e8659bda57813ddc14856502d4b07b85fef))\n - **REFACTOR**: Use canvas.drawImageNine in NineTileBox (#1314). ([d77e5efe](https://github.com/flame-engine/flame/commit/d77e5efee573ddcbc84a50be7728d7a9207287f7))\n - **PERF**: Allow components to have null children (#1231). ([66ad4b08](https://github.com/flame-engine/flame/commit/66ad4b08af6153fb767667a7bed42dac6fb8f2c7))\n - **FIX**: flame svg perfomance (#1373). ([bce24173](https://github.com/flame-engine/flame/commit/bce2417330b5165842f15d0409a213a1c5ad1cd3))\n - **FIX**: Fix collision detection comments and typo (#1422). ([dfeafdd6](https://github.com/flame-engine/flame/commit/dfeafdd6f3e962d6f5148340ab461a9e805652b7))\n - **FIX**: `ParallaxComponent` should have static `positionType` (#1350). ([cfa6bd12](https://github.com/flame-engine/flame/commit/cfa6bd127be620f6016442a269a479d162241a11))\n - **FIX**: Add missing `priority` argument for `JoystickComponent` (#1227). ([23b1dd8b](https://github.com/flame-engine/flame/commit/23b1dd8bbcc98ae3c59a86c10e03b074982b6adc))\n - **FIX**: Step time in SpriteAnimation must be positive (#1387). ([08e8eac1](https://github.com/flame-engine/flame/commit/08e8eac1734a63111a5b7aba4e1bfd20d503aaf4))\n - **FIX**: HudMarginComponent positioning on zoom (#1250). ([4f0fb2de](https://github.com/flame-engine/flame/commit/4f0fb2de6f12ad950705ddb75ebd2e80114321e5))\n - **FIX**: Call onCollisionEnd on removal of Collidable (#1247). ([5ddcc6f7](https://github.com/flame-engine/flame/commit/5ddcc6f7baaae60996747d37b4d92f4f890c7fa2))\n - **FIX**: Both places should have `strictMode = false` (#1272). ([72161ad8](https://github.com/flame-engine/flame/commit/72161ad8d2f2a0916f4448d67644272fdc9ceace))\n - **FIX**: remove vector_math dependency (#1361). ([56b33da2](https://github.com/flame-engine/flame/commit/56b33da29cfe547db75c89d97a09550a324fb0f5))\n - **FIX**: Deprecate pause and resume in GameLoop (#1240). ([dc37053f](https://github.com/flame-engine/flame/commit/dc37053fb6ed46bb68cebab0ed82248051ddf86a))\n - **FIX**: Deprecate Images.decodeImageFromPixels (#1318). ([1a80130c](https://github.com/flame-engine/flame/commit/1a80130c6632cc8b1f34c19aa928ac66364ecbe5))\n - **FIX**: Properly dispose images when cache is cleared (#1312). ([825fb0cc](https://github.com/flame-engine/flame/commit/825fb0cc7e5b30911e17a2075e28f74c8d69b593))\n - **FIX**: Fix SpriteAnimationWidget lifecycle (#1212). ([86394dd3](https://github.com/flame-engine/flame/commit/86394dd3e05079494c7c3c000c3104712faf7507))\n - **FIX**: redrawing bug in TextBoxComponent (#1279). ([8bef4805](https://github.com/flame-engine/flame/commit/8bef480597024f51ac2e4f534e1977f53d768df2))\n - **FIX**: Add missing paint argument to `SpriteComponent.fromImage` (#1294). ([254a60c8](https://github.com/flame-engine/flame/commit/254a60c8475da218a61d2b179d894f469efe5486))\n - **FIX**: black frame when activating overlays (#1093). ([85caf463](https://github.com/flame-engine/flame/commit/85caf463f48ce34fdf51bfd0f511c8188dcf4481))\n - **FIX**: `prepareComponent` should never run again on a prepared component (#1237). ([7d3eeb73](https://github.com/flame-engine/flame/commit/7d3eeb73c588f2465472cd6069f28d6136b0721d))\n - **FIX**: Allow most basic and advanced gesture detectors together (#1208). ([5828b6f3](https://github.com/flame-engine/flame/commit/5828b6f369b74b8f1ab2cc42905c647bbc7dfda5))\n - **FEAT**: Added SpeedEffectController (#1260). ([20f521f5](https://github.com/flame-engine/flame/commit/20f521f5beb5ee476d345d1766a30b4ba35c079b))\n - **FEAT**: Added SineEffectController (#1262). ([c888703d](https://github.com/flame-engine/flame/commit/c888703d6e002fe5f15a82d6204a0639f92aa66a))\n - **FEAT**: Added ZigzagEffectController (#1261). ([59adc5f3](https://github.com/flame-engine/flame/commit/59adc5f34c2eebd336f7d39a703a6845227b55ed))\n - **FEAT**: Add onReleased callback for HudButtonComponent (#1296). ([87ee34ca](https://github.com/flame-engine/flame/commit/87ee34cac72b6d09b8c8f870433541361ff383c1))\n - **FEAT**: Turn off `strictMode` for children (#1271). ([6936e1d9](https://github.com/flame-engine/flame/commit/6936e1d98b63c071787d3dea09fad7659bdf0473))\n - **FEAT**: `onCollisionStart` for `Collidable` and `HitboxShape` (#1251). ([9b95686b](https://github.com/flame-engine/flame/commit/9b95686ba57c16c9f029f920150e112d180bd584))\n - **FEAT**: adding has mounted to component (#1418). ([f8f9e045](https://github.com/flame-engine/flame/commit/f8f9e0451309bfdd29ec8cefbf9d8187209a314c))\n - **FEAT**: Added NoiseEffectController (#1356). ([fad9d1d5](https://github.com/flame-engine/flame/commit/fad9d1d54f4c3500611f82a9382ffa1fed9b52b8))\n - **FEAT**: exporting cache classes (#1368). ([3e058973](https://github.com/flame-engine/flame/commit/3e0589730c49663b5c4863fc28718b3fa81b7b60))\n - **FEAT**: Update scale events to contain pan info (#1327). ([70b96b07](https://github.com/flame-engine/flame/commit/70b96b071a8e936b5c5d6014cb18277b76c646db))\n - **FEAT**: Components are now always added in the correct order (#1337). ([c753fc46](https://github.com/flame-engine/flame/commit/c753fc4636d337d850a5a5cc684be8155f08b214))\n - **FEAT**: Added `transform` to `Rect` (#1360). ([1818be41](https://github.com/flame-engine/flame/commit/1818be41761015b33aee820a0a02f50327a4df4e))\n - **FEAT**: Camera as a component (#1355). ([c61a1c18](https://github.com/flame-engine/flame/commit/c61a1c18b5bdd0b27f3ab21d73d8bbddffd48ba2))\n - **FEAT**: Effect.onComplete callback as an alternative to onFinish() (#1201). ([932a8111](https://github.com/flame-engine/flame/commit/932a81118b0faba80def677cd0db28a598e15204))\n - **FEAT**: Add RandomEffectController (#1203). ([cdb2650b](https://github.com/flame-engine/flame/commit/cdb2650b29bee6e8412a666f9f49fabb68ce0265))\n - **FEAT**: `Component.childrenFactory` can be used to set up a global `ComponentSet` factory (#1193). ([223ab758](https://github.com/flame-engine/flame/commit/223ab75886ab018053cd75af33560a03e1b9d470))\n - **DOCS**: Added documentation for GameLoop class (#1234). ([b1d4e587](https://github.com/flame-engine/flame/commit/b1d4e5872e970f8bd4020a051c35b5cac4093b5e))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n - **BREAKING** **REFACTOR**: Separate ComponentSet from the Component (#1266). ([e2655b88](https://github.com/flame-engine/flame/commit/e2655b8817411ae6b1c505719fed75a170f67aeb))\n - **BREAKING** **FIX**: Remove pointerId from Draggable callbacks (#1313). ([27adda17](https://github.com/flame-engine/flame/commit/27adda17b7b4d8c229cca53799826c7b854eae95))\n - **BREAKING** **FEAT**: Use a broadphase to make collision detection more efficient (#1252). ([29dd09ca](https://github.com/flame-engine/flame/commit/29dd09ca925e934f3ca4e266a8a0cdb8ad62ef3b))\n - **BREAKING** **FEAT**: Added SequenceEffect (#1218). ([7c6ae6de](https://github.com/flame-engine/flame/commit/7c6ae6def36ae5feb813fb2ba15d6fb3b9aaf341))\n\n## 1.1.0-releasecandidate.1\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Clean up of top-level tests (#1386). ([e50003ed](https://github.com/flame-engine/flame/commit/e50003ed609fabe4268ceaa1e728b6f29f05939e))\n - **REFACTOR**: Remove Loadable, optional onLoads (#1333). ([05f7a4c3](https://github.com/flame-engine/flame/commit/05f7a4c3d6b1e3b67575c4ec920cf270691bbab4))\n - **REFACTOR**: Loadable no longer declares onGameResize (#1329). ([20776e86](https://github.com/flame-engine/flame/commit/20776e8659bda57813ddc14856502d4b07b85fef))\n - **REFACTOR**: Organize tests in the game/ folder (#1403). ([102a27cc](https://github.com/flame-engine/flame/commit/102a27cc75d15e1c0ec867d739c9ce3f7feaff56))\n - **REFACTOR**: Use canvas.drawImageNine in NineTileBox (#1314). ([d77e5efe](https://github.com/flame-engine/flame/commit/d77e5efee573ddcbc84a50be7728d7a9207287f7))\n - **REFACTOR**: Resize logic in GameRenderBox (#1308). ([17c45c28](https://github.com/flame-engine/flame/commit/17c45c28291862ba2c7c079fe91e994a71b1d807))\n - **REFACTOR**: Loadable mixin no longer declares onMount and onRemove (#1243). ([b1f6a34c](https://github.com/flame-engine/flame/commit/b1f6a34c198a732d51471bf0b79a71a4f3e60973))\n - **REFACTOR**: Removed parameter Component.updateTree({callOwnUpdate}) (#1224). ([ed227e7c](https://github.com/flame-engine/flame/commit/ed227e7c74bae51061e9622fe6852cf020ce6fa6))\n - **REFACTOR**: Add a few more rules to flame_lint, including use_key_in_widget_constructors (#1248). ([bac6c8a4](https://github.com/flame-engine/flame/commit/bac6c8a4469f2c5c2926335f2f589eec9b1a5b5b))\n - **REFACTOR**: Component.ancestors() is now an iterator (#1242). ([ce48d77a](https://github.com/flame-engine/flame/commit/ce48d77ad72a3c5f865c7ec40b753678a2fbebe4))\n - **REFACTOR**: Simplify GameWidgetState.loaderFuture (#1232). ([eb30c2e5](https://github.com/flame-engine/flame/commit/eb30c2e5e9b10e388a9d56283b90e3f09c5f9379))\n - **PERF**: Allow components to have null children (#1231). ([66ad4b08](https://github.com/flame-engine/flame/commit/66ad4b08af6153fb767667a7bed42dac6fb8f2c7))\n - **FIX**: `prepareComponent` should never run again on a prepared component (#1237). ([7d3eeb73](https://github.com/flame-engine/flame/commit/7d3eeb73c588f2465472cd6069f28d6136b0721d))\n - **FIX**: flame svg perfomance (#1373). ([bce24173](https://github.com/flame-engine/flame/commit/bce2417330b5165842f15d0409a213a1c5ad1cd3))\n - **FIX**: Deprecate pause and resume in GameLoop (#1240). ([dc37053f](https://github.com/flame-engine/flame/commit/dc37053fb6ed46bb68cebab0ed82248051ddf86a))\n - **FIX**: Deprecate Images.decodeImageFromPixels (#1318). ([1a80130c](https://github.com/flame-engine/flame/commit/1a80130c6632cc8b1f34c19aa928ac66364ecbe5))\n - **FIX**: Properly dispose images when cache is cleared (#1312). ([825fb0cc](https://github.com/flame-engine/flame/commit/825fb0cc7e5b30911e17a2075e28f74c8d69b593))\n - **FIX**: Add missing paint argument to `SpriteComponent.fromImage` (#1294). ([254a60c8](https://github.com/flame-engine/flame/commit/254a60c8475da218a61d2b179d894f469efe5486))\n - **FIX**: Add missing `priority` argument for `JoystickComponent` (#1227). ([23b1dd8b](https://github.com/flame-engine/flame/commit/23b1dd8bbcc98ae3c59a86c10e03b074982b6adc))\n - **FIX**: remove vector_math dependency (#1361). ([56b33da2](https://github.com/flame-engine/flame/commit/56b33da29cfe547db75c89d97a09550a324fb0f5))\n - **FIX**: redrawing bug in TextBoxComponent (#1279). ([8bef4805](https://github.com/flame-engine/flame/commit/8bef480597024f51ac2e4f534e1977f53d768df2))\n - **FIX**: Fix SpriteAnimationWidget lifecycle (#1212). ([86394dd3](https://github.com/flame-engine/flame/commit/86394dd3e05079494c7c3c000c3104712faf7507))\n - **FIX**: black frame when activating overlays (#1093). ([85caf463](https://github.com/flame-engine/flame/commit/85caf463f48ce34fdf51bfd0f511c8188dcf4481))\n - **FIX**: Call onCollisionEnd on removal of Collidable (#1247). ([5ddcc6f7](https://github.com/flame-engine/flame/commit/5ddcc6f7baaae60996747d37b4d92f4f890c7fa2))\n - **FIX**: HudMarginComponent positioning on zoom (#1250). ([4f0fb2de](https://github.com/flame-engine/flame/commit/4f0fb2de6f12ad950705ddb75ebd2e80114321e5))\n - **FIX**: Both places should have `strictMode = false` (#1272). ([72161ad8](https://github.com/flame-engine/flame/commit/72161ad8d2f2a0916f4448d67644272fdc9ceace))\n - **FIX**: `ParallaxComponent` should have static `positionType` (#1350). ([cfa6bd12](https://github.com/flame-engine/flame/commit/cfa6bd127be620f6016442a269a479d162241a11))\n - **FIX**: Allow most basic and advanced gesture detectors together (#1208). ([5828b6f3](https://github.com/flame-engine/flame/commit/5828b6f369b74b8f1ab2cc42905c647bbc7dfda5))\n - **FIX**: Step time in SpriteAnimation must be positive (#1387). ([08e8eac1](https://github.com/flame-engine/flame/commit/08e8eac1734a63111a5b7aba4e1bfd20d503aaf4))\n - **FEAT**: Update scale events to contain pan info (#1327). ([70b96b07](https://github.com/flame-engine/flame/commit/70b96b071a8e936b5c5d6014cb18277b76c646db))\n - **FEAT**: Add RandomEffectController (#1203). ([cdb2650b](https://github.com/flame-engine/flame/commit/cdb2650b29bee6e8412a666f9f49fabb68ce0265))\n - **FEAT**: Components are now always added in the correct order (#1337). ([c753fc46](https://github.com/flame-engine/flame/commit/c753fc4636d337d850a5a5cc684be8155f08b214))\n - **FEAT**: Effect.onComplete callback as an alternative to onFinish() (#1201). ([932a8111](https://github.com/flame-engine/flame/commit/932a81118b0faba80def677cd0db28a598e15204))\n - **FEAT**: exporting cache classes (#1368). ([3e058973](https://github.com/flame-engine/flame/commit/3e0589730c49663b5c4863fc28718b3fa81b7b60))\n - **FEAT**: Added NoiseEffectController (#1356). ([fad9d1d5](https://github.com/flame-engine/flame/commit/fad9d1d54f4c3500611f82a9382ffa1fed9b52b8))\n - **FEAT**: Added SineEffectController (#1262). ([c888703d](https://github.com/flame-engine/flame/commit/c888703d6e002fe5f15a82d6204a0639f92aa66a))\n - **FEAT**: Added SpeedEffectController (#1260). ([20f521f5](https://github.com/flame-engine/flame/commit/20f521f5beb5ee476d345d1766a30b4ba35c079b))\n - **FEAT**: Added ZigzagEffectController (#1261). ([59adc5f3](https://github.com/flame-engine/flame/commit/59adc5f34c2eebd336f7d39a703a6845227b55ed))\n - **FEAT**: Turn off `strictMode` for children (#1271). ([6936e1d9](https://github.com/flame-engine/flame/commit/6936e1d98b63c071787d3dea09fad7659bdf0473))\n - **FEAT**: `onCollisionStart` for `Collidable` and `HitboxShape` (#1251). ([9b95686b](https://github.com/flame-engine/flame/commit/9b95686ba57c16c9f029f920150e112d180bd584))\n - **FEAT**: `Component.childrenFactory` can be used to set up a global `ComponentSet` factory (#1193). ([223ab758](https://github.com/flame-engine/flame/commit/223ab75886ab018053cd75af33560a03e1b9d470))\n - **FEAT**: Added `transform` to `Rect` (#1360). ([1818be41](https://github.com/flame-engine/flame/commit/1818be41761015b33aee820a0a02f50327a4df4e))\n - **FEAT**: Add onReleased callback for HudButtonComponent (#1296). ([87ee34ca](https://github.com/flame-engine/flame/commit/87ee34cac72b6d09b8c8f870433541361ff383c1))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n - **DOCS**: Added documentation for GameLoop class (#1234). ([b1d4e587](https://github.com/flame-engine/flame/commit/b1d4e5872e970f8bd4020a051c35b5cac4093b5e))\n - **BREAKING** **REFACTOR**: Separate ComponentSet from the Component (#1266). ([e2655b88](https://github.com/flame-engine/flame/commit/e2655b8817411ae6b1c505719fed75a170f67aeb))\n - **BREAKING** **FIX**: Remove pointerId from Draggable callbacks (#1313). ([27adda17](https://github.com/flame-engine/flame/commit/27adda17b7b4d8c229cca53799826c7b854eae95))\n - **BREAKING** **FEAT**: Added SequenceEffect (#1218). ([7c6ae6de](https://github.com/flame-engine/flame/commit/7c6ae6def36ae5feb813fb2ba15d6fb3b9aaf341))\n\n# CHANGELOG\n\n## [1.0.0]\n - Add `ButtonComponent` backed by two `PositionComponent`s\n - Add `SpriteButtonComponent` backed by two `Sprite`s\n - Allow more flexible construction of `EffectController`s and make them able to run back in time\n - Remove old effects system\n - Export new effects system\n - Introduce `updateTree` to follow the `renderTree` convention\n - Fix `Parallax.load` with different loading times\n - Fix render order of components and add tests\n - Fix `HitboxCircle` when component is flipped\n - Add `ColorEffect`\n - `MoveAlongPathEffect` can now be absolute, and can auto-orient the object along the path\n - `ScaleEffect.by` now applies multiplicatively instead of additively\n - `isHud` replaced with `PositionType`\n - Remove web fallback for `drawAtlas` in SpriteBatch, but added flag `useAtlas` to activate it\n\n## [1.0.0-releasecandidate.18]\n - Forcing portrait and landscape mode is now supported on web\n - Fixed margin calculation in `HudMarginComponent` when using a viewport\n - Fixed position calculation in `HudMarginComponent` when using a viewport\n - Add noClip option to `FixedResolutionViewport`\n - Add a few missing helpers to SpriteAnimation\n\n## [1.0.0-releasecandidate.17]\n - Added `StandardEffectController` class\n - Refactored `Effect` class to use `EffectController`, added `Transform2DEffect` class\n - Clarified `TimerComponent` example\n - Fixed pause and resume engines when `GameWidget` had rebuilds\n - Removed `runOnCreation` attribute in favor of the `paused` attribute on `FlameGame`\n - Add `CustomPainterComponent`\n - Alternative implementation of `RotateEffect`, based on `Transform2DEffect`\n - Alternative implementation of `MoveEffect`, based on `Transform2DEffect`\n - Fix `onGameResize` margin bug in `HudMarginComponent`\n - `PositionComponent.size` now returns a `NotifyingVector2`\n - Possibility to manually remove `TimerComponent`\n - Rename `Hitbox` mixin to `HasHitboxes`\n - Added `RemoveEffect` and `SimpleEffectController`\n - Create default implementations of `RectangleComponent`, `CircleComponent` and `PolygonComponent`\n - Streamlined the argument list for all components extending `PositionComponent`\n - Improved interaction between viewport and isHud components\n - `randomColor` method in the `Color` extension\n - Calling super-method in `.render()` is now optional\n - Components that manipulate canvas state are now responsible for saving/restoring that state\n - Remove `super.render` calls that are no longer needed\n - Fixed typo in error message\n - `TextPaint` to use `TextStyle` (material) instead of `TextPaintConfig`\n - Underlying `Shape`s in `ShapeComponent` transform with components position, size and angle\n - `HitboxShape` takes parents ancestors transformations into consideration (not scaling)\n - Fixed black frame when updating game widget (ex: adding overlays)\n - Added possibility to extend `JoystickComponent`\n - Renamed `FlameTester` to `GameTester`\n - Modified `FlameTester` to be specific for `T extends FlameGame`\n - Improved `TimerComponent`\n - Removed methods `preRender()` and `postRender()` in `Component`\n - Use `FlameTester` everywhere where it makes sense in the tests\n - Improved `IsometricTileMap`\n - Fix `PositionComponent`'s `flipHorizontallyAroundCenter` and `flipVerticallyAroundCenter`\n - Initialization of all `PositionComponent`s can be done from `onLoad` instead of the constructor\n - Rename `HasTappableComponents` to `HasTappables`\n - Rename `HasDraggableComponents` to `HasDraggables`\n - Rename `HasHoverableComponents` to `HasHoverableis`\n - Added `SizeEffect` backed by the new effects engine\n - Added `ScaleEffect` backed by the new effects engine\n - Added `OpacityEffect` backed by the new effects engine\n - Update `OrderedSet` to 4.1.0\n - Update `OrderedSet` to 5.0.0\n\n## [1.0.0-releasecandidate.16]\n - `changePriority` no longer breaks game loop iteration\n - Move component mixin checks to their own files\n - Fix exception when game was rebuilt\n - Add `@mustCallSuper` on `Component.render`\n - Add `SpriteSheet.createAnimationVariable` method to allow animations with different `stepTime` for each sprite\n - Use the full delta in `JoystickComponent` so that it can't go to the wrong direction on the wrong side\n - Improved the menu for documentation version selection\n - Introduce `onDoubleTapDown` with info and `onDoubleTapCancel`\n - Changed `onHoverEnter` and `onHoverLeave` to return `bool` (breaking change)\n - Improved \"move effect\" example in the Dashbook\n - Use documentation versions generated from flame-docs-site\n\n## [1.0.0-releasecandidate.15]\n - Fix issue with `Draggable`s not being removed from `draggables` list\n - Increase Flutter SDK constraint to `>= 2.5.0`.\n - Method `PositionComponent.toRect()` now works for flipped/rotated components.\n - Make the root bundle exposed via `Flame.bundle` actually configurable\n - Take in an optional `Camera` as a parameter to `FlameGame`\n - Make super.onLoad mandatory to avoid memory leaks\n - `QueryableOrderedSet`'s `strictMode` is configurable so it is no longer necessary to call `register` before `query`\n - Add option to rotate `SpriteWidget`\n - Fix bug where `onRemove` was called during resizing\n - Add `onAttach` and `onDetach` to `Game`\n\n## [1.0.0-releasecandidate.14]\n - Reset effects after they are done so that they can be repeated\n - Remove integrated joystick buttons\n - Add `MarginHudComponent`, used when components need to have a margin to the viewport edge\n - Refactor `JoystickComponent`\n - Add `SpriteAnimationWidget.asset`\n - Add `SpriteWidget.asset`\n - Add `SpriteButton.asset`\n - Add `NineTileBox.asset`\n - Fix resolution of `TextBoxComponent`\n - Add `BaseGame.remove` and `BaseGame.removeAll` helpers for removing components\n - Add `BaseComponent.remove` and `BaseComponent.removeAll` helpers for removing children\n - Rename `Camera.cameraSpeed` to `Camera.speed`\n - Rename `addShape` to `addHitbox` in `Hitbox` mixin\n - Fix bug with Events and Draggables\n - Add generics to components with HasGameRef so that they can be extended and have another gameRef\n - Fix parallax fullscreen bug when game is resized\n - Generalize `paint` usage on components\n - Create `OpacityEffect`\n - Create `ColorEffect`\n - Adding ability to pause `SpriteAnimationComponent`\n - Adding `SpriteGroupComponent`\n - Fix truncated last frame in non-looping animations\n - Default size of `SpriteComponent` is `srcSize` instead of spritesheet size\n - Export test helper methods\n - Rename `ScaleEffect` to `SizeEffect`\n - Introduce `scale` on `PositionComponent`\n - Add `ScaleEffect` that works on `scale` instead of `size`\n - Add class `NotifyingVector2`\n - Add class `Transform2D`\n - Added helper functions `testRandom()` and `testWidgetsRandom()`\n - Remove `FPSCounter` from `BaseGame`\n - Refactor `PositionComponent` to work with `Transform2D`: the component now works more reliably\n   when nested\n - Properties `renderFlipX`, `renderFlipY` removed and replaced with methods\n   `flipHorizontally()` and `flipVertically()`.\n - Method `.angleTo` removed as it was not working properly.\n - In debug mode `PositionComponent` now displays an indicator for the anchor position.\n - Update `Camera` docs to showcase usage with `Game` class\n - Fixed a bug with `worldBounds` being set to `null` in `Camera`\n - Remove `.viewport` from `BaseGame`, use `camera.viewport` instead\n - `MockCanvas` is now strongly typed and matches numeric coordinates up to a tolerance\n - Add `loadAllImages` to `Images`, which loads all images from the prefixed path\n - Reviewed the keyboard API with new mixins (`KeyboardHandler` and `HasKeyboardHandlerComponents`)\n - Added `FocusNode` on the game widget and improved keyboard handling in the game.\n - Added ability to have custom mouse cursor on the `GameWidget` region\n - Change sprite component to default to the Sprite size if not provided\n - `TextBoxComponent` waits for cache to be filled on `onLoad`\n - `TextBoxComponent` can have customizable `pixelRatio`\n - Add `ContainsAtLeastMockCanvas` to facilitate testing with `MockCanvas`\n - Support for `drawImage` for `MockCanvas`\n - `Game` is now a `Component`\n - `ComponentEffect` is now a `Component`\n - `HasGameRef` can now operate independently from `Game`\n - `initialDelay` and `peakDelay` for effects to handle time before and after an effect\n - `component.onMount` now runs every time a component gets a new parent\n - Add collision detection between child components\n\n## [1.0.0-releasecandidate.13]\n - Fix camera not ending up in the correct position on long jumps\n - Make the `JoystickPlayer` a `PositionComponent`\n - Extract shared logic when handling components set in BaseComponent and BaseGame to ComponentSet.\n - Rename `camera.shake(amount: x)` to `camera.shake(duration: x)`\n - Fix `SpriteAnimationComponent` docs to use `Future.wait`\n - Add an empty `postRender` method that will run after each components render method\n - Rename `Tapable` to `Tappable`\n - Fix `SpriteAnimationComponent` docs to use `Future.wait`\n - Add an empty `postRender` method that will run after each components render method\n - Rename `HasTapableComponents` to `HasTappableComponents`\n - Rename `prepareCanvas` to `preRender`\n - Add `intensity` to `Camera.shake`\n - `FixedResolutionViewport` to use matrix transformations for `Canvas`\n\n## [1.0.0-releasecandidate.12]\n - Fix link to code in example stories\n - Fix RotateEffect with negative deltas\n - Add isDragged to Draggable\n - Fix anchor of rendered text in TextComponent\n - Add new extensions to handle math.Rectangles nicely\n - Implement color parsing methods\n - Migrated the `Particle` API to `Vector2`\n - Add copyWith function to TextRenderer\n - Fix debug mode is not propagated to children of non-Position components\n - Fix size property of TextComponent was not correctly set\n - Fix anchor property was being incorrectly passed along to text renderer\n - All components take priority as an argument on their constructors\n - Fix renderRotated\n - Use QueryableOrderedSet for Collidables\n - Refactor TextBoxComponent\n - Fix bugs with TextBoxComponent\n - Improve error message for composed components\n - Fix `game.size` to take zoom into consideration\n - Fix `camera.followComponent` when `zoom != 1`\n - Add `anchor` for `ShapeComponent` constructor\n - Fix rendering of polygons in `ShapeComponent`\n - Add `SpriteAnimation` support to parallax\n - Fix `Parallax` alignment for images with different width and height\n - Fix `ImageComposition` image bounds validation\n - Improved the internal `RenderObject` widget performance\n - Add `Matrix4` extensions\n - `Camera.apply` is done with matrix transformations\n - `Camera` zooming is taking current `relativeOffset` into account\n - Fix gestures for when `isHud = true` and `Camera` is modified\n - Fix `Camera` zoom behavior with offset/anchor\n\n## [1.0.0-releasecandidate.11]\n - Replace deprecated analysis option lines-of-executable-code with source-lines-of-code\n - Fix the anchor of SpriteWidget\n - Add test for re-adding previously removed component\n - Add possibility to dynamically change priority of components\n - Add onCollisionEnd to make it possible for the user to easily detect when a collision ends\n - Adding test coverage to packages\n - Possibility to have non-fullscreen ParallaxComponent\n - No need to send size in ParallaxComponent.fromParallax since Parallax already contains it\n - Fix Text Rendering not working properly\n - Add more useful methods to the IsometricTileMap component\n - Add Hoverables\n - Brief semantic update to second tutorial.\n\n## [1.0.0-rc10]\n - Updated tutorial documentation to indicate use of new version\n - Fix bounding box check in collision detection\n - Refactor on flame input system to correctly take camera into account\n - Adding `SpriteAnimationGroupComponent`\n - Allow isometric tile maps with custom heights\n - Add a new renderRect method to Sprite\n - Addresses the TODO to change the camera public APIs to take Anchors for relativePositions\n - Adds methods to support moving the camera relative to its current position\n - Abstracting the text api to allow custom text renderers on the framework\n - Set the same debug mode for children as for the parent when added\n - Fix camera projections when camera is zoomed\n - Fix collision detection system with angle and parentAngle\n - Fix rendering of shapes that aren't HitboxShape\n\n## [1.0.0-rc9]\n - Fix input bug with other anchors than center\n - Fixed `Shape` so that the `position` is now a `late`\n - Updated the documentation for the supported platforms\n - Add clear function to BaseGame to allow the removal of all components\n - Moving tutorials to the Flame main repository\n - Transforming `PaletteEntry.paint` to be a method instead of a getter\n - Adding some more basic colors entries to the `BasicPalette`\n - Fixing Flutter and Dart version constraints\n - Exporting Images and AssetsCache\n - Make `size` and `position` in `PositionComponent` final\n - Add a `snapTo` and `onPositionUpdate` method to the `Camera`\n - Remove the SpriteAnimationComponent when the animation is really done, not when it is on the last frame\n - Revamp all the docs to be up to date with v1.0.0\n - Make Assets and Images caches have a configurable prefix\n - Add `followVector2` method to the `Camera`\n - Make `gameRef` late\n - Fix Scroll example\n - Add a `renderPoint` method to `Canvas`\n - Add zoom to the camera\n - Add `moveToTarget` as an extension method to `Vector2`\n - Bring back collision detection examples\n - Fix collision detection in Collidable with multiple offset shapes\n - Publishing Flame examples on github pages\n\n## 1.0.0-rc8\n - Migrate to null safety\n - Refactor the joystick code\n - Fix example app\n - Rename points to intersectionPoints for collision detection\n - Added CollidableType to make collision detection more efficient\n - Rename CollidableType.static to passive\n - Add srcPosition and srcSize for SpriteComponent\n - Improve collision detection with bounding boxes\n\n## 1.0.0-rc7\n - Moving device related methods (like `fullScreen`) from `util.dart` to `device.dart`\n - Moving render functions from `util.dart` to `extensions/canvas.dart`\n - Adapting ParallaxComponent contructors to match the pattern followed on other components\n - Adapting SpriteBatchComponent constructors to match the pattern used on other components\n - Improving Parallax APIs regarding handling its size and the use outside FCS\n - Enabling direct import of Sprite and SpriteAnimation\n - Renamed `Composition` to `ImageComposition` to prevent confusion with the composition component\n - Added `rotation` and `anchor` arguments to `ImageComposition.add`\n - Added `Image` extensions\n - Added `Color` extensions\n - Change RaisedButton to ElevatedButton in timer example\n - Overhaul the draggables api to fix issues relating to local vs global positions\n - Preventing errors caused by the premature use of size property on game\n - Added a hitbox mixin for PositionComponent to make more accurate gestures\n - Added a collision detection system\n - Added geometrical shapes\n - Fix `SpriteAnimationComponent.shouldRemove` use `Component.shouldRemove`\n - Add assertion to make sure Draggables are safe to add\n - Add utility methods to the Anchor class to make it more \"enum like\"\n - Enable user-defined anchors\n - Added `toImage` method for the `Sprite` class\n - Uniform use of `dt` instead of `t` in all update methods\n - Add more optional arguments for unified constructors of components\n - Fix order that parent -> children render in\n\n## 1.0.0-rc6\n - Use `Offset` type directly in `JoystickAction.update` calculations\n - Changed `parseAnchor` in `examples/widgets` to throw an exception instead of returning null when it cannot parse an anchor name\n - Code improvements and preparing APIs to null-safety\n - BaseComponent removes children marked as shouldRemove during update\n - Use `find` instead of `globstar` pattern in `scripts/lint.sh` as the later isn't enabled by default in bash\n - Fixes Aseprite constructor bug\n - Improve error handling for the onLoad function\n - Add test for child removal\n - Fix bug where `Timer` callback doesn't fire for non-repeating timers, also fixing bug with `Particle` lifespan\n - Adding shortcut for loading Sprites and SpriteAnimation from the global cache\n - Adding loading methods for the different `ParallaxComponent` parts and refactor how the delta velocity works\n - Add tests for `Timer` and fix a bug where `progress` was not reported correctly\n - Refactored the `SpriteBatch` class to be more elegant and to support `Vector2`.\n - Added fallback support for the web on the `SpriteBatch` class\n - Added missing documentation on the `SpriteBatch` class\n - Added an utility method to load a `SpriteBatch` on the `Game` class\n - Updated the `widgets.md` documentation\n - Removing methods `initialDimensions` and `removeGestureRecognizer` to avoid confusion\n - Adding standard for `SpriteComponent` and `SpriteAnimationComponent` constructors\n - Added `Composition`, allows for composing multiple images into a single image.\n - Move files to comply with the dart package layout convention\n - Fix gesture detection bug of children of `PositionComponent`\n - The `game` argument on `GameWidget` is now required\n\n## 1.0.0-rc5\n - Option for overlays to be already visible on the GameWidget\n - Adding game to the overlay builder\n - Rename retreive -> Retrieve\n - Use internal children set in BaseComponent (fixes issue adding multiple children)\n - Remove develop branches from github workflow definition\n - BaseComponent to return UnmodifiableListView for children\n\n## 1.0.0-rc4\n - Rename Dragable -> Draggable\n - Set loop member variable when constructing SpriteAnimationComponent from SpriteAnimationData\n - Effect shouldn't affect unrelated properties on component\n - Fix rendering of children\n - Explicitly define what fields an effect on PositionComponent modifies\n - Properly propagate onMount and onRemove to children\n - Adding Canvas extensions\n - Remove Resizable mixin\n - Use config defaults for TextBoxComponent\n - Fixing Game Render Box for flutter >= 1.25\n - DebugMode to be variable instead of function on BaseGame\n\n## 1.0.0-rc3\n - Fix TextBoxComponent rendering\n - Add TextBoxConfig options; margins and growingBox\n - Fix debugConfig strokeWidth for web\n - Update Forge2D docs\n - Update PR template with removal of develop branch\n - Translate README to Russian\n - Split up Component and PositionComponent to BaseComponent\n - Unify multiple render methods on Sprite\n - Refactored how games are inserted into a flutter tree\n - Refactored the widgets overlay API\n - Creating new way of loading animations and sprites\n - Dragable mixin for components\n - Fix update+render of component children\n - Update documentation for SVG component\n - Update documentation for PositionComponent\n - Adding Component#onLoad\n - Moving size to Game instead of BaseGame\n - Fix bug with ConcurrentModificationError on add in onMount\n\n## 1.0.0-rc2\n - Improve IsometricTileMap and Spritesheet classes\n - Export full vector_math library from extension\n - Added warning about basic and advanced detectors\n - Ensuring sprite animation and sprite animation components don't get NPEs on initialization\n - Refactor timer class\n - include all changed that are included on 0.28.0\n - Rename game#resize to game#onResize\n - Test suite for basic effects\n - Effects duration and test suite for basic effects\n - Pause and resume for effects\n - Fix position bug in parallax effect\n - Simplification of BaseGame. Removal of addLater (add is now addLater) and rename markForRemoval.\n - Unify naming for removal of components from BaseGame\n\n## 1.0.0-rc1\n - Move all box2d related code and examples to the flame_box2d repo\n - Rename Animation to SpriteAnimation\n - Create extension of Vector2 and unify all tuples to use that class\n - Remove Position class in favor of new Vector2 extension\n - Remove Box2D as a dependency\n - Rebuild of Images, Sprite and SpriteAnimation initialization\n - Use isRelative on effects\n - Use Vector2 for position and size on PositionComponent\n - Removing all deprecated methods\n - Rename `resize` method on components to `onGameResize`\n - Make `Resizable` have a `gameSize` property instead of `size`\n - Fix bug with CombinedEffect inside SequenceEffect\n - Fix wrong end angle for relative rotational effects\n - Use a list of Vector2 for Move effect to open up for more advanced move effects\n - Generalize effects api to include all components\n - Extract all the audio related capabilities to a new package, flame_audio\n - Fix bug that sprite crashes without a position\n\n## 0.29.1-beta\n - Fixing Game Render Box for flutter >= 1.25\n\n## 0.29.0\n- Update audioplayers to latest version (now `assets` will not be added to prefixes automatically)\n- Fix lint issues with 0.28.0\n\n## 0.28.0\n- Fix spriteAsWidget deprecation message\n- Add `lineHeight` property to `TextConfig`\n- Adding pause and resume methods to time class\n\n## 0.27.0\n - Improved the accuracy of the `FPSCounter` by using Flutter's internal frame timings.\n - Adding MouseMovementDetector\n - Adding ScrollDetector\n - Fixes BGM error\n - Adding Isometric Tile Maps\n\n## 0.26.0\n - Improving Flame image auto cache\n - Fix bug in the Box2DGame's add and addLater method , when the Component extends BodyComponent and mixin HasGameRef or other mixins ,the mixins will not be set correctly\n\n## 0.25.0\n - Externalizing Tiled support to its own package `flame_tiled`\n - Preventing some crashs that could happen on web when some methods were called\n - Add mustCallSuper to BaseGame `update` and `render` methods\n - Moved FPS code from BaseGame to a mixin, BaseGame uses the new mixin.\n - Deprecate flare API in favor of the package `flame_flare`\n\n## 0.24.0\n - Outsourcing SVG support to an external package\n - Adding MemoryCache class\n - Fixing games crashes on Web\n - Update tiled dependency to 0.6.0 (objects' properties are now double)\n\n## 0.23.0\n - Add Joystick Component\n - Adding BaseGame#markToRemove\n - Upgrade tiled and flutter_svg dependencies\n - onComplete callback for effects\n - Adding Layers\n - Update tiled dep to 0.5.0 and add support for rotation with improved api\n\n## 0.22.1\n - Fix Box2DComponent render priority\n - Fix PositionComponentEffect drifting\n - Add possibility to combine effects\n - Update to newest box2d_flame which fixes torque bug\n - Adding SpriteSheet.fromImage\n\n## 0.22.0\n - Fixing BaseGame tap detectors issues\n - Adding SpriteWidget\n - Adding AnimationWidget\n - Upgrade Flutter SVG to fix for flame web\n - Add linting to all the examples\n - Run linting only on affected and changed examples\n - Add SequenceEffect\n - Fixed bug with travelTime in RotateEffect\n\n## 0.21.0\n- Adding AssetsCache.readBinaryFile\n- Splitting debugMode from recordFps mode\n- Adding support for multi touch tap and drag events\n- Fix animations example\n- Add possibility for infinite and alternating effects\n- Add rotational effect for PositionComponents\n\n## 0.20.2\n- Fix text component bug with anchor being applied twice\n\n## 0.20.1\n- Adding method to load image bases on base64 data url.\n- Fix Box2DGame to follow render priority\n- Fix games trying to use gameRef inside the resize function\n\n## 0.20.0\n- Refactor game.dart classes into separate files\n- Adding a GameLoop class which uses a Ticker for updating game\n- Adding sprites example\n- Made BaseGame non-abstract and removed SimpleGame\n- Adding SpriteButton Widget\n- Added SpriteBatch API, which renders sprites effectively using Canvas.drawAtlas\n- Introducing basic effects API, including MoveEffect and ScaleEffect\n- Adding ContactCallback controls in Box2DGame\n\n## 0.19.1\n - Bump AudioPlayers version to allow for web support\n - Adding Game#pauseEngine and Game#resumeEngine methods\n - Removing FlameBinding since it isn't used and clashes with newest flutter\n\n## 0.19.0\n - Fixing component lifecycle calls on BaseGame#addLater\n - Fixing Component#onDestroy, which was been called multiple times sometimes\n - Fixing Widget Overlay usage over many game instances\n\n## 0.18.3\n- Adding Component#onDestroy\n- Adding Keyboard events API\n- Adding Box2DGame, an extension of BaseGame to simplify lifecycle of Box2D components\n- Add onAnimateComplete for Animation (thanks @diegomgarcia)\n- Adding AnimationComponent#overridePaint\n- Adding SpriteComponent#overridePaint\n- Updating AudioPlayers to enable Web Audio support\n\n## 0.18.2\n- Add loop for AnimationComponent.sequenced() (thanks @wenxiangjiang)\n- TextComponent optimization (thanks @Gericop)\n- Adding Component#onMount\n- Check if chidren are loaded before rendering on ComposedComponent (thanks @wenxiangjiang)\n- Amend type for width and height properties on Animation.sequenced (thanks @wenxiangjiang)\n- Fixing Tapable position checking\n- Support line feed when create animation from a single image source (thanks @wenxiangjiang)\n- Fixing TextBoxComponent start/end of line bugs (thanks @kurtome)\n- Prevent widgets overlay controller from closing when in debug mode\n\n\n## 0.18.1\n- Expose stepTime paramter from the Animation class to the animation component\n- Updated versions for bugfixes + improved macOS support. (thanks @flowhorn)\n- Update flutter_svg to 0.17.1 (thanks @flowhorn)\n- Update audioplayers to 0.14.0 (thanks @flowhorn)\n- Update path_provider to 1.6.0 (thanks @flowhorn)\n- Update ordered_set to 1.1.5 (thanks @flowhorn)\n\n## 0.18.0\n- Improving FlareComponent API and updating FlareFlutter dependency\n- Adding HasWidgetsOverlay mixin\n- Adding NineTileBox widget\n\n## 0.17.4\n- Fixing compilations errors regarding changes on `box2_flame`\n- Add splash screen docs\n\n## 0.17.3\n- Tweaking text box rendering to reduce pixelated text (thanks, @kurtome)\n- Adding NineTileBox component\n\n## 0.17.2\n- Added backgroundColor method for overriding the game background (thanks @wolfenrain)\n- Update AudioPlayers version to 0.13.5\n- Bump SVG dependency plus fix example app\n\n## 0.17.1\n- Added default render function for Box2D ChainShape\n- Adding TimerComponent\n- Added particles subsystem (thanks @av)\n\n## 0.17.0\n- Fixing FlareAnimation API to match convention\n- Fixing FlareComponent renderization\n- New GestureDetector API to Game\n\n## 0.16.1\n- Added `Bgm` class for easy looping background music management.\n- Added options for flip rendering of PositionComponents easily (horizontal and vertical).\n\n## 0.16.0\n- Improve our mixin structure (breaking change)\n- Adds HasGameRef mixin\n- Fixes for ComposedComponent (for tapables and other apis using preAdd)\n- Added no-parameter alias functions for setting the game's orientation.\n- Prevent double completion on onMetricsChanged callback\n\n## 0.15.2\n- Exposing tile objects on TiledComponent (thanks @renatoferreira656)\n- Adding integrated API for taps on Game class and adding Tapeables mixin for PositionComponents\n\n## 0.15.1\n- Bumped version of svg dependency\n- Fixed warnings\n\n## 0.15.0\n- Refactoring ParallaxComponent (thanks @spydon)\n- Fixing flare animation with embed images\n- Adding override paint parameter to Sprite, and refactoring it have named optional parameters\n\n## 0.14.2\n- Refactoring BaseGame debugMode\n- Adding SpriteSheet class\n- Adding Flame.util.spriteAsWidget\n- Fixing AnimationComponent.empty()\n- Fixing FlameAudio.loopLongAudio\n\n## 0.14.1\n- Fixed build on travis\n- Updated readme badges\n- Fixed changelog\n- Fixed warning on AudioPool, added AudioPool example in docs\n\n## 0.14.0\n- Adding Timer#isRunning method\n- Adding Timer#progress getter\n- Updating Flame to work with Flutter >= 1.6.0\n\n## 0.13.1\n- Adding Timer utility class\n- Adding `destroyOnFinish` flag for AnimationComponent\n- Fixing release mode on examples that needed screen size\n- Bumping dependencies versions (audioplayers and path_provider)\n\n## 0.13.0\n- Downgrading flame support to stable channel.\n\n## 0.12.2\n- Added more functionality to the Position class (thanks, @illiapoplawski)\n\n## 0.12.1\n- Fixed PositionComponent#setByRect to comply with toRect (thanks, @illiapoplawski)\n\n## 0.12.0\n- Updating flutter_svg and pubspec to support the latest flutter version (1.6.0)\n- Adding Flare Support\n- Fixing PositionComponent#toRect which was not considering the anchor property (thanks, @illiapoplawski)\n\n## [0.11.2]\n- Fixed bug on animations with a single frame\n- Fixed warning on using specific version o flutter_svg on pubspec\n- ParallaxComponent is not abstract anymore, as it does not include any abstract method\n- Added some functionality to Position class\n\n## [0.11.1]\n- Fixed lack of paint update when using AnimationAsWidget as pointed in #78\n- Added travis (thanks, @renancarujo)\n\n## [0.11.0]\n- Implementing low latency api from audioplayers (breaking change)\n- Improved examples by adding some instructions on how to run\n- Add notice on readme about the channel\n- Upgrade path_provider to fix conflicts\n\n## [0.10.4]\n- Fix breaking change on svg plugin\n\n## [0.10.3]\n- Svg support\n- Adding `Animation#reversed` allowing a new reversed animation to be created from an existing animation.\n- Fix games inside regular apps when the component is inside a sliver\n- Support Aseprite animations\n\n## [0.10.2]\n- Fixed some warnings and formatting\n\n## [0.10.1]\n- Fixes some typos\n- Improved docs\n- Extracted gamepads to a new lib, lots of improvements there (thanks, @erickzanardo)\n- Added more examples and an article\n\n## [0.10.0]\n- Fixing a few minor bugs, typos, improving docs\n- Adding the Palette concept: easy access to white and black colors/paints, create your palette to keep your game organized.\n- Adding the Anchor concept: specify where thins should anchor, added to PositionComponent and to the new text related features.\n- Added a whole bunch of text related components: TextConfig allows you to easily define your typography information, TextComponent allows for easy rendering of stuff and TextBox can make sized texts and also typing effects.\n- Improved Utils to have better and more concise APIs, removed unused stuff.\n- Adding TiledComponent to integrate with tiled\n\n## [0.9.5]\n- Add `elapsed` property to Animation (thanks, @ianliu)\n- Fixed minor typo on documentation\n\n## [0.9.4]\n- Bumps audioplayers version\n\n## [0.9.3]\n- Fixes issue when switching between games where new game would not attach\n\n## [0.9.2]\n- Fixes to work with Dart 2.1\n\n## [0.9.1]\n- Updated audioplayers and box2d to fix bugs\n\n## [0.9.0]\n- Several API changes, using new audioplayers 0.6.x\n\n## [0.8.4]\n- Added more consistent APIs and tests\n\n## [0.8.3]\n- Need to review audioplayers 0.5.x\n\n## [0.8.2]\n- Added better documentation, tutorials and examples\n- Minor tweaks in the API\n- New audioplayers version\n\n## [0.8.1]\n- The Components Overhaul Update: This is major update, even though we are keeping things in alpha (version 0.*)\n- Several major upgrades regarding the component system, new component types, Sprites and SpriteSheets, better image caching, several improvements with structure, a BaseGame, a new Game as a widget, that allows you to embed inside apps and a stop method. More minor changes.\n\n## [0.6.1]\n - Bump required dart version\n\n## [0.6.0]\n - Adding audio support for iOS (bumping audioplayers version)\n\n## [0.5.0]\n - Adding a text method to Util to more easily render a Paragraph\n\n## [0.4.0]\n - Upgraded AudioPlayers, added method to disable logging\n - Created PositionComponent with some useful methods\n - A few refactorings\n\n## [0.3.0]\n - Added a pre-load method for Audio module\n\n## [0.2.0]\n - Added a loop method for playing audio on loop\n - Added the option to make rectangular SpriteComponents, not just squares\n\n## [0.1.0]\n - First release, basic utilities\n"
  },
  {
    "path": "packages/flame/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml"
  },
  {
    "path": "packages/flame/benchmark/collision_detection_benchmark.dart",
    "content": "import 'dart:math';\n\nimport 'package:benchmark_harness/benchmark_harness.dart';\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\n\nconst _amountComponents = 100;\nconst _amountTicks = 500;\n\n/// Benchmarks collision detection with simple flat hitboxes (no hierarchy).\n/// All components are direct children of the game world.\nclass FlatCollisionBenchmark extends AsyncBenchmarkBase {\n  final Random random;\n\n  late final FlameGame _game;\n  late final List<_MovingBlock> _blocks;\n\n  FlatCollisionBenchmark(this.random) : super('Flat collision detection');\n\n  static Future<void> main() async {\n    final r = Random(69420);\n    await FlatCollisionBenchmark(r).report();\n  }\n\n  @override\n  Future<void> setup() async {\n    _game = _CollisionGame();\n    _game.onGameResize(Vector2.all(800));\n\n    _blocks = List.generate(\n      _amountComponents,\n      (_) => _MovingBlock(\n        position: Vector2.random(random) * 700,\n        size: Vector2.random(random) * 50 + Vector2.all(10),\n        velocity: Vector2(\n          random.nextInt(200) * (random.nextBool() ? 1.0 : -1.0),\n          random.nextInt(200) * (random.nextBool() ? 1.0 : -1.0),\n        ),\n      ),\n    );\n\n    await _game.addAll(_blocks);\n    await _game.ready();\n  }\n\n  @override\n  Future<void> run() async {\n    for (var i = 0; i < _amountTicks; i++) {\n      _game.update(1 / 60);\n    }\n  }\n}\n\n/// Benchmarks collision detection with nested hierarchies where children have\n/// hitboxes and parents have non-uniform scale/rotation. This exercises the\n/// globalVertices() and AABB computation code paths.\nclass NestedCollisionBenchmark extends AsyncBenchmarkBase {\n  final Random random;\n\n  late final FlameGame _game;\n\n  NestedCollisionBenchmark(this.random)\n    : super('Nested hierarchy collision detection');\n\n  static Future<void> main() async {\n    final r = Random(69420);\n    await NestedCollisionBenchmark(r).report();\n  }\n\n  @override\n  Future<void> setup() async {\n    _game = _CollisionGame();\n    _game.onGameResize(Vector2.all(800));\n\n    final components = <Component>[];\n    for (var i = 0; i < _amountComponents; i++) {\n      final parent = PositionComponent(\n        position: Vector2.random(random) * 700,\n        size: Vector2.all(40),\n        scale: Vector2(\n          1 + random.nextDouble(),\n          1 + random.nextDouble(),\n        ),\n        angle: random.nextDouble() * 2 * pi,\n      );\n      final child = _MovingBlock(\n        position: Vector2.zero(),\n        size: Vector2.all(20),\n        velocity: Vector2(\n          random.nextInt(100) * (random.nextBool() ? 1.0 : -1.0),\n          random.nextInt(100) * (random.nextBool() ? 1.0 : -1.0),\n        ),\n      );\n      parent.add(child);\n      components.add(parent);\n    }\n\n    await _game.addAll(components);\n    await _game.ready();\n  }\n\n  @override\n  Future<void> run() async {\n    for (var i = 0; i < _amountTicks; i++) {\n      _game.update(1 / 60);\n    }\n  }\n}\n\n/// Benchmarks globalVertices() calls directly for polygon components in\n/// hierarchies with non-uniform scale and rotation.\nclass GlobalVerticesBenchmark extends AsyncBenchmarkBase {\n  final Random random;\n\n  late final FlameGame _game;\n  late final List<RectangleHitbox> _hitboxes;\n\n  GlobalVerticesBenchmark(this.random) : super('globalVertices() computation');\n\n  static Future<void> main() async {\n    final r = Random(69420);\n    await GlobalVerticesBenchmark(r).report();\n  }\n\n  @override\n  Future<void> setup() async {\n    _game = FlameGame();\n    _game.onGameResize(Vector2.all(800));\n\n    _hitboxes = [];\n    for (var i = 0; i < _amountComponents; i++) {\n      final parent = PositionComponent(\n        position: Vector2.random(random) * 700,\n        size: Vector2.all(40),\n        scale: Vector2(\n          1 + random.nextDouble(),\n          1 + random.nextDouble(),\n        ),\n        angle: random.nextDouble() * 2 * pi,\n      );\n      final child = PositionComponent(\n        position: Vector2.zero(),\n        size: Vector2.all(20),\n        angle: random.nextDouble() * 2 * pi,\n      );\n      final hitbox = RectangleHitbox();\n      child.add(hitbox);\n      parent.add(child);\n      _game.add(parent);\n      _hitboxes.add(hitbox);\n    }\n\n    await _game.ready();\n  }\n\n  @override\n  Future<void> run() async {\n    // Invalidate caches by moving parents slightly, then recompute.\n    for (final hitbox in _hitboxes) {\n      final parent = hitbox.parent!.parent! as PositionComponent;\n      parent.angle += 0.01;\n    }\n    for (final hitbox in _hitboxes) {\n      hitbox.globalVertices();\n    }\n  }\n}\n\nclass _CollisionGame extends FlameGame with HasCollisionDetection {}\n\nclass _MovingBlock extends PositionComponent with CollisionCallbacks {\n  final Vector2 velocity;\n\n  _MovingBlock({\n    required super.position,\n    required super.size,\n    required this.velocity,\n  }) {\n    add(RectangleHitbox());\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    if (position.length2 > 100000) {\n      velocity.negate();\n    }\n    position.add(velocity * dt);\n  }\n}\n\nFuture<void> main() async {\n  final r1 = Random(69420);\n  await FlatCollisionBenchmark(r1).report();\n\n  final r2 = Random(69420);\n  await NestedCollisionBenchmark(r2).report();\n\n  final r3 = Random(69420);\n  await GlobalVerticesBenchmark(r3).report();\n}\n"
  },
  {
    "path": "packages/flame/benchmark/components_at_point_benchmark.dart",
    "content": "import 'dart:math';\n\nimport 'package:benchmark_harness/benchmark_harness.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\n\nconst _maxDepth = 10;\nconst _amountHitTests = 500;\nconst _gameSize = 800.0;\n\n/// Baseline: no hit testing, just two componentsAtPoint calls per position\n/// (tap down + tap up delivery). Represents pre-PR behavior where the game\n/// caught all events without checking which component was hit.\n/// See PR: https://github.com/flame-engine/flame/pull/3815\nclass BaselineBenchmark extends AsyncBenchmarkBase {\n  final Random random;\n\n  late final FlameGame _game;\n  late final List<Vector2> _positions;\n\n  BaselineBenchmark(this.random) : super('Baseline (no hit test)');\n\n  static Future<void> main() async {\n    final r = Random(69420);\n    await BaselineBenchmark(r).report();\n  }\n\n  @override\n  Future<void> setup() async {\n    _game = _buildGame();\n    await _game.ready();\n    _positions = _generatePositions(random);\n  }\n\n  @override\n  Future<void> run() async {\n    for (final position in _positions) {\n      // Tap down delivery only.\n      final trace1 = <Vector2>[];\n      for (final component in _game.componentsAtPoint(position, trace1)) {\n        if (component is TapCallbacks) {\n          break;\n        }\n      }\n\n      // Tap up delivery only.\n      final trace2 = <Vector2>[];\n      for (final component in _game.componentsAtPoint(position, trace2)) {\n        if (component is TapCallbacks) {\n          break;\n        }\n      }\n    }\n  }\n}\n\n/// Benchmarks the real flow: containsEventHandlerAt (hit test with early-stop\n/// and caching) followed by two componentsAtPoint calls (tap down replays\n/// from cache, tap up does a real traversal).\nclass CachedHitTestBenchmark extends AsyncBenchmarkBase {\n  final Random random;\n\n  late final FlameGame _game;\n  late final List<Vector2> _positions;\n\n  CachedHitTestBenchmark(this.random) : super('Hit Test + Delivery (cached)');\n\n  static Future<void> main() async {\n    final r = Random(69420);\n    await CachedHitTestBenchmark(r).report();\n  }\n\n  @override\n  Future<void> setup() async {\n    _game = _buildGame();\n    await _game.ready();\n    _positions = _generatePositions(random);\n  }\n\n  @override\n  Future<void> run() async {\n    for (final position in _positions) {\n      final hasHandler = _game.containsEventHandlerAt(position);\n\n      // Simulate tap down delivery.\n      final trace1 = <Vector2>[];\n      for (final component in _game.componentsAtPoint(position, trace1)) {\n        if (component is TapCallbacks) {\n          break;\n        }\n      }\n\n      // Simulate tap up delivery.\n      if (hasHandler) {\n        final trace2 = <Vector2>[];\n        for (final component in _game.componentsAtPoint(position, trace2)) {\n          if (component is TapCallbacks) {\n            break;\n          }\n        }\n      }\n    }\n  }\n}\n\n/// Benchmarks the flow without cache: componentsAtPoint called 3 times\n/// per tap (hit test scan + tap down delivery + tap up delivery).\nclass UncachedHitTestBenchmark extends AsyncBenchmarkBase {\n  final Random random;\n\n  late final FlameGame _game;\n  late final List<Vector2> _positions;\n\n  UncachedHitTestBenchmark(this.random)\n    : super('Hit Test + Delivery (uncached)');\n\n  static Future<void> main() async {\n    final r = Random(69420);\n    await UncachedHitTestBenchmark(r).report();\n  }\n\n  @override\n  Future<void> setup() async {\n    _game = _buildGame();\n    await _game.ready();\n    _positions = _generatePositions(random);\n  }\n\n  @override\n  Future<void> run() async {\n    for (final position in _positions) {\n      // Simulate old hit test: plain componentsAtPoint scan (no cache write).\n      var hasHandler = false;\n      final trace0 = <Vector2>[];\n      for (final component in _game.componentsAtPoint(position, trace0)) {\n        if (component is TapCallbacks ||\n            component is DragCallbacks ||\n            component is DoubleTapCallbacks ||\n            component is ScaleCallbacks ||\n            component is SecondaryTapCallbacks) {\n          hasHandler = true;\n          break;\n        }\n      }\n\n      // Simulate tap down delivery.\n      final trace1 = <Vector2>[];\n      for (final component in _game.componentsAtPoint(position, trace1)) {\n        if (component is TapCallbacks) {\n          break;\n        }\n      }\n\n      // Simulate tap up delivery.\n      if (hasHandler) {\n        final trace2 = <Vector2>[];\n        for (final component in _game.componentsAtPoint(position, trace2)) {\n          if (component is TapCallbacks) {\n            break;\n          }\n        }\n      }\n    }\n  }\n}\n\nFlameGame _buildGame() {\n  final random = Random(12345);\n  final game = FlameGame();\n  game.onGameResize(Vector2.all(_gameSize));\n\n  // Build a tree 10 levels deep with varied sizes and positions.\n  // Each level has 3-4 children that subdivide the parent's area,\n  // with tappable leaves scattered throughout.\n  void buildTree(PositionComponent parent, int depth, double size) {\n    if (depth >= _maxDepth) {\n      return;\n    }\n    final childCount = 1 + random.nextInt(2); // 1 or 2 children\n    final childSize = size * (0.3 + random.nextDouble() * 0.3); // 30-60%\n    for (var i = 0; i < childCount; i++) {\n      final maxOffset = size - childSize;\n      final pos = Vector2(\n        random.nextDouble() * maxOffset.clamp(0, double.infinity),\n        random.nextDouble() * maxOffset.clamp(0, double.infinity),\n      );\n      final PositionComponent child;\n      if (depth == _maxDepth - 1 && i % 3 == 0) {\n        child = _TappableComponent()\n          ..position = pos\n          ..size = Vector2.all(childSize);\n      } else {\n        child = PositionComponent()\n          ..position = pos\n          ..size = Vector2.all(childSize);\n      }\n      parent.add(child);\n      buildTree(child, depth + 1, childSize);\n    }\n  }\n\n  // Create several top-level subtrees.\n  for (var i = 0; i < 4; i++) {\n    final root = PositionComponent()\n      ..position = Vector2(\n        (i % 2) * _gameSize / 2,\n        (i ~/ 2) * _gameSize / 2,\n      )\n      ..size = Vector2.all(_gameSize / 2);\n    game.add(root);\n    buildTree(root, 1, _gameSize / 2);\n  }\n\n  return game;\n}\n\nList<Vector2> _generatePositions(Random random) {\n  return List.generate(\n    _amountHitTests,\n    (_) => Vector2(\n      random.nextDouble() * _gameSize,\n      random.nextDouble() * _gameSize,\n    ),\n  );\n}\n\nclass _TappableComponent extends PositionComponent with TapCallbacks {}\n\nFuture<void> main() async {\n  final r1 = Random(69420);\n  await BaselineBenchmark(r1).report();\n\n  final r2 = Random(69420);\n  await CachedHitTestBenchmark(r2).report();\n\n  final r3 = Random(69420);\n  await UncachedHitTestBenchmark(r3).report();\n}\n"
  },
  {
    "path": "packages/flame/benchmark/main.dart",
    "content": "import 'render_components_benchmark.dart';\nimport 'update_components_benchmark.dart';\n\nFuture<void> main() async {\n  await RenderComponentsBenchmark.main();\n  await UpdateComponentsBenchmark.main();\n}\n"
  },
  {
    "path": "packages/flame/benchmark/render_components_benchmark.dart",
    "content": "import 'dart:async';\nimport 'dart:math';\nimport 'dart:ui';\n\nimport 'package:benchmark_harness/benchmark_harness.dart';\nimport 'package:canvas_test/canvas_test.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\n\nconst _amountComponents = 500;\nconst _amountTicks = 2;\nconst _depthMultiplier = 0.25;\n\nclass RenderComponentsBenchmark extends AsyncBenchmarkBase {\n  final Random random;\n\n  late final Canvas _canvas;\n  late final FlameGame _game;\n\n  RenderComponentsBenchmark(this.random) : super('Render Components Benchmark');\n\n  static Future<void> main() async {\n    final r = Random(69420);\n    await RenderComponentsBenchmark(r).report();\n  }\n\n  @override\n  Future<void> setup() async {\n    _canvas = MockCanvas();\n\n    _game = FlameGame();\n    _game.onGameResize(Vector2.all(100.0));\n\n    await _game.addAll(\n      List.generate(\n        _amountComponents,\n        (_) => _BenchmarkComponent(random: random, level: 1),\n      ),\n    );\n\n    await _game.ready();\n  }\n\n  @override\n  Future<void> run() async {\n    for (var i = 0; i < _amountTicks; i++) {\n      _game.render(_canvas);\n    }\n  }\n}\n\nclass _BenchmarkComponent extends PositionComponent {\n  final Random random;\n  final double level;\n\n  _BenchmarkComponent({\n    required this.random,\n    required this.level,\n  });\n\n  @override\n  Future<void> onLoad() async {\n    if (random.nextDouble() <= level) {\n      await addAll(\n        List.generate(\n          random.nextInt(2) + 1,\n          (_) {\n            return _BenchmarkComponent(\n              random: random,\n              level: level * _depthMultiplier,\n            );\n          },\n        ),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/benchmark/update_components_benchmark.dart",
    "content": "import 'dart:math';\n\nimport 'package:benchmark_harness/benchmark_harness.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\n\nconst _amountComponents = 1000;\nconst _amountTicks = 2000;\nconst _amountInputs = 500;\nconst _amountChildren = 10;\n\nclass UpdateComponentsBenchmark extends AsyncBenchmarkBase {\n  final Random random;\n\n  late final FlameGame _game;\n  late final List<_BenchmarkComponent> _components;\n  late final List<double> _dts;\n  late final Set<int> _inputTicks;\n\n  UpdateComponentsBenchmark(this.random)\n    : super('Updating Components Benchmark');\n\n  static Future<void> main() async {\n    final r = Random(69420);\n    await UpdateComponentsBenchmark(r).report();\n  }\n\n  @override\n  Future<void> setup() async {\n    _game = FlameGame();\n    await _game.addAll(\n      List.generate(_amountComponents, _BenchmarkComponent.new),\n    );\n\n    await _game.ready();\n\n    _components = _game.children.whereType<_BenchmarkComponent>().toList(\n      growable: false,\n    );\n\n    _dts = List.generate(_amountTicks, (_) => random.nextDouble());\n    _inputTicks = List.generate(\n      _amountInputs,\n      (_) => random.nextInt(_amountTicks),\n    ).toSet();\n  }\n\n  @override\n  Future<void> run() async {\n    for (final (index, dt) in _dts.indexed) {\n      if (_inputTicks.contains(index)) {\n        _components[random.nextInt(_amountComponents)].input(\n          xDirection: random.nextInt(3) - 1,\n          doJump: random.nextBool(),\n        );\n      }\n      _game.update(dt);\n    }\n  }\n}\n\nclass _BenchmarkComponent extends PositionComponent {\n  static const _groundY = -20.0;\n\n  final int id;\n  final Vector2 velocity = Vector2.zero();\n\n  _BenchmarkComponent(this.id);\n\n  @override\n  Future<void> onLoad() async {\n    for (var i = 0; i < _amountChildren; i++) {\n      await add(PositionComponent(position: Vector2(i * 2, 0)));\n    }\n  }\n\n  void input({\n    required int xDirection,\n    required bool doJump,\n  }) {\n    // move x\n    velocity.x = xDirection * 100;\n\n    // jump\n    if (position.y == _groundY) {\n      velocity.y -= 100;\n    }\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    position.add(velocity * dt);\n    velocity.add(Vector2(0, 10 * dt));\n\n    if (position.y > _groundY) {\n      position.y = _groundY;\n      velocity.setValues(0, 0);\n    }\n  }\n\n  @override\n  String toString() => '[Component $id]';\n}\n"
  },
  {
    "path": "packages/flame/example/README.md",
    "content": "# Small sample game\n\nA sample Flame game showcasing the basic game structure and the use of PositionComponents.\n\nThere are a lot more more [examples](../doc/examples) and [docs](../doc).\n\n\n## Running the sample app\n\nBefore running the sample app, be sure you have run `melos bootstrap` from the root\nof the `flame` repository.\n\nThen, run the sample app from your IDE or by running `flutter run` from this directory.\n\n\n## Try using the Flame developer tools when running this app\n\n`package:flame` provides developer tooling as a Flutter DevTools extension. When you\nare running an app that depends on `package:flame` from Pub, you will see the Flame\nDevTools extension automatically in Flutter DevTools and within your Flutter-supported\nIDE.\n\n**However, if you'd like to try the Flame developer tools while running this sample\napp, you will need to take one additional step.** Because the sample app depends\non `package:flame` from source (not Pub), the Flame DevTools extension assets will\nneed to be generated manually. These are normally generated on publish of the `flame`\npackage, but they are not checked into source control on Github.\n\nTo generate the assets for the Flame DevTools extension, run the following from the\nroot of the `flame` repository:\n\n```sh\nmelos devtools-build\n```\n\nNow, when you run this sample app, you can open Flutter DevTools in your IDE or in\nyour browser to try out the Flame developer tools.\n"
  },
  {
    "path": "packages/flame/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    public_member_api_docs: false\n"
  },
  {
    "path": "packages/flame/example/lib/main.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/material.dart';\n\n/// This example simply adds a rotating white square on the screen.\n/// If you press on a square, it will be removed.\n/// If you press anywhere else, another square will be added.\nvoid main() {\n  runApp(\n    GameWidget(\n      game: FlameGame(world: MyWorld()),\n    ),\n  );\n}\n\nclass MyWorld extends World with TapCallbacks {\n  @override\n  Future<void> onLoad() async {\n    add(Square(Vector2.zero()));\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    super.onTapDown(event);\n    if (!event.handled) {\n      final touchPoint = event.localPosition;\n      add(Square(touchPoint));\n    }\n  }\n}\n\nclass Square extends RectangleComponent with TapCallbacks {\n  static const speed = 3;\n  static const squareSize = 128.0;\n  static const indicatorSize = 6.0;\n\n  static final Paint red = BasicPalette.red.paint();\n  static final Paint blue = BasicPalette.blue.paint();\n\n  Square(Vector2 position)\n    : super(\n        position: position,\n        size: Vector2.all(squareSize),\n        anchor: Anchor.center,\n      );\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    add(\n      RectangleComponent(\n        size: Vector2.all(indicatorSize),\n        paint: blue,\n      ),\n    );\n    add(\n      RectangleComponent(\n        position: size / 2,\n        size: Vector2.all(indicatorSize),\n        anchor: Anchor.center,\n        paint: red,\n      ),\n    );\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    angle += speed * dt;\n    angle %= 2 * math.pi;\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    removeFromParent();\n    event.handled = true;\n  }\n}\n"
  },
  {
    "path": "packages/flame/example/pubspec.yaml",
    "content": "name: flame_example\nresolution: workspace\ndescription: Simple example of a Flame game\nversion: 0.1.0\npublish_to: 'none'\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n"
  },
  {
    "path": "packages/flame/extension/devtools/config.yaml",
    "content": "name: flame\nissueTracker: https://github.com/flame-engine/flame/issues\nversion: 0.1.0\nmaterialIconCodePoint: '0xe392'\n"
  },
  {
    "path": "packages/flame/lib/cache.dart",
    "content": "export 'src/cache/assets_cache.dart';\nexport 'src/cache/images.dart';\nexport 'src/cache/memory_cache.dart';\nexport 'src/cache/value_cache.dart';\n"
  },
  {
    "path": "packages/flame/lib/camera.dart",
    "content": "export 'src/camera/behaviors/bounded_position_behavior.dart'\n    show BoundedPositionBehavior;\nexport 'src/camera/behaviors/follow_behavior.dart' show FollowBehavior;\nexport 'src/camera/camera_component.dart' show CameraComponent;\nexport 'src/camera/viewfinder.dart' show Viewfinder;\nexport 'src/camera/viewport.dart' show Viewport;\nexport 'src/camera/viewports/circular_viewport.dart' show CircularViewport;\nexport 'src/camera/viewports/fixed_aspect_ratio_viewport.dart'\n    show FixedAspectRatioViewport;\nexport 'src/camera/viewports/fixed_resolution_viewport.dart'\n    show FixedResolutionViewport;\nexport 'src/camera/viewports/fixed_size_viewport.dart' show FixedSizeViewport;\nexport 'src/camera/viewports/max_viewport.dart' show MaxViewport;\nexport 'src/camera/world.dart' show World;\n"
  },
  {
    "path": "packages/flame/lib/collisions.dart",
    "content": "export 'src/collisions/broadphase/broadphase.dart';\nexport 'src/collisions/broadphase/prospect_pool.dart';\nexport 'src/collisions/broadphase/quadtree/has_quadtree_collision_detection.dart';\nexport 'src/collisions/broadphase/quadtree/quad_tree_broadphase.dart';\nexport 'src/collisions/broadphase/quadtree/quadtree.dart';\nexport 'src/collisions/broadphase/quadtree/quadtree_collision_detection.dart';\nexport 'src/collisions/broadphase/sweep/sweep.dart';\nexport 'src/collisions/collision_callbacks.dart';\nexport 'src/collisions/collision_detection.dart';\nexport 'src/collisions/collision_passthrough.dart';\nexport 'src/collisions/hitboxes/circle_hitbox.dart';\nexport 'src/collisions/hitboxes/composite_hitbox.dart';\nexport 'src/collisions/hitboxes/hitbox.dart';\nexport 'src/collisions/hitboxes/polygon_hitbox.dart';\nexport 'src/collisions/hitboxes/rectangle_hitbox.dart';\nexport 'src/collisions/hitboxes/screen_hitbox.dart';\nexport 'src/collisions/hitboxes/shape_hitbox.dart';\nexport 'src/collisions/standard_collision_detection.dart';\nexport 'src/experimental/raycast_result.dart';\n"
  },
  {
    "path": "packages/flame/lib/components.dart",
    "content": "/// {@canonicalFor anchor.Anchor}\nlibrary components;\n\nexport 'src/anchor.dart';\nexport 'src/camera/camera_component.dart' show CameraComponent;\nexport 'src/camera/world.dart' show World;\nexport 'src/collisions/has_collision_detection.dart';\nexport 'src/collisions/hitboxes/screen_hitbox.dart';\nexport 'src/components/clip_component.dart';\nexport 'src/components/component_pool.dart';\nexport 'src/components/components_notifier.dart';\nexport 'src/components/core/component.dart';\nexport 'src/components/core/component_key.dart';\nexport 'src/components/core/component_render_context.dart';\nexport 'src/components/custom_painter_component.dart';\nexport 'src/components/fps_component.dart';\nexport 'src/components/fps_text_component.dart';\nexport 'src/components/icon_component.dart';\nexport 'src/components/input/advanced_button_component.dart';\nexport 'src/components/input/joystick_component.dart';\nexport 'src/components/input/keyboard_listener_component.dart';\nexport 'src/components/input/toggle_button_component.dart';\nexport 'src/components/isometric_tile_map_component.dart';\nexport 'src/components/mixins/component_viewport_margin.dart';\nexport 'src/components/mixins/coordinate_transform.dart';\nexport 'src/components/mixins/gesture_hitboxes.dart';\nexport 'src/components/mixins/has_ancestor.dart';\nexport 'src/components/mixins/has_auto_batched_children.dart'\n    show HasAutoBatchedChildren;\nexport 'src/components/mixins/has_decorator.dart' show HasDecorator;\n// ignore: deprecated_member_use_from_same_package\nexport 'src/components/mixins/has_game_ref.dart' show HasGameRef;\nexport 'src/components/mixins/has_game_reference.dart' show HasGameReference;\nexport 'src/components/mixins/has_paint.dart';\nexport 'src/components/mixins/has_time_scale.dart';\nexport 'src/components/mixins/has_visibility.dart';\nexport 'src/components/mixins/has_world.dart';\nexport 'src/components/mixins/ignore_events.dart';\nexport 'src/components/mixins/keyboard_handler.dart';\nexport 'src/components/mixins/notifier.dart';\nexport 'src/components/mixins/parent_is_a.dart';\nexport 'src/components/mixins/single_child_particle.dart';\nexport 'src/components/mixins/snapshot.dart';\nexport 'src/components/nine_tile_box_component.dart';\nexport 'src/components/parallax_component.dart';\nexport 'src/components/particle_system_component.dart';\nexport 'src/components/position_component.dart';\nexport 'src/components/raster_sprite_component.dart';\nexport 'src/components/scroll_text_box_component.dart';\nexport 'src/components/spawn_component.dart';\nexport 'src/components/sprite_animation_component.dart';\nexport 'src/components/sprite_animation_group_component.dart';\nexport 'src/components/sprite_batch_component.dart';\nexport 'src/components/sprite_component.dart';\nexport 'src/components/sprite_group_component.dart';\nexport 'src/components/text_box_component.dart';\nexport 'src/components/text_component.dart';\nexport 'src/components/text_element_component.dart';\nexport 'src/components/timer_component.dart';\nexport 'src/extensions/vector2.dart';\nexport 'src/geometry/circle_component.dart';\nexport 'src/geometry/polygon_component.dart';\nexport 'src/geometry/rectangle_component.dart';\nexport 'src/geometry/shape_component.dart';\nexport 'src/math/block.dart';\nexport 'src/text/renderers/text_paint.dart';\nexport 'src/timer.dart';\n"
  },
  {
    "path": "packages/flame/lib/debug.dart",
    "content": "export 'src/components/debug/child_counter_component.dart';\nexport 'src/components/debug/time_track_component.dart';\nexport 'src/components/fps_component.dart';\nexport 'src/components/fps_text_component.dart';\nexport 'src/devtools/dev_tools_service.dart';\n"
  },
  {
    "path": "packages/flame/lib/devtools.dart",
    "content": "export 'src/devtools/connectors/component_tree_connector.dart'\n    show ComponentTreeNode;\n"
  },
  {
    "path": "packages/flame/lib/effects.dart",
    "content": "export 'src/effects/anchor_by_effect.dart' show AnchorByEffect;\nexport 'src/effects/anchor_effect.dart' show AnchorEffect;\nexport 'src/effects/anchor_to_effect.dart' show AnchorToEffect;\nexport 'src/effects/color_effect.dart';\nexport 'src/effects/combined_effect.dart';\nexport 'src/effects/component_effect.dart';\nexport 'src/effects/controllers/combined_effect_controller.dart';\nexport 'src/effects/controllers/curved_effect_controller.dart';\nexport 'src/effects/controllers/delayed_effect_controller.dart';\nexport 'src/effects/controllers/duration_effect_controller.dart';\nexport 'src/effects/controllers/effect_controller.dart';\nexport 'src/effects/controllers/infinite_effect_controller.dart';\nexport 'src/effects/controllers/linear_effect_controller.dart';\nexport 'src/effects/controllers/mixins/has_single_child_effect_controller.dart';\nexport 'src/effects/controllers/pause_effect_controller.dart';\nexport 'src/effects/controllers/random_effect_controller.dart';\nexport 'src/effects/controllers/repeated_effect_controller.dart';\nexport 'src/effects/controllers/reverse_curved_effect_controller.dart';\nexport 'src/effects/controllers/reverse_linear_effect_controller.dart';\nexport 'src/effects/controllers/sequence_effect_controller.dart';\nexport 'src/effects/controllers/sine_effect_controller.dart';\nexport 'src/effects/controllers/speed_effect_controller.dart';\nexport 'src/effects/controllers/zigzag_effect_controller.dart';\nexport 'src/effects/effect.dart';\nexport 'src/effects/effect_target.dart';\nexport 'src/effects/function_effect.dart';\nexport 'src/effects/glow_effect.dart';\nexport 'src/effects/hue_by_effect.dart';\nexport 'src/effects/hue_effect.dart';\nexport 'src/effects/hue_to_effect.dart';\nexport 'src/effects/move_along_path_effect.dart';\nexport 'src/effects/move_by_effect.dart';\nexport 'src/effects/move_effect.dart';\nexport 'src/effects/move_to_effect.dart';\nexport 'src/effects/opacity_effect.dart';\nexport 'src/effects/provider_interfaces.dart'\n    show\n        AnchorProvider,\n        AngleProvider,\n        ReadOnlyAngleProvider,\n        PositionProvider,\n        ScaleProvider,\n        SizeProvider,\n        ReadOnlyPositionProvider,\n        ReadOnlyScaleProvider,\n        ReadOnlySizeProvider,\n        OpacityProvider,\n        PaintProvider,\n        HueProvider;\nexport 'src/effects/remove_effect.dart';\nexport 'src/effects/rotate_around_effect.dart';\nexport 'src/effects/rotate_effect.dart';\nexport 'src/effects/scale_effect.dart';\nexport 'src/effects/sequence_effect.dart' show SequenceEffect;\nexport 'src/effects/size_effect.dart';\nexport 'src/effects/transform2d_effect.dart';\n"
  },
  {
    "path": "packages/flame/lib/events.dart",
    "content": "export 'src/events/component_mixins/double_tap_callbacks.dart'\n    show DoubleTapCallbacks;\nexport 'src/events/component_mixins/drag_callbacks.dart' show DragCallbacks;\nexport 'src/events/component_mixins/hover_callbacks.dart' show HoverCallbacks;\nexport 'src/events/component_mixins/pointer_move_callbacks.dart'\n    show PointerMoveCallbacks;\nexport 'src/events/component_mixins/scale_callbacks.dart' show ScaleCallbacks;\nexport 'src/events/component_mixins/secondary_tap_callbacks.dart'\n    show SecondaryTapCallbacks;\nexport 'src/events/component_mixins/tap_callbacks.dart' show TapCallbacks;\nexport 'src/events/flame_game_mixins/double_tap_dispatcher.dart'\n    show DoubleTapDispatcher, DoubleTapDispatcherKey;\nexport 'src/events/flame_game_mixins/multi_drag_dispatcher.dart'\n    show MultiDragDispatcher, MultiDragDispatcherKey;\nexport 'src/events/flame_game_mixins/multi_tap_dispatcher.dart'\n    show MultiTapDispatcher, MultiTapDispatcherKey;\nexport 'src/events/flame_game_mixins/pointer_move_dispatcher.dart'\n    show PointerMoveDispatcher, MouseMoveDispatcherKey;\nexport 'src/events/flame_game_mixins/secondary_tap_dispatcher.dart'\n    show SecondaryTapDispatcher, SecondaryTapDispatcherKey;\nexport 'src/events/game_mixins/multi_touch_drag_detector.dart'\n    show MultiTouchDragDetector;\nexport 'src/events/game_mixins/multi_touch_tap_detector.dart'\n    show MultiTouchTapDetector;\nexport 'src/events/hardware_keyboard_detector.dart'\n    show HardwareKeyboardDetector;\nexport 'src/events/interfaces/multi_drag_listener.dart' show MultiDragListener;\nexport 'src/events/interfaces/multi_tap_listener.dart' show MultiTapListener;\nexport 'src/events/messages/double_tap_cancel_event.dart'\n    show DoubleTapCancelEvent;\nexport 'src/events/messages/double_tap_down_event.dart' show DoubleTapDownEvent;\nexport 'src/events/messages/double_tap_event.dart' show DoubleTapEvent;\nexport 'src/events/messages/drag_cancel_event.dart' show DragCancelEvent;\nexport 'src/events/messages/drag_end_event.dart' show DragEndEvent;\nexport 'src/events/messages/drag_start_event.dart' show DragStartEvent;\nexport 'src/events/messages/drag_update_event.dart' show DragUpdateEvent;\nexport 'src/events/messages/pointer_move_event.dart' show PointerMoveEvent;\nexport 'src/events/messages/scale_end_event.dart' show ScaleEndEvent;\nexport 'src/events/messages/scale_start_event.dart' show ScaleStartEvent;\nexport 'src/events/messages/scale_update_event.dart' show ScaleUpdateEvent;\nexport 'src/events/messages/secondary_tap_cancel_event.dart'\n    show SecondaryTapCancelEvent;\nexport 'src/events/messages/secondary_tap_down_event.dart'\n    show SecondaryTapDownEvent;\nexport 'src/events/messages/secondary_tap_up_event.dart'\n    show SecondaryTapUpEvent;\nexport 'src/events/messages/tap_cancel_event.dart' show TapCancelEvent;\nexport 'src/events/messages/tap_down_event.dart' show TapDownEvent;\nexport 'src/events/messages/tap_up_event.dart' show TapUpEvent;\nexport 'src/game/mixins/keyboard.dart'\n    show HasKeyboardHandlerComponents, KeyboardEvents;\nexport 'src/gestures/detectors.dart'\n    show\n        DoubleTapDetector,\n        ForcePressDetector,\n        HorizontalDragDetector,\n        LongPressDetector,\n        MouseMovementDetector,\n        PanDetector,\n        ScaleDetector,\n        ScrollDetector,\n        TertiaryTapDetector,\n        SecondaryTapDetector,\n        VerticalDragDetector;\nexport 'src/gestures/events.dart'\n    show\n        DragDownInfo,\n        DragEndInfo,\n        DragStartInfo,\n        DragUpdateInfo,\n        ForcePressInfo,\n        LongPressEndInfo,\n        LongPressMoveUpdateInfo,\n        LongPressStartInfo,\n        PointerHoverInfo,\n        PointerScrollInfo,\n        PositionInfo,\n        ScaleEndInfo,\n        ScaleStartInfo,\n        ScaleUpdateInfo,\n        TapDownInfo,\n        TapUpInfo;\n"
  },
  {
    "path": "packages/flame/lib/experimental.dart",
    "content": "/// Classes and components in this sub-module are considered experimental,\n/// that is, their API may still be incomplete and subject to change at a more\n/// rapid pace than the rest of the Flame code.\n///\n/// However, do not feel discouraged to use this functionality: on the contrary,\n/// consider this as a way to help the Flame community by beta-testing new\n/// components.\n///\n/// After the components lived here for some time, and when we gain more\n/// confidence in their robustness, they will be moved out into the main Flame\n/// library.\nlibrary experimental;\n\nexport 'src/experimental/column_component.dart' show ColumnComponent;\nexport 'src/experimental/expanded_component.dart' show ExpandedComponent;\nexport 'src/experimental/geometry/shapes/circle.dart' show Circle;\nexport 'src/experimental/geometry/shapes/polygon.dart' show Polygon;\nexport 'src/experimental/geometry/shapes/rectangle.dart' show Rectangle;\nexport 'src/experimental/geometry/shapes/rounded_rectangle.dart'\n    show RoundedRectangle;\nexport 'src/experimental/geometry/shapes/shape.dart' show Shape;\nexport 'src/experimental/layout_component.dart'\n    show LayoutComponent, LayoutAxis;\nexport 'src/experimental/linear_layout_component.dart'\n    show LinearLayoutComponent, Direction;\nexport 'src/experimental/padding_component.dart' show PaddingComponent;\nexport 'src/experimental/row_component.dart' show RowComponent;\nexport 'src/experimental/single_layout_component.dart'\n    show SingleLayoutComponent;\n"
  },
  {
    "path": "packages/flame/lib/extensions.dart",
    "content": "export 'src/extensions/aabb.dart';\nexport 'src/extensions/canvas.dart';\nexport 'src/extensions/color.dart';\nexport 'src/extensions/double.dart';\nexport 'src/extensions/fragment_shader.dart';\nexport 'src/extensions/image.dart';\nexport 'src/extensions/list.dart';\nexport 'src/extensions/matrix4.dart';\nexport 'src/extensions/offset.dart';\nexport 'src/extensions/paint.dart';\nexport 'src/extensions/path.dart';\nexport 'src/extensions/picture.dart';\nexport 'src/extensions/random.dart';\nexport 'src/extensions/rect.dart';\nexport 'src/extensions/rectangle.dart';\nexport 'src/extensions/size.dart';\nexport 'src/extensions/vector2.dart';\n"
  },
  {
    "path": "packages/flame/lib/flame.dart",
    "content": "export 'src/flame.dart';\n"
  },
  {
    "path": "packages/flame/lib/game.dart",
    "content": "/// {@canonicalFor text.TextPaint}\n/// {@canonicalFor text.TextRenderer}\nlibrary game;\n\nexport 'src/collisions/has_collision_detection.dart';\nexport 'src/components/router/overlay_route.dart' show OverlayRoute;\nexport 'src/components/router/route.dart' show Route;\nexport 'src/components/router/router_component.dart' show RouterComponent;\nexport 'src/components/router/value_route.dart' show ValueRoute;\nexport 'src/components/router/world_route.dart' show WorldRoute;\nexport 'src/extensions/vector2.dart';\nexport 'src/game/flame_game.dart';\nexport 'src/game/game.dart';\nexport 'src/game/game_widget/game_widget.dart';\nexport 'src/game/mixins/has_performance_tracker.dart';\nexport 'src/game/mixins/single_game_instance.dart';\nexport 'src/game/notifying_vector2.dart';\nexport 'src/game/transform2d.dart';\nexport 'src/text/renderers/text_paint.dart';\n"
  },
  {
    "path": "packages/flame/lib/geometry.dart",
    "content": "export 'src/geometry/circle_component.dart';\nexport 'src/geometry/constants.dart';\nexport 'src/geometry/line.dart';\nexport 'src/geometry/line_segment.dart';\nexport 'src/geometry/polygon_component.dart';\nexport 'src/geometry/polygon_ray_intersection.dart';\nexport 'src/geometry/ray2.dart';\nexport 'src/geometry/rectangle_component.dart';\nexport 'src/geometry/shape_component.dart';\nexport 'src/geometry/shape_intersections.dart';\n"
  },
  {
    "path": "packages/flame/lib/image_composition.dart",
    "content": "export 'src/image_composition.dart';\n"
  },
  {
    "path": "packages/flame/lib/input.dart",
    "content": "/// {@canonicalFor joystick_component.JoystickComponent}\n/// {@canonicalFor joystick_component.JoystickDirection}\nlibrary input;\n\nexport 'src/components/input/button_component.dart';\nexport 'src/components/input/hud_button_component.dart';\nexport 'src/components/input/hud_margin_component.dart';\nexport 'src/components/input/joystick_component.dart';\nexport 'src/components/input/sprite_button_component.dart';\nexport 'src/events/game_mixins/multi_touch_drag_detector.dart';\nexport 'src/events/game_mixins/multi_touch_tap_detector.dart';\nexport 'src/events/tap_config.dart';\nexport 'src/extensions/vector2.dart';\nexport 'src/game/mixins/keyboard.dart';\nexport 'src/gestures/detectors.dart';\n"
  },
  {
    "path": "packages/flame/lib/layers.dart",
    "content": "export 'src/layers/layer.dart';\nexport 'src/layers/processors.dart';\n"
  },
  {
    "path": "packages/flame/lib/layout.dart",
    "content": "export 'src/layout/align_component.dart' show AlignComponent;\n"
  },
  {
    "path": "packages/flame/lib/math.dart",
    "content": "export 'src/math/block.dart';\nexport 'src/math/random_fallback.dart';\nexport 'src/math/solve_cubic.dart';\nexport 'src/math/solve_quadratic.dart';\n"
  },
  {
    "path": "packages/flame/lib/palette.dart",
    "content": "export 'src/extensions/color.dart';\nexport 'src/extensions/paint.dart';\nexport 'src/palette.dart';\n"
  },
  {
    "path": "packages/flame/lib/parallax.dart",
    "content": "export 'src/parallax.dart';\n"
  },
  {
    "path": "packages/flame/lib/particles.dart",
    "content": "export 'src/anchor.dart';\nexport 'src/particles/accelerated_particle.dart';\nexport 'src/particles/circle_particle.dart';\nexport 'src/particles/component_particle.dart';\nexport 'src/particles/composed_particle.dart';\nexport 'src/particles/computed_particle.dart';\nexport 'src/particles/curved_particle.dart';\nexport 'src/particles/image_particle.dart';\nexport 'src/particles/moving_particle.dart';\nexport 'src/particles/paint_particle.dart';\nexport 'src/particles/particle.dart';\nexport 'src/particles/rotating_particle.dart';\nexport 'src/particles/scaled_particle.dart';\nexport 'src/particles/scaling_particle.dart';\nexport 'src/particles/sprite_animation_particle.dart';\nexport 'src/particles/sprite_particle.dart';\nexport 'src/particles/translated_particle.dart';\n"
  },
  {
    "path": "packages/flame/lib/post_process.dart",
    "content": "export 'src/extensions/fragment_shader.dart';\nexport 'src/post_process/post_process.dart';\nexport 'src/post_process/post_process_component.dart';\n"
  },
  {
    "path": "packages/flame/lib/rendering.dart",
    "content": "export 'src/rendering/decorator.dart' show Decorator;\nexport 'src/rendering/hue_decorator.dart' show HueDecorator, hueRotationMatrix;\nexport 'src/rendering/mutable_transform.dart' show MutableRSTransform;\nexport 'src/rendering/paint_decorator.dart' show PaintDecorator;\nexport 'src/rendering/rotate3d_decorator.dart' show Rotate3DDecorator;\nexport 'src/rendering/shadow3d_decorator.dart' show Shadow3DDecorator;\nexport 'src/rendering/transform2d_decorator.dart' show Transform2DDecorator;\n"
  },
  {
    "path": "packages/flame/lib/sprite.dart",
    "content": "/// {@canonicalFor sprite_animation.SpriteAnimation}\n/// {@canonicalFor sprite_animation.SpriteAnimationData}\n/// {@canonicalFor sprite_animation.SpriteAnimationFrame}\n/// {@canonicalFor sprite_animation.SpriteAnimationFrameData}\nlibrary sprite;\n\nexport 'src/sprite.dart';\nexport 'src/sprite_animation.dart';\nexport 'src/sprite_animation_ticker.dart';\nexport 'src/sprite_batch.dart' hide FlippedAtlasStatus;\nexport 'src/sprite_sheet.dart';\n"
  },
  {
    "path": "packages/flame/lib/src/anchor.dart",
    "content": "import 'package:flame/src/extensions/vector2.dart';\nimport 'package:meta/meta.dart';\n\n/// Represents a relative position inside some 2D object with a rectangular\n/// size or bounding box.\n///\n/// Think of it as the place where you \"grab\" or \"hold\" the object.\n/// In Components, the Anchor is where the component position is measured from.\n/// For example, if a component position is (100, 100) the anchor reflects what\n/// exact point of the component that is positioned at (100, 100), as a relative\n/// fraction of the size of the object.\n///\n/// The \"default\" anchor in most cases is topLeft.\n///\n/// The Anchor is represented by a fraction of the size (in each axis),\n/// where 0 in x-axis means left, 0 in y-axis means top, 1 in x-axis means right\n/// and 1 in y-axis means bottom.\n@immutable\nclass Anchor {\n  static const Anchor topLeft = Anchor(0.0, 0.0);\n  static const Anchor topCenter = Anchor(0.5, 0.0);\n  static const Anchor topRight = Anchor(1.0, 0.0);\n  static const Anchor centerLeft = Anchor(0.0, 0.5);\n  static const Anchor center = Anchor(0.5, 0.5);\n  static const Anchor centerRight = Anchor(1.0, 0.5);\n  static const Anchor bottomLeft = Anchor(0.0, 1.0);\n  static const Anchor bottomCenter = Anchor(0.5, 1.0);\n  static const Anchor bottomRight = Anchor(1.0, 1.0);\n\n  /// The relative x position with respect to the object's width;\n  /// 0 means totally to the left (beginning) and 1 means totally to the\n  /// right (end).\n  final double x;\n\n  /// The relative y position with respect to the object's height;\n  /// 0 means totally to the top (beginning) and 1 means totally to the\n  /// bottom (end).\n  final double y;\n\n  /// Returns [x] and [y] as a Vector2. Note that this is still a relative\n  /// fractional representation.\n  Vector2 toVector2() => Vector2(x, y);\n\n  const Anchor(this.x, this.y);\n\n  /// Take your position [position] that is on this anchor and give back what\n  /// that position it would be on in anchor [otherAnchor] with a size of\n  /// [size].\n  Vector2 toOtherAnchorPosition(\n    Vector2 position,\n    Anchor otherAnchor,\n    Vector2 size, {\n    Vector2? out,\n  }) {\n    if (this == otherAnchor) {\n      return position;\n    } else {\n      return (out ?? Vector2.zero())\n        ..setValues(otherAnchor.x - x, otherAnchor.y - y)\n        ..multiply(size)\n        ..add(position);\n    }\n  }\n\n  /// Returns a string representation of this Anchor.\n  ///\n  /// This should only be used for serialization purposes.\n  String get name {\n    return _valueNames[this] ?? 'Anchor($x, $y)';\n  }\n\n  /// Returns the anchor on the opposite side of this anchor.\n  Anchor get opposite => Anchor(1 - x, 1 - y);\n\n  @override\n  bool operator ==(Object other) {\n    return other is Anchor && x == other.x && y == other.y;\n  }\n\n  @override\n  int get hashCode => x.hashCode * 31 + y.hashCode;\n\n  /// Returns a string representation of this Anchor.\n  ///\n  /// This is the same as `name` and should be used only for debugging or\n  /// serialization.\n  @override\n  String toString() => name;\n\n  static final Map<Anchor, String> _valueNames = {\n    topLeft: 'topLeft',\n    topCenter: 'topCenter',\n    topRight: 'topRight',\n    centerLeft: 'centerLeft',\n    center: 'center',\n    centerRight: 'centerRight',\n    bottomLeft: 'bottomLeft',\n    bottomCenter: 'bottomCenter',\n    bottomRight: 'bottomRight',\n  };\n\n  /// List of all predefined anchor values.\n  static final List<Anchor> values = _valueNames.keys.toList();\n\n  /// This should only be used for de-serialization purposes.\n  ///\n  /// If you need to convert anchors to serializable data (like JSON),\n  /// use the `toString()` and `valueOf` methods.\n  factory Anchor.valueOf(String name) {\n    if (_valueNames.containsValue(name)) {\n      return _valueNames.entries.singleWhere((e) => e.value == name).key;\n    } else {\n      final regexp = RegExp(r'^\\Anchor\\(([^,]+), ([^\\)]+)\\)');\n      final matches = regexp.firstMatch(name)?.groups([1, 2]);\n      assert(\n        matches != null && matches.length == 2,\n        'Bad anchor format: $name',\n      );\n      return Anchor(double.parse(matches![0]!), double.parse(matches[1]!));\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/cache/assets_cache.dart",
    "content": "import 'dart:convert';\nimport 'dart:typed_data';\n\nimport 'package:flame/flame.dart';\nimport 'package:flutter/services.dart' show AssetBundle;\n\n/// A class that loads, and caches files.\n///\n/// It automatically looks for files in the `assets` directory.\nclass AssetsCache {\n  AssetsCache({\n    this.prefix = 'assets/',\n    AssetBundle? bundle,\n  }) : bundle = bundle ?? Flame.bundle;\n\n  /// The [AssetBundle] from which assets are loaded.\n  /// defaults to [Flame.bundle].\n  AssetBundle bundle;\n\n  String prefix;\n  final Map<String, _Asset<dynamic>> _files = {};\n\n  /// Removes the file from the cache.\n  void clear(String file) {\n    _files.remove(file);\n  }\n\n  /// Removes all the files from the cache.\n  void clearCache() {\n    _files.clear();\n  }\n\n  /// Returns the number of files in the cache.\n  int get cacheCount => _files.length;\n\n  /// Reads a file from assets folder.\n  Future<String> readFile(String fileName, {String? package}) async {\n    final cacheKey = package == null ? fileName : 'packages/$package/$fileName';\n    if (!_files.containsKey(cacheKey)) {\n      _files[cacheKey] = await _readFile(fileName, package: package);\n    }\n    assert(\n      _files[cacheKey] is _StringAsset,\n      '\"$cacheKey\" was previously loaded as a binary file',\n    );\n    return (_files[cacheKey]! as _StringAsset).value;\n  }\n\n  /// Reads a binary file from assets folder.\n  Future<Uint8List> readBinaryFile(String fileName, {String? package}) async {\n    final cacheKey = package == null ? fileName : 'packages/$package/$fileName';\n    if (!_files.containsKey(cacheKey)) {\n      _files[cacheKey] = await _readBinary(fileName, package: package);\n    }\n    assert(\n      _files[cacheKey] is _BinaryAsset,\n      '\"$cacheKey\" was previously loaded as a text file',\n    );\n    return (_files[cacheKey]! as _BinaryAsset).value;\n  }\n\n  /// Reads a json file from the assets folder.\n  Future<Map<String, dynamic>> readJson(\n    String fileName, {\n    String? package,\n  }) async {\n    final cacheKey = package == null ? fileName : 'packages/$package/$fileName';\n    if (!_files.containsKey(cacheKey)) {\n      _files[cacheKey] = await _readJson(fileName, package: package);\n    }\n    assert(\n      _files[cacheKey] is _JsonAsset,\n      '\"$cacheKey\" was previously loaded as a different type',\n    );\n    return (_files[cacheKey]! as _JsonAsset).value;\n  }\n\n  Future<_StringAsset> _readFile(String fileName, {String? package}) async {\n    final fullPrefix = package == null ? prefix : 'packages/$package/$prefix';\n    final string = await bundle.loadString('$fullPrefix$fileName');\n    return _StringAsset(string);\n  }\n\n  Future<_BinaryAsset> _readBinary(String fileName, {String? package}) async {\n    final fullPrefix = package == null ? prefix : 'packages/$package/$prefix';\n    final data = await bundle.load('$fullPrefix$fileName');\n    final bytes = Uint8List.view(data.buffer);\n    return _BinaryAsset(bytes);\n  }\n\n  Future<_JsonAsset> _readJson(String fileName, {String? package}) async {\n    final string = await _readFile(fileName, package: package);\n    final json = jsonDecode(string.value) as Map<String, dynamic>;\n    return _JsonAsset(json);\n  }\n\n  /// This method provides synchronous access to cached assets, similar to\n  /// [AssetsCache.fromCache].\n  T fromCache<T>(String fileName) {\n    final asset = _files[fileName];\n    assert(\n      asset != null,\n      'Tried to access an asset \"$fileName\" that does not exist in the cache. '\n      'Make sure to load the asset using readFile(), readBinaryFile(), or '\n      'readJson() before accessing it with fromCache()',\n    );\n    assert(\n      asset!.value is T,\n      'Tried to access asset \"$fileName\" as type $T, but it was loaded as '\n      '${asset.value.runtimeType}. Make sure to use the correct type when '\n      'calling fromCache<T>()',\n    );\n\n    return asset!.value as T;\n  }\n}\n\nsealed class _Asset<T> {\n  T value;\n  _Asset(this.value);\n}\n\nclass _StringAsset extends _Asset<String> {\n  _StringAsset(super.value);\n}\n\nclass _BinaryAsset extends _Asset<Uint8List> {\n  _BinaryAsset(super.value);\n}\n\nclass _JsonAsset extends _Asset<Map<String, dynamic>> {\n  _JsonAsset(super.value);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/cache/images.dart",
    "content": "import 'dart:async';\nimport 'dart:convert' show base64;\nimport 'dart:ui';\n\nimport 'package:flame/src/flame.dart';\nimport 'package:flutter/painting.dart';\nimport 'package:flutter/services.dart';\n\nclass Images {\n  Images({\n    String prefix = 'assets/images/',\n    AssetBundle? bundle,\n  }) : _prefix = prefix,\n       bundle = bundle ?? Flame.bundle;\n\n  final Map<String, _ImageAsset> _assets = {};\n\n  /// The [AssetBundle] from which images are loaded.\n  /// defaults to [Flame.bundle].\n  AssetBundle bundle;\n\n  /// Path prefix to the project's directory with images.\n  ///\n  /// This path is relative to the project's root, and the default prefix is\n  /// \"assets/images/\". If necessary, you may change this prefix at any time.\n  /// A prefix must be a valid directory name and end with \"/\" (empty prefix is\n  /// also allowed).\n  ///\n  /// The prefix is **not** part of the keys of the images stored in this cache.\n  /// For example, if you load image `player.png`, then it will be searched at\n  /// location `prefix + \"player.png\"` but stored in the cache under the key\n  /// `\"player.png\"`.\n  String get prefix => _prefix;\n  late String _prefix;\n  set prefix(String value) {\n    assert(\n      value.isEmpty || value.endsWith('/'),\n      'Prefix must be empty or end with a \"/\"',\n    );\n    _prefix = value;\n  }\n\n  /// Adds the [image] into the cache under the key [name].\n  ///\n  /// The cache will assume the ownership of the [image], and will properly\n  /// dispose of it at the end.\n  void add(String name, Image image) {\n    _assets[name]?.dispose();\n    _assets[name] = _ImageAsset.fromImage(image);\n  }\n\n  /// Transform the base64 encoded image into an [Image] and adds it into the\n  /// cache.\n  Future<void> addFromBase64Data(String name, String base64Data) async {\n    _assets[name]?.dispose();\n    final image = await _fetchFromBase64(base64Data);\n    _assets[name] = _ImageAsset.fromImage(image);\n  }\n\n  /// If the image with [name] exists in the cache that is returned, otherwise\n  /// the image generated by [imageGenerator] is returned.\n  ///\n  /// If the [imageGenerator] is used, the resulting [Image] is stored with\n  /// [name] in the cache.\n  Future<Image> fetchOrGenerate(\n    String name,\n    Future<Image> Function() imageGenerator,\n  ) {\n    return (_assets[name] ??= _ImageAsset.future(\n      imageGenerator(),\n    )).retrieveAsync();\n  }\n\n  /// Removes the image [name] from the cache.\n  ///\n  /// No error is raised if the image [name] is not present in the cache.\n  ///\n  /// This calls [Image.dispose], so make sure that you don't use the previously\n  /// cached image once it is cleared (removed) from the cache.\n  void clear(String name) {\n    final removedAsset = _assets.remove(name);\n    removedAsset?.dispose();\n  }\n\n  /// Removes all cached images.\n  ///\n  /// This calls [Image.dispose] for all images in the cache, so make sure that\n  /// you don't use any of the previously cached images once [clearCache] has\n  /// been called.\n  void clearCache() {\n    _assets.forEach((_, asset) => asset.dispose());\n    _assets.clear();\n  }\n\n  /// Returns the image [name] from the cache.\n  ///\n  /// The image returned can be used as long as it remains in the cache, but\n  /// doesn't need to be explicitly disposed.\n  ///\n  /// If you want to retain the image even after you remove it from the cache,\n  /// then you can call `Image.clone()` on it.\n  Image fromCache(String name) {\n    final asset = _assets[name];\n    assert(\n      asset != null,\n      'Tried to access an image \"$name\" that does not exist in the cache. Make '\n      'sure to load() an image before accessing it',\n    );\n    assert(\n      asset!.image != null,\n      'Tried to access an image \"$name\" before it was loaded. Make sure to '\n      'await the future from load() before using this method',\n    );\n    return asset!.image!;\n  }\n\n  /// Loads the specified image with [fileName] into the cache.\n  /// By default the key in the cache is the [fileName], if another key is\n  /// desired, specify the optional [key] argument.\n  Future<Image> load(String fileName, {String? key, String? package}) {\n    return (_assets[key ?? fileName] ??= _ImageAsset.future(\n      _fetchToMemory(fileName, package: package),\n    )).retrieveAsync();\n  }\n\n  /// Loads all images with the specified [fileNames] into the cache.\n  Future<List<Image>> loadAll(List<String> fileNames) {\n    return Future.wait(fileNames.map(load));\n  }\n\n  /// Loads all images from the specified (or default) [prefix] into the cache.\n  Future<List<Image>> loadAllImages() {\n    return loadAllFromPattern(\n      RegExp(\n        r'\\.(png|jpg|jpeg|svg|gif|webp|bmp|wbmp)$',\n        caseSensitive: false,\n      ),\n    );\n  }\n\n  /// Loads all images in the [prefix]ed path that are matching the specified\n  /// pattern.\n  Future<List<Image>> loadAllFromPattern(Pattern pattern) async {\n    final manifest = await AssetManifest.loadFromAssetBundle(bundle);\n    final imagePaths = manifest\n        .listAssets()\n        .where((path) {\n          return path.startsWith(_prefix) &&\n              path.toLowerCase().contains(pattern);\n        })\n        .map((path) => path.replaceFirst(_prefix, ''));\n    return loadAll(imagePaths.toList());\n  }\n\n  /// Whether the cache contains the specified [key] or not.\n  bool containsKey(String key) => _assets.containsKey(key);\n\n  /// Returns the list of keys in the cache.\n  List<String> get keys => _assets.keys.toList();\n\n  String? findKeyForImage(Image image) {\n    return _assets.keys.firstWhere(\n      (k) => _assets[k]?.image?.isCloneOf(image) ?? false,\n    );\n  }\n\n  /// Waits until all currently pending image loading operations complete.\n  Future<void> ready() {\n    return Future.wait(_assets.values.map((asset) => asset.retrieveAsync()));\n  }\n\n  Future<Image> fromBase64(String key, String base64) {\n    return (_assets[key] ??= _ImageAsset.future(\n      _fetchFromBase64(base64),\n    )).retrieveAsync();\n  }\n\n  Future<Image> _fetchFromBase64(String base64Data) {\n    final data = base64Data.substring(base64Data.indexOf(',') + 1);\n    final bytes = base64.decode(data);\n    return decodeImageFromList(bytes);\n  }\n\n  Future<Image> _fetchToMemory(String name, {String? package}) async {\n    final prefix = package == null ? _prefix : 'packages/$package/$_prefix';\n    final data = await bundle.load('$prefix$name');\n    final bytes = Uint8List.view(data.buffer);\n    return decodeImageFromList(bytes);\n  }\n}\n\n/// Individual entry in the [Images] cache.\n///\n/// This class owns the [Image] object, which can be disposed of using the\n/// [dispose] method.\nclass _ImageAsset {\n  _ImageAsset.future(Future<Image> future) : _future = future {\n    _future!.then((image) {\n      _image = image;\n      _future = null;\n    });\n  }\n\n  _ImageAsset.fromImage(Image image) : _image = image;\n\n  Image? get image => _image;\n  Image? _image;\n\n  Future<Image>? _future;\n\n  Future<Image> retrieveAsync() => _future ?? Future.value(_image);\n\n  /// Properly dispose of an image asset.\n  void dispose() {\n    if (_image != null) {\n      _image!.dispose();\n      _image = null;\n    }\n    if (_future != null) {\n      _future!.then((image) => image.dispose());\n      _future = null;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/cache/matrix_pool.dart",
    "content": "import 'dart:typed_data' show Float64List, Float32List;\nimport 'dart:ui';\n\nclass MatrixPool {\n  static final List<Float64List> _pool = [];\n  static const int _bufferSize = 16;\n\n  static Float64List getBuffer() {\n    if (_pool.isEmpty) {\n      return Float64List(_bufferSize);\n    }\n    return _pool.removeLast();\n  }\n\n  static void releaseBuffer(Float64List buffer) {\n    _pool.add(buffer);\n  }\n}\n\n/// Applies Canvas.transform to a Float64List buffer,\n/// allowing the Matrix4 and Lists to be 32 bit.\nvoid canvasTransform(Canvas canvas, Float32List matrix4) {\n  final buffer = MatrixPool.getBuffer();\n  for (var i = 0; i < 16; i++) {\n    buffer[i] = matrix4[i];\n  }\n  canvas.transform(buffer);\n  MatrixPool.releaseBuffer(buffer);\n}\n\n/// Applies Path.transform to a Float64List buffer,\n/// allowing the Matrix4 and Lists to be 32 bit.\nPath pathTransform(Path path, Float32List matrix4) {\n  final buffer = MatrixPool.getBuffer();\n  for (var i = 0; i < 16; i++) {\n    buffer[i] = matrix4[i];\n  }\n  final transformed = path.transform(buffer);\n  MatrixPool.releaseBuffer(buffer);\n\n  return transformed;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/cache/memory_cache.dart",
    "content": "import 'dart:collection';\n\n/// Simple class to cache values with size based eviction.\nclass MemoryCache<K, V> {\n  final LinkedHashMap<K, V> _cache = LinkedHashMap();\n  final int cacheSize;\n\n  MemoryCache({this.cacheSize = 10});\n\n  /// Adds the [value] to the cache under [key].\n  ///\n  /// If that [key] is already used, updates the value.\n  void setValue(K key, V value) {\n    final preexisting = _cache.containsKey(key);\n    _cache[key] = value;\n\n    if (!preexisting) {\n      while (_cache.length > cacheSize) {\n        final k = _cache.keys.first;\n        _cache.remove(k);\n      }\n    }\n  }\n\n  /// Removes the value from the cache.\n  void clear(K key) {\n    _cache.remove(key);\n  }\n\n  /// Removes all the values from the cache.\n  void clearCache() {\n    _cache.clear();\n  }\n\n  /// Gets the value under [key].\n  V? getValue(K key) => _cache[key];\n\n  /// Checks whether the cache has any value under [key].\n  bool containsKey(K key) => _cache.containsKey(key);\n\n  /// Returns the number of values saved in this memory cache.\n  int get size => _cache.length;\n\n  /// Iterates over all existing keys with values.\n  Iterable<K> get keys => _cache.keys;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/cache/value_cache.dart",
    "content": "/// Used for caching calculated values, the cache is determined to be valid by\n/// comparing a list of values that can be of any type and is compared to the\n/// values that was last used when the cache was updated.\nclass ValueCache<T> {\n  T? value;\n\n  List<dynamic> _lastValidCacheValues = <dynamic>[];\n\n  ValueCache();\n\n  bool isCacheValid<F>(List<F> validCacheValues) {\n    if (value == null) {\n      return false;\n    }\n    for (var i = 0; i < _lastValidCacheValues.length; ++i) {\n      if (_lastValidCacheValues[i] != validCacheValues[i]) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  T updateCache<F>(T value, List<F> validCacheValues) {\n    this.value = value;\n    _lastValidCacheValues = validCacheValues;\n    return value;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/camera/behaviors/bounded_position_behavior.dart",
    "content": "import 'package:flame/src/components/core/component.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\nimport 'package:flame/src/experimental/geometry/shapes/shape.dart';\nimport 'package:flame/src/extensions/vector2.dart';\n\n/// This behavior ensures that the target's position stays within the specified\n/// [bounds].\n///\n/// On each game tick this behavior checks whether the target's position remains\n/// within the bounds. If it does, then no adjustment are made. However, if this\n/// component detects that the target has left the permitted region, it will\n/// return it into the [bounds] by moving towards the last known good position\n/// and stopping as close to the boundary as possible. The [precision] parameter\n/// controls how close to the boundary we want to get before stopping.\n///\n/// Here [target] is typically the component to which this behavior is attached,\n/// but it can also be set explicitly in the constructor. If the target is not\n/// passed explicitly in the constructor, then the parent component must be a\n/// [PositionProvider].\nclass BoundedPositionBehavior extends Component {\n  BoundedPositionBehavior({\n    required Shape bounds,\n    PositionProvider? target,\n    double precision = 0.5,\n    super.priority,\n    super.key,\n  }) : assert(precision > 0, 'Precision must be positive: $precision'),\n       _bounds = bounds,\n       _target = target,\n       _previousPosition = Vector2.zero(),\n       _precision = precision;\n\n  /// The region within which the target's position must be kept.\n  Shape get bounds => _bounds;\n  Shape _bounds;\n  set bounds(Shape newBounds) {\n    _bounds = newBounds;\n    if (!isValidPoint(_previousPosition)) {\n      _previousPosition.setFrom(_bounds.center);\n      if (_target != null) {\n        update(0);\n      }\n    }\n  }\n\n  bool isValidPoint(Vector2 point) => _bounds.containsPoint(point);\n\n  PositionProvider get target => _target!;\n  PositionProvider? _target;\n\n  double get precision => _precision;\n  final double _precision;\n\n  /// Saved position from the last game tick.\n  final Vector2 _previousPosition;\n\n  @override\n  void onMount() {\n    if (_target == null) {\n      assert(\n        parent is PositionProvider,\n        'Can only apply this behavior to a PositionProvider',\n      );\n      _target = parent! as PositionProvider;\n    }\n    if (_target != null && isValidPoint(target.position)) {\n      _previousPosition.setFrom(target.position);\n    } else {\n      _previousPosition.setFrom(_bounds.center);\n      update(0);\n    }\n  }\n\n  @override\n  void update(double dt) {\n    final currentPosition = _target!.position;\n    if (isValidPoint(currentPosition)) {\n      _previousPosition.setFrom(currentPosition);\n    } else {\n      final closestBoundaryPoint = _bounds.nearestPoint(currentPosition);\n      _previousPosition.setFrom(closestBoundaryPoint);\n      _target!.position = closestBoundaryPoint;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/camera/behaviors/follow_behavior.dart",
    "content": "import 'package:flame/src/camera/viewfinder.dart';\nimport 'package:flame/src/camera/viewport.dart';\nimport 'package:flame/src/components/core/component.dart';\nimport 'package:flame/src/components/position_component.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// This behavior will make the [owner] follow the [target].\n///\n/// Here, both the owner and the target are [PositionProvider]s, which could be\n/// either [PositionComponent]s, or camera's [Viewfinder]/[Viewport], or any\n/// other objects, including custom implementations.\n///\n/// The [maxSpeed] parameter controls the maximum speed with which the [owner]\n/// is allowed to move as it pursues the [target]. By default, the max speed is\n/// infinite, allowing the owner to stay on top of the target all the time.\n///\n/// The flags [horizontalOnly]/[verticalOnly] allow constraining the [owner]'s\n/// movement to the horizontal/vertical directions respectively.\nclass FollowBehavior extends Component {\n  FollowBehavior({\n    required ReadOnlyPositionProvider target,\n    PositionProvider? owner,\n    double maxSpeed = double.infinity,\n    this.horizontalOnly = false,\n    this.verticalOnly = false,\n    super.priority,\n    super.key,\n  }) : _target = target,\n       _owner = owner,\n       _speed = maxSpeed,\n       assert(maxSpeed > 0, 'maxSpeed must be positive: $maxSpeed'),\n       assert(\n         !(horizontalOnly && verticalOnly),\n         'The behavior cannot be both horizontalOnly and verticalOnly',\n       );\n\n  ReadOnlyPositionProvider get target => _target;\n  final ReadOnlyPositionProvider _target;\n\n  PositionProvider get owner => _owner!;\n  PositionProvider? _owner;\n\n  double get maxSpeed => _speed;\n  final double _speed;\n\n  final bool horizontalOnly;\n  final bool verticalOnly;\n\n  final _tempDelta = Vector2.zero();\n\n  @override\n  void onMount() {\n    if (_owner == null) {\n      assert(\n        parent is PositionProvider,\n        'Can only apply this behavior to a PositionProvider',\n      );\n      _owner = parent! as PositionProvider;\n    }\n  }\n\n  @override\n  void update(double dt) {\n    _tempDelta.setValues(\n      verticalOnly ? 0 : target.position.x - owner.position.x,\n      horizontalOnly ? 0 : target.position.y - owner.position.y,\n    );\n\n    final distance = _tempDelta.length;\n    final deltaOffset = _speed * dt;\n    if (distance > deltaOffset) {\n      _tempDelta.scale(deltaOffset / distance);\n    }\n    if (_tempDelta.x != 0 || _tempDelta.y != 0) {\n      owner.position = _tempDelta..add(owner.position);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/camera/behaviors/viewport_aware_bounds_behavior.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame/src/camera/behaviors/bounded_position_behavior.dart';\nimport 'package:flame/src/camera/camera_component.dart';\nimport 'package:flame/src/camera/viewfinder.dart';\nimport 'package:flame/src/camera/viewport.dart';\nimport 'package:flame/src/components/core/component.dart';\nimport 'package:flame/src/components/mixins/parent_is_a.dart';\nimport 'package:flame/src/experimental/geometry/shapes/circle.dart';\nimport 'package:flame/src/experimental/geometry/shapes/rectangle.dart';\nimport 'package:flame/src/experimental/geometry/shapes/rounded_rectangle.dart';\nimport 'package:flame/src/experimental/geometry/shapes/shape.dart';\n\n/// This behavior ensures that none of the viewport can go outside\n/// of the bounds, when it is false only the viewfinder anchor is considered.\n/// Note that it only works with [Rectangle], [RoundedRectangle] and [Circle]\n/// shapes.\nclass ViewportAwareBoundsBehavior extends Component with ParentIsA<Viewfinder> {\n  Shape _boundsShape;\n  late Rect _visibleWorldRect;\n\n  ViewportAwareBoundsBehavior({\n    required Shape boundsShape,\n    super.key,\n  }) : _boundsShape = boundsShape;\n\n  @override\n  void onLoad() {\n    _visibleWorldRect = parent.visibleWorldRect;\n    parent.transform.scale.addListener(_updateCameraBoundsIfNeeded);\n    viewport.transform.scale.addListener(_updateCameraBoundsIfNeeded);\n  }\n\n  @override\n  void onMount() {\n    super.onMount();\n    _updateCameraBounds();\n  }\n\n  @override\n  void onRemove() {\n    viewport.transform.scale.removeListener(_updateCameraBoundsIfNeeded);\n    parent.transform.scale.removeListener(_updateCameraBoundsIfNeeded);\n  }\n\n  /// Returns the bounds that do not take the viewport into account.\n  /// These bounds are automatically updated when [CameraComponent.setBounds]\n  /// is being called.\n  Shape get boundsShape => _boundsShape;\n\n  /// Changes the original camera bounds.\n  /// This setter is used when you call [CameraComponent.setBounds].\n  set boundsShape(Shape boundsShape) {\n    _boundsShape = boundsShape;\n    _updateCameraBounds();\n  }\n\n  /// Returns the camera viewport.\n  Viewport get viewport => parent.camera.viewport;\n\n  /// Calls [_updateCameraBounds] if the [_visibleWorldRect] differs from\n  /// viewfinder visible world rect.\n  void _updateCameraBoundsIfNeeded() {\n    if (_visibleWorldRect != parent.visibleWorldRect) {\n      _updateCameraBounds();\n    }\n  }\n\n  /// Triggers an update of the current camera bounds.\n  void _updateCameraBounds() {\n    _visibleWorldRect = parent.visibleWorldRect;\n    final boundedBehavior = parent.firstChild<BoundedPositionBehavior>();\n    boundedBehavior?.bounds = _calculateViewportAwareBounds();\n  }\n\n  /// This method calculates adapts the [_boundsShape] so that none\n  /// of the viewport can go outside of the bounds.\n  /// It returns the [_boundsShape] if it fails to calculates new bounds.\n  Shape _calculateViewportAwareBounds() {\n    final worldSize = Vector2(\n      _boundsShape\n          .support(\n            _boundsShape.nearestPoint(\n              _boundsShape.center + Vector2(1, 0),\n            ),\n          )\n          .x,\n      _boundsShape\n          .support(\n            _boundsShape.nearestPoint(\n              _boundsShape.center + Vector2(0, 1),\n            ),\n          )\n          .y,\n    );\n\n    final viewportSize = viewport.virtualSize;\n    if (_boundsShape is Rectangle) {\n      return Rectangle.fromCenter(\n        center: _boundsShape.center,\n        size: worldSize - viewportSize,\n      );\n    } else if (_boundsShape is RoundedRectangle) {\n      final halfSize = (worldSize - viewportSize) / 2;\n      return RoundedRectangle.fromPoints(\n        _boundsShape.center - halfSize,\n        _boundsShape.center + halfSize,\n        (_boundsShape as RoundedRectangle).radius,\n      );\n    } else if (_boundsShape is Circle) {\n      final diameter =\n          max(worldSize.x, worldSize.y) - max(viewportSize.x, viewportSize.y);\n      final radius = diameter / 2;\n      return Circle(\n        _boundsShape.center,\n        radius,\n      );\n    }\n    return _boundsShape;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/camera/camera_component.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/post_process.dart';\nimport 'package:flame/src/camera/behaviors/bounded_position_behavior.dart';\nimport 'package:flame/src/camera/behaviors/follow_behavior.dart';\nimport 'package:flame/src/camera/behaviors/viewport_aware_bounds_behavior.dart';\nimport 'package:flame/src/camera/viewfinder.dart';\nimport 'package:flame/src/camera/viewport.dart';\nimport 'package:flame/src/camera/viewports/fixed_resolution_viewport.dart';\nimport 'package:flame/src/camera/viewports/max_viewport.dart';\nimport 'package:flame/src/components/core/component_tree_root.dart';\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/move_by_effect.dart';\nimport 'package:flame/src/effects/move_effect.dart';\nimport 'package:flame/src/effects/move_to_effect.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\nimport 'package:flame/src/experimental/geometry/shapes/circle.dart';\nimport 'package:flame/src/experimental/geometry/shapes/rectangle.dart';\nimport 'package:flame/src/experimental/geometry/shapes/rounded_rectangle.dart';\nimport 'package:flame/src/experimental/geometry/shapes/shape.dart';\nimport 'package:flame/src/game/flame_game.dart';\n\n/// [CameraComponent] is a component through which a [World] is observed.\n///\n/// A camera consists of two parts: a [Viewport], and a [Viewfinder]. It also\n/// references a [World] component, which is not mounted to the camera, but the\n/// camera still knows about it. The world must be mounted somewhere else in\n/// the game tree.\n///\n/// A camera is a regular component that can be placed anywhere in the game\n/// tree. Most games will have at least one \"main\" camera for displaying the\n/// main game world. However, additional cameras may also be used for some\n/// special effects. These extra cameras may be placed either in parallel with\n/// the main camera, or within the world. It is even possible to create a camera\n/// that looks at itself. [FlameGame] has one [CameraComponent] added by default\n/// which is called just [FlameGame.camera].\n///\n/// Since [CameraComponent] is a [Component], it is possible to attach other\n/// components to it. In particular, adding components directly to the camera is\n/// equivalent to adding them to the camera's parent. Components added to the\n/// viewport will be affected by the viewport's position, but not by its clip\n/// mask. Such components will be rendered on top of the viewport. Components\n/// added to the viewfinder will be rendered as if they were part of the world.\n/// That is, they will be affected both by the viewport and the viewfinder.\n///\n///\n/// A [PostProcess] can be applied to the camera, which will affect the\n/// rendering of the world. This is useful for applying effects such as bloom,\n/// blur, or other fragment shader effects to the world. See [postProcess] for\n/// more information.\nclass CameraComponent extends Component {\n  CameraComponent({\n    this.world,\n    Viewport? viewport,\n    Viewfinder? viewfinder,\n    Component? backdrop,\n    List<Component>? hudComponents,\n    super.children,\n    super.key,\n  }) : _viewport = (viewport ?? MaxViewport())..addAll(hudComponents ?? []),\n       _viewfinder = viewfinder ?? Viewfinder(),\n       _backdrop = backdrop ?? Component(),\n       // The priority is set to the max here to avoid some bugs for the users,\n       // if they for example would add any components that modify positions\n       // before the CameraComponent, since it then will render the positions\n       // of the last tick each tick.\n       super(priority: 0x7fffffff) {\n    children.register<PostProcessComponent>();\n    addAll([_backdrop, _viewport, _viewfinder]);\n  }\n\n  /// Create a camera that shows a portion of the game world of fixed size\n  /// [width] x [height].\n  ///\n  /// The viewport will be centered within the canvas, expanding as much as\n  /// possible on all sides while maintaining the [width]:[height] aspect ratio.\n  /// The zoom level will be set such in such a way that exactly [width] x\n  /// [height] pixels are visible within the viewport. The viewfinder will be\n  /// initially set up to show world coordinates (0, 0) at the center of the\n  /// viewport.\n  CameraComponent.withFixedResolution({\n    required double width,\n    required double height,\n    World? world,\n    Viewfinder? viewfinder,\n    Component? backdrop,\n    List<Component>? hudComponents,\n    Iterable<Component>? children,\n    ComponentKey? key,\n  }) : this(\n         world: world,\n         viewport: FixedResolutionViewport(resolution: Vector2(width, height)),\n         viewfinder: viewfinder ?? Viewfinder(),\n         backdrop: backdrop,\n         hudComponents: hudComponents,\n         children: children,\n         key: key,\n       );\n\n  /// The [viewport] is the \"window\" through which the game world is observed.\n  ///\n  /// Imagine that the world is covered with an infinite sheet of paper, but\n  /// there is a hole in it. That hole is the viewport: through that aperture\n  /// the world can be observed. The viewport's size is equal to or smaller\n  /// than the size of the game canvas. If it is smaller, then the viewport's\n  /// position specifies where exactly it is placed on the canvas.\n  Viewport get viewport => _viewport;\n\n  set viewport(Viewport newViewport) {\n    _viewport.removeFromParent();\n    _viewport = newViewport;\n    add(_viewport);\n    _viewfinder.updateTransform();\n  }\n\n  Viewport _viewport;\n\n  /// The [viewfinder] controls which part of the world is seen through the\n  /// viewport.\n  ///\n  /// Thus, viewfinder's `position` is the world point which is seen at the\n  /// center of the viewport. In addition, viewfinder controls the zoom level\n  /// (i.e. how much of the world is seen through the viewport), and,\n  /// optionally, rotation.\n  Viewfinder get viewfinder => _viewfinder;\n\n  set viewfinder(Viewfinder newViewfinder) {\n    _viewfinder.removeFromParent();\n    _viewfinder = newViewfinder;\n    add(_viewfinder);\n  }\n\n  Viewfinder _viewfinder;\n\n  /// Special component that is designed to be the root of a game world.\n  ///\n  /// Multiple cameras can observe the same [world] simultaneously, and the\n  /// world may itself contain cameras that look into other worlds, or even into\n  /// itself.\n  ///\n  /// The [world] component is generally mounted externally to the camera, and\n  /// this variable is a mere reference to it.\n  World? world;\n\n  /// The [backdrop] component is rendered statically behind the world.\n  ///\n  /// Here you can add things like the parallax component which should be static\n  /// when the camera moves around.\n  Component get backdrop => _backdrop;\n  Component _backdrop;\n  set backdrop(Component newBackdrop) {\n    _backdrop.removeFromParent();\n    add(newBackdrop);\n    _backdrop = newBackdrop;\n  }\n\n  /// The axis-aligned bounding rectangle of a [world] region which is currently\n  /// visible through the viewport.\n  ///\n  /// This property can be useful in order to determine which components within\n  /// the game's world are currently visible to the player, and which aren't.\n  ///\n  /// If the viewport is non-rectangular, or if the world's view is rotated,\n  /// then the [visibleWorldRect] will be larger than the actual viewing area.\n  /// Thus, this property is \"conservative\": everything outside of this rect\n  /// is definitely not visible, while the points inside may or may not be\n  /// visible.\n  ///\n  /// This property is cached, and is recalculated whenever the camera moves\n  /// or the viewport is resized. At the same time, it may only be accessed\n  /// after the camera was fully mounted.\n  Rect get visibleWorldRect {\n    assert(\n      parent != null,\n      \"This property can't be accessed before the camera is added to the game. \"\n      'If you are using visibleWorldRect from another component (for example '\n      'the World), make sure that the CameraComponent is added before that '\n      'Component.',\n    );\n    return viewfinder.visibleWorldRect;\n  }\n\n  /// Renders the [world] as seen through this camera.\n  ///\n  /// If the world is not mounted yet, only the viewport and viewfinder elements\n  /// will be rendered.\n  @override\n  void renderTree(Canvas canvas) {\n    canvas.save();\n    canvas.translate(\n      viewport.position.x - viewport.anchor.x * viewport.size.x,\n      viewport.position.y - viewport.anchor.y * viewport.size.y,\n    );\n    // Render the world through the viewport\n    if ((world?.isMounted ?? false) &&\n        currentCameras.length < maxCamerasDepth) {\n      canvas.save();\n      viewport.clip(canvas);\n      viewport.transformCanvas(canvas);\n      backdrop.renderTree(canvas);\n      canvas.save();\n      try {\n        currentCameras.add(this);\n        void renderWorld(Canvas canvas) {\n          canvas.transform2D(viewfinder.transform);\n          world!.renderFromCamera(canvas);\n\n          // Render the viewfinder elements, which will be in front of\n          // the world,\n          // but with the same transforms applied to them.\n          viewfinder.renderTree(canvas);\n        }\n\n        final postProcessors = children.query<PostProcessComponent>();\n        if (postProcessors.isNotEmpty) {\n          assert(\n            postProcessors.length == 1,\n            'Only one post process component is allowed per camera.',\n          );\n          final postProcessor = postProcessors.first.postProcess;\n          postProcessor.render(\n            canvas,\n            viewport.virtualSize,\n            renderWorld,\n            (context) {\n              renderContext.currentPostProcess = context;\n            },\n          );\n        } else {\n          renderWorld(canvas);\n        }\n      } finally {\n        currentCameras.removeLast();\n      }\n      canvas.restore();\n      // Render the viewport elements, which will be in front of the world.\n      viewport.renderTree(canvas);\n      canvas.restore();\n    }\n    canvas.restore();\n  }\n\n  /// Converts from the global (canvas) coordinate space to\n  /// local (camera = viewport + viewfinder).\n  ///\n  /// Opposite of [localToGlobal].\n  Vector2 globalToLocal(Vector2 point, {Vector2? output}) {\n    final viewportPosition = viewport.globalToLocal(point, output: output);\n    return viewfinder.globalToLocal(viewportPosition, output: output);\n  }\n\n  /// Converts from the local (camera = viewport + viewfinder) coordinate space\n  /// to global (canvas).\n  ///\n  /// Opposite of [globalToLocal].\n  Vector2 localToGlobal(Vector2 position, {Vector2? output}) {\n    final viewfinderPosition = viewfinder.localToGlobal(\n      position,\n      output: output,\n    );\n    return viewport.localToGlobal(viewfinderPosition, output: output);\n  }\n\n  @override\n  Iterable<Component> componentsAtLocation<T>(\n    T locationContext,\n    List<T>? nestedContexts,\n    T? Function(CoordinateTransform, T) transformContext,\n    bool Function(Component, T) checkContains,\n  ) sync* {\n    final viewportPoint = transformContext(viewport, locationContext);\n    if (viewportPoint == null) {\n      return;\n    }\n\n    yield* viewport.componentsAtLocation(\n      viewportPoint,\n      nestedContexts,\n      transformContext,\n      checkContains,\n    );\n    if ((world?.isMounted ?? false) &&\n        currentCameras.length < maxCamerasDepth) {\n      if (checkContains(viewport, viewportPoint)) {\n        currentCameras.add(this);\n        final worldPoint = transformContext(viewfinder, viewportPoint);\n        if (worldPoint == null) {\n          return;\n        }\n        yield* viewfinder.componentsAtLocation(\n          worldPoint,\n          nestedContexts,\n          transformContext,\n          checkContains,\n        );\n        yield* world!.componentsAtLocation(\n          worldPoint,\n          nestedContexts,\n          transformContext,\n          checkContains,\n        );\n        currentCameras.removeLast();\n      }\n    }\n  }\n\n  /// A camera that currently performs rendering.\n  ///\n  /// This variable is set to `this` when we begin rendering the world through\n  /// this particular camera, and reset back to `null` at the end. This variable\n  /// is not set when rendering components that are attached to the viewport.\n  static CameraComponent? get currentCamera {\n    return currentCameras.isEmpty ? null : currentCameras[0];\n  }\n\n  /// Stack of all current cameras in the render tree.\n  static final List<CameraComponent> currentCameras = [];\n\n  /// Maximum number of nested cameras that will be rendered.\n  ///\n  /// This variable helps prevent infinite recursion when a camera is set to\n  /// look at the world that contains that camera.\n  static int maxCamerasDepth = 4;\n\n  /// Makes the [viewfinder] follow the given [target].\n  ///\n  /// The [target] here can be any read-only [PositionProvider]. For example, a\n  /// [PositionComponent] is the most common choice of target. Alternatively,\n  /// you can use [PositionProviderImpl] to construct the target dynamically.\n  ///\n  /// This method adds a [FollowBehavior] to the viewfinder. If there is another\n  /// [FollowBehavior] currently applied to the viewfinder, it will be removed\n  /// first.\n  ///\n  /// Parameters [maxSpeed], [horizontalOnly] and [verticalOnly] have the same\n  /// meaning as in the [FollowBehavior.new] constructor.\n  ///\n  /// If [snap] is true, then the viewfinder's starting position will be set to\n  /// the target's current location. If [snap] is false, then the viewfinder\n  /// will move from its current position to the target's position at the given\n  /// speed.\n  void follow(\n    ReadOnlyPositionProvider target, {\n    double maxSpeed = double.infinity,\n    bool horizontalOnly = false,\n    bool verticalOnly = false,\n    bool snap = false,\n  }) {\n    stop();\n    viewfinder.add(\n      FollowBehavior(\n        target: target,\n        owner: viewfinder,\n        maxSpeed: maxSpeed,\n        horizontalOnly: horizontalOnly,\n        verticalOnly: verticalOnly,\n      ),\n    );\n    if (snap) {\n      viewfinder.position = target.position;\n    }\n  }\n\n  /// Removes all movement effects or behaviors from the viewfinder.\n  void stop() {\n    viewfinder.children.toList().forEach((child) {\n      if (child is FollowBehavior || child is MoveEffect) {\n        child.removeFromParent();\n      }\n    });\n  }\n\n  /// Moves the camera towards the specified world [point].\n  void moveTo(Vector2 point, {double speed = double.infinity}) {\n    stop();\n    viewfinder.add(\n      MoveToEffect(point, EffectController(speed: speed)),\n    );\n  }\n\n  /// Move the camera by the given [offset].\n  void moveBy(Vector2 offset, {double speed = double.infinity}) {\n    stop();\n    viewfinder.add(MoveByEffect(offset, EffectController(speed: speed)));\n  }\n\n  /// Sets or clears the world bounds for the camera's viewfinder.\n  ///\n  /// The bound is a [Shape], given in the world coordinates. The viewfinder's\n  /// position will be restricted to always remain inside this region.\n  ///\n  /// When [considerViewport] is true none of the viewport can go outside\n  /// of the bounds, when it is false only the viewfinder anchor is considered.\n  /// Note that this option only works with [Rectangle], [RoundedRectangle] and\n  /// [Circle] shapes.\n  void setBounds(Shape? bounds, {bool considerViewport = false}) {\n    final boundedBehavior = viewfinder.firstChild<BoundedPositionBehavior>();\n    final viewPortAwareBoundsBehavior = viewfinder\n        .firstChild<ViewportAwareBoundsBehavior>();\n\n    if (bounds == null) {\n      // When bounds is null, all bounds-related components need to be dropped.\n      boundedBehavior?.removeFromParent();\n      viewPortAwareBoundsBehavior?.removeFromParent();\n      return;\n    }\n\n    Future<void>? boundedBehaviorFuture;\n    if (boundedBehavior == null) {\n      final BoundedPositionBehavior ref;\n      viewfinder.add(\n        ref = BoundedPositionBehavior(\n          bounds: bounds,\n          priority: 1000,\n        ),\n      );\n\n      boundedBehaviorFuture = ref.mounted;\n    } else {\n      boundedBehavior.bounds = bounds;\n    }\n\n    if (!considerViewport) {\n      // Edge case: remove pre-existing viewport aware components.\n      viewPortAwareBoundsBehavior?.removeFromParent();\n      return;\n    }\n\n    // Param `considerViewPort` was true and we have a bounds.\n    // Add a ViewportAwareBoundsBehavior component with\n    // our desired bounds shape or update the boundsShape if the\n    // component already exists.\n    if (viewPortAwareBoundsBehavior == null) {\n      switch (boundedBehaviorFuture) {\n        case null:\n          // This represents the case when BoundedPositionBehavior was mounted\n          // earlier in another cycle. This allows us to immediately add the\n          // ViewportAwareBoundsBehavior component which will subsequently adapt\n          // the camera to the virtual resolution this frame.\n          _addViewPortAwareBoundsBehavior(bounds);\n        case _:\n          // This represents the case when BoundedPositionBehavior was added\n          // in this exact cycle but did not mount into the tree.\n          // We must wait for that component to mount first in order for\n          // ViewportAwareBoundsBehavior to correctly affect the camera.\n          boundedBehaviorFuture.whenComplete(\n            () => _addViewPortAwareBoundsBehavior(bounds),\n          );\n      }\n    } else {\n      viewPortAwareBoundsBehavior.boundsShape = bounds;\n    }\n  }\n\n  void _addViewPortAwareBoundsBehavior(Shape bounds) {\n    viewfinder.add(\n      ViewportAwareBoundsBehavior(\n        boundsShape: bounds,\n      ),\n    );\n  }\n\n  /// Returns true if this camera is able to see the [component].\n  /// Will always return false if\n  /// - [world] is null or\n  /// - [world] is not mounted or\n  /// - [component] is not mounted or\n  /// - [componentWorld] is non-null and does not match with [world]\n  ///\n  /// If [componentWorld] is null, this method does not take into consideration\n  /// the world to which the given [component] belongs (if any). This means, in\n  /// such cases, any component overlapping the [visibleWorldRect] will be\n  /// reported as visible, even if it is not part of the [world] this camera is\n  /// currently looking at. This can be changed by passing the component's\n  /// world as [componentWorld].\n  bool canSee(PositionComponent component, {World? componentWorld}) {\n    if (!(world?.isMounted ?? false) ||\n        !component.isMounted ||\n        (componentWorld != null && componentWorld != world)) {\n      return false;\n    }\n\n    return visibleWorldRect.overlaps(component.toAbsoluteRect());\n  }\n\n  @override\n  final CameraRenderContext renderContext = CameraRenderContext();\n\n  /// A [PostProcess] that is applied to the world of a camera.\n  ///\n  /// Do note that only one [postProcess] can be active on the camera at once.\n  /// If the [postProcess] is set to null, the previous post process will\n  /// be removed.\n  /// If the [postProcess] is set to not null, it will be added to the camera,\n  /// and any previously active post process will be removed.\n  ///\n  /// See also:\n  /// - [PostProcess] for the base class for post processes and more information\n  /// about how to create them.\n  /// - [PostProcessComponent] for a component that can be used to apply a\n  /// post process to a specific component.\n  PostProcess? get postProcess =>\n      children.query<PostProcessComponent>().firstOrNull?.postProcess;\n  set postProcess(PostProcess? postProcess) {\n    final postProcessComponents = children\n        .query<PostProcessComponent>()\n        .toList();\n    final queuedPostProcessAdds = findGame()?.queue\n        .where(\n          (event) =>\n              event.kind == LifecycleEventKind.add &&\n              event.child is PostProcessComponent,\n        )\n        .map((event) => event.child!);\n    removeAll([...postProcessComponents, ...?queuedPostProcessAdds]);\n    if (postProcess != null) {\n      add(PostProcessComponent(postProcess: postProcess));\n    }\n  }\n}\n\nclass CameraRenderContext extends ComponentRenderContext {\n  PostProcess? currentPostProcess;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/camera/viewfinder.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/src/camera/behaviors/viewport_aware_bounds_behavior.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\nimport 'package:flame/src/game/transform2d.dart';\nimport 'package:meta/meta.dart';\n\n/// [Viewfinder] is a part of a [CameraComponent] system that controls which\n/// part of the game world is currently visible through a viewport.\n///\n/// The viewfinder contains the game point that is currently at the\n/// \"cross-hairs\" of the viewport ([position]), the [zoom] level, and the\n/// [angle] of rotation of the camera.\n///\n/// If you add children to the [Viewfinder] they will appear like HUDs i.e.\n/// statically in front of the world.\nclass Viewfinder extends Component\n    implements\n        AnchorProvider,\n        AngleProvider,\n        PositionProvider,\n        ScaleProvider,\n        CoordinateTransform {\n  Viewfinder({\n    super.key,\n  });\n\n  /// Transform matrix used by the viewfinder.\n  final Transform2D transform = Transform2D();\n\n  /// The game coordinates of a point that is to be positioned at the center\n  /// of the viewport.\n  @override\n  Vector2 get position => -transform.offset;\n  @override\n  set position(Vector2 value) {\n    transform.offset = -value;\n    visibleRect = null;\n  }\n\n  /// Zoom level of the game.\n  ///\n  /// The default zoom value of 1 means that the world coordinates are in 1:1\n  /// correspondence with the pixels on the screen. Zoom levels higher than 1\n  /// make the world appear closer: each unit of game coordinate systems maps\n  /// to [zoom] pixels on the screen. Conversely, when [zoom] is less than 1,\n  /// the game world will appear further away and smaller in size.\n  ///\n  /// See also: [visibleGameSize] for setting the zoom level dynamically.\n  double get zoom => transform.scale.x;\n  set zoom(double value) {\n    assert(value > 0, 'zoom level must be positive: $value');\n    transform.scale = Vector2.all(value);\n    visibleRect = null;\n  }\n\n  /// Rotation angle of the game world, in radians.\n  ///\n  /// The rotation is around the axis that is perpendicular to the screen.\n  @override\n  double get angle => -transform.angle;\n  @override\n  set angle(double value) {\n    transform.angle = -value;\n    visibleRect = null;\n  }\n\n  /// The point within the viewport that is considered the \"logical center\" of\n  /// the camera.\n  ///\n  /// This anchor is relative to the viewport's bounding rect, and by default\n  /// is at the center of the viewport.\n  ///\n  /// The \"logical center\" of the camera means the point within the viewport\n  /// where the viewfinder's focus is located at. It is at this point within\n  /// the viewport that the world's point [position] will be displayed.\n  @override\n  Anchor get anchor => _anchor;\n  Anchor _anchor = Anchor.center;\n  @override\n  set anchor(Anchor value) {\n    _anchor = value;\n    onViewportResize();\n  }\n\n  /// The [considerViewport] flag is read-only and cannot be set except through\n  /// [CameraComponent.setBounds] as an optional parameter.\n  ///\n  /// If this value is true, a child component [ViewportAwareBoundsBehavior]\n  /// exists whose purpose is to keep the viewfinder's visible area in bounds\n  /// of the viewport w.r.t. the bounds shape.\n  ///\n  /// If this value is false, then no child [ViewportAwareBoundsBehavior]\n  /// will be present. False is the initial value.\n  bool get considerViewport =>\n      firstChild<ViewportAwareBoundsBehavior>() != null;\n\n  /// Reference to the parent camera.\n  CameraComponent get camera => parent! as CameraComponent;\n\n  /// Convert a point from the global coordinate system to the viewfinder's\n  /// coordinate system.\n  ///\n  /// Use [output] to send in a Vector2 object that will be used to avoid\n  /// creating a new Vector2 object in this method.\n  ///\n  /// Opposite of [localToGlobal].\n  Vector2 globalToLocal(Vector2 point, {Vector2? output}) {\n    return transform.globalToLocal(point, output: output);\n  }\n\n  /// Convert a point from the viewfinder's coordinate system to the global\n  /// coordinate system.\n  ///\n  /// Use [output] to send in a Vector2 object that will be used to avoid\n  /// creating a new Vector2 object in this method.\n  ///\n  /// Opposite of [globalToLocal].\n  Vector2 localToGlobal(Vector2 point, {Vector2? output}) {\n    return transform.localToGlobal(point, output: output);\n  }\n\n  /// How much of a game world ought to be visible through the viewport.\n  ///\n  /// When this property is non-null, the viewfinder will automatically select\n  /// the maximum zoom level such that a rectangle of size [visibleGameSize]\n  /// (in game coordinates) is visible through the viewport. If you want a\n  /// certain dimension to be unconstrained, set it to zero.\n  ///\n  /// For example, if `visibleGameSize` is set to `[100.0, 0.0]`, the zoom level\n  /// will be chosen such that 100 game units will be visible across the width\n  /// of the viewport. Likewise, setting `visibleGameSize` to `[5.0, 10.0]`\n  /// will ensure that 5 or more game units are visible across the width of the\n  /// viewport, and 10 or more game units across the height.\n  ///\n  /// This property is an alternative way to set the [zoom] level for the\n  /// viewfinder. It is persistent too: if the game size changes, the zoom\n  /// will be recalculated to fit the constraint.\n  ///\n  /// If you set the [visibleGameSize] you will remove any fixed resolution\n  /// constraints that you might have previously put.\n  Vector2? get visibleGameSize => _visibleGameSize;\n  Vector2? _visibleGameSize;\n  set visibleGameSize(Vector2? value) {\n    if (value == null || (value.x == 0 && value.y == 0)) {\n      _visibleGameSize = null;\n    } else {\n      assert(\n        value.x >= 0 && value.y >= 0,\n        'visibleGameSize cannot be negative: $value',\n      );\n      _visibleGameSize = value;\n      _updateZoom();\n    }\n  }\n\n  final Vector2 _zeroVector = Vector2.zero();\n  final Vector2 _topLeft = Vector2.zero();\n  final Vector2 _bottomRight = Vector2.zero();\n  final Vector2 _topRight = Vector2.zero();\n  final Vector2 _bottomLeft = Vector2.zero();\n\n  /// See [CameraComponent.visibleWorldRect].\n  @internal\n  Rect get visibleWorldRect => visibleRect ??= computeVisibleRect();\n  @internal\n  Rect? visibleRect;\n  @protected\n  Rect computeVisibleRect() {\n    final viewportSize = camera.viewport.virtualSize;\n    final currentTransform = transform;\n    currentTransform.globalToLocal(_zeroVector, output: _topLeft);\n    currentTransform.globalToLocal(viewportSize, output: _bottomRight);\n    var minX = min(_topLeft.x, _bottomRight.x);\n    var minY = min(_topLeft.y, _bottomRight.y);\n    var maxX = max(_topLeft.x, _bottomRight.x);\n    var maxY = max(_topLeft.y, _bottomRight.y);\n    if (angle != 0) {\n      _topRight.setValues(viewportSize.x, 0);\n      _bottomLeft.setValues(0, viewportSize.y);\n      currentTransform.globalToLocal(_topRight, output: _topRight);\n      currentTransform.globalToLocal(_bottomLeft, output: _bottomLeft);\n      minX = min(minX, min(_topRight.x, _bottomLeft.x));\n      minY = min(minY, min(_topRight.y, _bottomLeft.y));\n      maxX = max(maxX, max(_topRight.x, _bottomLeft.x));\n      maxY = max(maxY, max(_topRight.y, _bottomLeft.y));\n    }\n    return Rect.fromLTRB(minX, minY, maxX, maxY);\n  }\n\n  /// Set [zoom] level based on the [_visibleGameSize].\n  void _updateZoom() {\n    if (_visibleGameSize != null) {\n      final viewportSize = camera.viewport.size;\n      final zoomX = viewportSize.x / _visibleGameSize!.x;\n      final zoomY = viewportSize.y / _visibleGameSize!.y;\n      zoom = min(zoomX, zoomY);\n    }\n  }\n\n  @override\n  Vector2 parentToLocal(Vector2 point) {\n    return globalToLocal(point);\n  }\n\n  @override\n  Vector2 localToParent(Vector2 point) {\n    return localToGlobal(point);\n  }\n\n  @override\n  void onGameResize(Vector2 size) {\n    _updateZoom();\n    super.onGameResize(size);\n  }\n\n  /// Called by the viewport when its size changes.\n  @internal\n  void onViewportResize() {\n    if (parent != null) {\n      final viewportSize = camera.viewport.virtualSize;\n      transform.position.x = viewportSize.x * _anchor.x;\n      transform.position.y = viewportSize.y * _anchor.y;\n      visibleRect = null;\n    }\n  }\n\n  @mustCallSuper\n  @override\n  void onLoad() {\n    // This has to be done here and on onMount so that it is available for\n    // the CameraComponent.visibleWorldRect calculation in onLoad of the game.\n    updateTransform();\n  }\n\n  @mustCallSuper\n  @override\n  void onMount() {\n    assert(\n      parent! is CameraComponent,\n      'Viewfinder can only be mounted to a CameraComponent',\n    );\n    super.onMount();\n    updateTransform();\n  }\n\n  @internal\n  void updateTransform() {\n    _updateZoom();\n    onViewportResize();\n  }\n\n  /// [ScaleProvider]'s API.\n  @internal\n  @override\n  Vector2 get scale => transform.scale;\n  @internal\n  @override\n  set scale(Vector2 value) {\n    assert(\n      value.x == value.y,\n      'Non-uniform scale cannot be applied to a Viewfinder: $value',\n    );\n    assert(value.x > 0, 'Zoom must be positive: ${value.x}');\n    transform.scale = value;\n    visibleRect = null;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/camera/viewport.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/src/camera/viewports/fixed_resolution_viewport.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\nimport 'package:meta/meta.dart';\n\n/// [Viewport] is a part of a [CameraComponent] system.\n///\n/// The viewport describes a \"window\" through which the underlying game world\n/// is observed. At the same time, the viewport is agnostic of the game world,\n/// and only contain properties that describe the \"window\" itself. These\n/// properties are: the window's size, shape, and position on the screen.\n///\n/// There are several implementations of [Viewport], which differ by their\n/// shape, and also by their behavior in response to changes to the canvas size.\n/// Users may also create their own implementations.\n///\n/// A viewport establishes its own local coordinate system, with the origin at\n/// the top left corner of the viewport's bounding box.\nabstract class Viewport extends Component\n    implements\n        AnchorProvider,\n        PositionProvider,\n        SizeProvider,\n        CoordinateTransform {\n  Viewport({\n    super.children,\n    super.key,\n  });\n\n  final Vector2 _size = Vector2.zero();\n  bool _isInitialized = false;\n\n  @internal\n  final Transform2D transform = Transform2D();\n\n  /// Position of the viewport's anchor in the parent's coordinate frame.\n  ///\n  /// Changing this position will move the viewport around the screen, but will\n  /// not affect which portion of the game world is visible. Thus, the game\n  /// world will appear as a static picture inside the viewport.\n  @override\n  Vector2 get position => _position;\n  final Vector2 _position = Vector2.zero();\n  @override\n  set position(Vector2 value) => _position.setFrom(value);\n\n  /// The logical \"center\" of the viewport.\n  ///\n  /// This point will be used to establish the placement of the viewport in the\n  /// parent's coordinate frame.\n  @override\n  Anchor anchor = Anchor.topLeft;\n\n  /// Size of the viewport, i.e. its width and height.\n  ///\n  /// This property represents the bounding box of the viewport. If the viewport\n  /// is rectangular in shape, then [size] describes the dimensions of that\n  /// rectangle. If the viewport has any other shape (for example, circular),\n  /// then [size] describes the dimensions of the bounding box of the viewport.\n  ///\n  /// Changing the size at runtime triggers the [onViewportResize] event. The\n  /// size cannot be negative.\n  @override\n  Vector2 get size {\n    if (!_isInitialized && camera.parent is FlameGame) {\n      // This is so that the size can be accessed before the viewport is fully\n      // mounted.\n      onGameResize((camera.parent! as FlameGame).canvasSize);\n    }\n    return _size;\n  }\n\n  /// In most cases [virtualSize] is the same as [size], but in the cases when\n  /// the viewport is emulating a different size, this is the size of the\n  /// emulated viewport, for example the resolution for the\n  /// [FixedResolutionViewport].\n  Vector2 get virtualSize => size;\n\n  @override\n  set size(Vector2 value) {\n    assert(\n      value.x >= 0 && value.y >= 0,\n      \"Viewport's size cannot be negative: $value\",\n    );\n    _size.setFrom(value);\n    _isInitialized = true;\n    if (parent != null) {\n      camera.viewfinder.onViewportResize();\n    }\n    onViewportResize();\n    if (hasChildren) {\n      for (final child in children) {\n        child.onParentResize(virtualSize);\n      }\n    }\n  }\n\n  /// Reference to the parent camera.\n  CameraComponent get camera => parent! as CameraComponent;\n\n  /// Apply clip mask to the [canvas].\n  ///\n  /// The mask must be in the viewport's local coordinate system, where the\n  /// top left corner  of the viewport has coordinates (0, 0). The overall size\n  /// of the clip mask's shape must match the [size] of the viewport.\n  ///\n  /// This API must be implemented by all viewports.\n  void clip(Canvas canvas);\n\n  /// Tests whether the given point lies within the viewport.\n  ///\n  /// This method must be consistent with the action of [clip], in the sense\n  /// that [containsLocalPoint] must return true if and only if that point on\n  /// the canvas is not clipped by [clip].\n  @override\n  bool containsLocalPoint(Vector2 point);\n\n  /// Called after the size of the viewport has changed.\n  ///\n  /// The new size will be stored in the [size] property. This method could be\n  /// invoked either when the user explicitly changes the size of the viewport,\n  /// or when the size changes automatically in response to the change in game\n  /// canvas size.\n  ///\n  /// A typical implementation would need to adjust the viewport's clip mask to\n  /// match the new size.\n  @protected\n  void onViewportResize();\n\n  /// Converts a point from the global coordinate system to the local\n  /// coordinate system of the viewport.\n  ///\n  /// Use [output] to send in a Vector2 object that will be used to avoid\n  /// creating a new Vector2 object in this method.\n  ///\n  /// Opposite of [localToGlobal].\n  Vector2 globalToLocal(Vector2 point, {Vector2? output}) {\n    final x = point.x - position.x + anchor.x * size.x;\n    final y = point.y - position.y + anchor.y * size.y;\n    return (output?..setValues(x, y)) ?? Vector2(x, y);\n  }\n\n  /// Converts a point from the local coordinate system of the viewport to the\n  /// global coordinate system.\n  ///\n  /// Use [output] to send in a Vector2 object that will be used to avoid\n  /// creating a new Vector2 object in this method.\n  ///\n  /// Opposite of [globalToLocal].\n  Vector2 localToGlobal(Vector2 point, {Vector2? output}) {\n    final x = point.x + position.x - anchor.x * size.x;\n    final y = point.y + position.y - anchor.y * size.y;\n    return (output?..setValues(x, y)) ?? Vector2(x, y);\n  }\n\n  @override\n  Vector2 parentToLocal(Vector2 point) {\n    return globalToLocal(point);\n  }\n\n  @override\n  Vector2 localToParent(Vector2 point) {\n    return localToGlobal(point);\n  }\n\n  void transformCanvas(Canvas canvas) {\n    canvas.transform2D(transform);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/camera/viewports/circular_viewport.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/camera/viewport.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// A fixed-size viewport in the shape of a circle (or ellipse).\n///\n/// This viewport does not adjust its size or position on the screen in response\n/// to game resize events. However, the [size] of the viewport can be changed\n/// manually at runtime.\nclass CircularViewport extends Viewport {\n  CircularViewport(double radius, {super.children}) {\n    // This will also call [onViewportResize]\n    size = Vector2.all(2 * radius);\n  }\n\n  CircularViewport.ellipse(double radiusX, double radiusY, {super.children}) {\n    size = Vector2(2 * radiusX, 2 * radiusY);\n  }\n\n  Path _clipPath = Path();\n  double _radiusX = 0.0;\n  double _radiusY = 0.0;\n\n  @override\n  void clip(Canvas canvas) => canvas.clipPath(_clipPath, doAntiAlias: false);\n\n  @override\n  bool containsLocalPoint(Vector2 point) {\n    final fx = point.x / _radiusX - 1;\n    final fy = point.y / _radiusY - 1;\n    return fx * fx + fy * fy <= 1;\n  }\n\n  @override\n  void onViewportResize() {\n    _radiusX = size.x / 2;\n    _radiusY = size.y / 2;\n    _clipPath = Path()..addOval(Rect.fromLTRB(0, 0, size.x, size.y));\n  }\n\n  @override\n  void renderDebugMode(Canvas canvas) {\n    canvas.drawPath(_clipPath, debugPaint);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/camera/viewports/fixed_aspect_ratio_viewport.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/camera/viewport.dart';\nimport 'package:meta/meta.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// [FixedAspectRatioViewport] is a rectangular viewport which auto-expands to\n/// take as much space as possible within the canvas, while maintaining a fixed\n/// aspect ratio.\n///\n/// This viewport will automatically adjust its size and position when the\n/// game canvas changes in size. At the same time, manually changing the size\n/// of this viewport is not supported.\nclass FixedAspectRatioViewport extends Viewport {\n  FixedAspectRatioViewport({\n    required this.aspectRatio,\n    super.children,\n  }) : assert(aspectRatio > 0);\n\n  /// The ratio of width to height of the viewport.\n  final double aspectRatio;\n\n  Rect _clipRect = Rect.zero;\n\n  @override\n  @mustCallSuper\n  void onLoad() {\n    final canvasSize = findGame()!.canvasSize;\n    _handleResize(canvasSize);\n  }\n\n  @override\n  void onGameResize(Vector2 size) {\n    if (isLoaded) {\n      super.onGameResize(size);\n    }\n    _handleResize(size);\n  }\n\n  void _handleResize(Vector2 canvasSize) {\n    final availableWidth = canvasSize.x;\n    final availableHeight = canvasSize.y;\n    size = (availableHeight * aspectRatio > availableWidth)\n        ? Vector2(availableWidth, availableWidth / aspectRatio)\n        : Vector2(availableHeight * aspectRatio, availableHeight);\n    position.x = (availableWidth - size.x) / 2 + anchor.x * size.x;\n    position.y = (availableHeight - size.y) / 2 + anchor.y * size.y;\n    _clipRect = Rect.fromLTRB(0, 0, size.x, size.y);\n  }\n\n  @override\n  void clip(Canvas canvas) => canvas.clipRect(_clipRect, doAntiAlias: false);\n\n  @override\n  bool containsLocalPoint(Vector2 point) {\n    final x = point.x;\n    final y = point.y;\n    return x >= 0 && y >= 0 && x <= size.x && y <= size.y;\n  }\n\n  @override\n  void onViewportResize() {\n    final desiredWidth = size.y * aspectRatio;\n    if (desiredWidth > size.x) {\n      size.y = size.x / aspectRatio;\n    } else {\n      size.x = desiredWidth;\n    }\n\n    final x = size.x / 2;\n    final y = size.y / 2;\n    _clipRect = Rect.fromLTRB(-x, -y, x, y);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/camera/viewports/fixed_resolution_viewport.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/camera.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\n\n/// The [FixedResolutionViewport] is a rectangular viewport which auto-expands\n/// to take as much space as possible within the canvas, while maintaining a\n/// fixed resolution and aspect ratio. I.e. the viewport will always show the\n/// same dimensions ([resolution]) of the game world, regardless of the size of\n/// the canvas.\n///\n/// This viewport will automatically adjust its size and position when the\n/// game canvas changes in size. At the same time, manually changing the size\n/// of this viewport is not supported.\nclass FixedResolutionViewport extends FixedAspectRatioViewport\n    implements ReadOnlyScaleProvider {\n  FixedResolutionViewport({\n    required this.resolution,\n    super.children,\n  }) : super(aspectRatio: resolution.x / resolution.y);\n\n  /// The resolution that the viewport should adhere to.\n  final Vector2 resolution;\n\n  @override\n  Vector2 get virtualSize => resolution;\n\n  @override\n  Vector2 get scale => transform.scale;\n\n  final Vector2 _scaleVector = Vector2.zero();\n\n  @override\n  bool containsLocalPoint(Vector2 point) {\n    final x = point.x;\n    final y = point.y;\n    return x >= 0 && y >= 0 && x <= virtualSize.x && y <= virtualSize.y;\n  }\n\n  @override\n  void onViewportResize() {\n    super.onViewportResize();\n    final scaleX = size.x / resolution.x;\n    final scaleY = size.y / resolution.y;\n    _scaleVector.setAll(min(scaleX, scaleY));\n    transform.scale = _scaleVector;\n    camera.viewfinder.visibleRect = null;\n  }\n\n  @override\n  Vector2 globalToLocal(Vector2 point, {Vector2? output}) {\n    final viewportPoint = super.globalToLocal(point, output: output);\n    return transform.globalToLocal(viewportPoint, output: output);\n  }\n\n  @override\n  Vector2 localToGlobal(Vector2 point, {Vector2? output}) {\n    final viewportPoint = transform.localToGlobal(point, output: output);\n    return super.localToGlobal(viewportPoint, output: output);\n  }\n\n  @override\n  void transformCanvas(Canvas canvas) {\n    canvas.translate(size.x / 2, size.y / 2);\n    super.transformCanvas(canvas);\n    canvas.translate(-(size.x / 2) / scale.x, -(size.y / 2) / scale.y);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/camera/viewports/fixed_size_viewport.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/camera/viewport.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// A rectangular viewport with fixed dimensions.\n///\n/// You can change the size of this viewport at runtime, but it will not\n/// auto-resize when its parent changes size.\nclass FixedSizeViewport extends Viewport {\n  FixedSizeViewport(\n    double width,\n    double height, {\n    super.children,\n  }) {\n    size = Vector2(width, height);\n  }\n\n  late Rect _clipRect;\n\n  @override\n  void clip(Canvas canvas) => canvas.clipRect(_clipRect, doAntiAlias: false);\n\n  @override\n  bool containsLocalPoint(Vector2 point) {\n    final x = point.x;\n    final y = point.y;\n    return x >= 0 && x <= size.x && y >= 0 && y <= size.y;\n  }\n\n  @override\n  void onViewportResize() {\n    _clipRect = Rect.fromLTWH(0, 0, size.x, size.y);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/camera/viewports/max_viewport.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/camera/viewport.dart';\nimport 'package:meta/meta.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// The default viewport, which is as big as the game canvas allows.\n///\n/// This viewport does not perform any clipping.\nclass MaxViewport extends Viewport {\n  MaxViewport({super.children});\n\n  @override\n  @mustCallSuper\n  void onLoad() {\n    size = findGame()!.canvasSize;\n  }\n\n  @override\n  void onGameResize(Vector2 size) {\n    this.size = size;\n    super.onGameResize(size);\n  }\n\n  @override\n  void clip(Canvas canvas) {}\n\n  @override\n  bool containsLocalPoint(Vector2 point) => true;\n\n  @override\n  void onViewportResize() {}\n}\n"
  },
  {
    "path": "packages/flame/lib/src/camera/world.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/camera/camera_component.dart';\nimport 'package:flame/src/components/core/component.dart';\nimport 'package:flame/src/components/mixins/coordinate_transform.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// The root component for all game world elements.\n///\n/// The primary feature of this component is that it disables regular rendering,\n/// and allows itself to be rendered through a [CameraComponent] only. The\n/// updates proceed through the world tree normally.\n///\n/// The [priority] of the world by default is the maximum 32bit negative int\n/// value to ensure it will always be earlier in the component tree than a\n/// [CameraComponent].\nclass World extends Component implements CoordinateTransform {\n  World({\n    super.children,\n    super.priority = -0x7fffffff,\n    super.key,\n  });\n\n  @override\n  void renderTree(Canvas canvas) {}\n\n  /// The rendering method invoked by the [CameraComponent].\n  ///\n  /// If you want to do changes to the rendering of the world, this is the\n  /// method that you want to override, not [renderTree].\n  void renderFromCamera(Canvas canvas) {\n    assert(CameraComponent.currentCamera != null);\n    super.renderTree(canvas);\n  }\n\n  @override\n  bool containsLocalPoint(Vector2 point) => true;\n\n  @override\n  Vector2? localToParent(Vector2 point) => null;\n\n  @override\n  Vector2? parentToLocal(Vector2 point) => null;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/broadphase/broadphase.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/collisions.dart';\n\n/// The [Broadphase] class is used to make collision detection more efficient\n/// by doing a rough estimation of which hitboxes that can collide before their\n/// actual intersections are calculated.\n///\n/// Currently there are two implementations of [Broadphase]:\n///\n/// - [Sweep] is the simplest system. It simply short-circuits potential\n///   collisions based on the horizontal (x) position of the components\n///   in question. It is the default implementation when you use\n///   `HasCollisionDetection`.\n/// - [QuadTree] works faster in some cases. It requires additional setup\n///   and works only with fixed-size maps. See [HasQuadTreeCollisionDetection]\n///   for details.\n///\n/// Always experiment to see which approach works best for your game.\nabstract class Broadphase<T extends Hitbox<T>> {\n  Broadphase();\n\n  /// This method can be used if there are things that needs to be prepared in\n  /// each tick.\n  void update() {}\n\n  /// Returns a flat List of items regardless of what data structure is used to\n  /// store collision information.\n  List<T> get items;\n\n  /// Adds an item to the broadphase. Should be called in a\n  /// [CollisionDetection] class while adding a hitbox into its collision\n  /// detection system.\n  void add(T item);\n\n  void addAll(Iterable<T> items) {\n    for (final item in items) {\n      add(item);\n    }\n  }\n\n  /// Removes an item from the broadphase. Should be called in a\n  /// [CollisionDetection] class while removing a hitbox from its collision\n  /// detection system.\n  void remove(T item);\n\n  void removeAll(Iterable<T> items) {\n    for (final item in items) {\n      remove(item);\n    }\n  }\n\n  /// Returns the potential hitbox collisions\n  Iterable<CollisionProspect<T>> query();\n}\n\n/// A [CollisionProspect] is a tuple that is used to contain two potentially\n/// colliding hitboxes.\nclass CollisionProspect<T> {\n  T _a;\n  T _b;\n\n  T get a => _a;\n  T get b => _b;\n\n  int get hash => _hash;\n  int _hash;\n\n  CollisionProspect(this._a, this._b)\n    : _hash = _pairHash(_a.hashCode, _b.hashCode);\n\n  /// Computes a hash for an unordered pair of hash codes that is much less\n  /// likely to collide than a simple XOR.\n  static int _pairHash(int h1, int h2) {\n    return Object.hash(min(h1, h2), max(h1, h2));\n  }\n\n  /// Sets the prospect to contain [a] and [b] instead of what it previously\n  /// contained.\n  void set(T a, T b) {\n    _a = a;\n    _b = b;\n    _hash = _pairHash(a.hashCode, b.hashCode);\n  }\n\n  /// Sets the prospect to contain the content of [other].\n  void setFrom(CollisionProspect<T> other) {\n    _a = other._a;\n    _b = other._b;\n    _hash = other._hash;\n  }\n\n  /// Creates a new prospect object with the same content.\n  CollisionProspect<T> clone() => CollisionProspect(_a, _b);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/broadphase/prospect_pool.dart",
    "content": "import 'package:flame/src/collisions/broadphase/broadphase.dart';\nimport 'package:flame/src/collisions/hitboxes/hitbox.dart';\n\n/// This pool is used to not create unnecessary [CollisionProspect] objects\n/// during collision detection, but to re-use the ones that have already been\n/// created.\nclass ProspectPool<T extends Hitbox<T>> {\n  ProspectPool({this.incrementSize = 1000});\n\n  /// How much the pool should increase in size every time it needs to be made\n  /// larger.\n  final int incrementSize;\n  final _storage = <CollisionProspect<T>>[];\n  int get length => _storage.length;\n\n  /// The size of the pool will expand with [incrementSize] amount of\n  /// [CollisionProspect]s that are initially populated with two [dummyItem]s.\n  void expand(T dummyItem) {\n    for (var i = 0; i < incrementSize; i++) {\n      _storage.add(CollisionProspect<T>(dummyItem, dummyItem));\n    }\n  }\n\n  CollisionProspect<T> operator [](int index) => _storage[index];\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/broadphase/quadtree/has_quadtree_collision_detection.dart",
    "content": "import 'package:flame/camera.dart';\nimport 'package:flame/collisions.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\n\n/// This should be applied to a [FlameGame] to bring QuadTree collision\n/// support.\n///\n/// Use [HasQuadTreeCollisionDetection] if you have lots of collidable entities\n/// in your game, but most of them are static (such as platforms, walls, trees,\n/// buildings).\n///\n/// Always experiment before deciding which collision detection\n/// method to use. It's not unheard of to see better performance with\n/// the default [HasCollisionDetection] mixin.\n///\n/// [initializeCollisionDetection] should be called in the game's [onLoad]\n/// method.\nmixin HasQuadTreeCollisionDetection<W extends World> on FlameGame<W>\n    implements HasCollisionDetection<QuadTreeBroadphase> {\n  late QuadTreeCollisionDetection _collisionDetection;\n\n  @override\n  QuadTreeCollisionDetection get collisionDetection => _collisionDetection;\n\n  @override\n  set collisionDetection(\n    CollisionDetection<ShapeHitbox, QuadTreeBroadphase> cd,\n  ) {\n    if (cd is! QuadTreeCollisionDetection) {\n      throw 'Must be QuadTreeCollisionDetection!';\n    }\n    _collisionDetection = cd;\n  }\n\n  /// Initialize the QuadTree.\n  ///\n  /// - [mapDimensions] describes the collision area coordinates and size.\n  ///   Should match to game map's position and size.\n  /// - [maxObjects] (optional) - maximum amount of objects in one quadrant.\n  /// - [maxLevels] (optional) - maximum number of nested quadrants.\n  /// - [minimumDistance] (optional) - specify minimum distance between objects\n  ///   to consider them as possibly colliding. You can also implement the\n  ///   [minimumDistanceCheck] if you need some custom behavior.\n  ///\n  /// The [onComponentTypeCheck] checks if objects of different types should\n  /// collide.\n  /// The result of the calculation is cached so you should not check any\n  /// dynamical parameters here, the function is intended to be used as pure\n  /// type checker.\n  /// It should usually not be overridden, see\n  /// [CollisionCallbacks.onComponentTypeCheck] instead\n  void initializeCollisionDetection({\n    required Rect mapDimensions,\n    double? minimumDistance,\n    int maxObjects = 25,\n    int maxLevels = 10,\n  }) {\n    _collisionDetection = QuadTreeCollisionDetection(\n      mapDimensions: mapDimensions,\n      maxDepth: maxLevels,\n      maxObjects: maxObjects,\n      onComponentTypeCheck: onComponentTypeCheck,\n      minimumDistanceCheck: minimumDistanceCheck,\n    );\n    this.minimumDistance = minimumDistance;\n  }\n\n  double? minimumDistance;\n\n  bool minimumDistanceCheck(Vector2 activeItemCenter, Vector2 potentialCenter) {\n    return minimumDistance == null ||\n        !((activeItemCenter.x - potentialCenter.x).abs() > minimumDistance! ||\n            (activeItemCenter.y - potentialCenter.y).abs() > minimumDistance!);\n  }\n\n  bool onComponentTypeCheck(ShapeHitbox first, ShapeHitbox second) {\n    return first.onComponentTypeCheck(second) &&\n        second.onComponentTypeCheck(first);\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    collisionDetection.run();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/broadphase/quadtree/quad_tree_broadphase.dart",
    "content": "import 'dart:collection';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/extensions.dart';\n\ntypedef ExternalBroadphaseCheck =\n    bool Function(\n      ShapeHitbox first,\n      ShapeHitbox second,\n    );\n\ntypedef ExternalMinDistanceCheck =\n    bool Function(\n      Vector2 activeItemCenter,\n      Vector2 potentialCenter,\n    );\n\n/// Performs Quad Tree broadphase check.\n///\n/// See [HasQuadTreeCollisionDetection.initializeCollisionDetection] for a\n/// detailed description of its initialization parameters.\nclass QuadTreeBroadphase extends Broadphase<ShapeHitbox> {\n  QuadTreeBroadphase({\n    required Rect mainBoxSize,\n    required this.broadphaseCheck,\n    required this.minimumDistanceCheck,\n    int maxObjects = 25,\n    int maxDepth = 10,\n  }) : tree = QuadTree<ShapeHitbox>(\n         mainBoxSize: mainBoxSize,\n         maxObjects: maxObjects,\n         maxDepth: maxDepth,\n       );\n\n  final QuadTree<ShapeHitbox> tree;\n\n  final activeHitboxes = HashSet<ShapeHitbox>();\n\n  ExternalBroadphaseCheck broadphaseCheck;\n  ExternalMinDistanceCheck minimumDistanceCheck;\n  final _broadphaseCheckCache = <ShapeHitbox, Map<ShapeHitbox, bool>>{};\n\n  final _cachedCenters = <ShapeHitbox, Vector2>{};\n\n  final _potentials = <int, CollisionProspect<ShapeHitbox>>{};\n  final _potentialsTmp = <ShapeHitbox>[];\n  final _prospectPool = ProspectPool<ShapeHitbox>();\n\n  @override\n  List<ShapeHitbox> get items => tree.hitboxes;\n\n  @override\n  Iterable<CollisionProspect<ShapeHitbox>> query() {\n    _potentials.clear();\n    _potentialsTmp.clear();\n\n    for (final activeItem in activeHitboxes) {\n      if (activeItem.isRemoving || !activeItem.isMounted) {\n        tree.remove(activeItem);\n        continue;\n      }\n\n      final itemCenter = activeItem.aabb.center;\n      final potentiallyCollide = tree.query(activeItem);\n      for (final potential in potentiallyCollide.entries.first.value) {\n        if (potential.collisionType == CollisionType.inactive) {\n          continue;\n        }\n\n        if (_broadphaseCheckCache[activeItem]?[potential] == false) {\n          continue;\n        }\n\n        if (!potential.allowSiblingCollision &&\n            potential.hitboxParent == activeItem.hitboxParent &&\n            potential.isMounted) {\n          continue;\n        }\n\n        final distanceCloseEnough = minimumDistanceCheck.call(\n          itemCenter,\n          _cacheCenterOfHitbox(potential),\n        );\n        if (distanceCloseEnough == false) {\n          continue;\n        }\n\n        _potentialsTmp\n          ..add(activeItem)\n          ..add(potential);\n      }\n    }\n\n    if (_potentialsTmp.isNotEmpty) {\n      for (var i = 0; i < _potentialsTmp.length; i += 2) {\n        final item0 = _potentialsTmp[i];\n        final item1 = _potentialsTmp[i + 1];\n        if (broadphaseCheck(item0, item1)) {\n          final CollisionProspect<ShapeHitbox> prospect;\n          if (_prospectPool.length <= i) {\n            _prospectPool.expand(item0);\n          }\n          prospect = _prospectPool[i]..set(item0, item1);\n          _potentials[prospect.hash] = prospect;\n        } else {\n          if (_broadphaseCheckCache[item0] == null) {\n            _broadphaseCheckCache[item0] = {};\n          }\n          _broadphaseCheckCache[item0]![item1] = false;\n        }\n      }\n    }\n    return _potentials.values;\n  }\n\n  void updateTransform(ShapeHitbox item) {\n    tree.remove(item, keepOldPosition: true);\n    _cacheCenterOfHitbox(item);\n    tree.add(item);\n  }\n\n  @override\n  void add(ShapeHitbox item) {\n    tree.add(item);\n    if (item.collisionType == CollisionType.active) {\n      activeHitboxes.add(item);\n    }\n    _cacheCenterOfHitbox(item);\n  }\n\n  @override\n  void remove(ShapeHitbox item) {\n    tree.remove(item);\n    _cachedCenters.remove(item);\n    if (item.collisionType == CollisionType.active) {\n      activeHitboxes.remove(item);\n    }\n\n    final checkCache = _broadphaseCheckCache[item];\n    if (checkCache != null) {\n      for (final entry in checkCache.entries) {\n        _broadphaseCheckCache[entry.key]?.remove(item);\n      }\n      _broadphaseCheckCache.remove(item);\n    }\n  }\n\n  void clear() {\n    tree.clear();\n    activeHitboxes.clear();\n    _broadphaseCheckCache.clear();\n    _cachedCenters.clear();\n  }\n\n  /// Caches hitbox center because calculating on-the-fly is too expensive\n  /// whereas many of game objects could not change theirs position or size\n  Vector2 _cacheCenterOfHitbox(ShapeHitbox hitbox) {\n    var cache = _cachedCenters[hitbox];\n    if (cache == null) {\n      _cachedCenters[hitbox] = hitbox.aabb.center;\n      cache = _cachedCenters[hitbox];\n    }\n    return cache!;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/broadphase/quadtree/quadtree.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/extensions.dart';\n\n/// QuadTree calculation class not bound to Flame. Could be used anywhere\n/// outside of Flame, for example in isolates to calculate background logic.\n///\n/// Usage:\n/// 1. Create new instance.\n/// 2. Use [add] to add hitboxes to simulation.\n/// 3. Use [query] to get list of hitboxes that potentially collide.\n/// 4. Use [remove] to remove a hitbox from the tree.\n/// 5. Use sequence of [remove] and [add] to simulate hitbox movement.\n/// 6. Use [hasMoved] to determine if a hitbox ever changed its position.\n/// 7. Call [clear] to remove all data.\n///\n/// Use [optimize] to scan the tree and remove unused quadrants.\nclass QuadTree<T extends Hitbox<T>> {\n  QuadTree({\n    this.maxObjects = 25,\n    this.maxDepth = 10,\n    this.mainBoxSize = Rect.zero,\n  });\n\n  final int maxObjects;\n  final int maxDepth;\n\n  Rect mainBoxSize;\n\n  var _rootNode = QuadTreeNode<T>();\n  int _nodeLastId = 0;\n  final _oldPositionByItem = <ShapeHitbox, Aabb2>{};\n  final _hitboxAtNode = <ShapeHitbox, QuadTreeNode>{};\n\n  List<T> get hitboxes => _rootNode.valuesRecursive;\n\n  Rect _getBoxOfValue(T value) {\n    final minOffset = Offset(\n      value.aabb.min.x < 0 ? 0 : value.aabb.min.x,\n      value.aabb.min.y < 0 ? 0 : value.aabb.min.y,\n    );\n    return Rect.fromPoints(minOffset, value.aabb.max.toOffset());\n  }\n\n  bool _noChildren(QuadTreeNode node) => node.children[0] == null;\n\n  void clear() {\n    _rootNode = QuadTreeNode<T>();\n    _nodeLastId = 0;\n    _hitboxAtNode.clear();\n    _oldPositionByItem.clear();\n  }\n\n  static Rect _computeBox(Rect box, _QuadTreeZone zone) {\n    final origin = box.topLeft;\n    final childSize = (box.size / 2).toOffset();\n    switch (zone) {\n      case _QuadTreeZone.topLeft:\n        return Rect.fromLTWH(origin.dx, origin.dy, childSize.dx, childSize.dy);\n      case _QuadTreeZone.topRight:\n        return Rect.fromLTWH(\n          origin.dx + childSize.dx,\n          origin.dy,\n          childSize.dx,\n          childSize.dy,\n        );\n      case _QuadTreeZone.bottomLeft:\n        return Rect.fromLTWH(\n          origin.dx,\n          origin.dy + childSize.dy,\n          childSize.dx,\n          childSize.dy,\n        );\n      case _QuadTreeZone.bottomRight:\n        final position = origin + childSize;\n        return Rect.fromLTWH(\n          position.dx,\n          position.dy,\n          childSize.dx,\n          childSize.dy,\n        );\n      default:\n        assert(false, 'Invalid child index $box $zone');\n        return Rect.zero;\n    }\n  }\n\n  _QuadTreeZone _getQuadrant(Rect nodeBox, Rect valueBox) {\n    final center = nodeBox.center;\n    if (valueBox.right <= center.dx) {\n      if (valueBox.bottom <= center.dy) {\n        return _QuadTreeZone.topLeft;\n      } else if (valueBox.top > center.dy) {\n        return _QuadTreeZone.bottomLeft;\n      } else {\n        return _QuadTreeZone.root;\n      }\n    } else if (valueBox.left > center.dx) {\n      if (valueBox.bottom <= center.dy) {\n        return _QuadTreeZone.topRight;\n      } else if (valueBox.top > center.dy) {\n        return _QuadTreeZone.bottomRight;\n      } else {\n        return _QuadTreeZone.root;\n      }\n    } else {\n      return _QuadTreeZone.root;\n    }\n  }\n\n  void add(T hitbox) {\n    final node = _add(_rootNode, 0, mainBoxSize, hitbox, null);\n    _oldPositionByItem[hitbox as ShapeHitbox] = Aabb2.copy(hitbox.aabb);\n    _hitboxAtNode[hitbox as ShapeHitbox] = node;\n  }\n\n  QuadTreeNode<T> _add(\n    QuadTreeNode<T> node,\n    int depth,\n    Rect box,\n    T value,\n    QuadTreeNode? parent,\n  ) {\n    QuadTreeNode<T> finalNode;\n    if (_noChildren(node)) {\n      // Insert the value in this node if possible.\n      if (depth >= maxDepth || node.hitboxes.length < maxObjects) {\n        node.hitboxes.add(value);\n        finalNode = node;\n      }\n      // Otherwise, we split and we try again.\n      else {\n        _split(node, box);\n        finalNode = _add(node, depth, box, value, parent);\n      }\n    } else {\n      final quadrant = _getQuadrant(box, _getBoxOfValue(value));\n      // Add the value in a child if the value is entirely contained in it.\n      if (quadrant != _QuadTreeZone.root) {\n        final children = node.children[quadrant.value];\n        if (children == null) {\n          throw 'Invalid index $quadrant';\n        }\n        finalNode = _add(\n          children as QuadTreeNode<T>,\n          depth + 1,\n          _computeBox(box, quadrant),\n          value,\n          node,\n        );\n      }\n      // Otherwise, we add the value in the current node.\n      else {\n        node.hitboxes.add(value);\n        finalNode = node;\n      }\n    }\n    if (parent != null && finalNode.parent == null) {\n      finalNode.parent = parent;\n    }\n    return finalNode;\n  }\n\n  void _split(QuadTreeNode node, Rect box) {\n    assert(_noChildren(node), 'Only leaves can be split');\n    // Create children\n    for (var i = 0; i < node.children.length; i++) {\n      final newId = ++_nodeLastId;\n      node.children[i] = QuadTreeNode<T>()\n        ..parent = node\n        ..id = newId;\n    }\n\n    // Assign values to children\n    final moveValues = <T>[]; // New values for this node\n    for (final value in node.hitboxes) {\n      final quadrant = _getQuadrant(box, _getBoxOfValue(value as T));\n      if (quadrant != _QuadTreeZone.root) {\n        final children = node.children[quadrant.value];\n        if (children == null) {\n          throw 'Invalid index $quadrant';\n        }\n        children.hitboxes.add(value);\n        _hitboxAtNode[value as ShapeHitbox] = children;\n      } else {\n        moveValues.add(value);\n        _hitboxAtNode[value as ShapeHitbox] = node;\n      }\n    }\n    node.hitboxes = moveValues;\n  }\n\n  void remove(T hitbox, {bool keepOldPosition = false}) {\n    final node = _hitboxAtNode.remove(hitbox as ShapeHitbox);\n    if (node != null) {\n      node.hitboxes.remove(hitbox);\n      if (!keepOldPosition) {\n        _oldPositionByItem.remove(hitbox as ShapeHitbox);\n      }\n    }\n  }\n\n  void optimize() {\n    _tryMergeRecursive(_rootNode);\n  }\n\n  void _tryMergeRecursive(QuadTreeNode node) {\n    if (_noChildren(node)) {\n      return;\n    }\n\n    var tryMerge = true;\n    var hitboxesInside = node.hitboxes.length;\n    for (final child in node.children) {\n      if (child == null) {\n        throw 'Child must be not null';\n      }\n      _tryMergeRecursive(child);\n      if (_noChildren(child)) {\n        hitboxesInside += child.hitboxes.length;\n      } else {\n        tryMerge = false;\n      }\n    }\n\n    if (hitboxesInside <= maxObjects && tryMerge) {\n      for (var i = 0; i < node.children.length; i++) {\n        final child = node.children[i];\n        if (child == null) {\n          throw 'Child must be not null';\n        }\n        node.hitboxes.addAll(child.hitboxes);\n        child.hitboxes.clear();\n        node.children[i] = null;\n      }\n    }\n  }\n\n  Map<int, List<T>> query(T value) {\n    final node = _hitboxAtNode[value as ShapeHitbox];\n    var id = -1;\n    final values = <T>[];\n    if (node == null) {\n      throw '$node not found';\n    }\n    id = node.id;\n    values.addAll(node.hitboxes as List<T>);\n    values.addAll(_getChildrenItems(node));\n    values.addAll(_getParentItems(node));\n    return {id: values};\n  }\n\n  List<T> _getChildrenItems(QuadTreeNode parent) {\n    final list = <T>[];\n    for (final child in parent.children) {\n      if (child != null) {\n        list.addAll(child.hitboxes as List<T>);\n        if (child.children[0] != null) {\n          list.addAll(_getChildrenItems(child));\n        }\n      }\n    }\n    return list;\n  }\n\n  List<T> _getParentItems(QuadTreeNode node) {\n    final list = <T>[];\n    final parent = node.parent;\n    if (parent != null) {\n      list.addAll(parent.hitboxes as List<T>);\n      list.addAll(_getParentItems(parent));\n    }\n    return list;\n  }\n\n  bool hasMoved(T hitbox) {\n    final lastPos = _oldPositionByItem[hitbox as ShapeHitbox];\n    if (lastPos == null) {\n      return true;\n    }\n    return lastPos.min != hitbox.aabb.min || lastPos.max == hitbox.aabb.max;\n  }\n}\n\n/// Public interface to QuadTree internal data structures.\n///\n/// Allows to read a node's data without risk of affecting the outcome of\n/// collisions.\n/// Use [QuadTreeNodeDebugInfo.init] to initialize the class for the\n/// current collision detection. You need only this instance to get all\n/// another nodes and hitboxes using [nodes] and the\n/// [allElements] / [ownElements] methods.\n/// Use [rect] to get node's computed box;\n/// The class might be useful to render debugging info.\n/// See examples for details.\nclass QuadTreeNodeDebugInfo {\n  QuadTreeNodeDebugInfo(this.rect, this.node, this.cd);\n\n  factory QuadTreeNodeDebugInfo.init(QuadTreeCollisionDetection cd) {\n    final node = cd.broadphase.tree._rootNode;\n    final rect = cd.broadphase.tree.mainBoxSize;\n    return QuadTreeNodeDebugInfo(rect, node, cd);\n  }\n\n  final Rect rect;\n  final QuadTreeNode node;\n  final QuadTreeCollisionDetection cd;\n\n  List<ShapeHitbox> get ownElements => node.hitboxes as List<ShapeHitbox>;\n\n  List<ShapeHitbox> get allElements =>\n      node.valuesRecursive as List<ShapeHitbox>;\n\n  bool get noChildren => node.children[0] == null;\n\n  int get id => node.id;\n\n  List<QuadTreeNodeDebugInfo> get nodes {\n    final list = <QuadTreeNodeDebugInfo>[this];\n    for (var i = 0; i < node.children.length; i++) {\n      final node = this.node.children[i];\n      if (node == null) {\n        continue;\n      }\n\n      final nodeRect = QuadTree._computeBox(rect, _QuadTreeZone.fromIndex(i));\n      final dbg = QuadTreeNodeDebugInfo(nodeRect, node, cd);\n      list.add(dbg);\n      list.addAll(dbg.nodes);\n    }\n    return list;\n  }\n}\n\nclass QuadTreeNode<T extends Hitbox<T>> {\n  final List<QuadTreeNode?> children = List.generate(\n    4,\n    (index) => null,\n    growable: false,\n  );\n\n  List<T> hitboxes = <T>[];\n\n  QuadTreeNode? parent;\n  int id = 0;\n\n  List<T> get valuesRecursive {\n    final data = <T>[];\n\n    data.addAll(hitboxes);\n    for (final child in children) {\n      if (child == null) {\n        continue;\n      }\n      data.addAll(child.valuesRecursive as List<T>);\n    }\n    return data;\n  }\n\n  @override\n  String toString() {\n    return 'node $id';\n  }\n}\n\nenum _QuadTreeZone {\n  root(-1),\n  topLeft(0),\n  topRight(1),\n  bottomLeft(2),\n  bottomRight(3)\n  ;\n\n  const _QuadTreeZone(this.value);\n\n  factory _QuadTreeZone.fromIndex(int i) {\n    return switch (i) {\n      0 => _QuadTreeZone.topLeft,\n      1 => _QuadTreeZone.topRight,\n      2 => _QuadTreeZone.bottomLeft,\n      3 => _QuadTreeZone.bottomRight,\n      _ => _QuadTreeZone.root,\n    };\n  }\n\n  final int value;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/broadphase/quadtree/quadtree_collision_detection.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flutter/widgets.dart';\n\n/// Collision detection modification to support a Quad Tree broadphase.\n///\n/// Do not use standard [items] list for components. Instead adds all components\n/// into [QuadTreeBroadphase] class.\nclass QuadTreeCollisionDetection\n    extends StandardCollisionDetection<QuadTreeBroadphase> {\n  QuadTreeCollisionDetection({\n    required Rect mapDimensions,\n    required ExternalBroadphaseCheck onComponentTypeCheck,\n    required ExternalMinDistanceCheck minimumDistanceCheck,\n    int maxObjects = 25,\n    int maxDepth = 10,\n  }) : super(\n         broadphase: QuadTreeBroadphase(\n           mainBoxSize: mapDimensions,\n           maxObjects: maxObjects,\n           maxDepth: maxDepth,\n           broadphaseCheck: onComponentTypeCheck,\n           minimumDistanceCheck: minimumDistanceCheck,\n         ),\n       );\n\n  final _listenerCollisionType = <ShapeHitbox, VoidCallback>{};\n  final _scheduledUpdate = <ShapeHitbox>{};\n\n  @override\n  void add(ShapeHitbox item) {\n    item.onAabbChanged = () => _scheduledUpdate.add(item);\n    void listenerCollisionType() {\n      if (item.isMounted) {\n        if (item.collisionType == CollisionType.active) {\n          broadphase.activeHitboxes.add(item);\n        } else {\n          broadphase.activeHitboxes.remove(item);\n        }\n      }\n    }\n\n    item.collisionTypeNotifier.addListener(listenerCollisionType);\n    _listenerCollisionType[item] = listenerCollisionType;\n\n    super.add(item);\n  }\n\n  @override\n  void addAll(Iterable<ShapeHitbox> items) {\n    for (final item in items) {\n      add(item);\n    }\n  }\n\n  @override\n  void remove(ShapeHitbox item) {\n    item.onAabbChanged = null;\n    final listenerCollisionType = _listenerCollisionType[item];\n    if (listenerCollisionType != null) {\n      item.collisionTypeNotifier.removeListener(listenerCollisionType);\n      _listenerCollisionType.remove(item);\n    }\n\n    super.remove(item);\n  }\n\n  @override\n  void removeAll(Iterable<ShapeHitbox> items) {\n    broadphase.clear();\n    for (final item in items) {\n      remove(item);\n    }\n  }\n\n  @override\n  void run() {\n    for (final hitbox in _scheduledUpdate) {\n      broadphase.updateTransform(hitbox);\n    }\n    _scheduledUpdate.clear();\n    super.run();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/broadphase/sweep/sweep.dart",
    "content": "import 'package:flame/collisions.dart';\n\nclass Sweep<T extends Hitbox<T>> extends Broadphase<T> {\n  Sweep({List<T>? items}) : items = items ?? [];\n\n  @override\n  final List<T> items;\n\n  final _active = <T>[];\n  final _potentials = <int, CollisionProspect<T>>{};\n  final _prospectPool = ProspectPool<T>();\n\n  @override\n  void add(T item) => items.add(item);\n\n  @override\n  void remove(T item) => items.remove(item);\n\n  @override\n  void update() {\n    items.sort((a, b) => a.aabb.min.x.compareTo(b.aabb.min.x));\n  }\n\n  @override\n  Iterable<CollisionProspect<T>> query() {\n    _active.clear();\n    _potentials.clear();\n\n    for (final item in items) {\n      if (item.collisionType == CollisionType.inactive) {\n        continue;\n      }\n      if (_active.isEmpty) {\n        _active.add(item);\n        continue;\n      }\n      final currentBox = item.aabb;\n      final currentMin = currentBox.min.x;\n      for (var i = _active.length - 1; i >= 0; i--) {\n        final activeItem = _active[i];\n        final activeBox = activeItem.aabb;\n        if (activeBox.max.x >= currentMin) {\n          if (item.collisionType == CollisionType.active ||\n              activeItem.collisionType == CollisionType.active) {\n            if (_prospectPool.length <= _potentials.length) {\n              _prospectPool.expand(item);\n            }\n            final prospect = _prospectPool[_potentials.length]\n              ..set(item, activeItem);\n            _potentials[prospect.hash] = prospect;\n          }\n        } else {\n          _active.remove(activeItem);\n        }\n      }\n      _active.add(item);\n    }\n    return _potentials.values;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/collision_callbacks.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flutter/foundation.dart';\n\n/// The [CollisionType] is used to determine which other hitboxes that it\n/// should collide with.\nenum CollisionType {\n  /// Collides with other hitboxes of type active or passive.\n  active,\n\n  /// Collides with other hitboxes of type active.\n  passive,\n\n  /// Will not collide with any other hitboxes.\n  inactive,\n}\n\n/// Utility class allows to subscribe on collision type changing event\nclass CollisionTypeNotifier with ChangeNotifier {\n  CollisionTypeNotifier(CollisionType type) : _value = type;\n  CollisionType _value = CollisionType.active;\n\n  set value(CollisionType type) {\n    _value = type;\n    notifyListeners();\n  }\n\n  CollisionType get value => _value;\n}\n\n/// The [GenericCollisionCallbacks] mixin can be used to get callbacks from the\n/// collision detection system, potentially without using the Flame component\n/// system.\n/// The default implementation used with FCS is [CollisionCallbacks].\n/// The generic type [T] here is the type of the object that has the hitboxes\n/// are attached to, for example it is [PositionComponent] in the\n/// [StandardCollisionDetection].\nmixin GenericCollisionCallbacks<T> {\n  Set<T>? _activeCollisions;\n\n  /// The objects that the object is currently colliding with.\n  Set<T> get activeCollisions => _activeCollisions ??= {};\n\n  /// Whether the object is currently colliding or not.\n  bool get isColliding {\n    return _activeCollisions?.isNotEmpty ?? false;\n  }\n\n  /// Whether the object is colliding with [other] or not.\n  bool collidingWith(T other) {\n    return _activeCollisions?.contains(other) ?? false;\n  }\n\n  /// [onCollision] is called in every tick when this object is colliding with\n  /// [other].\n  @mustCallSuper\n  void onCollision(Set<Vector2> intersectionPoints, T other) {\n    onCollisionCallback?.call(intersectionPoints, other);\n  }\n\n  /// [onCollisionStart] is called in the first tick when this object starts\n  /// colliding with [other].\n  @mustCallSuper\n  void onCollisionStart(Set<Vector2> intersectionPoints, T other) {\n    activeCollisions.add(other);\n    onCollisionStartCallback?.call(intersectionPoints, other);\n  }\n\n  /// [onCollisionEnd] is called once when this object has stopped colliding\n  /// with [other].\n  @mustCallSuper\n  void onCollisionEnd(T other) {\n    activeCollisions.remove(other);\n    onCollisionEndCallback?.call(other);\n  }\n\n  /// Works only for the QuadTree collision detection.\n  /// If you need to prevent collision of items of different types -\n  /// reimplement [onComponentTypeCheck]. The result of calculation is cached\n  /// so you should not check any dynamical parameters here, the function\n  /// intended to be used as pure type checker.\n  /// Call super.onComponentTypeCheck to get the parent's result of the\n  /// type check if needed. In other causes this call is redundant in game code.\n  bool onComponentTypeCheck(PositionComponent other);\n\n  /// Assign your own [CollisionCallback] if you want a callback when this\n  /// shape collides with another [T].\n  CollisionCallback<T>? onCollisionCallback;\n\n  /// Assign your own [CollisionCallback] if you want a callback when this\n  /// shape starts to collide with another [T].\n  CollisionCallback<T>? onCollisionStartCallback;\n\n  /// Assign your own [CollisionEndCallback] if you want a callback when this\n  /// shape stops colliding with another [T].\n  CollisionEndCallback<T>? onCollisionEndCallback;\n}\n\nmixin CollisionCallbacks on Component\n    implements GenericCollisionCallbacks<PositionComponent> {\n  @override\n  Set<PositionComponent>? _activeCollisions;\n  @override\n  Set<PositionComponent> get activeCollisions => _activeCollisions ??= {};\n\n  @override\n  bool get isColliding {\n    return _activeCollisions != null && _activeCollisions!.isNotEmpty;\n  }\n\n  @override\n  bool collidingWith(PositionComponent other) {\n    return _activeCollisions != null && activeCollisions.contains(other);\n  }\n\n  @override\n  @mustCallSuper\n  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {\n    onCollisionCallback?.call(intersectionPoints, other);\n  }\n\n  @override\n  @mustCallSuper\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    activeCollisions.add(other);\n    onCollisionStartCallback?.call(intersectionPoints, other);\n  }\n\n  @override\n  @mustCallSuper\n  void onCollisionEnd(PositionComponent other) {\n    activeCollisions.remove(other);\n    onCollisionEndCallback?.call(other);\n  }\n\n  @override\n  bool onComponentTypeCheck(PositionComponent other) {\n    final myParent = parent;\n    final otherParent = other.parent;\n    if (myParent is CollisionCallbacks && otherParent is PositionComponent) {\n      return myParent.onComponentTypeCheck(otherParent);\n    }\n\n    return true;\n  }\n\n  /// Assign your own [CollisionCallback] if you want a callback when this\n  /// shape collides with another [PositionComponent].\n  @override\n  CollisionCallback<PositionComponent>? onCollisionCallback;\n\n  /// Assign your own [CollisionCallback] if you want a callback when this\n  /// shape starts to collide with another [PositionComponent].\n  @override\n  CollisionCallback<PositionComponent>? onCollisionStartCallback;\n\n  /// Assign your own [CollisionEndCallback] if you want a callback when this\n  /// shape stops colliding with another [PositionComponent].\n  @override\n  CollisionEndCallback<PositionComponent>? onCollisionEndCallback;\n}\n\n/// Can be used used to implement an `onCollisionCallback` or an\n/// `onCollisionStartCallback`.\ntypedef CollisionCallback<T> =\n    void Function(\n      Set<Vector2> intersectionPoints,\n      T other,\n    );\n\n/// Can be used used to implement an `onCollisionEndCallback`.\ntypedef CollisionEndCallback<T> = void Function(T other);\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/collision_detection.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flutter/material.dart';\n\n/// [CollisionDetection] is the foundation of the collision detection system in\n/// Flame.\n///\n/// If the [HasCollisionDetection] mixin is added to the game, [run] is called\n/// every tick to check for collisions.\nabstract class CollisionDetection<\n  T extends Hitbox<T>,\n  B extends Broadphase<T>\n> {\n  final B broadphase;\n\n  List<T> get items => broadphase.items;\n  final _lastPotentials = <CollisionProspect<T>>[];\n  final collisionsCompletedNotifier = CollisionDetectionCompletionNotifier();\n\n  CollisionDetection({required this.broadphase});\n\n  void add(T item) => broadphase.add(item);\n\n  void addAll(Iterable<T> items) => items.forEach(add);\n\n  /// Removes the [item] from the collision detection, if you just want to\n  /// temporarily inactivate it you can set\n  /// `collisionType = CollisionType.inactive;` instead.\n  void remove(T item) => broadphase.remove(item);\n\n  /// Removes all [items] from the collision detection, see [remove].\n  void removeAll(Iterable<T> items) => items.forEach(remove);\n\n  /// Run collision detection for the current state of [items].\n  void run() {\n    broadphase.update();\n    final potentials = broadphase.query();\n    final hashes = Set.unmodifiable(potentials.map((p) => p.hash));\n\n    for (final potential in potentials) {\n      final itemA = potential.a;\n      final itemB = potential.b;\n\n      if (itemA.possiblyIntersects(itemB)) {\n        final intersectionPoints = intersections(itemA, itemB);\n        if (intersectionPoints.isNotEmpty) {\n          if (!itemA.collidingWith(itemB)) {\n            handleCollisionStart(intersectionPoints, itemA, itemB);\n          }\n          handleCollision(intersectionPoints, itemA, itemB);\n        } else if (itemA.collidingWith(itemB)) {\n          handleCollisionEnd(itemA, itemB);\n        }\n      } else if (itemA.collidingWith(itemB)) {\n        handleCollisionEnd(itemA, itemB);\n      }\n    }\n\n    // Handles callbacks for an ended collision that the broadphase didn't\n    // report as a potential collision anymore.\n    for (final prospect in _lastPotentials) {\n      if (!hashes.contains(prospect.hash) &&\n          prospect.a.collidingWith(prospect.b)) {\n        handleCollisionEnd(prospect.a, prospect.b);\n      }\n    }\n    _updateLastPotentials(potentials);\n\n    // Let all listeners know that the collision detection step has completed\n    collisionsCompletedNotifier.notifyListeners();\n  }\n\n  final _lastPotentialsPool = <CollisionProspect<T>>[];\n  void _updateLastPotentials(Iterable<CollisionProspect<T>> potentials) {\n    _lastPotentials.clear();\n    for (final potential in potentials) {\n      final CollisionProspect<T> lastPotential;\n      if (_lastPotentialsPool.length > _lastPotentials.length) {\n        lastPotential = _lastPotentialsPool[_lastPotentials.length]\n          ..setFrom(potential);\n      } else {\n        lastPotential = potential.clone();\n        _lastPotentialsPool.add(lastPotential);\n      }\n      _lastPotentials.add(lastPotential);\n    }\n  }\n\n  /// Check what the intersection points of two items are,\n  /// returns an empty list if there are no intersections.\n  Set<Vector2> intersections(T itemA, T itemB);\n\n  void handleCollisionStart(Set<Vector2> intersectionPoints, T itemA, T itemB);\n\n  void handleCollision(Set<Vector2> intersectionPoints, T itemA, T itemB);\n\n  void handleCollisionEnd(T itemA, T itemB);\n\n  /// Returns the first hitbox that the given [ray] hits and the associated\n  /// intersection information; or null if the ray doesn't hit any hitbox.\n  ///\n  /// [maxDistance] can be provided to limit the raycast to only return hits\n  /// within this distance from the ray origin.\n  ///\n  /// You can provide a [hitboxFilter] callback to define which hitboxes\n  /// to consider and which to ignore. This callback will be called with\n  /// every prospective hitbox, and only if the callback returns `true`\n  /// will the hitbox be considered. Otherwise, the ray will go straight\n  /// through it. One common use case is ignoring the component that is\n  /// shooting the ray.\n  ///\n  /// If you have a list of hitboxes to ignore in advance,\n  /// you can provide them via the [ignoreHitboxes] argument.\n  ///\n  /// If [out] is provided that object will be modified and returned with the\n  /// result.\n  RaycastResult<T>? raycast(\n    Ray2 ray, {\n    double? maxDistance,\n    bool Function(T candidate)? hitboxFilter,\n    List<T>? ignoreHitboxes,\n    RaycastResult<T>? out,\n  });\n\n  /// Casts rays uniformly between [startAngle] to [startAngle]+[sweepAngle]\n  /// from the given [origin] and returns all hitboxes and intersection points\n  /// the rays hit.\n  /// [numberOfRays] is the number of rays that should be casted.\n  ///\n  /// [maxDistance] can be provided to limit the raycasts to only return hits\n  /// within this distance from the ray origin.\n  ///\n  /// If the [rays] argument is provided its [Ray2]s are populated with the rays\n  /// needed to perform the operation.\n  /// If there are less objects in [rays] than the operation requires, the\n  /// missing [Ray2] objects will be created and added to [rays].\n  ///\n  /// You can provide a [hitboxFilter] callback to define which hitboxes\n  /// to consider and which to ignore. This callback will be called with\n  /// every prospective hitbox, and only if the callback returns `true`\n  /// will the hitbox be considered. Otherwise, the ray will go straight\n  /// through it. One common use case is ignoring the component that is\n  /// shooting the ray.\n  ///\n  /// If you have a list of hitboxes to ignore in advance,\n  /// you can provide them via the [ignoreHitboxes] argument.\n  ///\n  /// If [out] is provided the [RaycastResult]s in that list be modified and\n  /// returned with the result. If there are less objects in [out] than the\n  /// result requires, the missing [RaycastResult] objects will be created.\n  List<RaycastResult<T>> raycastAll(\n    Vector2 origin, {\n    required int numberOfRays,\n    double startAngle = 0,\n    double sweepAngle = tau,\n    double? maxDistance,\n    List<Ray2>? rays,\n    bool Function(T candidate)? hitboxFilter,\n    List<T>? ignoreHitboxes,\n    List<RaycastResult<T>>? out,\n  });\n\n  /// Follows the ray and its reflections until [maxDepth] is reached and then\n  /// returns all hitboxes, intersection points, normals and reflection rays\n  /// (bundled in a list of [RaycastResult]s) from where the ray hits.\n  ///\n  /// [maxDepth] is how many times the ray should collide before returning a\n  /// result, defaults to 10.\n  ///\n  /// You can provide a [hitboxFilter] callback to define which hitboxes\n  /// to consider and which to ignore. This callback will be called with\n  /// every prospective hitbox, and only if the callback returns `true`\n  /// will the hitbox be considered. Otherwise, the ray will go straight\n  /// through it. One common use case is ignoring the component that is\n  /// shooting the ray.\n  ///\n  /// If you have a list of hitboxes to ignore in advance,\n  /// you can provide them via the [ignoreHitboxes] argument.\n  ///\n  /// If [out] is provided the [RaycastResult]s in that list be modified and\n  /// returned with the result. If there are less objects in [out] than the\n  /// result requires, the missing [RaycastResult] objects will be created.\n  Iterable<RaycastResult<T>> raytrace(\n    Ray2 ray, {\n    int maxDepth = 10,\n    bool Function(T candidate)? hitboxFilter,\n    List<T>? ignoreHitboxes,\n    List<RaycastResult<T>>? out,\n  });\n}\n\n/// A class to handle callbacks for when the collision detection is done each\n/// tick.\nclass CollisionDetectionCompletionNotifier extends ChangeNotifier {\n  @override\n  void notifyListeners() => super.notifyListeners();\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/collision_passthrough.dart",
    "content": "import 'package:collection/collection.dart';\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:meta/meta.dart';\n\n/// This mixin can be used if you want to pass the [CollisionCallbacks] to the\n/// next ancestor that can receive them. It can be used to group hitboxes\n/// together on a component, that then is added to another component that also\n/// cares about the collision events of the hitboxes.\nmixin CollisionPassthrough on CollisionCallbacks {\n  /// The parent that the events should be passed on to.\n  CollisionCallbacks? passthroughParent;\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    super.onMount();\n    passthroughParent =\n        ancestors().firstWhereOrNull(\n              (c) => c is CollisionCallbacks,\n            )\n            as CollisionCallbacks?;\n  }\n\n  @override\n  @mustCallSuper\n  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {\n    super.onCollision(intersectionPoints, other);\n    passthroughParent?.onCollision(intersectionPoints, other);\n  }\n\n  @override\n  @mustCallSuper\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n    passthroughParent?.onCollisionStart(intersectionPoints, other);\n  }\n\n  @override\n  @mustCallSuper\n  void onCollisionEnd(PositionComponent other) {\n    super.onCollisionEnd(other);\n    passthroughParent?.onCollisionEnd(other);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/has_collision_detection.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\n\n/// Keeps track of all the [ShapeHitbox]s in the component's tree and initiates\n/// collision detection every tick.\n///\n/// Hitboxes are only part of the collision detection performed by its closest\n/// parent with the [HasCollisionDetection] mixin, if there are multiple nested\n/// classes that has [HasCollisionDetection].\n///\n/// You can experiment with non-standard collision detection methods, such\n/// as `HasQuadTreeCollisionDetection`. This can sometimes bring better\n/// performance, but it's not guaranteed.\nmixin HasCollisionDetection<B extends Broadphase<ShapeHitbox>> on Component {\n  CollisionDetection<ShapeHitbox, B> _collisionDetection =\n      StandardCollisionDetection();\n  CollisionDetection<ShapeHitbox, B> get collisionDetection =>\n      _collisionDetection;\n\n  set collisionDetection(CollisionDetection<ShapeHitbox, B> cd) {\n    cd.addAll(_collisionDetection.items);\n    _collisionDetection = cd;\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    collisionDetection.run();\n  }\n}\n\n/// This mixin is useful if you have written your own collision detection which\n/// isn't operating on [ShapeHitbox] since you can have any hitbox here.\n///\n/// Do note that [collisionDetection] has to be initialized before the game\n/// starts the update loop for the collision detection to work.\nmixin HasGenericCollisionDetection<T extends Hitbox<T>, B extends Broadphase<T>>\n    on Component {\n  CollisionDetection<T, B>? _collisionDetection;\n  CollisionDetection<T, B> get collisionDetection => _collisionDetection!;\n\n  set collisionDetection(CollisionDetection<T, B> cd) {\n    if (_collisionDetection != null) {\n      cd.addAll(_collisionDetection!.items);\n    }\n    _collisionDetection = cd;\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    _collisionDetection?.run();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/hitboxes/circle_hitbox.dart",
    "content": "// ignore_for_file: comment_references\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/geometry.dart';\n\n/// A [Hitbox] in the shape of a circle.\nclass CircleHitbox extends CircleComponent with ShapeHitbox {\n  @override\n  final bool shouldFillParent;\n\n  CircleHitbox({\n    super.radius,\n    super.position,\n    super.angle,\n    super.anchor,\n    bool isSolid = false,\n    CollisionType collisionType = CollisionType.active,\n  }) : shouldFillParent = radius == null && position == null {\n    this.isSolid = isSolid;\n    this.collisionType = collisionType;\n  }\n\n  /// With this constructor you define the [CircleHitbox] in relation to the\n  /// [parentSize]. For example having a [relation] of 0.5 would create a circle\n  /// that fills half of the [parentSize].\n  CircleHitbox.relative(\n    super.relation, {\n    required super.parentSize,\n    super.position,\n    super.angle,\n    super.anchor,\n    bool isSolid = false,\n    CollisionType collisionType = CollisionType.active,\n  }) : shouldFillParent = false,\n       super.relative() {\n    this.isSolid = isSolid;\n    this.collisionType = collisionType;\n  }\n\n  @override\n  void fillParent() {\n    // There is no need to do anything here since the size already is bound to\n    // the parent size and the radius is defined from the shortest side.\n  }\n\n  static final _temporaryLineSegment = LineSegment.zero();\n  static final _temporaryNormal = Vector2.zero();\n  static final _temporaryCenter = Vector2.zero();\n  static final _temporaryAbsoluteCenter = Vector2.zero();\n  static final _temporaryOrigin = Vector2.zero();\n\n  @override\n  RaycastResult<ShapeHitbox>? rayIntersection(\n    Ray2 ray, {\n    RaycastResult<ShapeHitbox>? out,\n  }) {\n    var isInsideHitbox = false;\n    _temporaryLineSegment.from.setFrom(ray.origin);\n    // Adding a small value to the origin to avoid the ray to be on the edge\n    // of the circle and then directly intersecting and causing the reflecting\n    // ray to go in the wrong direction.\n    _temporaryOrigin.setValues(\n      ray.origin.x + ray.direction.x * 0.00001,\n      ray.origin.y + ray.direction.y * 0.00001,\n    );\n    _temporaryAbsoluteCenter.setFrom(absoluteCenter);\n    _temporaryCenter\n      ..setFrom(_temporaryAbsoluteCenter)\n      ..sub(ray.origin);\n\n    if (_temporaryCenter.isZero()) {\n      // If _temporaryCenter is zero, it's projection onto ray.direction\n      // will be zero. In that case, directly use ray.direction as temp\n      // end point of line segment.\n      _temporaryLineSegment.to.setFrom(ray.direction);\n    } else {\n      _temporaryCenter.projection(ray.direction, out: _temporaryLineSegment.to);\n      _temporaryLineSegment.to\n        ..x *= (ray.direction.x.sign * _temporaryLineSegment.to.x.sign)\n        ..y *= (ray.direction.y.sign * _temporaryLineSegment.to.y.sign);\n    }\n\n    final effectiveRadius = scaledRadius;\n    if (_temporaryOrigin.distanceToSquared(_temporaryAbsoluteCenter) <\n        effectiveRadius * effectiveRadius) {\n      _temporaryLineSegment.to.scaleTo(2 * effectiveRadius);\n      isInsideHitbox = true;\n    }\n    _temporaryLineSegment.to.add(ray.origin);\n    final intersections = lineSegmentIntersections(_temporaryLineSegment).where(\n      (i) => i.distanceToSquared(ray.origin) > 0.0000001,\n    );\n    if (intersections.isEmpty) {\n      out?.reset();\n      return null;\n    } else {\n      final result = out ?? RaycastResult();\n      final intersectionPoint = intersections.first;\n      _temporaryNormal\n        ..setFrom(intersectionPoint)\n        ..sub(_temporaryAbsoluteCenter)\n        ..normalize();\n      if (isInsideHitbox) {\n        _temporaryNormal.invert();\n      }\n      final reflectionDirection =\n          (out?.reflectionRay?.direction ?? Vector2.zero())\n            ..setFrom(ray.direction)\n            ..reflect(_temporaryNormal);\n      // Reflect() can introduce sub-epsilon drift. Normalize to keep Ray2's\n      // unit-length assertion satisfied.\n      reflectionDirection.normalize();\n\n      final reflectionRay =\n          (out?.reflectionRay?..setWith(\n            origin: intersectionPoint,\n            direction: reflectionDirection,\n          )) ??\n          Ray2(\n            origin: intersectionPoint,\n            direction: reflectionDirection,\n          );\n\n      result.setWith(\n        hitbox: this,\n        reflectionRay: reflectionRay,\n        normal: _temporaryNormal,\n        distance: ray.origin.distanceTo(intersectionPoint),\n        isInsideHitbox: isInsideHitbox,\n      );\n      return result;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/hitboxes/composite_hitbox.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\n\n/// In this [PositionComponent] hitboxes can be added to emulate a hitbox\n/// that is a composition of other hitboxes.\n///\n/// If you want to form a hat for example you might want to use to\n/// [RectangleHitbox]s to follow that hats edges properly, then you can add\n/// those hitboxes to an instance of this class and react to collisions to the\n/// whole hat, instead of for just each hitbox separately.\nclass CompositeHitbox extends PositionComponent\n    with CollisionCallbacks, CollisionPassthrough {\n  CompositeHitbox({\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    Iterable<ShapeHitbox>? super.children,\n    super.priority,\n  });\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/hitboxes/hitbox.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/extensions.dart';\n\n/// The [Hitbox] is the default building block to determine whether two objects\n/// have collided with each other. [ShapeHitbox] is the default implementation\n/// used in FCS.\nabstract class Hitbox<T extends Hitbox<T>>\n    implements GenericCollisionCallbacks<T> {\n  /// Whether the hitbox should:\n  ///   * [CollisionType.active] - actively collide with other hitboxes.\n  ///   * [CollisionType.passive] - passively collide with other hitboxes (only\n  ///   collide with hitboxes that are active, but not other passive ones).\n  ///   * [CollisionType.inactive] - not collide with any other hitboxes.\n  CollisionType get collisionType;\n\n  /// The axis-aligned bounding box of the [Hitbox], this is used to make a\n  /// rough estimation of whether two hitboxes can possibly collide or not.\n  Aabb2 get aabb;\n\n  /// Whether the hitbox is solid or hollow.\n  ///\n  /// If it is solid, intersections will occur even if the other component is\n  /// fully enclosed by the other hitbox. The intersection point in such cases\n  /// will be the center of the enclosed [Hitbox].\n  /// A hollow shape that is fully enclosed by a solid hitbox will cause an\n  /// intersection result, but not the other way around.\n  bool get isSolid;\n\n  /// Checks whether the [Hitbox] contains the [point].\n  bool containsPoint(Vector2 point);\n\n  /// Where this [Hitbox] has intersection points with another [Hitbox].\n  Set<Vector2> intersections(T other);\n\n  /// This should be a cheaper calculation than comparing the exact boundaries\n  /// if the exact calculation is expensive.\n  /// This method could for example check two [Rect]s or [Aabb2]s against each\n  /// other.\n  bool possiblyIntersects(T other);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/hitboxes/polygon_hitbox.dart",
    "content": "// ignore_for_file: comment_references\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:meta/meta.dart';\n\n/// A [Hitbox] in the shape of a polygon.\nclass PolygonHitbox extends PolygonComponent\n    with ShapeHitbox, PolygonRayIntersection {\n  PolygonHitbox(\n    super.vertices, {\n    super.position,\n    super.angle,\n    super.anchor,\n    bool isSolid = false,\n    CollisionType collisionType = CollisionType.active,\n  }) {\n    this.isSolid = isSolid;\n    this.collisionType = collisionType;\n  }\n\n  /// With this constructor you define the [PolygonHitbox] in relation to the\n  /// [parentSize] of the hitbox.\n  ///\n  /// Example: `[[1.0, 0.0], [0.0, -1.0], [-1.0, 0.0], [0.0, 1.0]]`\n  /// This will form a diamond shape within the bounding size box.\n  /// NOTE: Always define your shape in a counter-clockwise fashion (in the\n  /// screen coordinate system)\n  PolygonHitbox.relative(\n    super.relation, {\n    required super.parentSize,\n    super.position,\n    double super.angle = 0,\n    super.anchor,\n    bool isSolid = false,\n    CollisionType collisionType = CollisionType.active,\n  }) : super.relative(shrinkToBounds: true) {\n    this.isSolid = isSolid;\n    this.collisionType = collisionType;\n  }\n\n  @override\n  @protected\n  void computeAabb(Aabb2 aabb) {\n    final vertices = globalVertices();\n    if (vertices.isEmpty) {\n      super.computeAabb(aabb);\n      return;\n    }\n    var minX = vertices[0].x;\n    var minY = vertices[0].y;\n    var maxX = vertices[0].x;\n    var maxY = vertices[0].y;\n    for (var i = 1; i < vertices.length; i++) {\n      final v = vertices[i];\n      if (v.x < minX) {\n        minX = v.x;\n      }\n      if (v.y < minY) {\n        minY = v.y;\n      }\n      if (v.x > maxX) {\n        maxX = v.x;\n      }\n      if (v.y > maxY) {\n        maxY = v.y;\n      }\n    }\n    // Add a small epsilon since points on the AABB edge are counted as outside.\n    const epsilon = 0.000000000000001;\n    aabb.min.setValues(minX - epsilon, minY - epsilon);\n    aabb.max.setValues(maxX + epsilon, maxY + epsilon);\n  }\n\n  @override\n  void fillParent() {\n    throw UnsupportedError(\n      'Use the RectangleHitbox if you want to fill the parent',\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/hitboxes/rectangle_hitbox.dart",
    "content": "// ignore_for_file: comment_references\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/src/geometry/polygon_ray_intersection.dart';\nimport 'package:meta/meta.dart';\n\n/// A [Hitbox] in the shape of a rectangle (a simplified polygon).\nclass RectangleHitbox extends RectangleComponent\n    with ShapeHitbox, PolygonRayIntersection<RectangleHitbox> {\n  @override\n  final bool shouldFillParent;\n\n  RectangleHitbox({\n    super.position,\n    super.size,\n    super.angle,\n    super.anchor,\n    super.priority,\n    bool isSolid = false,\n    CollisionType collisionType = CollisionType.active,\n  }) : shouldFillParent = size == null && position == null {\n    this.isSolid = isSolid;\n    this.collisionType = collisionType;\n  }\n\n  /// With this constructor you define the [RectangleHitbox] in relation to\n  /// the [parentSize]. For example having [relation] as of (0.8, 0.5) would\n  /// create a rectangle that fills 80% of the width and 50% of the height of\n  /// [parentSize].\n  RectangleHitbox.relative(\n    super.relation, {\n    required super.parentSize,\n    super.position,\n    super.angle,\n    super.anchor,\n    bool isSolid = false,\n    CollisionType collisionType = CollisionType.active,\n  }) : shouldFillParent = false,\n       super.relative(\n         shrinkToBounds: true,\n       ) {\n    this.isSolid = isSolid;\n    this.collisionType = collisionType;\n  }\n\n  @override\n  @protected\n  void computeAabb(Aabb2 aabb) {\n    final vertices = globalVertices();\n    if (vertices.isEmpty) {\n      super.computeAabb(aabb);\n      return;\n    }\n    var minX = vertices[0].x;\n    var minY = vertices[0].y;\n    var maxX = vertices[0].x;\n    var maxY = vertices[0].y;\n    for (var i = 1; i < vertices.length; i++) {\n      final v = vertices[i];\n      if (v.x < minX) {\n        minX = v.x;\n      }\n      if (v.y < minY) {\n        minY = v.y;\n      }\n      if (v.x > maxX) {\n        maxX = v.x;\n      }\n      if (v.y > maxY) {\n        maxY = v.y;\n      }\n    }\n    // Add a small epsilon since points on the AABB edge are counted as outside.\n    const epsilon = 0.000000000000001;\n    aabb.min.setValues(minX - epsilon, minY - epsilon);\n    aabb.max.setValues(maxX + epsilon, maxY + epsilon);\n  }\n\n  @override\n  void fillParent() {\n    refreshVertices(\n      newVertices: RectangleComponent.sizeToVertices(size, anchor),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/hitboxes/screen_hitbox.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/src/collisions/collision_callbacks.dart';\nimport 'package:flame/src/collisions/hitboxes/rectangle_hitbox.dart';\n\n/// This component is used to detect hitboxes colliding into the edges of the\n/// viewport of the game.\nclass ScreenHitbox<T extends FlameGame> extends PositionComponent\n    with CollisionCallbacks, HasGameReference<T> {\n  bool _hasWorldAncestor = false;\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    add(RectangleHitbox());\n    _hasWorldAncestor = findParent<World>() != null;\n    if (_hasWorldAncestor) {\n      game.camera.viewfinder.transform.addListener(_updateTransform);\n      _updateTransform();\n    }\n  }\n\n  final Vector2 _tmpPosition = Vector2.zero();\n\n  void _updateTransform() {\n    final viewfinder = game.camera.viewfinder;\n    final visibleRect = game.camera.visibleWorldRect;\n    size.setValues(visibleRect.width, visibleRect.height);\n    _tmpPosition.setValues(visibleRect.topLeft.dx, visibleRect.topLeft.dy);\n    position = Anchor.topLeft.toOtherAnchorPosition(\n      _tmpPosition,\n      viewfinder.anchor,\n      size,\n    );\n    anchor = viewfinder.anchor;\n    angle = viewfinder.angle;\n    if (angle != 0) {\n      final cosTheta = cos(angle).abs();\n      final sinTheta = sin(angle).abs();\n      final newWidth = (size.x * cosTheta) + (size.y * sinTheta);\n      final newHeight = (size.x * sinTheta) + (size.y * cosTheta);\n\n      // Shrink the new dimensions to keep the original AABB size before the\n      // rotation.\n      final scaleWidth = size.x / newWidth;\n      final scaleHeight = size.y / newHeight;\n\n      size.setValues(newWidth * scaleWidth, newHeight * scaleHeight);\n    }\n  }\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    this.size = size;\n    if (_hasWorldAncestor) {\n      _updateTransform();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/hitboxes/shape_hitbox.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/src/geometry/shape_intersections.dart'\n    as intersection_system;\nimport 'package:meta/meta.dart';\n\n/// A [ShapeHitbox] turns a [ShapeComponent] into a [Hitbox].\n/// It is currently used by [CircleHitbox], [RectangleHitbox] and\n/// [PolygonHitbox].\nmixin ShapeHitbox on ShapeComponent implements Hitbox<ShapeHitbox> {\n  @internal\n  final collisionTypeNotifier = CollisionTypeNotifier(CollisionType.active);\n\n  set collisionType(CollisionType type) {\n    if (collisionTypeNotifier.value == type) {\n      return;\n    }\n    collisionTypeNotifier.value = type;\n  }\n\n  @override\n  Color debugColor = const Color(0xFFFFFF00);\n\n  @override\n  CollisionType get collisionType => collisionTypeNotifier.value;\n\n  /// Whether the hitbox is allowed to collide with another hitbox that is\n  /// added to the same parent.\n  bool allowSiblingCollision = false;\n\n  /// Whether hitbox collisions with other hitboxes should trigger the\n  /// \"onCollision\" functions for the hitbox's parent component.\n  bool triggersParentCollision = true;\n\n  @override\n  Aabb2 get aabb => _validAabb ? _aabb : _recalculateAabb();\n  final Aabb2 _aabb = Aabb2();\n  bool _validAabb = false;\n\n  @override\n  Set<ShapeHitbox> get activeCollisions => _activeCollisions ??= {};\n  Set<ShapeHitbox>? _activeCollisions;\n\n  @override\n  bool get isColliding {\n    return _activeCollisions != null && _activeCollisions!.isNotEmpty;\n  }\n\n  @override\n  bool collidingWith(Hitbox other) {\n    return _activeCollisions != null && activeCollisions.contains(other);\n  }\n\n  CollisionDetection? _collisionDetection;\n  final List<Transform2D> _transformAncestors = [];\n  late Function() _transformListener;\n\n  @internal\n  Function()? onAabbChanged;\n\n  final Vector2 _halfExtents = Vector2.zero();\n  static const double _extentEpsilon = 0.000000000000001;\n  final Matrix3 _rotationMatrix = Matrix3.zero();\n\n  @override\n  bool renderShape = false;\n\n  late PositionComponent _hitboxParent;\n\n  PositionComponent get hitboxParent => _hitboxParent;\n  void Function()? _parentSizeListener;\n  @protected\n  bool shouldFillParent = false;\n\n  @override\n  void onMount() {\n    super.onMount();\n    _hitboxParent =\n        ancestors().firstWhere(\n              (c) => c is PositionComponent && c is! CompositeHitbox,\n              orElse: () {\n                throw StateError(\n                  'A ShapeHitbox needs a PositionComponent ancestor',\n                );\n              },\n            )\n            as PositionComponent;\n\n    _transformListener = () {\n      _validAabb = false;\n      onAabbChanged?.call();\n    };\n    final positionComponents = ancestors(\n      includeSelf: true,\n    ).whereType<PositionComponent>();\n    for (final ancestor in positionComponents) {\n      _transformAncestors.add(ancestor.transform);\n      ancestor.transform.addListener(_transformListener);\n    }\n\n    if (shouldFillParent) {\n      _parentSizeListener = () {\n        size = hitboxParent.size;\n        fillParent();\n      };\n      _parentSizeListener?.call();\n      hitboxParent.size.addListener(_parentSizeListener!);\n    }\n\n    // This should be placed after the hitbox parent listener\n    // since the correct hitbox size is required by the QuadTree.\n    final parent = findParent<HasCollisionDetection>();\n    if (parent is HasCollisionDetection) {\n      _collisionDetection = parent.collisionDetection;\n      _collisionDetection?.add(this);\n    }\n  }\n\n  @override\n  void onRemove() {\n    if (_parentSizeListener != null) {\n      hitboxParent.size.removeListener(_parentSizeListener!);\n    }\n    _transformAncestors.forEach((t) => t.removeListener(_transformListener));\n\n    // End all active collisions before removing from collision detection.\n    // This ensures the parent component's activeCollisions is properly cleaned\n    // up during processLifecycleEvents(), before collisionDetection.run()\n    // processes new collisions in the same tick.\n    if (_activeCollisions != null && _activeCollisions!.isNotEmpty) {\n      for (final other in _activeCollisions!.toList()) {\n        onCollisionEnd(other);\n        other.onCollisionEnd(this);\n      }\n    }\n\n    _collisionDetection?.remove(this);\n    super.onRemove();\n  }\n\n  /// Checks whether the [ShapeHitbox] contains the [point], where [point] is\n  /// a position in the global coordinate system of your game.\n  @override\n  bool containsPoint(Vector2 point) {\n    return _possiblyContainsPoint(point) && super.containsPoint(point);\n  }\n\n  /// Since this is a cheaper calculation than checking towards all shapes this\n  /// check can be done first to see if it even is possible that the shapes can\n  /// contain the point, since the shapes have to be within the size of the\n  /// component.\n  bool _possiblyContainsPoint(Vector2 point) {\n    return aabb.containsVector2(point);\n  }\n\n  /// Where this [ShapeComponent] has intersection points with another shape\n  @override\n  Set<Vector2> intersections(Hitbox other) {\n    assert(\n      other is ShapeComponent,\n      'The intersection can only be performed between shapes',\n    );\n    return intersection_system.intersections(this, other as ShapeComponent);\n  }\n\n  /// Since this is a cheaper calculation than checking towards all shapes, this\n  /// check can be done first to see if it even is possible that the shapes can\n  /// overlap, since the shapes have to be within the size of the component.\n  @override\n  bool possiblyIntersects(ShapeHitbox other) {\n    final collisionAllowed =\n        allowSiblingCollision || hitboxParent != other.hitboxParent;\n    return collisionAllowed && aabb.intersectsWithAabb2(other.aabb);\n  }\n\n  /// Returns information about how the ray intersects the shape.\n  ///\n  /// If you are only interested in the intersection point use\n  /// [RaycastResult.intersectionPoint] of the result.\n  RaycastResult<ShapeHitbox>? rayIntersection(\n    Ray2 ray, {\n    RaycastResult<ShapeHitbox>? out,\n  });\n\n  /// This determines how the shape should scale if it should try to fill its\n  /// parents boundaries.\n  void fillParent();\n\n  /// Computes the axis-aligned bounding box for this hitbox.\n  ///\n  /// Subclasses can override this to provide a tighter AABB. The default\n  /// implementation uses the absolute scaled size and rotation.\n  @protected\n  void computeAabb(Aabb2 aabb) {\n    final size = absoluteScaledSize;\n    // This has double.minPositive since a point on the edge of the AABB is\n    // currently counted as outside.\n    _halfExtents.setValues(\n      size.x / 2 + _extentEpsilon,\n      size.y / 2 + _extentEpsilon,\n    );\n    _rotationMatrix.setRotationZ(absoluteAngle);\n    aabb\n      ..setCenterAndHalfExtents(absoluteCenter, _halfExtents)\n      ..rotate(_rotationMatrix);\n  }\n\n  Aabb2 _recalculateAabb() {\n    computeAabb(_aabb);\n    _validAabb = true;\n    return _aabb;\n  }\n\n  //#region CollisionCallbacks methods\n\n  @override\n  @mustCallSuper\n  void onCollision(Set<Vector2> intersectionPoints, ShapeHitbox other) {\n    onCollisionCallback?.call(intersectionPoints, other);\n    if (hitboxParent is CollisionCallbacks &&\n        triggersParentCollision &&\n        other.triggersParentCollision) {\n      (hitboxParent as CollisionCallbacks).onCollision(\n        intersectionPoints,\n        other.hitboxParent,\n      );\n    }\n  }\n\n  @override\n  @mustCallSuper\n  void onCollisionStart(Set<Vector2> intersectionPoints, ShapeHitbox other) {\n    activeCollisions.add(other);\n    onCollisionStartCallback?.call(intersectionPoints, other);\n    if (hitboxParent is CollisionCallbacks &&\n        triggersParentCollision &&\n        other.triggersParentCollision) {\n      (hitboxParent as CollisionCallbacks).onCollisionStart(\n        intersectionPoints,\n        other.hitboxParent,\n      );\n    }\n  }\n\n  @override\n  @mustCallSuper\n  void onCollisionEnd(ShapeHitbox other) {\n    activeCollisions.remove(other);\n    onCollisionEndCallback?.call(other);\n    if (hitboxParent is CollisionCallbacks &&\n        triggersParentCollision &&\n        other.triggersParentCollision) {\n      (hitboxParent as CollisionCallbacks).onCollisionEnd(other.hitboxParent);\n    }\n  }\n\n  /// Defines whether the [other] component should be able to collide with\n  /// this component.\n  ///\n  /// If the [hitboxParent] is not `CollisionCallbacks` but `PositionComponent`,\n  /// there is no [CollisionCallbacks.onComponentTypeCheck] in that component.\n  /// As a result, it will always be able to collide with all other types.\n  @override\n  @mustCallSuper\n  bool onComponentTypeCheck(PositionComponent other) {\n    final otherHitboxParent = (other as ShapeHitbox).hitboxParent;\n\n    final thisCanCollideWithOther =\n        (hitboxParent is! CollisionCallbacks) ||\n        (hitboxParent as CollisionCallbacks).onComponentTypeCheck(\n          otherHitboxParent,\n        );\n\n    final otherCanCollideWithThis =\n        (otherHitboxParent is! CollisionCallbacks) ||\n        (otherHitboxParent as CollisionCallbacks).onComponentTypeCheck(\n          hitboxParent,\n        );\n\n    return thisCanCollideWithOther && otherCanCollideWithThis;\n  }\n\n  @override\n  CollisionCallback<ShapeHitbox>? onCollisionCallback;\n\n  @override\n  CollisionCallback<ShapeHitbox>? onCollisionStartCallback;\n\n  @override\n  CollisionEndCallback<ShapeHitbox>? onCollisionEndCallback;\n\n  //#endregion\n}\n"
  },
  {
    "path": "packages/flame/lib/src/collisions/standard_collision_detection.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/geometry.dart';\n\n/// The default implementation of [CollisionDetection].\n/// Checks whether any [ShapeHitbox]s in [items] collide with each other and\n/// calls their callback methods accordingly.\n///\n/// By default the [Sweep] broadphase is used, this can be configured by\n/// passing in another [Broadphase] to the constructor.\nclass StandardCollisionDetection<B extends Broadphase<ShapeHitbox>>\n    extends CollisionDetection<ShapeHitbox, B> {\n  StandardCollisionDetection({B? broadphase})\n    : super(broadphase: broadphase ?? Sweep<ShapeHitbox>() as B);\n\n  /// Check what the intersection points of two collidables are,\n  /// returns an empty list if there are no intersections.\n  @override\n  Set<Vector2> intersections(\n    ShapeHitbox hitboxA,\n    ShapeHitbox hitboxB,\n  ) {\n    return hitboxA.intersections(hitboxB);\n  }\n\n  /// Calls the two colliding hitboxes when they first starts to collide.\n  /// They are called with the [intersectionPoints] and instances of each other,\n  /// so that they can determine what hitbox (and what\n  /// [ShapeHitbox.hitboxParent] that they have collided with.\n  @override\n  void handleCollisionStart(\n    Set<Vector2> intersectionPoints,\n    ShapeHitbox hitboxA,\n    ShapeHitbox hitboxB,\n  ) {\n    hitboxA.onCollisionStart(intersectionPoints, hitboxB);\n    hitboxB.onCollisionStart(intersectionPoints, hitboxA);\n  }\n\n  /// Calls the two colliding hitboxes every tick when they are colliding.\n  /// They are called with the [intersectionPoints] and instances of each other,\n  /// so that they can determine what hitbox (and what\n  /// [ShapeHitbox.hitboxParent] that they have collided with.\n  @override\n  void handleCollision(\n    Set<Vector2> intersectionPoints,\n    ShapeHitbox hitboxA,\n    ShapeHitbox hitboxB,\n  ) {\n    hitboxA.onCollision(intersectionPoints, hitboxB);\n    hitboxB.onCollision(intersectionPoints, hitboxA);\n  }\n\n  /// Calls the two colliding hitboxes once when two hitboxes have stopped\n  /// colliding.\n  /// They are called with instances of each other, so that they can determine\n  /// what hitbox (and what [ShapeHitbox.hitboxParent] that they have stopped\n  /// colliding with.\n  @override\n  void handleCollisionEnd(ShapeHitbox hitboxA, ShapeHitbox hitboxB) {\n    hitboxA.onCollisionEnd(hitboxB);\n    hitboxB.onCollisionEnd(hitboxA);\n  }\n\n  static final _temporaryRaycastResult = RaycastResult<ShapeHitbox>();\n\n  static final _temporaryRayAabb = Aabb2();\n\n  @override\n  RaycastResult<ShapeHitbox>? raycast(\n    Ray2 ray, {\n    double? maxDistance,\n    bool Function(ShapeHitbox candidate)? hitboxFilter,\n    List<ShapeHitbox>? ignoreHitboxes,\n    RaycastResult<ShapeHitbox>? out,\n  }) {\n    var finalResult = out?..reset();\n    _updateRayAabb(ray, maxDistance);\n    for (final item in items) {\n      if (ignoreHitboxes?.contains(item) ?? false) {\n        continue;\n      }\n      if (hitboxFilter != null) {\n        if (!hitboxFilter(item)) {\n          continue;\n        }\n      }\n      if (!item.aabb.intersectsWithAabb2(_temporaryRayAabb)) {\n        continue;\n      }\n      final currentResult = item.rayIntersection(\n        ray,\n        out: _temporaryRaycastResult,\n      );\n      final possiblyFirstResult = !(finalResult?.isActive ?? false);\n      if (currentResult != null &&\n          (possiblyFirstResult ||\n              currentResult.distance! < finalResult!.distance!) &&\n          currentResult.distance! <= (maxDistance ?? double.infinity)) {\n        if (finalResult == null) {\n          finalResult = currentResult.clone();\n        } else {\n          finalResult.setFrom(currentResult);\n        }\n      }\n    }\n    return (finalResult?.isActive ?? false) ? finalResult : null;\n  }\n\n  @override\n  List<RaycastResult<ShapeHitbox>> raycastAll(\n    Vector2 origin, {\n    required int numberOfRays,\n    double startAngle = 0,\n    double sweepAngle = tau,\n    double? maxDistance,\n    List<Ray2>? rays,\n    bool Function(ShapeHitbox candidate)? hitboxFilter,\n    List<ShapeHitbox>? ignoreHitboxes,\n    List<RaycastResult<ShapeHitbox>>? out,\n  }) {\n    final isFullCircle = (sweepAngle % tau).abs() < 0.0001;\n    final angle = sweepAngle / (numberOfRays + (isFullCircle ? 0 : -1));\n    final results = <RaycastResult<ShapeHitbox>>[];\n    final direction = Vector2(1, 0);\n    for (var i = 0; i < numberOfRays; i++) {\n      Ray2 ray;\n      if (i < (rays?.length ?? 0)) {\n        ray = rays![i];\n      } else {\n        ray = Ray2.zero();\n        rays?.add(ray);\n      }\n      ray.origin.setFrom(origin);\n      direction\n        ..setValues(0, -1)\n        ..rotate(startAngle - angle * i);\n      ray.direction = direction;\n\n      RaycastResult<ShapeHitbox>? result;\n      if (i < (out?.length ?? 0)) {\n        result = out![i];\n      } else {\n        result = RaycastResult();\n        out?.add(result);\n      }\n      result = raycast(\n        ray,\n        maxDistance: maxDistance,\n        hitboxFilter: hitboxFilter,\n        ignoreHitboxes: ignoreHitboxes,\n        out: result,\n      );\n\n      if (result != null) {\n        results.add(result);\n      }\n    }\n    return results;\n  }\n\n  @override\n  Iterable<RaycastResult<ShapeHitbox>> raytrace(\n    Ray2 ray, {\n    int maxDepth = 10,\n    bool Function(ShapeHitbox candidate)? hitboxFilter,\n    List<ShapeHitbox>? ignoreHitboxes,\n    List<RaycastResult<ShapeHitbox>>? out,\n  }) sync* {\n    if (out != null) {\n      for (final result in out) {\n        result.reset();\n      }\n    }\n    var currentRay = ray;\n    for (var i = 0; i < maxDepth; i++) {\n      final hasResultObject = (out?.length ?? 0) > i;\n      final storeResult = hasResultObject\n          ? out![i]\n          : RaycastResult<ShapeHitbox>();\n      final currentResult = raycast(\n        currentRay,\n        hitboxFilter: hitboxFilter,\n        ignoreHitboxes: ignoreHitboxes,\n        out: storeResult,\n      );\n      if (currentResult != null) {\n        currentRay = storeResult.reflectionRay!;\n        if (!hasResultObject && out != null) {\n          out.add(storeResult);\n        }\n        yield storeResult;\n      } else {\n        break;\n      }\n    }\n  }\n\n  /// Computes an axis-aligned bounding box for a [ray].\n  ///\n  /// When [maxDistance] is provided, this will be the bounding box around\n  /// the origin of the ray and its ending point. When [maxDistance]\n  /// is `null`, the bounding box will encompass the whole quadrant\n  /// of space, from the ray's origin to infinity.\n  void _updateRayAabb(Ray2 ray, double? maxDistance) {\n    final x1 = ray.origin.x;\n    final y1 = ray.origin.y;\n    double x2;\n    double y2;\n\n    if (maxDistance != null) {\n      x2 = ray.origin.x + ray.direction.x * maxDistance;\n      y2 = ray.origin.y + ray.direction.y * maxDistance;\n    } else {\n      x2 = ray.direction.x > 0 ? double.infinity : double.negativeInfinity;\n      y2 = ray.direction.y > 0 ? double.infinity : double.negativeInfinity;\n    }\n\n    _temporaryRayAabb\n      ..min.setValues(math.min(x1, x2), math.min(y1, y2))\n      ..max.setValues(math.max(x1, x2), math.max(y1, y2));\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/clip_component.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/experimental.dart';\n\n/// A function that creates a shape based on a size represented by a [Vector2]\ntypedef ShapeBuilder = Shape Function(Vector2 size);\n\n/// {@template clip_component}\n/// A component that will clip its content.\n/// {@endtemplate}\nclass ClipComponent extends PositionComponent {\n  /// {@macro clip_component}\n  ///\n  /// Clips the canvas based its shape and size.\n  ClipComponent({\n    required ShapeBuilder builder,\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.key,\n  }) : _builder = builder;\n\n  /// {@macro circle_clip_component}\n  ///\n  /// Clips the canvas in the form of a circle based on its size.\n  ClipComponent.circle({\n    Vector2? position,\n    Vector2? size,\n    Vector2? scale,\n    double? angle,\n    Anchor? anchor,\n    Iterable<Component>? children,\n    int? priority,\n    ComponentKey? key,\n  }) : this(\n         builder: (size) => Circle(size / 2, size.x / 2),\n         position: position,\n         size: size,\n         scale: scale,\n         angle: angle,\n         anchor: anchor,\n         children: children,\n         priority: priority,\n         key: key,\n       );\n\n  /// {@macro rectangle_clip_component}\n  ///\n  /// Clips the canvas in the form of a rectangle based on its size.\n  ClipComponent.rectangle({\n    Vector2? position,\n    Vector2? size,\n    Vector2? scale,\n    double? angle,\n    Anchor? anchor,\n    Iterable<Component>? children,\n    int? priority,\n    ComponentKey? key,\n  }) : this(\n         builder: (size) => Rectangle.fromRect(size.toRect()),\n         position: position,\n         size: size,\n         scale: scale,\n         angle: angle,\n         anchor: anchor,\n         children: children,\n         priority: priority,\n         key: key,\n       );\n\n  /// {@macro polygon_clip_component}\n  ///\n  /// Clips the canvas in the form of a polygon based on its size.\n  ClipComponent.polygon({\n    required List<Vector2> points,\n    Vector2? position,\n    Vector2? size,\n    Vector2? scale,\n    double? angle,\n    Anchor? anchor,\n    Iterable<Component>? children,\n    int? priority,\n    ComponentKey? key,\n  }) : this(\n         builder: _polygonShapeBuilder(points),\n         position: position,\n         size: size,\n         scale: scale,\n         angle: angle,\n         anchor: anchor,\n         children: children,\n         priority: priority,\n         key: key,\n       );\n\n  late Path _path;\n  late Shape _shape;\n  final ShapeBuilder _builder;\n\n  @override\n  Future<void> onLoad() async {\n    _prepare();\n    size.addListener(_prepare);\n  }\n\n  void _prepare() {\n    _shape = _builder(size);\n    _path = _shape.asPath();\n  }\n\n  @override\n  void render(Canvas canvas) => canvas.clipPath(_path);\n\n  @override\n  bool containsPoint(Vector2 point) {\n    return _shape.containsPoint(point - position);\n  }\n\n  @override\n  bool containsLocalPoint(Vector2 point) {\n    return _shape.containsPoint(point);\n  }\n\n  /// Returns the [ShapeBuilder] function that builds a polygon\n  ///\n  /// this allows us to use an assertion during Constructor initialization\n  /// rather than at the execution of the builder function.\n  static ShapeBuilder _polygonShapeBuilder(List<Vector2> points) {\n    assert(\n      points.length >= 3,\n      'PolygonClipComponent requires at least 3 points.',\n    );\n\n    return (Vector2 size) => _polygonBuilder(points, size);\n  }\n\n  static Shape _polygonBuilder(List<Vector2> points, Vector2 size) {\n    final translatedPoints = points\n        .map(\n          (p) => p.clone()..multiply(size),\n        )\n        .toList();\n    return Polygon(translatedPoints);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/component_pool.dart",
    "content": "import 'package:flame/components.dart';\n\n/// A pool for managing reusable components.\n///\n/// This class allows you to efficiently reuse components, reducing the overhead\n/// of creating and destroying them frequently. The pool automatically manages\n/// component lifecycle by listening to the component's [Component.removed]\n/// future, returning components to the pool when they are removed from their\n/// parent.\n///\n/// Example usage:\n/// ```dart\n/// final bulletPool = ComponentPool<MyBullet>(\n///   factory: () => MyBullet(),\n///   maxSize: 50,\n///   initialSize: 10,\n/// );\n///\n/// // Acquire a bullet from the pool\n/// final bullet = bulletPool.acquire();\n/// // Configure the bullet as needed\n/// bullet.position = Vector2(10, 20);\n/// bullet.velocity = Vector2(100, 0);\n/// // Add it to the game\n/// game.add(bullet);\n///\n/// // Later, when the bullet should be destroyed:\n/// bullet.removeFromParent(); // Automatically returns to pool\n/// ```\n///\n/// The pool automatically returns components when they are removed from their\n/// parent, so you don't need to manually release them. Simply call\n/// [Component.removeFromParent] when the component should be destroyed, and it\n/// will be automatically returned to the pool for reuse.\nclass ComponentPool<T extends Component> {\n  final T Function() _factory;\n  final List<T> _available = [];\n\n  /// The maximum number of components that can be stored in the pool. If the\n  /// pool reaches this limit, additional components released back to the pool\n  /// will be discarded.\n  final int maxSize;\n\n  /// Creates a new component pool with the specified factory, maximum size, and\n  /// initial size. The [factory] is a function that creates new instances of\n  /// the component type. The [maxSize] parameter limits the number of\n  /// components that can be stored in the pool, while the [initialSize]\n  /// parameter determines how many components are created initially.\n  ///\n  /// If the [initialSize] exceeds the [maxSize], only [maxSize] components\n  /// will be created and added to the pool.\n  ComponentPool({\n    required T Function() factory,\n    this.maxSize = 100,\n    int initialSize = 0,\n  }) : _factory = factory {\n    for (var i = 0; i < initialSize && i < maxSize; i++) {\n      _available.add(_factory());\n    }\n  }\n\n  /// The number of components currently available in the pool for acquisition.\n  ///\n  /// This does not include components that are currently in use (i.e., those\n  /// that have been acquired but not yet returned to the pool). It only counts\n  /// the components that are ready to be acquired.\n  int get availableCount => _available.length;\n\n  /// Acquires a component from the pool. If the pool is empty, a new component\n  /// is created using the factory function. Otherwise, the last available\n  /// component is removed from the pool and returned.\n  ///\n  /// A listener is automatically attached that will return the component to\n  /// the pool when it is removed from its parent. The listener waits for the\n  /// component to be [Component.mounted] first, then listens for\n  /// [Component.removed]. This ensures recycled components (whose `removed`\n  /// future is already completed) don't get immediately returned to the pool\n  /// before they have a chance to mount.\n  ///\n  /// Just call [Component.removeFromParent] when done with the component.\n  T acquire() {\n    final component = _available.isEmpty ? _factory() : _available.removeLast();\n    component.mounted.then((_) {\n      component.removed.then((_) => _release(component));\n    });\n    return component;\n  }\n\n  /// Returns a component to the pool. This is called automatically when\n  /// a component's [Component.removed] future completes (i.e., after it has\n  /// been removed from its parent).\n  ///\n  /// The component is only added back to the pool if the pool has not reached\n  /// its [maxSize].\n  void _release(T component) {\n    if (!_available.contains(component) && _available.length < maxSize) {\n      _available.add(component);\n    }\n  }\n\n  /// Clears all available components from the pool. This can be useful if you\n  /// want to free up memory or reset the pool state.\n  ///\n  /// Note: This does not affect components that are currently in use.\n  void clear() {\n    _available.clear();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/components_notifier.dart",
    "content": "import 'dart:collection';\n\nimport 'package:flame/components.dart';\nimport 'package:flutter/material.dart';\nimport 'package:meta/meta.dart';\n\n/// A [ChangeNotifier] that notifies its listeners when a [Component] is\n/// added or removed, or updated. The meaning of an updated component\n/// will vary depending on the component implementation, this is something\n/// defined and executed by the component itself.\n///\n/// For example, in a Player component, that holds a health variable\n/// you may want to notify changes when that variable has changed.\nclass ComponentsNotifier<T extends Component> extends ChangeNotifier {\n  ComponentsNotifier(List<T> initial) : _components = initial;\n\n  final List<T> _components;\n\n  /// The list of components.\n  List<T> get components => UnmodifiableListView(_components);\n\n  /// Returns a single element of the components on the game.\n  ///\n  /// Returns null if there is no component.\n  T? get single {\n    if (_components.isNotEmpty) {\n      return _components.first;\n    }\n    return null;\n  }\n\n  @internal\n  bool applicable(Component component) => component is T;\n\n  @internal\n  void add(T component) {\n    _components.add(component);\n    notifyListeners();\n  }\n\n  @internal\n  void remove(T component) {\n    _components.remove(component);\n    notifyListeners();\n  }\n\n  @internal\n  void notify() => notifyListeners();\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/core/component.dart",
    "content": "import 'dart:async';\nimport 'dart:math' as math;\n\nimport 'package:collection/collection.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/src/cache/value_cache.dart';\nimport 'package:flame/src/camera/viewport.dart';\nimport 'package:flame/src/components/core/component_tree_root.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\nimport 'package:flutter/painting.dart';\nimport 'package:meta/meta.dart';\nimport 'package:ordered_set/ordered_set.dart';\nimport 'package:ordered_set/read_only_ordered_set.dart';\n\n/// [Component]s are the basic building blocks for a [FlameGame].\n///\n/// Components are quite similar to widgets in Flutter, or to GameObjects in\n/// Unity. Any entity within the game can be represented as a Component,\n/// especially if that entity has some visual appearance, or if it changes over\n/// time. For example, the player, the enemies, flying bullets, clouds in the\n/// sky, buildings, rocks, etc -- all can be represented as components. Some\n/// components may also represent more abstract entities: effects, behaviors,\n/// data stores, groups.\n///\n/// Components are designed to be organized into a component tree, where every\n/// component belongs to a single parent and owns any number of children\n/// components. A [Component] must be added to the component tree in order for\n/// it to become fully operational. Typically, the [FlameGame] is the root of\n/// the component tree.\n///\n/// The components are added into the component tree using the [add] method, or\n/// [addToParent]; and then later can be removed using [remove] or\n/// [removeFromParent]. Note that neither adding nor removing are immediate,\n/// typically these operations complete by the next game tick.\n///\n/// Each component goes through several lifecycle stages during its lifetime,\n/// at each stage invoking certain user-defined \"lifecycle methods\":\n///  - [onLoad] when the component is first added into the tree;\n///  - [onGameResize] + [onMount] when done loading, or when the component is\n///    re-added to the component tree after having been removed;\n///  - [onRemove] if the component is later removed from the component tree.\n///\n/// The [onLoad] is only invoked once during the lifetime of the component,\n/// which means you can treat it as \"async constructor\". When [onLoad] is\n/// invoked, we guarantee that the game instance can be found via [findGame],\n/// and that this game instance already has layout (i.e. knows the size of the\n/// canvas).\n///\n/// The [onMount] is invoked when the component is done loading, and when its\n/// parent is properly mounted. If a component is removed from the tree and\n/// later added to another component, the [onMount] will be called again. For\n/// every call to [onMount] there will be a corresponding call to [onRemove].\n///\n/// While the component is mounted, the following user-overridable methods are\n/// invoked:\n///  - [update] on every game tick;\n///  - [render] after all components are done updating;\n///  - [updateTree] and [renderTree] are more advanced versions of the update\n///    and render callbacks, but they rarely need to be overridden by the user;\n///  - [onGameResize] every time the size game's Flutter widget changes.\n///\n/// The [update] and [render] are two most important methods of the component's\n/// lifecycle. On every game tick, first the entire component tree will be\n/// [update]d, and then all the components will be [render]ed.\n///\n/// You may also need to override [containsLocalPoint] if the component needs to\n/// respond to tap events or similar; the [componentsAtLocation] may also need\n/// to be overridden if you have reimplemented [renderTree].\nclass Component {\n  Component({\n    Iterable<Component>? children,\n    int? priority,\n    this.key,\n  }) : _priority = priority ?? 0 {\n    if (children != null) {\n      addAll(children);\n    }\n  }\n\n  //#region Lifecycle state\n\n  /// Bitfield which keeps track of the current state of the component: which\n  /// lifecycle events it has already executed, and which are currently being\n  /// executed.\n  ///\n  /// [_initial]: the original state of the component as it was just created. In\n  ///     this state no events has occurred so far.\n  ///\n  /// [_loading]: this flag is set while the component is running its [onLoad]\n  ///     method, and can be checked via [isLoading] getter. This bit is turned\n  ///     on when the component starts loading, and then off when it has\n  ///     finished loading.\n  ///\n  /// [_loaded]: this flag is set after the component finishes running its\n  ///     [onLoad] method, and can be checked via the [isLoaded] getter. Once\n  ///     set, this bit is never turned off.\n  ///\n  /// [_mounted]: this flag is set when the component becomes properly mounted\n  ///     to the component tree, and then turned off when the component is\n  ///     removed from the tree.\n  ///\n  /// [_removing]: this bit indicates that the component is scheduled for\n  ///     removal at the earliest possible opportunity, and then cleared when\n  ///     the component is actually removed.\n  ///\n  /// The lifecycle process of a component is quite complicated. This happens\n  /// for several reasons: partly because it consists of several stages, between\n  /// which there are asynchronous or even physical execution gaps. In addition,\n  /// the lifecycle invokes a number of user-provided callbacks, and those\n  /// callbacks may attempt to modify the component.\n  ///\n  /// This is how a typical component's lifecycle progresses:\n  ///  - First, the component is created with the [_state] variable = 0. At this\n  ///    point the only operations that can be done are: to [add] it to another\n  ///    component, or to add other components to it.\n  ///  - When the component is [add]ed to another component (the parent), we do\n  ///    the following:\n  ///    - set the [_parent] variable;\n  ///    - add the component to the parent's queue of pending children;\n  ///    - if the component has been [_loaded] before, then do nothing else and\n  ///      wait until its parent will do the mounting.\n  ///    - otherwise, if the [Game] instance is accessible via [findGame], then\n  ///      we start loading the component;\n  ///    - otherwise we will start loading when the parent becomes mounted. This\n  ///      means we're entering into an execution gap here. During this gap the\n  ///      component is still in the [_initial] state, and it can be [remove]d\n  ///      by the user. When the user removes the component in this state, we\n  ///      simply set the [_parent] to null and remove it from the parent's\n  ///      queue of pending children.\n  ///  - When we [_startLoading] the component, we set the [_loading] bit,\n  ///    and then [onLoad] immediately\n  ///    afterwards. The onLoad will be either sync or async, in both cases we\n  ///    arrange to turn on the [_loaded] bit at the end of [onLoad]'s run.\n  ///  - At this point we're in an execution gap: either the async [onLoad] is\n  ///    waiting to be run, or it already completed, or it was sync to begin\n  ///    with -- in either case we're waiting until the component can be\n  ///    mounted, and in that time the [_loading] bit is still on.\n  ///    During this time the user may request to [remove] the component. If at\n  ///    that moment the component is already loaded, then we remove it by\n  ///    setting parent to null and deleting it from the parent's pending\n  ///    children queue. If, on the other hand, the component is not loaded yet,\n  ///    then we turn on the [_removing] flag only -- this is because we don't\n  ///    want to set [_parent] to null while the [onLoad] may still try to\n  ///    access it.\n  ///  - The next step in the component's lifecycle comes when its parent\n  ///    processes own pending events queue, which only happens after the parent\n  ///    gets mounted. For each component in its queue of pending children, the\n  ///    following checks are performed:\n  ///      - if the component is already [_loaded], then it will now be\n  ///        [_mount]ed;\n  ///      - otherwise, if the component is not even [_loading], then it will\n  ///        now [_startLoading];\n  ///      - otherwise do nothing: need to wait until the component finishes\n  ///        loading.\n  ///  - During [_mount]ing, we perform the following sequence:\n  ///      - first we run [onGameResize];\n  ///      - check if the component was scheduled for removal while waiting in\n  ///        the queue -- if so, remove it immediately without mounting;\n  ///      - clear the [_loading] flag and start the [onMount] callback;\n  ///      - set the [_mounted] bit;\n  ///      - add the component to parent's list of [children];\n  ///      - if the component has its own list of existing children, then mount\n  ///        those;\n  ///      - if the component has a list of pending children, then process the\n  ///        lifecycle events queue, which would attempt to load and/or mount\n  ///        these pending children.\n  ///\n  /// At this point the component would be at its normal, mounted state. When\n  /// [remove] is invoked in this state, we (1) turn on the [_removing] bit, and\n  /// (2) add the component to the \"removals\" lifecycle queue of its parent. The\n  /// next time the parent processes its lifecycle event queue, it would take\n  /// all the components from the \"removals\", and for each one call the\n  /// [onRemove] method, clear the [_mounted] and [_removing] flags, and lastly\n  /// remove the component from the official list of children.\n  ///\n  /// After a component was removed, it will be [_loaded], but not [_mounted],\n  /// and its [_parent] will be null. Such component can be re-added into the\n  /// component tree if needed.\n  int _state = _initial;\n\n  static const int _initial = 0;\n  static const int _loading = 1;\n  static const int _loaded = 2;\n  static const int _mounting = 32;\n  static const int _mounted = 4;\n  static const int _removing = 8;\n  static const int _removed = 16;\n\n  /// Whether the component is currently executing its [onLoad] step.\n  bool get isLoading => (_state & _loading) != 0;\n  void _setLoadingBit() => _state |= _loading;\n  void _clearLoadingBit() => _state &= ~_loading;\n\n  /// Whether this component has completed its [onLoad] step.\n  bool get isLoaded => (_state & _loaded) != 0;\n  void _setLoadedBit() => _state |= _loaded;\n\n  @internal\n  bool get isMounting => (_state & _mounting) != 0;\n  void _setMountingBit() => _state |= _mounting;\n  void _clearMountingBit() => _state &= ~_mounting;\n\n  /// Whether this component is currently added to a component tree.\n  bool get isMounted => (_state & _mounted) != 0;\n  void _setMountedBit() => _state |= _mounted;\n  void _clearMountedBit() => _state &= ~_mounted;\n\n  /// Whether the component is scheduled to be removed.\n  bool get isRemoving => (_state & _removing) != 0;\n  void _setRemovingBit() => _state |= _removing;\n  void _clearRemovingBit() => _state &= ~_removing;\n\n  /// Whether the component has been removed. Originally this flag is `false`,\n  /// but it becomes `true` after the component was mounted and then removed\n  /// from its parent. The flag becomes `false` again when the component is\n  /// mounted to a new parent.\n  bool get isRemoved => (_state & _removed) != 0;\n  void _setRemovedBit() => _state |= _removed;\n  void _clearRemovedBit() => _state &= ~_removed;\n\n  Completer<void>? _loadCompleter;\n  Completer<void>? _mountCompleter;\n  Completer<void>? _removeCompleter;\n\n  /// A future that completes when this component finishes loading.\n  ///\n  /// If the component is already loaded (see [isLoaded]), this returns an\n  /// already completed future.\n  Future<void> get loaded {\n    return isLoaded\n        ? Future.value()\n        : (_loadCompleter ??= Completer<void>()).future;\n  }\n\n  /// A future that will complete once the component is mounted on its parent.\n  ///\n  /// If the component is already mounted (see [isMounted]), this returns an\n  /// already completed future.\n  Future<void> get mounted {\n    return isMounted\n        ? Future.value()\n        : (_mountCompleter ??= Completer<void>()).future;\n  }\n\n  /// A future that completes when this component is removed from its parent.\n  ///\n  /// If the component is already removed (see [isRemoved]), this returns an\n  /// already completed future.\n  Future<void> get removed {\n    return isRemoved\n        ? Future.value()\n        : (_removeCompleter ??= Completer<void>()).future;\n  }\n\n  //#endregion\n\n  //#region Component tree\n\n  /// Who owns this component in the component tree.\n  ///\n  /// This can be null if the component hasn't been added to the component tree\n  /// yet, or if it is the root of component tree.\n  ///\n  /// Setting this property to `null` is equivalent to [removeFromParent].\n  /// Setting it to a new parent component is equivalent to calling\n  /// [addToParent] and will properly remove this component from its current\n  /// parent, if any.\n  ///\n  /// Note that the [parent] setter, like [add] and similar methods,\n  /// merely enqueues the move from one parent to another. For example:\n  ///\n  /// ```dart\n  /// coin.parent = inventory;\n  /// // The inventory.children set does not include coin yet.\n  /// await game.lifecycleEventsProcessed;\n  /// // The inventory.children set now includes coin.\n  /// ```\n  Component? get parent => _parent;\n  Component? _parent;\n  set parent(Component? newParent) {\n    if (newParent == null) {\n      removeFromParent();\n    } else {\n      addToParent(newParent);\n    }\n  }\n\n  /// This field should be used internally for functionality when you don't need\n  /// to create a component set for the children if one doesn't already exist.\n  ///\n  /// This makes it possible to have lighter components that don't have any\n  /// children.\n  OrderedSet<Component>? _children;\n\n  /// This field should be used internally for functionality when you need to\n  /// make sure that the component set is created if it doesn't already exist.\n  OrderedSet<Component> get _internalChildren =>\n      _children ??= createComponentSet();\n\n  void rebalanceChildren() {\n    if (_children != null) {\n      _children!.rebalanceAll();\n    }\n  }\n\n  /// The children components of this component.\n  ///\n  /// This getter will automatically create the [OrderedSet] container within\n  /// the current object if it didn't exist before. Check the [hasChildren]\n  /// property in order to avoid instantiating the children container.\n  ReadOnlyOrderedSet<Component> get children =>\n      _children ??= createComponentSet();\n\n  /// Whether this component has any children.\n  /// Avoids the creation of the children container if not necessary.\n  bool get hasChildren => _children?.isNotEmpty ?? false;\n\n  /// `Component.childrenFactory` is the default method for creating children\n  /// containers within all components. Replace this method if you want to have\n  /// customized (non-default) [OrderedSet] instances in your project.\n  static OrderedSet<Component> Function() childrenFactory = () {\n    return OrderedSet.mapping(\n      _componentPriorityMapper,\n      strictMode: strictQueryMode,\n    );\n  };\n\n  /// Whether OrderedSet's strict mode mode should be enabled for all children\n  /// sets.\n  static bool strictQueryMode = false;\n\n  static num _componentPriorityMapper(Component component) {\n    return component.priority;\n  }\n\n  /// This method creates the children container for the current component.\n  /// Override this method if you need to have a custom [OrderedSet] within\n  /// a particular class.\n  OrderedSet<Component> createComponentSet() => childrenFactory();\n\n  /// Returns the closest parent further up the hierarchy that satisfies type=T,\n  /// or null if no such parent can be found.\n  ///\n  /// If [includeSelf] is set to true (default is false) then the component\n  /// which the call is made for is also included in the search.\n  T? findParent<T extends Component>({bool includeSelf = false}) {\n    return ancestors(includeSelf: includeSelf).whereType<T>().firstOrNull;\n  }\n\n  /// Returns the first child that matches the given type [T], or null if there\n  /// are no such children.\n  T? firstChild<T extends Component>() {\n    return children.whereType<T>().firstOrNull;\n  }\n\n  /// Returns the last child that matches the given type [T], or null if there\n  /// are no such children.\n  T? lastChild<T extends Component>() {\n    return children.reversed().whereType<T>().firstOrNull;\n  }\n\n  /// An iterator producing this component's parent, then its parent's parent,\n  /// then the great-grand-parent, and so on, until it reaches a component\n  /// without a parent.\n  Iterable<Component> ancestors({bool includeSelf = false}) sync* {\n    var current = includeSelf ? this : parent;\n    while (current != null) {\n      yield current;\n      current = current.parent;\n    }\n  }\n\n  /// Recursively enumerates all nested [children].\n  ///\n  /// The search is depth-first in preorder. In other words, it explores the\n  /// first child completely before visiting the next sibling, and the root\n  /// component is visited before its children.\n  ///\n  /// This ordering of descendants is considered standard in Flame: it is the\n  /// same order in which the components will normally be updated and rendered\n  /// on every game cycle. The optional parameter [reversed] allows iterating\n  /// through the same set of descendants in reverse order.\n  ///\n  /// The [Iterable] produced by this method is \"lazy\", which means it will only\n  /// traverse the component tree when required. This allows efficient chaining\n  /// of various iterable methods, such as filtering, early stopping, folding,\n  /// and so on -- see the documentation of the [Iterable] class for details.\n  Iterable<Component> descendants({\n    bool includeSelf = false,\n    bool reversed = false,\n  }) sync* {\n    if (includeSelf && !reversed) {\n      yield this;\n    }\n    if (hasChildren) {\n      final childrenIterable = reversed ? children.reversed() : children;\n      for (final child in childrenIterable) {\n        yield* child.descendants(includeSelf: true, reversed: reversed);\n      }\n    }\n    if (includeSelf && reversed) {\n      yield this;\n    }\n  }\n\n  /// This method first calls the passed handler on the leaves in the tree,\n  /// the children without any children of their own.\n  /// Then it continues through all other children. The propagation continues\n  /// until the handler returns false, which means \"do not continue\", or when\n  /// the handler has been called with all children.\n  ///\n  /// This method is important to be used by the engine to propagate actions\n  /// like rendering, taps, etc, but you can call it yourself if you need to\n  /// apply an action to the whole component chain.\n  /// It will only consider components of type T in the hierarchy,\n  /// so use T = Component to target everything.\n  bool propagateToChildren<T extends Component>(\n    bool Function(T) handler, {\n    bool includeSelf = false,\n  }) {\n    return descendants(\n      reversed: true,\n      includeSelf: includeSelf,\n    ).whereType<T>().every(handler);\n  }\n\n  @internal\n  static Game? staticGameInstance;\n\n  /// Fetches the nearest [FlameGame] ancestor to the component.\n  FlameGame? findGame() {\n    assert(\n      staticGameInstance is FlameGame || staticGameInstance == null,\n      'A component needs to have a FlameGame as the root.',\n    );\n    final gameInstance = staticGameInstance is FlameGame\n        ? staticGameInstance! as FlameGame\n        : null;\n    return gameInstance ??\n        ((this is FlameGame) ? (this as FlameGame) : _parent?.findGame());\n  }\n\n  /// Fetches the root [FlameGame] ancestor to the component.\n  FlameGame? findRootGame() {\n    var game = findGame();\n    while (game?.parent != null) {\n      game = game!.parent!.findGame();\n    }\n    return game;\n  }\n\n  /// Whether the children list contains the given component.\n  ///\n  /// This method uses reference equality.\n  bool contains(Component c) => _children?.contains(c) ?? false;\n\n  //#endregion\n\n  //#region Component lifecycle methods\n\n  /// Called whenever the size of the top-level Canvas changes.\n  ///\n  /// In addition, this method will be invoked before each [onMount].\n  @mustCallSuper\n  void onGameResize(Vector2 size) => handleResize(size);\n\n  /// Late initialization method for [Component].\n  ///\n  /// Usually, this method is the main place where you initialize your\n  /// component. This has several advantages over the traditional constructor:\n  ///   - this method can be `async`;\n  ///   - it is invoked when the size of the game canvas is already known.\n  ///\n  /// If your loading logic requires knowing the size of the game canvas, then\n  /// add [HasGameReference] mixin and then query `game.size` or\n  /// `game.canvasSize`.\n  ///\n  /// The default implementation returns `null`, indicating that there is no\n  /// need to await anything. When overriding this method, you have a choice\n  /// whether to create a regular or async function.\n  ///\n  /// If you need an asynchronous [onLoad], make your override return\n  /// non-nullable `Future<void>`:\n  /// ```dart\n  /// @override\n  /// Future<void> onLoad() async {\n  ///   // your code here\n  /// }\n  /// ```\n  ///\n  /// Alternatively, if your [onLoad] function doesn't use any `await`ing, then\n  /// you can declare it as a regular method returning `void`:\n  /// ```dart\n  /// @override\n  /// void onLoad() {\n  ///   // your code here\n  /// }\n  /// ```\n  ///\n  /// The engine ensures that this method will be called exactly once during\n  /// the lifetime of the [Component] object. Do not call this method manually.\n  FutureOr<void> onLoad() => null;\n\n  /// Called when the component is added to its parent.\n  ///\n  /// This method only runs when the component is fully loaded, i.e. after\n  /// [onLoad]. However, [onLoad] only ever runs once for the component, whereas\n  /// [onMount] runs every time the component is inserted into the game tree.\n  ///\n  /// This method runs when the component is about to be added to its parent.\n  /// At this point the [parent] property already holds a reference to this\n  /// component's parent, however the parent doesn't have this component among\n  /// its [children] yet.\n  ///\n  /// After this method completes, the component is added to the parent's\n  /// children set, and then the flag [isMounted] set to true.\n  ///\n  /// Example:\n  /// ```dart\n  /// @override\n  /// void onMount() {\n  ///   position = parent!.size / 2;\n  /// }\n  /// ```\n  ///\n  /// See also:\n  /// - [onRemove] that is called every time the component is removed from the\n  /// game tree\n  void onMount() {}\n\n  /// Called right before the component is removed from its parent\n  /// and also before it changes parents (and is thus temporarily removed\n  /// from the component tree).\n  ///\n  /// This method will only run for a component that was previously mounted into\n  /// a component tree. If a component was never mounted (for example, when it\n  /// is removed before it had a chance to mount), then this callback will not\n  /// trigger. Thus, [onRemove] runs if and only if there was a corresponding\n  /// [onMount] call before.\n  void onRemove() {}\n\n  /// Called whenever the parent of this component changes size; and also once\n  /// before [onMount].\n  ///\n  /// The component may change its own size or perform layout in response to\n  /// this call. If the component changes size, then it should call\n  /// [onParentResize] for all its children.\n  void onParentResize(Vector2 maxSize) {}\n\n  /// This method is called periodically by the game engine to request that your\n  /// component updates itself.\n  ///\n  /// The time [dt] in seconds (with microseconds precision provided by Flutter)\n  /// since the last update cycle.\n  /// This time can vary according to hardware capacity, so make sure to update\n  /// your state considering this.\n  /// All components in the tree are always updated by the same amount. The time\n  /// each one takes to update adds up to the next update cycle.\n  void update(double dt) {}\n\n  /// This method traverses the component tree and calls [update] on all its\n  /// children according to their [priority] order, relative to the\n  /// priority of the direct siblings, not the children or the ancestors.\n  void updateTree(double dt) {\n    update(dt);\n    final children = _children;\n    if (children != null) {\n      for (final child in children) {\n        child.updateTree(dt);\n      }\n    }\n  }\n\n  /// This method will be invoked from lifecycle if [child] has been added\n  /// to or removed from its parent children list.\n  void onChildrenChanged(Component child, ChildrenChangeType type) {}\n\n  void render(Canvas canvas) {}\n\n  /// Renders a single [child] component onto [canvas].\n  ///\n  /// Override this method (instead of [renderTree]) when you need to intercept\n  /// per-child rendering — for example, to accumulate draw calls for batching.\n  /// The default implementation propagates the parent's render contexts into\n  /// the child before delegating to the child's [Component.renderTree], and\n  /// cleans them up afterwards.\n  @protected\n  void renderChild(Canvas canvas, Component child) {\n    int? originalLength;\n    final hasContext = _renderContexts.isNotEmpty;\n    if (hasContext) {\n      originalLength = child._renderContexts.length;\n      child._renderContexts.addAll(_renderContexts);\n    }\n    child.renderTree(canvas);\n    if (hasContext) {\n      child._renderContexts.removeRange(\n        originalLength!,\n        child._renderContexts.length,\n      );\n    }\n  }\n\n  /// Called once after all children have been rendered in [renderTree].\n  ///\n  /// Override to flush any state accumulated across [renderChild] calls\n  /// (e.g. a pending sprite batch).\n  @protected\n  void afterChildrenRendered(Canvas canvas) {}\n\n  void renderTree(Canvas canvas) {\n    final context = renderContext;\n    if (context != null) {\n      _renderContexts.add(context);\n    }\n\n    render(canvas);\n    final children = _children;\n    if (children != null) {\n      for (final child in children) {\n        renderChild(canvas, child);\n      }\n      afterChildrenRendered(canvas);\n    }\n\n    // Any debug rendering should be rendered on top of everything\n    if (debugMode) {\n      renderDebugMode(canvas);\n    }\n\n    if (context != null) {\n      _renderContexts.removeLast();\n    }\n  }\n\n  //#endregion\n\n  //#region Add/remove components\n\n  /// Schedules [component] to be added as a child to this component.\n  ///\n  /// This method is robust towards being called from any place in the user\n  /// code: you can call it while iterating over the component tree, during\n  /// mounting or async loading, when the Game object is already loaded or not.\n  ///\n  /// The cost of this flexibility is that the component won't be added right\n  /// away. Instead, it will be placed into a queue, and then added later, after\n  /// it has finished loading, but no sooner than on the next game tick.\n  /// You can await [FlameGame.lifecycleEventsProcessed] like so:\n  ///\n  /// ```dart\n  /// world.add(coin);\n  /// await game.lifecycleEventsProcessed;\n  /// // The coin is now guaranteed to be added.\n  /// ```\n  ///\n  /// When multiple children are scheduled to be added to the same parent, we\n  /// start loading all of them as soon as possible. Nevertheless, the children\n  /// will end up being added to the parent in exactly the same order as they\n  /// were originally scheduled by the user, regardless of how fast or slow\n  /// each of them loads.\n  ///\n  /// A component can be added to a parent which may not be mounted to the game\n  /// tree yet. In such case, the component will start loading immediately, but\n  /// its mounting will be delayed until such time when the parent becomes\n  /// mounted.\n  ///\n  /// This method returns a future that completes when the component is done\n  /// loading, and mounting if the parent is currently mounted. However, this\n  /// future will not guarantee that the component will become \"fully mounted\":\n  /// it still needs to be added to the parent's children list, and that\n  /// operation will only be done on the next game tick.\n  ///\n  /// A component can only be added to one parent at a time. It is an error to\n  /// try to add it to multiple parents, or even to the same parent multiple\n  /// times. If you need to change the parent of a component, use the\n  /// [parent] setter.\n  FutureOr<void> add(Component component) => _addChild(component);\n\n  /// Adds this component as a child of [parent] (see [add] for details).\n  FutureOr<void> addToParent(Component parent) => parent._addChild(this);\n\n  /// A convenience method to [add] multiple children at once.\n  Future<void> addAll(Iterable<Component> components) async {\n    List<Future<void>>? futures;\n    for (final component in components) {\n      final future = add(component);\n      if (future is Future) {\n        (futures ??= []).add(future);\n      }\n    }\n    if (futures != null) {\n      await Future.wait(futures);\n    }\n  }\n\n  FutureOr<void> _addChild(Component child) {\n    final game = findGame() ?? child.findGame();\n    if ((!isMounted && !child.isMounted) || game == null) {\n      child._parent?._internalChildren.remove(child);\n      child._parent = this;\n      _internalChildren.add(child);\n    } else if (child._parent != null) {\n      if (child.isRemoving) {\n        game.dequeueRemove(child);\n        _clearRemovingBit();\n      }\n      game.enqueueMove(child, this);\n    } else if (isMounted && !child.isMounted) {\n      child._parent = this;\n      game.enqueueAdd(child, this);\n    } else {\n      child._parent = this;\n      // This will be reconciled during the mounting stage\n      _internalChildren.add(child);\n    }\n    if (!child.isLoaded && !child.isLoading && (game?.hasLayout ?? false)) {\n      return child._startLoading();\n    }\n  }\n\n  /// Removes a component from the component tree.\n  ///\n  /// This will call [onRemove] for the component and its children, but only if\n  /// there was an [onMount] call previously, i.e. when removing a component\n  /// that was properly mounted.\n  ///\n  /// A component can be removed even before it finishes mounting, however such\n  /// component cannot be added back into the tree until it at least finishes\n  /// loading.\n  void remove(Component component) => _removeChild(component);\n\n  /// Remove the component from its parent in the next tick.\n  void removeFromParent() => _parent?.remove(this);\n\n  /// Removes all the children in the list and calls [onRemove] for all of them\n  /// and their children.\n  void removeAll(Iterable<Component> components) {\n    components.toList(growable: false).forEach(_removeChild);\n  }\n\n  /// Removes all the children for which the [test] function returns true.\n  void removeWhere(bool Function(Component component) test) {\n    children.where(test).toList(growable: false).forEach(_removeChild);\n  }\n\n  void _removeChild(Component child) {\n    assert(\n      child._parent != null,\n      \"Trying to remove a component that doesn't belong to any parent\",\n    );\n    assert(\n      child._parent == this,\n      'Trying to remove a component that belongs to a different parent: this = '\n      \"$this, component's parent = ${child._parent}\",\n    );\n    if (isMounted) {\n      final root = findGame()!;\n      if (child.isMounted || child.isMounting) {\n        if (!child.isRemoving) {\n          root.enqueueRemove(child, this);\n          child._setRemovingBit();\n        }\n      } else if (!child.isRemoved) {\n        root.dequeueAdd(child, this);\n        child._parent = null;\n      } else if (isRemoving) {\n        // This parent is being removed from the tree, and the child was\n        // already marked as removed during ancestor removal propagation.\n        // The child is now being explicitly removed by user code (e.g.\n        // via removeAll(children) in onRemove), so detach it.\n        _internalChildren.remove(child);\n        child._parent = null;\n      }\n    } else {\n      _children?.remove(child);\n      child._parent = null;\n    }\n  }\n\n  //#endregion\n\n  //#region Hit Testing\n\n  /// Checks whether the [point] is within this component's bounds.\n  ///\n  /// This method should be implemented for any component that has a visual\n  /// representation and non-zero size. The [point] is in the local coordinate\n  /// space.\n  bool containsLocalPoint(Vector2 point) => false;\n\n  /// Same as [containsLocalPoint], but for a \"global\" [point].\n  ///\n  /// This will be deprecated in the future, due to the notion of \"global\" point\n  /// not being well-defined.\n  bool containsPoint(Vector2 point) => containsLocalPoint(point);\n\n  /// An iterable of descendant components intersecting the given point. The\n  /// [point] is in the local coordinate space.\n  ///\n  /// More precisely, imagine a ray originating at a certain point (x, y) on\n  /// the screen, and extending perpendicularly to the screen's surface into\n  /// your game's world. The purpose of this method is to find all components\n  /// that intersect with this ray, in the order from those that are closest to\n  /// the user to those that are farthest.\n  ///\n  /// The return value is an [Iterable] of components. If the [nestedPoints]\n  /// parameter is given, then it will also report the points of intersection\n  /// for each component in its local coordinate space. Specifically, the last\n  /// element in the list is the point in the coordinate space of the returned\n  /// component, the element before the last is in that component's parent's\n  /// coordinate space, and so on. The [nestedPoints] list must be growable and\n  /// modifiable.\n  ///\n  /// The default implementation relies on the [CoordinateTransform] interface\n  /// to translate from the parent's coordinate system into the local one. Make\n  /// sure that your component implements this interface if it alters the\n  /// coordinate system when rendering.\n  ///\n  /// If your component overrides [renderTree], then it almost certainly needs\n  /// to override this method as well, so that this method can find all rendered\n  /// components wherever they are.\n  Iterable<Component> componentsAtPoint(\n    Vector2 point, [\n    List<Vector2>? nestedPoints,\n  ]) {\n    return componentsAtLocation<Vector2>(\n      point,\n      nestedPoints,\n      (transform, point) => transform.parentToLocal(point),\n      (component, point) => component.containsLocalPoint(point),\n    );\n  }\n\n  /// This is a generic implementation of [componentsAtPoint]; refer to those\n  /// docs for context.\n  ///\n  /// This will find components intersecting a given location context [T]. The\n  /// context can be a single point or a more complicated structure. How to\n  /// interpret the structure T is determined by the provided lambdas,\n  /// [transformContext] and [checkContains].\n  ///\n  /// A simple choice of T would be a simple point (i.e. Vector2). In that case\n  /// transformContext needs to be able to transform a Vector2 on the parent\n  /// coordinate space into the coordinate space of a provided\n  /// [CoordinateTransform]; and [checkContains] must be able to determine if\n  /// a given [Component] \"contains\" the Vector2 (the definition of \"contains\"\n  /// will vary and shall be determined by the nature of the chosen location\n  /// context [T]).\n  Iterable<Component> componentsAtLocation<T>(\n    T locationContext,\n    List<T>? nestedContexts,\n    T? Function(CoordinateTransform, T) transformContext,\n    bool Function(Component, T) checkContains,\n  ) sync* {\n    nestedContexts?.add(locationContext);\n    if (_children != null) {\n      for (final child in _children!.reversed()) {\n        if (child is IgnoreEvents && child.ignoreEvents) {\n          continue;\n        }\n        T? childPoint = locationContext;\n        if (child is CoordinateTransform) {\n          childPoint = transformContext(\n            child as CoordinateTransform,\n            locationContext,\n          );\n        }\n        if (childPoint != null) {\n          yield* child.componentsAtLocation(\n            childPoint,\n            nestedContexts,\n            transformContext,\n            checkContains,\n          );\n        }\n      }\n    }\n    final shouldIgnoreEvents =\n        this is IgnoreEvents && (this as IgnoreEvents).ignoreEvents;\n    if (checkContains(this, locationContext) && !shouldIgnoreEvents) {\n      yield this;\n    }\n    nestedContexts?.removeLast();\n  }\n\n  //#endregion\n\n  //#region Priority\n\n  /// Render priority of this component. This allows you to control the order in\n  /// which your components are rendered.\n  ///\n  /// Components are always updated and rendered in the order defined by what\n  /// this number is when the component is added to the game.\n  /// The smaller the priority, the sooner your component will be\n  /// updated/rendered.\n  /// It can be any integer (negative, zero, or positive).\n  /// If two components share the same priority, they will be updated and\n  /// rendered in the order they were added.\n  ///\n  /// Note that setting the priority is relatively expensive if the component is\n  /// already added to a component tree since all siblings have to be re-added\n  /// to the parent.\n  int get priority => _priority;\n  int _priority;\n  set priority(int newPriority) {\n    if (_priority != newPriority) {\n      _priority = newPriority;\n      final parent = _parent;\n      final game = findGame();\n      if (game != null && parent != null) {\n        game.enqueuePriorityChange(parent, this);\n      }\n    }\n  }\n\n  //#endregion\n\n  //#region Internal lifecycle management\n\n  @internal\n  LifecycleEventStatus handleLifecycleEventAdd(Component parent) {\n    assert(!isMounted);\n    if (parent.isMounted && isLoaded) {\n      _parent ??= parent;\n      _mount();\n      return LifecycleEventStatus.done;\n    } else {\n      if (parent.isMounted && !isLoading) {\n        _startLoading();\n      } else if (parent.isRemoved) {\n        // This case happens when the child is added to a parent that is being\n        // removed in the same tick.\n        _parent = parent;\n        parent._internalChildren.add(this);\n        return LifecycleEventStatus.done;\n      }\n      return LifecycleEventStatus.block;\n    }\n  }\n\n  @internal\n  LifecycleEventStatus handleLifecycleEventRemove(Component parent) {\n    if (_parent == null) {\n      parent._children?.remove(this);\n    } else {\n      _remove(parent);\n      assert(_parent == null);\n    }\n    return LifecycleEventStatus.done;\n  }\n\n  @internal\n  LifecycleEventStatus handleLifecycleEventMove(Component newParent) {\n    final parent = _parent;\n    if (parent != null) {\n      _remove(parent);\n    }\n    if (newParent.isMounted) {\n      _parent = newParent;\n      _mount();\n    } else {\n      newParent.add(this);\n    }\n    return LifecycleEventStatus.done;\n  }\n\n  @mustCallSuper\n  @internal\n  void handleResize(Vector2 size) {\n    final children = _children;\n    if (children != null) {\n      for (final child in children) {\n        if (child.isLoading || child.isLoaded) {\n          child.onGameResize(size);\n        }\n      }\n    }\n  }\n\n  /// Called when Flutter's hot reload is triggered.\n  ///\n  /// Override this method to reload assets, recalculate cached values,\n  /// or perform other actions in response to hot reload.\n  ///\n  /// This is only called in debug mode.\n  @mustCallSuper\n  void onHotReload() => handleHotReload();\n\n  @mustCallSuper\n  @internal\n  void handleHotReload() {\n    final children = _children;\n    if (children != null) {\n      for (final child in children) {\n        if (child.isLoading || child.isLoaded) {\n          child.onHotReload();\n        }\n      }\n    }\n  }\n\n  FutureOr<void> _startLoading() {\n    assert(_state == _initial);\n    assert(_parent != null);\n    assert(_parent!.findGame() != null);\n    assert(_parent!.findGame()!.hasLayout);\n    _setLoadingBit();\n    final onLoadFuture = onLoad();\n    if (onLoadFuture is Future) {\n      return onLoadFuture.then((dynamic _) => _finishLoading());\n    } else {\n      _finishLoading();\n    }\n  }\n\n  void _finishLoading() {\n    _clearLoadingBit();\n    _setLoadedBit();\n    _loadCompleter?.complete();\n    _loadCompleter = null;\n  }\n\n  /// Mount the component that is already loaded and has a mounted parent.\n  void _mount() {\n    assert(_parent != null && _parent!.isMounted);\n    assert(isLoaded && !isLoading);\n    _setMountingBit();\n    onGameResize(_parent!.findGame()!.canvasSize);\n    if (_parent is ReadOnlySizeProvider) {\n      if (_parent is Viewport) {\n        onParentResize((_parent! as Viewport).virtualSize);\n      } else {\n        onParentResize((_parent! as ReadOnlySizeProvider).size);\n      }\n    }\n    if (isRemoved) {\n      _clearRemovedBit();\n    } else if (isRemoving) {\n      _parent = null;\n      _clearRemovingBit();\n      _setRemovedBit();\n      return;\n    }\n    debugMode |= _parent!.debugMode;\n    onMount();\n    _setMountedBit();\n    _mountCompleter?.complete();\n    _mountCompleter = null;\n    _parent!._internalChildren.add(this);\n    _reAddChildren();\n    _parent!.onChildrenChanged(this, ChildrenChangeType.added);\n    _clearMountingBit();\n\n    if (key != null) {\n      final currentGame = findGame();\n      if (currentGame is FlameGame) {\n        currentGame.registerKey(key!, this);\n      }\n    }\n  }\n\n  /// Used by [_reAddChildren].\n  static final List<Component> _tmpChildren = [];\n  static final Set<Component> _tmpPendingRemoves = {};\n\n  /// At the end of mounting, we remove all children components and then re-add\n  /// them one-by-one. The reason for this is that before the current component\n  /// was mounted, its [children] may have contained components in arbitrary\n  /// state -- initial, loading, unmounted, etc. However, we don't want to\n  /// have such components in a component tree. By removing and then re-adding\n  /// them, we ensure that they are placed in a queue, and will only be placed\n  /// into [children] once they are fully mounted.\n  void _reAddChildren() {\n    if (_children != null && _children!.isNotEmpty) {\n      assert(_tmpChildren.isEmpty);\n      _tmpChildren.addAll(_children!);\n      _children!.clear();\n      assert(_tmpPendingRemoves.isEmpty);\n      findGame()?.cancelQueuedRemoves(_tmpChildren, _tmpPendingRemoves);\n      for (final child in _tmpChildren) {\n        child._parent = null;\n        if (_tmpPendingRemoves.contains(child)) {\n          continue;\n        }\n        _addChild(child);\n      }\n      _tmpChildren.clear();\n      _tmpPendingRemoves.clear();\n    }\n  }\n\n  /// Used by the [FlameGame] to set the loaded state of the component, since\n  /// the game isn't going through the whole normal component life cycle.\n  @internal\n  void setLoaded() {\n    _setLoadedBit();\n    _loadCompleter?.complete();\n    _loadCompleter = null;\n  }\n\n  /// Used by the [FlameGame] to set the mounted state of the component, since\n  /// the game isn't going through the whole normal component life cycle.\n  @internal\n  void setMounted() {\n    _setMountedBit();\n    _mountCompleter?.complete();\n    _mountCompleter = null;\n    _reAddChildren();\n  }\n\n  /// Used by the [FlameGame] to set the removed state of the component, since\n  /// the game isn't going through the whole normal component life cycle.\n  @internal\n  void setRemoved() {\n    _setRemovedBit();\n    _removeCompleter?.complete();\n    _removeCompleter = null;\n  }\n\n  void _remove(Component parent) {\n    parent._internalChildren.remove(this);\n    propagateToChildren(\n      (Component component) {\n        component\n          ..onRemove()\n          .._unregisterKey()\n          .._clearMountedBit()\n          .._clearRemovingBit()\n          .._setRemovedBit()\n          .._removeCompleter?.complete()\n          .._removeCompleter = null\n          .._parent!.onChildrenChanged(component, ChildrenChangeType.removed);\n        return true;\n      },\n      includeSelf: true,\n    );\n    _parent = null;\n  }\n\n  void _unregisterKey() {\n    if (key != null) {\n      final game = findGame();\n      if (game is FlameGame) {\n        game.unregisterKey(key!);\n      }\n    }\n  }\n\n  //#endregion\n\n  //#region Context\n\n  final QueueList<ComponentRenderContext> _renderContexts = QueueList();\n\n  /// Override this method if you want your component to provide a custom\n  /// render context to all its children (recursively).\n  ComponentRenderContext? get renderContext => null;\n\n  T? findRenderContext<T extends ComponentRenderContext>() {\n    return _renderContexts.whereType<T>().lastOrNull;\n  }\n\n  //#endregion\n\n  //#region Debugging assistance\n\n  /// Returns whether this [Component] is in debug mode or not.\n  /// When a child is added to the [Component] it gets the same [debugMode] as\n  /// its parent has when it is prepared.\n  ///\n  /// Returns `false` by default. Override it, or set it to true, to use debug\n  /// mode.\n  /// You can use this value to enable debug behaviors for your game and many\n  /// components will\n  /// show extra information on the screen when debug mode is activated.\n  bool debugMode = false;\n\n  /// How many decimal digits to print when displaying coordinates in the\n  /// debug mode. Setting this to null will suppress all coordinates from\n  /// the output.\n  int? debugCoordinatesPrecision = 0;\n\n  /// A key that can be used to identify this component in the tree.\n  ///\n  /// It can be used to retrieve this component from anywhere in the tree.\n  final ComponentKey? key;\n\n  /// The color that the debug output should be rendered with.\n  Color debugColor = const Color(0xFFFF00FF);\n\n  final ValueCache<Paint> _debugPaintCache = ValueCache<Paint>();\n  final ValueCache<TextPaint> _debugTextPaintCache = ValueCache<TextPaint>();\n\n  /// The [debugColor] represented as a [Paint] object.\n  Paint get debugPaint {\n    if (!_debugPaintCache.isCacheValid([debugColor])) {\n      final paint = Paint()\n        ..color = debugColor\n        ..strokeWidth =\n            0 // hairline-width\n        ..style = PaintingStyle.stroke;\n      _debugPaintCache.updateCache(paint, [debugColor]);\n    }\n    return _debugPaintCache.value!;\n  }\n\n  /// Returns a [TextPaint] object with the [debugColor] set as color for the\n  /// text.\n  TextPaint get debugTextPaint {\n    final viewfinder = CameraComponent.currentCamera?.viewfinder;\n    final viewport = CameraComponent.currentCamera?.viewport;\n    final zoom = viewfinder?.zoom ?? 1.0;\n\n    final viewportScale = math.max(\n      viewport?.transform.scale.x ?? 1,\n      viewport?.transform.scale.y ?? 1,\n    );\n\n    if (!_debugTextPaintCache.isCacheValid([debugColor])) {\n      final textPaint = TextPaint(\n        style: TextStyle(\n          color: debugColor,\n          fontSize: 12 / zoom / viewportScale,\n        ),\n      );\n      _debugTextPaintCache.updateCache(textPaint, [debugColor]);\n    }\n    return _debugTextPaintCache.value!;\n  }\n\n  void renderDebugMode(Canvas canvas) {}\n\n  //#endregion\n}\n\nenum ChildrenChangeType { added, removed }\n"
  },
  {
    "path": "packages/flame/lib/src/components/core/component_key.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:meta/meta.dart';\n\nvar _index = 0;\n\n/// A key that can be used to identify a component and later\n/// retrieve it from its [FlameGame] ancestor.\n@immutable\nclass ComponentKey {\n  /// Creates a key that is equal to keys with the same name.\n  ComponentKey.named(String name) : _internalHash = name.hashCode;\n\n  /// Creates a key that is unique, each instance will only\n  /// be equal to itself.\n  ComponentKey.unique() : _internalHash = _index++;\n\n  final int _internalHash;\n\n  @override\n  int get hashCode => _internalHash;\n\n  @override\n  bool operator ==(Object other) =>\n      other is ComponentKey && other._internalHash == _internalHash;\n\n  @override\n  String toString() => 'ComponentKey($_internalHash)';\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/core/component_render_context.dart",
    "content": "abstract class ComponentRenderContext {}\n"
  },
  {
    "path": "packages/flame/lib/src/components/core/component_tree_root.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/src/components/core/recycled_queue.dart';\nimport 'package:meta/meta.dart';\n\n/// **ComponentTreeRoot** is a component that can be used as a root node of a\n/// component tree.\n///\n/// This class is just a regular [Component], with some additional\n/// functionality, namely: it contains global lifecycle events for the component\n/// tree.\nclass ComponentTreeRoot extends Component {\n  ComponentTreeRoot({\n    super.children,\n    super.key,\n  }) : queue = RecycledQueue(LifecycleEvent.new),\n       _blocked = <int>{};\n\n  @internal\n  final RecycledQueue<LifecycleEvent> queue;\n  final Set<int> _blocked;\n  late final Map<ComponentKey, Component> _index = {};\n  Completer<void>? _lifecycleEventsCompleter;\n\n  @internal\n  void enqueueAdd(Component child, Component parent) {\n    queue.addLast()\n      ..kind = LifecycleEventKind.add\n      ..child = child\n      ..parent = parent;\n  }\n\n  @internal\n  void dequeueAdd(Component child, Component parent) {\n    for (final event in queue) {\n      if (event.kind == LifecycleEventKind.add &&\n          event.child == child &&\n          event.parent == parent) {\n        event.kind = LifecycleEventKind.unknown;\n        return;\n      }\n    }\n    throw AssertionError(\n      'Cannot find a lifecycle event Add(child=$child, parent=$parent)',\n    );\n  }\n\n  @internal\n  void enqueueRemove(Component child, Component parent) {\n    queue.addLast()\n      ..kind = LifecycleEventKind.remove\n      ..child = child\n      ..parent = parent;\n  }\n\n  @internal\n  void dequeueRemove(Component child) {\n    for (final event in queue) {\n      if (event.kind == LifecycleEventKind.remove && event.child == child) {\n        event.kind = LifecycleEventKind.unknown;\n      }\n    }\n  }\n\n  /// Finds all children in [candidates] that have a pending REMOVE event,\n  /// cancels those events, and adds the matched children to [result].\n  ///\n  /// Scans the queue once in O(Q) time. Safe to call during queue iteration.\n  @internal\n  void cancelQueuedRemoves(\n    List<Component> candidates,\n    Set<Component> result,\n  ) {\n    final candidateSet = candidates.toSet();\n    queue.forEachWhere(\n      (event) =>\n          event.kind == LifecycleEventKind.remove &&\n          candidateSet.contains(event.child),\n      (event) {\n        result.add(event.child!);\n        event.kind = LifecycleEventKind.unknown;\n      },\n    );\n  }\n\n  @internal\n  void enqueueMove(Component child, Component newParent) {\n    queue.addLast()\n      ..kind = LifecycleEventKind.move\n      ..child = child\n      ..parent = newParent;\n  }\n\n  @internal\n  void enqueuePriorityChange(\n    Component parent,\n    Component child,\n  ) {\n    queue.addLast()\n      ..kind = LifecycleEventKind.rebalance\n      ..child = child\n      ..parent = parent;\n  }\n\n  bool get hasLifecycleEvents => queue.isNotEmpty;\n\n  /// A future that will complete once all lifecycle events have been\n  /// processed.\n  ///\n  /// If there are no lifecycle events to be processed ([hasLifecycleEvents]\n  /// is `false`), then this future returns immediately.\n  ///\n  /// This is useful when you modify the component tree\n  /// (by adding, moving or removing a component) and you want to make sure\n  /// you react to the changed state, not the current one.\n  /// Remember, methods like [Component.add] don't act immediately and instead\n  /// enqueue their action. This action also cannot be awaited\n  /// with something like `await world.add(something)` since that future\n  /// completes _before_ the lifecycle events are processed.\n  ///\n  /// Example usage:\n  ///\n  /// ```dart\n  /// player.inventory.addAll(enemy.inventory.children);\n  /// await game.lifecycleEventsProcessed;\n  /// updateUi(player.inventory);\n  /// ```\n  Future<void> get lifecycleEventsProcessed {\n    return !hasLifecycleEvents\n        ? Future.value()\n        : (_lifecycleEventsCompleter ??= Completer<void>()).future;\n  }\n\n  void processLifecycleEvents() {\n    // reorder events to process later grouped by parent\n    final reorderParents = <Component>{};\n    LifecycleEventStatus handleReorderEvent(Component parent) {\n      reorderParents.add(parent);\n      return LifecycleEventStatus.done;\n    }\n\n    assert(_blocked.isEmpty);\n    var repeatLoop = true;\n    while (repeatLoop) {\n      repeatLoop = false;\n      for (final event in queue) {\n        final child = event.child!;\n        final parent = event.parent!;\n        if (_blocked.contains(identityHashCode(child)) ||\n            _blocked.contains(identityHashCode(parent))) {\n          continue;\n        }\n\n        final status = switch (event.kind) {\n          LifecycleEventKind.add => child.handleLifecycleEventAdd(parent),\n          LifecycleEventKind.remove => child.handleLifecycleEventRemove(parent),\n          LifecycleEventKind.move => child.handleLifecycleEventMove(parent),\n          LifecycleEventKind.rebalance => handleReorderEvent(parent),\n          LifecycleEventKind.unknown => LifecycleEventStatus.done,\n        };\n\n        switch (status) {\n          case LifecycleEventStatus.done:\n            queue.removeCurrent();\n            repeatLoop = true;\n          case LifecycleEventStatus.block:\n            _blocked.add(identityHashCode(child));\n            _blocked.add(identityHashCode(parent));\n          default:\n        }\n      }\n      _blocked.clear();\n    }\n\n    for (final parent in reorderParents) {\n      parent.rebalanceChildren();\n    }\n\n    if (!hasLifecycleEvents && _lifecycleEventsCompleter != null) {\n      _lifecycleEventsCompleter!.complete();\n      _lifecycleEventsCompleter = null;\n    }\n  }\n\n  @mustCallSuper\n  @override\n  @internal\n  void handleResize(Vector2 size) {\n    super.handleResize(size);\n    for (final event in queue) {\n      if ((event.kind == LifecycleEventKind.add) &&\n          (event.child!.isLoading || event.child!.isLoaded)) {\n        event.child!.onGameResize(size);\n      }\n    }\n  }\n\n  @mustCallSuper\n  @override\n  @internal\n  void handleHotReload() {\n    super.handleHotReload();\n    for (final event in queue) {\n      if ((event.kind == LifecycleEventKind.add) &&\n          (event.child!.isLoading || event.child!.isLoaded)) {\n        event.child!.onHotReload();\n      }\n    }\n  }\n\n  @mustCallSuper\n  @internal\n  void registerKey(ComponentKey key, Component component) {\n    assert(!_index.containsKey(key), 'Key $key is already registered');\n    _index[key] = component;\n  }\n\n  @mustCallSuper\n  @internal\n  void unregisterKey(ComponentKey key) {\n    _index.remove(key);\n  }\n\n  T? findByKey<T extends Component>(ComponentKey key) {\n    final component = _index[key];\n    return component as T?;\n  }\n\n  T? findByKeyName<T extends Component>(String name) {\n    return findByKey(ComponentKey.named(name));\n  }\n}\n\n/// The status of processing a Lifecycle event.\nenum LifecycleEventStatus {\n  /// The event cannot be processed, move over to the next one.\n  skip,\n\n  /// Same as [skip], but also prevent processing of any other events for the\n  /// same child or parent.\n  block,\n\n  /// The event was fully processed and can now be removed from the queue.\n  done,\n}\n\n@internal\nenum LifecycleEventKind {\n  unknown,\n  add,\n  remove,\n  move,\n  rebalance,\n}\n\n@visibleForTesting\nclass LifecycleEvent implements Disposable {\n  LifecycleEventKind kind = LifecycleEventKind.unknown;\n  Component? child;\n  Component? parent;\n\n  @override\n  void dispose() {\n    kind = LifecycleEventKind.unknown;\n    child = null;\n    parent = null;\n  }\n\n  @override\n  String toString() {\n    return 'LifecycleEvent.${kind.name}(child: $child, parent: $parent)';\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/core/recycled_queue.dart",
    "content": "import 'dart:math';\n\n/// [RecycledQueue] is a simple FIFO queue where the elements are recycled.\n///\n/// The elements [T] in this queue are created and owned by the queue and will\n/// not get destroyed when removed from the queue. Instead, these objects will\n/// be `dispose`-d and then released into the pool of unused elements. When new\n/// objects are added to the queue, the previously disposed elements will be\n/// reused.\n///\n/// The API of this class is slightly different from the traditional Queue:\n/// - [addLast] mints a new element and appends it to the end of the queue,\n///   then returns that element for the user to fill.\n/// - [removeFirst] deletes and disposes of the first element without returning\n///   it (use [first] to retrieve the first element beforehand).\n///\n/// In addition, the queue can be iterated over, and modified during that\n/// iteration via the methods [removeCurrent] and [addLast]. However, only one\n/// iterator is allowed at a time.\n///\n/// Internally, the queue is backed by a circular list.\nclass RecycledQueue<T extends Disposable> extends Iterable<T>\n    implements Iterator<T> {\n  RecycledQueue(this.factory, {int initialCapacity = 8})\n    : _elements = List.generate(initialCapacity, (i) => factory()),\n      _startIndex = -1,\n      _endIndex = -1,\n      _currentIndex = -1;\n\n  /// Function for creating new elements in the queue.\n  final T Function() factory;\n\n  /// Index of the first element in the queue, or -1 if the queue is empty.\n  int _startIndex;\n\n  /// Index of the last element in the queue, or -1 if the queue is empty. This\n  /// index is inclusive, and can be greater, equal, or less than [_startIndex].\n  int _endIndex;\n\n  /// Index of the [current] element while iterating, or -1 if not iterating.\n  /// Also, the value -2 indicates the start of a new iteration.\n  int _currentIndex;\n\n  /// The backing container of elements [T].\n  ///\n  /// Some of the items in this list are considered \"active\" while others are\n  /// \"disposed\". The [_startIndex] and [_endIndex] describe the range of items\n  /// in the list which are \"active\".\n  ///\n  /// Two data layouts are possible: the normal one, when [_endIndex] is larger\n  /// than or equal to the [_startIndex]:\n  /// ```\n  ///   [----S############E--]\n  /// ```\n  /// and the wrap-around layout ([_endIndex] < [_startIndex]), which occurs\n  /// when the active elements reach the end of the allocated list and start\n  /// reusing items in the beginning:\n  /// ```\n  ///   [##E------S##########]\n  /// ```\n  final List<T> _elements;\n\n  /// The list of indices of elements that ought to be removed: this list is\n  /// populated when elements are removed during the iteration, and then the\n  /// elements are physically removed at the end of the iteration.\n  List<int> _indicesToRemove = [];\n\n  @override\n  bool get isEmpty => _startIndex < 0;\n\n  @override\n  bool get isNotEmpty => _startIndex >= 0;\n\n  @override\n  int get length {\n    return isEmpty\n        ? 0\n        : _endIndex >= _startIndex\n        ? _endIndex - _startIndex + 1\n        : _elements.length - _startIndex + _endIndex + 1;\n  }\n\n  @override\n  T get first {\n    assert(isNotEmpty, 'Cannot retrieve elements from an empty queue');\n    return _elements[_startIndex];\n  }\n\n  @override\n  T get last {\n    assert(isNotEmpty, 'Cannot retrieve elements from an empty queue');\n    return _elements[_endIndex];\n  }\n\n  /// Adds a new element to the end of the queue, and returns the object added.\n  ///\n  /// This method can be called even while iterating over the queue.\n  T addLast() {\n    // \"empty\" layout: [---------------]\n    if (isEmpty) {\n      _startIndex = 0;\n      _endIndex = 0;\n      if (_elements.isEmpty) {\n        _elements.add(factory());\n      }\n    }\n    // \"normal\" layout: [---S######E----]\n    else if (_endIndex >= _startIndex) {\n      _endIndex += 1;\n      if (_endIndex == _elements.length) {\n        if (_startIndex == 0) {\n          final newElement = factory();\n          _elements.add(newElement);\n        } else {\n          _endIndex = 0;\n        }\n      }\n    }\n    // \"wrap-around\": [#######ES######]\n    else if (_endIndex == _startIndex - 1) {\n      final numItemsToAdd = min(_elements.length, 32);\n      final newEntries = List<T>.generate(numItemsToAdd, (i) => factory());\n      _elements.insertAll(_startIndex, newEntries);\n      _startIndex += numItemsToAdd;\n      if (_currentIndex > _endIndex) {\n        _currentIndex += numItemsToAdd;\n      }\n      for (var i = 0; i < _indicesToRemove.length; i++) {\n        if (_indicesToRemove[i] > _endIndex) {\n          _indicesToRemove[i] += numItemsToAdd;\n        }\n      }\n      _endIndex += 1;\n      assert(_endIndex < _startIndex);\n    }\n    // \"holey\" layout: [##E-----S######]\n    else {\n      _endIndex += 1;\n      assert(_endIndex < _startIndex);\n    }\n    return _elements[_endIndex];\n  }\n\n  /// Removes and disposes of the first element in the queue. The queue must\n  /// not be empty.\n  ///\n  /// The removed element is not returned (because it is disposed). Use [first]\n  /// in order to peek at the first element before removing it.\n  ///\n  /// Calling this method while iterating will stop iteration.\n  void removeFirst() {\n    assert(isNotEmpty, 'Cannot remove elements from an empty queue');\n    _elements[_startIndex].dispose();\n    if (_startIndex == _endIndex) {\n      _startIndex = -1;\n      _endIndex = -1;\n    } else {\n      _startIndex += 1;\n      if (_startIndex == _elements.length) {\n        _startIndex = 0;\n      }\n    }\n    _currentIndex = -1;\n  }\n\n  /// Removes and disposes the [current] element, while iterating over the\n  /// queue. It is an error to call this method while not iterating, or to\n  /// access the [current] element after it was removed.\n  void removeCurrent() {\n    assert(\n      _currentIndex >= 0,\n      'Cannot remove current element if not iterating',\n    );\n    _elements[_currentIndex].dispose();\n    if (_startIndex == _endIndex) {\n      assert(_currentIndex == _startIndex);\n      _startIndex = -1;\n      _endIndex = -1;\n      _currentIndex = -1;\n    } else if (_currentIndex == _startIndex) {\n      _startIndex += 1;\n      if (_startIndex == _elements.length) {\n        _startIndex = 0;\n      }\n    } else {\n      _indicesToRemove.add(_currentIndex);\n    }\n  }\n\n  /// Calls [action] for each element matching [test] by directly traversing\n  /// the internal storage. Unlike iteration, this can be safely called while\n  /// another iteration is in progress.\n  void forEachWhere(bool Function(T) test, void Function(T) action) {\n    if (isEmpty) {\n      return;\n    }\n    var i = _startIndex;\n    while (true) {\n      if (!_indicesToRemove.contains(i) && test(_elements[i])) {\n        action(_elements[i]);\n      }\n      if (i == _endIndex) {\n        break;\n      }\n      i += 1;\n      if (i == _elements.length) {\n        i = 0;\n      }\n    }\n  }\n\n  @override\n  Iterator<T> get iterator {\n    _garbageCollect();\n    _currentIndex = -2;\n    return this;\n  }\n\n  @override\n  T get current {\n    assert(\n      _currentIndex >= 0,\n      'The [current] getter is only accessible while iterating',\n    );\n    return _elements[_currentIndex];\n  }\n\n  @override\n  bool moveNext() {\n    if (isEmpty || _currentIndex == -1) {\n      _currentIndex = -1;\n      return false;\n    }\n    if (_currentIndex < 0) {\n      _currentIndex = _startIndex;\n    } else if (_currentIndex == _endIndex) {\n      _currentIndex = -1;\n      _garbageCollect();\n      return false;\n    } else {\n      _currentIndex += 1;\n      if (_currentIndex == _elements.length) {\n        _currentIndex = 0;\n      }\n    }\n    return true;\n  }\n\n  void _garbageCollect() {\n    if (_indicesToRemove.isEmpty) {\n      return;\n    }\n    final it = _indicesToRemove.iterator;\n    var nextIndexToRemove = (it..moveNext()).current;\n    var lastValidIndex = -1;\n    var i = _startIndex;\n    var j = _startIndex;\n    int advanceIndex(int i) {\n      return (i == _endIndex)\n          ? -1\n          : (i == _elements.length - 1)\n          ? 0\n          : i + 1;\n    }\n\n    while (i != -1) {\n      if (i == nextIndexToRemove) {\n        if (it.moveNext()) {\n          nextIndexToRemove = it.current;\n        } else {\n          nextIndexToRemove = -1;\n        }\n        i = advanceIndex(i);\n      } else {\n        if (i != j) {\n          final t = _elements[i];\n          _elements[i] = _elements[j];\n          _elements[j] = t;\n        }\n        lastValidIndex = j;\n        i = advanceIndex(i);\n        j = advanceIndex(j);\n      }\n    }\n    assert(nextIndexToRemove == -1);\n    assert(lastValidIndex != -1);\n    _endIndex = lastValidIndex;\n    _indicesToRemove.clear();\n  }\n\n  /// [toString] can be called while iterating, though it may show up disposed\n  /// elements if the main iteration is also removing elements from the queue.\n  @override\n  String toString() {\n    final savedIndicesToRemove = _indicesToRemove;\n    final savedCurrentIndex = _currentIndex;\n    _currentIndex = -1;\n    _indicesToRemove = const <int>[];\n    final out = 'RecycledQueue${super.toString()}';\n    _currentIndex = savedCurrentIndex;\n    _indicesToRemove = savedIndicesToRemove;\n    return out;\n  }\n}\n\n/// The interface for the elements allowed in the [RecycledQueue].\nabstract class Disposable {\n  void dispose();\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/custom_painter_component.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flutter/widgets.dart';\n\n/// A [PositionComponent] that renders a [CustomPainter] at the designated\n/// position, scaled to have the designated size and rotated to the specified\n/// angle.\n///\n/// This component makes it possible to provide a Flutter [CustomPainter] to\n/// render on the canvas.\n///\n/// Note that given the active rendering nature of a game, `shouldRepaint` is\n/// ignored by this component.\nclass CustomPainterComponent extends PositionComponent {\n  /// The [CustomPainter] used to render this component\n  CustomPainter? painter;\n\n  CustomPainterComponent({\n    this.painter,\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n  });\n\n  @override\n  @mustCallSuper\n  void render(Canvas canvas) {\n    painter?.paint(canvas, size.toSize());\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/debug/child_counter_component.dart",
    "content": "import 'dart:async';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/text.dart';\n\n/// A debug component that shows the number of children of a given type from\n/// a target component.\n///\n/// Add it to the game to start seeing the count.\nclass ChildCounterComponent<T extends Component> extends TextComponent {\n  ChildCounterComponent({\n    required this.target,\n    super.position,\n  });\n\n  final Component target;\n  late String _label;\n\n  @override\n  FutureOr<void> onLoad() async {\n    await super.onLoad();\n    _label = T.toString();\n\n    textRenderer = TextPaint(\n      style: const TextStyle(\n        color: Color(0xFFFFFFFF),\n        fontSize: 12,\n      ),\n    );\n\n    add(\n      TimerComponent(\n        period: 1,\n        repeat: true,\n        onTick: () {\n          text = '$_label: ${target.children.query<T>().length}';\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/debug/time_track_component.dart",
    "content": "import 'dart:async';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/text.dart';\n\n/// A component that offers a simple way to track time spent in different parts\n/// of your game.\n///\n/// This component is meant to be used for debugging purposes only and should\n/// not be added in production builds.\n///\n/// To start tracking time, call [TimeTrackComponent.start] with an identifier\n/// and [TimeTrackComponent.end] with that same identifier to finish tracking.\n///\n/// To see the recorded times, simply add this component to your game.\nclass TimeTrackComponent extends TextComponent {\n  TimeTrackComponent({super.position});\n\n  static final Map<String, int> _startTimes = {};\n  static final Map<String, int> _endTimes = {};\n\n  static void clear() {\n    _startTimes.clear();\n    _endTimes.clear();\n  }\n\n  static void start(String name) {\n    _startTimes[name] = DateTime.now().microsecondsSinceEpoch;\n  }\n\n  static void end(String name) {\n    _endTimes[name] = DateTime.now().microsecondsSinceEpoch;\n  }\n\n  @override\n  FutureOr<void> onLoad() async {\n    await super.onLoad();\n\n    textRenderer = TextPaint(\n      style: const TextStyle(\n        color: Color(0xFFFFFFFF),\n        fontSize: 12,\n      ),\n    );\n\n    add(\n      TimerComponent(\n        period: 1,\n        repeat: true,\n        onTick: () {\n          final sb = StringBuffer();\n          for (final entry in _startTimes.entries) {\n            final name = entry.key;\n            final startTime = entry.value;\n            final endTime = _endTimes[name];\n            final duration = endTime == null ? 0 : endTime - startTime;\n            sb.writeln('$name: $duration');\n          }\n          text = sb.toString();\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/fps_component.dart",
    "content": "import 'dart:collection';\n\nimport 'package:flame/components.dart';\n\n/// The [FpsComponent] is a non-visual component which you can get the current\n/// fps of the game with by calling [fps], once the component has been added to\n/// the component tree.\nclass FpsComponent extends Component {\n  FpsComponent({\n    this.windowSize = 60,\n    super.key,\n  });\n\n  /// The sliding window size, i.e. the number of game ticks over which the fps\n  /// measure will be averaged.\n  final int windowSize;\n\n  /// The queue of the recent game tick durations.\n  /// The length of this queue will not exceed [windowSize].\n  final Queue<double> window = Queue();\n\n  /// The sum of all values in the [window] queue.\n  double _sum = 0;\n\n  @override\n  void update(double dt) {\n    window.addLast(dt);\n    _sum += dt;\n    if (window.length > windowSize) {\n      _sum -= window.removeFirst();\n    }\n  }\n\n  /// Get the current average FPS over the last [windowSize] frames.\n  double get fps {\n    return window.isEmpty ? 0 : window.length / _sum;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/fps_text_component.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/text.dart';\n\n/// The [FpsTextComponent] is a [TextComponent] that writes out the current FPS.\n/// It has a [FpsComponent] as a child which does the actual calculations.\nclass FpsTextComponent<T extends TextRenderer> extends TextComponent {\n  FpsTextComponent({\n    int windowSize = 60,\n    this.decimalPlaces = 0,\n    T? super.textRenderer,\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    int? priority,\n  }) : fpsComponent = FpsComponent(windowSize: windowSize),\n       super(\n         priority: priority ?? double.maxFinite.toInt(),\n       ) {\n    add(fpsComponent);\n  }\n\n  final int decimalPlaces;\n  final FpsComponent fpsComponent;\n\n  @override\n  void update(double dt) {\n    text = '${fpsComponent.fps.toStringAsFixed(decimalPlaces)} FPS';\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/icon_component.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flutter/widgets.dart' show IconData;\nimport 'package:meta/meta.dart';\n\n/// A [PositionComponent] that renders a Flutter [IconData] as a Flame\n/// component.\n///\n/// The icon is rasterized to a [Image] once during [onLoad] using a\n/// [ParagraphBuilder], and then rendered each frame via\n/// [Canvas.drawImageRect] with the component's [Paint]. This enables all\n/// paint-based effects (ColorFilter, opacity, tint, glow, blend modes).\n///\n/// The icon is rasterized in white so that it can be tinted to any color\n/// using [HasPaint.tint], [HasPaint.setColor], or a [ColorFilter].\n///\n/// Example usage:\n/// ```dart\n/// import 'package:flutter/material.dart';\n///\n/// final icon = IconComponent(\n///   icon: Icons.star,\n///   iconSize: 64,\n///   position: Vector2(100, 100),\n/// )..tint(const Color(0xFFFFD700)); // Gold tint\n/// ```\nclass IconComponent extends PositionComponent with HasPaint {\n  /// The icon to render.\n  IconData? _icon;\n\n  /// The size at which to rasterize the icon. This controls the resolution\n  /// of the rasterized image, independent of the component's display [size].\n  double _iconSize;\n\n  /// The rasterized icon image (rendered in white for tinting).\n  @visibleForTesting\n  Image? image;\n\n  /// Whether the icon needs to be re-rasterized on the next [update].\n  bool _needsRasterize = false;\n\n  /// Cached source rect (image dimensions), updated when the image changes.\n  Rect _srcRect = Rect.zero;\n\n  /// Cached destination rect (component size), updated via a size listener.\n  Rect _dstRect = Rect.zero;\n\n  /// Creates an [IconComponent] that renders [icon] as a Flame component.\n  ///\n  /// - [icon]: The [IconData] to render (e.g., `Icons.star`).\n  /// - [iconSize]: The resolution at which to rasterize the icon (default 64).\n  /// - [paint]: Optional paint for rendering effects.\n  /// - [size]: The display size of the component. Defaults to\n  ///   `Vector2.all(iconSize)` if not specified.\n  IconComponent({\n    IconData? icon,\n    double iconSize = 64,\n    Paint? paint,\n    super.position,\n    Vector2? size,\n    super.scale,\n    super.angle,\n    super.nativeAngle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.key,\n  }) : _icon = icon,\n       _iconSize = iconSize,\n       super(size: size ?? Vector2.all(iconSize)) {\n    if (paint != null) {\n      this.paint = paint;\n    }\n    _dstRect = this.size.toRect();\n    this.size.addListener(_updateDstRect);\n  }\n\n  /// The icon to render.\n  IconData? get icon => _icon;\n\n  set icon(IconData? value) {\n    if (_icon != value) {\n      _icon = value;\n      _needsRasterize = true;\n    }\n  }\n\n  /// The rasterization resolution of the icon.\n  double get iconSize => _iconSize;\n\n  set iconSize(double value) {\n    if (_iconSize != value) {\n      _iconSize = value;\n      _needsRasterize = true;\n    }\n  }\n\n  @override\n  @mustCallSuper\n  Future<void> onLoad() async {\n    await super.onLoad();\n    if (_icon != null) {\n      image = await _rasterizeIcon();\n      _updateSrcRect();\n    }\n  }\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    assert(\n      _icon != null,\n      'You have to set the icon in either the constructor or in onLoad',\n    );\n    assert(\n      image != null,\n      'The icon image must be rasterized before mounting',\n    );\n  }\n\n  @override\n  @mustCallSuper\n  void update(double dt) {\n    if (_needsRasterize) {\n      _needsRasterize = false;\n      _rerasterize();\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    final cachedImage = image;\n    if (cachedImage == null) {\n      return;\n    }\n    canvas.drawImageRect(\n      cachedImage,\n      _srcRect,\n      _dstRect,\n      paint,\n    );\n  }\n\n  @override\n  @mustCallSuper\n  void onRemove() {\n    super.onRemove();\n    size.removeListener(_updateDstRect);\n    image?.dispose();\n    image = null;\n  }\n\n  /// Rasterizes the current [icon] to a white [Image].\n  Future<Image> _rasterizeIcon() {\n    final iconData = _icon!;\n    final fontFamily =\n        iconData.fontFamily != null && iconData.fontPackage != null\n        ? 'packages/${iconData.fontPackage}/${iconData.fontFamily}'\n        : iconData.fontFamily;\n    final paragraphBuilder =\n        ParagraphBuilder(\n            ParagraphStyle(\n              fontSize: _iconSize,\n              fontFamily: fontFamily,\n            ),\n          )\n          ..pushStyle(\n            TextStyle(\n              color: const Color(0xFFFFFFFF),\n              fontSize: _iconSize,\n              fontFamily: fontFamily,\n              fontFamilyFallback: iconData.fontFamilyFallback,\n            ),\n          )\n          ..addText(String.fromCharCode(iconData.codePoint))\n          ..pop();\n\n    final paragraph = paragraphBuilder.build()\n      ..layout(ParagraphConstraints(width: _iconSize));\n\n    final pictureWidth = paragraph.maxIntrinsicWidth.ceil();\n    final pictureHeight = paragraph.height.ceil();\n\n    final recorder = PictureRecorder();\n    final bounds = Rect.fromLTWH(\n      0,\n      0,\n      pictureWidth.toDouble(),\n      pictureHeight.toDouble(),\n    );\n    Canvas(recorder, bounds).drawParagraph(paragraph, Offset.zero);\n\n    return recorder.endRecording().toImageSafe(pictureWidth, pictureHeight);\n  }\n\n  /// Disposes the old image (with a delay) and creates a new one.\n  void _rerasterize() {\n    final oldImage = image;\n    if (oldImage != null) {\n      // Delay disposal to avoid using disposed images in the rendering\n      // pipeline. See issue #1618 for details.\n      Future.delayed(const Duration(milliseconds: 100), () {\n        if (isMounted) {\n          oldImage.dispose();\n        }\n      });\n    }\n    if (_icon != null) {\n      _rasterizeIcon().then((newImage) {\n        image = newImage;\n        _updateSrcRect();\n      });\n    }\n  }\n\n  void _updateSrcRect() {\n    final img = image;\n    if (img != null) {\n      _srcRect = Rect.fromLTWH(\n        0,\n        0,\n        img.width.toDouble(),\n        img.height.toDouble(),\n      );\n    }\n  }\n\n  void _updateDstRect() {\n    _dstRect = size.toRect();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/input/advanced_button_component.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/layout.dart';\nimport 'package:flutter/foundation.dart';\n\n/// The [AdvancedButtonComponent] has different skins for\n/// different button states.\n/// The [defaultSkin] must be added to the constructor or\n/// if you are inheriting - defined in the onLod method.\n///\n/// The label is a [PositionComponent] and is added\n/// to the foreground of the button. The label is automatically aligned to\n/// the center of the button.\n///\n/// Note: You have to set the skins that you want to use ([defaultSkin],\n/// [downSkin], [hoverSkin], [disabledSkin], [defaultLabel]) in [onLoad]\n/// if you are not passing them in through the constructor.\nclass AdvancedButtonComponent extends PositionComponent\n    with HoverCallbacks, TapCallbacks {\n  AdvancedButtonComponent({\n    this.onPressed,\n    this.onReleased,\n    this.onChangeState,\n    PositionComponent? defaultSkin,\n    PositionComponent? downSkin,\n    PositionComponent? hoverSkin,\n    PositionComponent? disabledSkin,\n    PositionComponent? defaultLabel,\n    PositionComponent? disabledLabel,\n    super.size,\n    super.position,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n  }) {\n    this.defaultSkin = defaultSkin;\n    this.downSkin = downSkin;\n    this.hoverSkin = hoverSkin;\n    this.disabledSkin = disabledSkin;\n    this.defaultLabel = defaultLabel;\n    this.disabledLabel = disabledLabel;\n    size.addListener(_updateSizes);\n  }\n\n  /// Callback for what should happen when the button is pressed.\n  void Function()? onPressed;\n\n  /// Callback for what should happen when the button is released.\n  void Function()? onReleased;\n\n  /// Callback when button state changes\n  void Function(ButtonState state)? onChangeState;\n\n  @mustCallSuper\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    add(skinContainer);\n    add(labelAlignContainer);\n  }\n\n  @protected\n  final skinContainer = Component();\n\n  @protected\n  AlignComponent labelAlignContainer = AlignComponent(alignment: Anchor.center);\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    super.onMount();\n    assert(\n      defaultSkin != null,\n      'The defaultSkin has to either be passed '\n      'in as an argument or set in onLoad',\n    );\n    if (_state.isDefault && !contains(defaultSkin!)) {\n      defaultSkin!.parent = skinContainer;\n    }\n  }\n\n  @protected\n  bool isPressed = false;\n\n  @override\n  @mustCallSuper\n  void onTapDown(TapDownEvent event) {\n    if (_isDisabled) {\n      return;\n    }\n    onPressed?.call();\n    isPressed = true;\n    updateState();\n  }\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    if (_isDisabled) {\n      return;\n    }\n    isPressed = false;\n    updateState();\n    onReleased?.call();\n  }\n\n  @override\n  void onHoverEnter() {\n    updateState();\n  }\n\n  @override\n  void onHoverExit() {\n    isPressed = false;\n    updateState();\n  }\n\n  Map<ButtonState, PositionComponent?> skinsMap = {};\n\n  PositionComponent? get defaultSkin => skinsMap[ButtonState.up];\n\n  set defaultSkin(PositionComponent? value) {\n    skinsMap[ButtonState.up] = value;\n    if (size.isZero()) {\n      size = skinsMap[ButtonState.up]?.size ?? Vector2.zero();\n    }\n    invalidateSkins();\n  }\n\n  set downSkin(PositionComponent? value) {\n    skinsMap[ButtonState.down] = value;\n    invalidateSkins();\n  }\n\n  set hoverSkin(PositionComponent? value) {\n    skinsMap[ButtonState.hover] = value;\n    invalidateSkins();\n  }\n\n  set disabledSkin(PositionComponent? value) {\n    skinsMap[ButtonState.disabled] = value;\n    invalidateSkins();\n  }\n\n  Map<ButtonState, PositionComponent?> labelsMap = {};\n\n  PositionComponent? get defaultLabel => labelsMap[ButtonState.up];\n\n  set defaultLabel(PositionComponent? value) {\n    labelsMap[ButtonState.up] = value;\n    updateLabel();\n  }\n\n  set disabledLabel(PositionComponent? value) {\n    labelsMap[ButtonState.disabled] = value;\n    updateLabel();\n  }\n\n  @protected\n  void invalidateSkins() {\n    _updateSizes();\n    _updateSkin();\n  }\n\n  bool _isDisabled = false;\n\n  bool get isDisabled => _isDisabled;\n\n  set isDisabled(bool value) {\n    if (_isDisabled == value) {\n      return;\n    }\n    _isDisabled = value;\n    updateState();\n  }\n\n  void _updateSizes() {\n    for (final skin in skinsMap.values) {\n      skin?.size = size;\n    }\n  }\n\n  @protected\n  void updateState() {\n    if (isDisabled) {\n      setState(ButtonState.disabled);\n      return;\n    }\n    if (isPressed) {\n      setState(ButtonState.down);\n      return;\n    }\n    if (isHovered) {\n      setState(ButtonState.hover);\n      return;\n    }\n    setState(ButtonState.up);\n  }\n\n  ButtonState _state = ButtonState.up;\n\n  @protected\n  void setState(ButtonState value) {\n    if (_state == value) {\n      return;\n    }\n    _state = value;\n    _updateSkin();\n    updateLabel();\n    onChangeState?.call(_state);\n  }\n\n  void _updateSkin() {\n    _removeSkins();\n    setSkin(_state);\n  }\n\n  @protected\n  void setSkin(ButtonState state) {\n    (skinsMap[state] ?? defaultSkin)?.parent = skinContainer;\n  }\n\n  void _removeSkins() {\n    for (final skins in skinsMap.values) {\n      skins?.parent = null;\n    }\n  }\n\n  @protected\n  void updateLabel() {\n    _removeLabels();\n    addLabel(_state);\n  }\n\n  @protected\n  void addLabel(ButtonState state) {\n    labelAlignContainer.child = labelsMap[state] ?? defaultLabel;\n  }\n\n  void _removeLabels() {\n    for (final label in labelsMap.values) {\n      label?.parent = null;\n    }\n  }\n\n  @protected\n  bool hasSkinForState(ButtonState state) {\n    return skinsMap[state] != null;\n  }\n}\n\nenum ButtonState {\n  up,\n  upAndSelected,\n  down,\n  downAndSelected,\n  hover,\n  hoverAndSelected,\n  disabled,\n  disabledAndSelected\n  ;\n\n  const ButtonState();\n\n  bool get isDefault {\n    return this == ButtonState.up;\n  }\n\n  bool get isDefaultSelected {\n    return this == ButtonState.upAndSelected;\n  }\n\n  bool get isNotDefault {\n    return !isDefault;\n  }\n\n  bool get isDown {\n    return this == ButtonState.down;\n  }\n\n  bool get isDownAndSelected {\n    return this == ButtonState.downAndSelected;\n  }\n\n  bool get isHover {\n    return this == ButtonState.hover;\n  }\n\n  bool get isHoverAndSelected {\n    return this == ButtonState.hoverAndSelected;\n  }\n\n  bool get isDisabled {\n    return this == ButtonState.disabled;\n  }\n\n  bool get isDisabledAndSelected {\n    return this == ButtonState.disabledAndSelected;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/input/button_component.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:meta/meta.dart';\n\n/// The [ButtonComponent] bundles two [PositionComponent]s, one that shows while\n/// the button is being pressed, and one that shows otherwise.\n///\n/// Note: You have to set the [button] in [onLoad] if you are not passing it in\n/// through the constructor.\nclass ButtonComponent extends PositionComponent with TapCallbacks {\n  PositionComponent? button;\n  PositionComponent? buttonDown;\n\n  /// Callback for what should happen when the button is pressed.\n  void Function()? onPressed;\n\n  /// Callback for what should happen when the button is released.\n  void Function()? onReleased;\n\n  /// Callback for what should happen when the button is cancelled.\n  void Function()? onCancelled;\n\n  ButtonComponent({\n    this.button,\n    this.buttonDown,\n    this.onPressed,\n    this.onReleased,\n    this.onCancelled,\n    super.position,\n    Vector2? size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n  }) : super(\n         size: size ?? button?.size,\n       );\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    super.onMount();\n    assert(\n      button != null,\n      'The button has to either be passed in as an argument or set in onLoad',\n    );\n    if (size.isZero()) {\n      size = button!.size;\n    }\n    if (!contains(button!)) {\n      add(button!);\n    }\n  }\n\n  @override\n  @mustCallSuper\n  void onTapDown(TapDownEvent event) {\n    if (buttonDown != null) {\n      button!.removeFromParent();\n      buttonDown!.parent = this;\n    }\n    onPressed?.call();\n  }\n\n  @override\n  @mustCallSuper\n  void onTapUp(TapUpEvent event) {\n    if (buttonDown != null) {\n      buttonDown!.removeFromParent();\n      button!.parent = this;\n    }\n    onReleased?.call();\n  }\n\n  @override\n  @mustCallSuper\n  void onTapCancel(TapCancelEvent event) {\n    if (buttonDown != null) {\n      buttonDown!.removeFromParent();\n      button!.parent = this;\n    }\n    onCancelled?.call();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/input/hud_button_component.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/input.dart';\nimport 'package:flutter/rendering.dart' show EdgeInsets;\n\n/// The [HudButtonComponent] bundles two [PositionComponent]s, one that shows\n/// when the button is being pressed, and one that shows otherwise.\n///\n/// Note: You have to set the [button] in [onLoad] if you are not passing it in\n/// through the constructor.\nclass HudButtonComponent extends ButtonComponent\n    with HasGameReference, ComponentViewportMargin {\n  HudButtonComponent({\n    super.button,\n    super.buttonDown,\n    EdgeInsets? margin,\n    Function()? super.onPressed,\n    Function()? super.onReleased,\n    Function()? super.onCancelled,\n    super.position,\n    Vector2? size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n  }) : super(\n         size: size ?? button?.size,\n       ) {\n    this.margin = margin;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/input/hud_margin_component.dart",
    "content": "import 'package:collection/collection.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\nimport 'package:flutter/widgets.dart' show EdgeInsets;\nimport 'package:meta/meta.dart';\n\n/// The [HudMarginComponent] positions itself by a margin to the edge of the\n/// parent instead of by an absolute position on the screen or on the game, so\n/// if the parent is resized the component will move to keep its margin.\n///\n/// Note that the margin is calculated to the [Anchor], not to the edge of the\n/// component.\n///\n/// If you set the position of the component instead of a margin when\n/// initializing the component, the margin to the edge of the screen from that\n/// position will be used.\nclass HudMarginComponent extends PositionComponent {\n  HudMarginComponent({\n    this.margin,\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.key,\n  }) : assert(\n         margin != null || position != null,\n         'Either margin or position must be defined',\n       );\n\n  /// Instead of setting a position of the [HudMarginComponent] a margin\n  /// from the edges of the viewport can be used instead.\n  EdgeInsets? margin;\n\n  late ReadOnlySizeProvider? _sizeProvider;\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    super.onMount();\n    _sizeProvider =\n        ancestors().firstWhereOrNull((c) => c is ReadOnlySizeProvider)\n            as ReadOnlySizeProvider?;\n    assert(\n      _sizeProvider != null,\n      'The parent of a HudMarginComponent needs to provide a size, for example '\n      'by being a PositionComponent.',\n    );\n    final sizeProvider = _sizeProvider!;\n\n    if (margin == null) {\n      final topLeft = anchor.toOtherAnchorPosition(\n        position,\n        Anchor.topLeft,\n        scaledSize,\n      );\n      final bottomRight =\n          sizeProvider.size -\n          anchor.toOtherAnchorPosition(\n            position,\n            Anchor.bottomRight,\n            scaledSize,\n          );\n      margin = EdgeInsets.fromLTRB(\n        topLeft.x,\n        topLeft.y,\n        bottomRight.x,\n        bottomRight.y,\n      );\n    } else {\n      size.addListener(_updateMargins);\n    }\n    if (sizeProvider.size is NotifyingVector2) {\n      (sizeProvider.size as NotifyingVector2).addListener(_updateMargins);\n    }\n    _updateMargins();\n  }\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    if (isMounted && _sizeProvider != null) {\n      _updateMargins();\n    }\n  }\n\n  void _updateMargins() {\n    final margin = this.margin!;\n    final x = margin.left != 0\n        ? margin.left + scaledSize.x / 2\n        : _sizeProvider!.size.x - margin.right - scaledSize.x / 2;\n    final y = margin.top != 0\n        ? margin.top + scaledSize.y / 2\n        : _sizeProvider!.size.y - margin.bottom - scaledSize.y / 2;\n    position.setValues(x, y);\n    position = Anchor.center.toOtherAnchorPosition(\n      position,\n      anchor,\n      scaledSize,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/input/joystick_component.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flutter/widgets.dart';\n\nenum JoystickDirection {\n  up,\n  upLeft,\n  upRight,\n  right,\n  down,\n  downRight,\n  downLeft,\n  left,\n  idle,\n}\n\nclass JoystickComponent extends PositionComponent\n    with HasGameReference, ComponentViewportMargin, DragCallbacks {\n  late final PositionComponent? knob;\n  late final PositionComponent? background;\n\n  /// The percentage `[0.0, 1.0]` the knob is dragged from the center to the\n  /// edge.\n  double intensity = 0.0;\n\n  /// The amount the knob is dragged from the center, scaled to fit inside the\n  /// bounds of the joystick.\n  final Vector2 delta = Vector2.zero();\n\n  /// The total amount the knob is dragged from the center of the joystick.\n  final Vector2 _unscaledDelta = Vector2.zero();\n\n  /// The percentage, presented as a [Vector2], and direction that the knob is\n  /// currently pulled from its base position to a edge of the joystick.\n  Vector2 get relativeDelta => delta / knobRadius;\n\n  /// The radius from the center of the knob to the edge of as far as the knob\n  /// can be dragged.\n  late double knobRadius;\n\n  /// The position where the knob rests.\n  late Vector2 _baseKnobPosition;\n\n  JoystickComponent({\n    this.knob,\n    this.background,\n    super.position,\n    EdgeInsets? margin,\n    double? size,\n    double? knobRadius,\n    Anchor super.anchor = Anchor.center,\n    super.children,\n    super.priority,\n    super.key,\n  }) : assert(\n         size != null || background != null,\n         'Either size or background must be defined',\n       ),\n       assert(\n         (knob?.position.isZero() ?? true) &&\n             (background?.position.isZero() ?? true),\n         'Positions should not be set for the knob or the background',\n       ),\n       super(\n         size: background?.size ?? Vector2.all(size ?? 0),\n       ) {\n    this.margin = margin;\n    this.knobRadius = knobRadius ?? this.size.x / 2;\n  }\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    super.onMount();\n    assert(\n      knob != null,\n      'The knob has to either be passed in as an argument or set in onLoad',\n    );\n\n    knob!.anchor = Anchor.center;\n    knob!.position = size / 2;\n    _baseKnobPosition = knob!.position.clone();\n\n    if (background != null) {\n      add(background!);\n    }\n    add(knob!);\n  }\n\n  @override\n  void update(double dt) {\n    final knobRadius2 = knobRadius * knobRadius;\n    delta.setFrom(_unscaledDelta);\n    if (delta.isZero() && _baseKnobPosition != knob!.position) {\n      knob!.position = _baseKnobPosition;\n    } else if (delta.length2 > knobRadius2) {\n      delta.scaleTo(knobRadius);\n    }\n    if (!delta.isZero()) {\n      knob!.position\n        ..setFrom(_baseKnobPosition)\n        ..add(delta);\n    }\n    intensity = delta.length2 / knobRadius2;\n  }\n\n  @override\n  bool onDragStart(DragStartEvent event) {\n    super.onDragStart(event);\n    return false;\n  }\n\n  @override\n  bool onDragUpdate(DragUpdateEvent event) {\n    _unscaledDelta.add(event.localDelta);\n    return false;\n  }\n\n  @override\n  bool onDragEnd(DragEndEvent event) {\n    super.onDragEnd(event);\n    onDragStop();\n    return false;\n  }\n\n  @override\n  bool onDragCancel(DragCancelEvent event) {\n    super.onDragCancel(event);\n    onDragStop();\n    return false;\n  }\n\n  void onDragStop() {\n    _unscaledDelta.setZero();\n  }\n\n  static const double _eighthOfPi = pi / 8;\n\n  JoystickDirection get direction {\n    if (delta.isZero()) {\n      return JoystickDirection.idle;\n    }\n\n    var knobAngle = delta.screenAngle();\n    // Since screenAngle and angleTo doesn't care about \"direction\" of the angle\n    // we have to use angleToSigned and create an only increasing angle by\n    // removing negative angles from 2*pi.\n    knobAngle = knobAngle < 0 ? 2 * pi + knobAngle : knobAngle;\n    if (knobAngle >= 0 && knobAngle <= _eighthOfPi) {\n      return JoystickDirection.up;\n    } else if (knobAngle > 1 * _eighthOfPi && knobAngle <= 3 * _eighthOfPi) {\n      return JoystickDirection.upRight;\n    } else if (knobAngle > 3 * _eighthOfPi && knobAngle <= 5 * _eighthOfPi) {\n      return JoystickDirection.right;\n    } else if (knobAngle > 5 * _eighthOfPi && knobAngle <= 7 * _eighthOfPi) {\n      return JoystickDirection.downRight;\n    } else if (knobAngle > 7 * _eighthOfPi && knobAngle <= 9 * _eighthOfPi) {\n      return JoystickDirection.down;\n    } else if (knobAngle > 9 * _eighthOfPi && knobAngle <= 11 * _eighthOfPi) {\n      return JoystickDirection.downLeft;\n    } else if (knobAngle > 11 * _eighthOfPi && knobAngle <= 13 * _eighthOfPi) {\n      return JoystickDirection.left;\n    } else if (knobAngle > 13 * _eighthOfPi && knobAngle <= 15 * _eighthOfPi) {\n      return JoystickDirection.upLeft;\n    } else if (knobAngle > 15 * _eighthOfPi) {\n      return JoystickDirection.up;\n    } else {\n      return JoystickDirection.idle;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/input/keyboard_listener_component.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/src/game/mixins/keyboard.dart';\nimport 'package:flutter/services.dart';\n\n/// The signature for a key handle function\ntypedef KeyHandlerCallback = bool Function(Set<LogicalKeyboardKey>);\n\n/// {@template keyboard_listener_component}\n/// A [Component] that receives keyboard input and executes registered methods.\n/// This component is based on [KeyboardHandler], which requires the [FlameGame]\n/// which is used to be mixed with [HasKeyboardHandlerComponents].\n/// {@endtemplate}\nclass KeyboardListenerComponent extends Component with KeyboardHandler {\n  /// {@macro keyboard_listener_component}\n  KeyboardListenerComponent({\n    Map<LogicalKeyboardKey, KeyHandlerCallback> keyUp = const {},\n    Map<LogicalKeyboardKey, KeyHandlerCallback> keyDown = const {},\n    super.key,\n  }) : _keyUp = keyUp,\n       _keyDown = keyDown;\n\n  final Map<LogicalKeyboardKey, KeyHandlerCallback> _keyUp;\n  final Map<LogicalKeyboardKey, KeyHandlerCallback> _keyDown;\n\n  @override\n  bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {\n    final isUp = event is KeyUpEvent;\n\n    final handlers = isUp ? _keyUp : _keyDown;\n    final handler = handlers[event.logicalKey];\n\n    if (handler != null) {\n      return handler(keysPressed);\n    }\n\n    return true;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/input/sprite_button_component.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:meta/meta.dart';\n\nenum ButtonState {\n  up,\n  down,\n}\n\n/// The [SpriteButtonComponent] bundles two [Sprite]s, one that shows while\n/// the button is being pressed, and one that shows otherwise.\n///\n/// Each of the two [Sprite]s correspond to one of the two [ButtonState]s.\n/// If needed, state of the button can be manually changed by modifying\n/// [current].\n///\n/// Note: You have to set the [button] in [onLoad] if you are not passing it in\n/// through the constructor.\nclass SpriteButtonComponent extends SpriteGroupComponent<ButtonState>\n    with TapCallbacks {\n  SpriteButtonComponent({\n    Sprite? button,\n    Sprite? buttonDown,\n    this.onPressed,\n    super.position,\n    Vector2? size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n  }) : _button = button,\n       _buttonDown = buttonDown,\n       super(\n         current: ButtonState.up,\n         size: size ?? button?.originalSize,\n       );\n\n  /// Callback for what should happen when the button is pressed.\n  void Function()? onPressed;\n\n  Sprite? _button;\n  Sprite? _buttonDown;\n\n  Sprite get button => _button!;\n  Sprite get buttonDown => _buttonDown ?? button;\n\n  set button(Sprite value) {\n    _button = value;\n    if (isLoaded) {\n      updateSprite(ButtonState.up, value);\n    }\n  }\n\n  set buttonDown(Sprite value) {\n    _buttonDown = value;\n    if (isLoaded) {\n      updateSprite(ButtonState.down, value);\n    }\n  }\n\n  @override\n  void onMount() {\n    assert(\n      _button != null,\n      'The button sprite has to be set either in onLoad or in the constructor',\n    );\n    if (size.isZero()) {\n      size = _button!.originalSize;\n    }\n    sprites = {\n      ButtonState.up: _button!,\n      ButtonState.down: buttonDown,\n    };\n    super.onMount();\n  }\n\n  @override\n  @mustCallSuper\n  void onTapDown(_) {\n    current = ButtonState.down;\n  }\n\n  @override\n  @mustCallSuper\n  void onTapUp(_) {\n    current = ButtonState.up;\n    onPressed?.call();\n  }\n\n  @override\n  @mustCallSuper\n  void onTapCancel(_) {\n    current = ButtonState.up;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/input/toggle_button_component.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flutter/foundation.dart';\n\n/// The [ToggleButtonComponent] is an [AdvancedButtonComponent] that can switch\n/// between the selected and not selected state, imagine for example a switch\n/// widget or a tab that can be selected.\n///\n/// Note: You have to set the [defaultSkin], [defaultSelectedSkin]\n/// and other skins that you want to use in [onLoad] if you are not passed in\n/// through the constructor.\nclass ToggleButtonComponent extends AdvancedButtonComponent {\n  ToggleButtonComponent({\n    super.onPressed,\n    this.onSelectedChanged,\n    super.onChangeState,\n    super.defaultSkin,\n    super.downSkin,\n    super.hoverSkin,\n    super.disabledSkin,\n    PositionComponent? defaultSelectedSkin,\n    PositionComponent? downAndSelectedSkin,\n    PositionComponent? hoverAndSelectedSkin,\n    PositionComponent? disabledAndSelectedSkin,\n    super.defaultLabel,\n    super.disabledLabel,\n    PositionComponent? defaultSelectedLabel,\n    PositionComponent? disabledAndSelectedLabel,\n    super.size,\n    super.position,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n  }) {\n    this.defaultSelectedSkin = defaultSelectedSkin;\n    this.downAndSelectedSkin = downAndSelectedSkin;\n    this.hoverAndSelectedSkin = hoverAndSelectedSkin;\n    this.disabledAndSelectedSkin = disabledAndSelectedSkin;\n    this.defaultSelectedLabel = defaultSelectedLabel;\n    this.disabledAndSelectedLabel = disabledAndSelectedLabel;\n  }\n\n  /// Callback when button selected changed\n  ValueChanged<bool>? onSelectedChanged;\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    assert(\n      defaultSelectedSkin != null,\n      'The defaultSelectedSkin has to either be passed '\n      'in as an argument or set in onLoad',\n    );\n    super.onMount();\n  }\n\n  PositionComponent? get defaultSelectedSkin =>\n      skinsMap[ButtonState.upAndSelected];\n\n  set defaultSelectedSkin(PositionComponent? value) {\n    skinsMap[ButtonState.upAndSelected] = value;\n    invalidateSkins();\n  }\n\n  set downAndSelectedSkin(PositionComponent? value) {\n    skinsMap[ButtonState.downAndSelected] = value;\n    invalidateSkins();\n  }\n\n  set hoverAndSelectedSkin(PositionComponent? value) {\n    skinsMap[ButtonState.hoverAndSelected] = value;\n    invalidateSkins();\n  }\n\n  set disabledAndSelectedSkin(PositionComponent? value) {\n    skinsMap[ButtonState.disabledAndSelected] = value;\n    invalidateSkins();\n  }\n\n  PositionComponent? get defaultSelectedLabel =>\n      labelsMap[ButtonState.upAndSelected];\n\n  set defaultSelectedLabel(PositionComponent? value) {\n    labelsMap[ButtonState.upAndSelected] = value;\n    updateLabel();\n  }\n\n  set disabledAndSelectedLabel(PositionComponent? value) {\n    labelsMap[ButtonState.disabledAndSelected] = value;\n    updateLabel();\n  }\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    isSelected = !_isSelected;\n    super.onTapUp(event);\n  }\n\n  bool _isSelected = false;\n\n  bool get isSelected => _isSelected;\n\n  set isSelected(bool value) {\n    if (_isSelected == value) {\n      return;\n    }\n    _isSelected = value;\n    updateState();\n    onSelectedChanged?.call(_isSelected);\n  }\n\n  @override\n  @protected\n  void setSkin(ButtonState state) {\n    var skin = skinsMap[state];\n    if (state.isDisabledAndSelected && !hasSkinForState(state)) {\n      skin = skinsMap[ButtonState.disabled];\n    }\n    if (state.isDownAndSelected && !hasSkinForState(state)) {\n      skin = skinsMap[ButtonState.down];\n    }\n    if (state.isHoverAndSelected && !hasSkinForState(state)) {\n      skin = skinsMap[ButtonState.hover];\n    }\n    if (state.isDownAndSelected && !hasSkinForState(state)) {\n      skin = skinsMap[ButtonState.down];\n    }\n    skin = skin ?? (isSelected ? defaultSelectedSkin : defaultSkin);\n    skin?.parent = skinContainer;\n  }\n\n  @override\n  @protected\n  void addLabel(ButtonState state) {\n    labelAlignContainer.child =\n        labelsMap[state] ?? (isSelected ? defaultSelectedLabel : defaultLabel);\n  }\n\n  @mustCallSuper\n  @protected\n  @override\n  void updateState() {\n    if (isDisabled) {\n      setState(\n        _isSelected ? ButtonState.disabledAndSelected : ButtonState.disabled,\n      );\n      return;\n    }\n    if (isPressed) {\n      setState(\n        _isSelected ? ButtonState.downAndSelected : ButtonState.down,\n      );\n      return;\n    }\n    if (isHovered) {\n      setState(\n        _isSelected ? ButtonState.hoverAndSelected : ButtonState.hover,\n      );\n      return;\n    }\n    setState(\n      _isSelected ? ButtonState.upAndSelected : ButtonState.up,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/isometric_tile_map_component.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/src/sprite_sheet.dart';\n\n/// This component renders a tilemap, represented by an int matrix, given a\n/// tileset, in which the integers are the block ids.\n///\n/// It can change the scale of each block by using the optional destTileSize\n/// property.\nclass IsometricTileMapComponent extends PositionComponent {\n  /// This is the tileset that will be used to render this map.\n  SpriteSheet tileset;\n\n  /// The positions of each block will be placed respecting this matrix.\n  List<List<int>> matrix;\n\n  /// Optionally provide a new tile size to render it scaled.\n  Vector2? destTileSize;\n\n  /// This is the vertical height of each block in the tile set.\n  ///\n  /// Note: this must be measured in the destination space.\n  double? tileHeight;\n\n  /// Where the tileset's image is stored.\n  Sprite _renderSprite;\n\n  /// Displacement applied so that the origin of the component\n  /// matches the origin of the AABB.\n  final Vector2 _offset = Vector2.zero();\n\n  IsometricTileMapComponent(\n    this.tileset,\n    this.matrix, {\n    this.destTileSize,\n    this.tileHeight,\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.key,\n  }) : _renderSprite = Sprite(tileset.image) {\n    _recomputeSizeAndOffset();\n  }\n\n  /// This is the size the tiles will be drawn (either original or overwritten).\n  Vector2 get effectiveTileSize => destTileSize ?? tileset.srcSize;\n\n  /// The current scaling factor for the isometric view.\n  double get scalingFactor => effectiveTileSize.y / effectiveTileSize.x;\n\n  /// This is the vertical height of each block; by default it's half the\n  /// tile size.\n  double get effectiveTileHeight => tileHeight ?? (effectiveTileSize.y / 2);\n\n  @override\n  void render(Canvas canvas) {\n    final size = effectiveTileSize;\n    for (var i = 0; i < matrix.length; i++) {\n      for (var j = 0; j < matrix[i].length; j++) {\n        final element = matrix[i][j];\n        if (element != -1) {\n          _renderSprite = tileset.getSpriteById(element);\n          final blockPosition = getBlockRenderPositionInts(j, i);\n          _renderSprite.render(\n            canvas,\n            position: blockPosition,\n            size: size,\n          );\n        }\n      }\n    }\n  }\n\n  @override\n  void update(double dt) {\n    _recomputeSizeAndOffset();\n  }\n\n  /// Get the position in which a block is rendered in, in the isometric space.\n  ///\n  /// This does not include the (x,y) PositionComponent offset!\n  /// This assumes the tile sprite as a rectangular tile.\n  /// This is the opposite of [getBlockRenderedAt].\n  Vector2 getBlockRenderPosition(Block block) {\n    return getBlockRenderPositionInts(block.x, block.y);\n  }\n\n  final Vector2 _blockRenderPositionCache = Vector2.zero();\n  final Vector2 _cartesianPositionCache = Vector2.zero();\n\n  /// Same as getBlockRenderPosition but the arguments are exploded as integers.\n  Vector2 getBlockRenderPositionInts(int i, int j) {\n    final halfTile = _blockRenderPositionCache\n      ..setValues(\n        effectiveTileSize.x / 2,\n        (effectiveTileSize.y / 2) / scalingFactor,\n      )\n      ..multiply(scale);\n    final cartesianPosition = _cartesianPositionCache\n      ..setValues(i.toDouble(), j.toDouble())\n      ..multiply(halfTile);\n    return cartToIso(cartesianPosition)\n      ..add(_offset)\n      ..sub(halfTile);\n  }\n\n  /// Get the position of the center of the surface of the isometric tile in\n  /// the cartesian coordinate space.\n  ///\n  /// This is the opposite of [getBlock].\n  Vector2 getBlockCenterPosition(Block block) {\n    final tile = effectiveTileSize;\n    return getBlockRenderPosition(block)..translate(\n      (tile.x / 2) * scale.x,\n      (tile.y - effectiveTileHeight - tile.y / 4) * scale.y,\n    );\n  }\n\n  /// Converts a coordinate from the isometric space to the cartesian space.\n  Vector2 isoToCart(Vector2 p) {\n    final x = p.y / scalingFactor + p.x / 2;\n    final y = p.y - p.x * scalingFactor / 2;\n    return Vector2(x, y);\n  }\n\n  /// Converts a coordinate from the cartesian space to the isometric space.\n  Vector2 cartToIso(Vector2 p) {\n    final x = p.x - p.y;\n    final y = ((p.x + p.y) * scalingFactor) / 2;\n    return Vector2(x, y);\n  }\n\n  final Vector2 _getBlockCache = Vector2.zero();\n  final Vector2 _getBlockIsoCache = Vector2.zero();\n\n  /// Get which block's surface is at isometric position [p].\n  ///\n  /// This can be used to handle clicks or hovers.\n  /// This is the opposite of [getBlockCenterPosition].\n  Block getBlock(Vector2 p) {\n    final halfTile = _getBlockCache\n      ..setFrom(effectiveTileSize)\n      ..multiply(scale / 2);\n    final multiplier = 1 - halfTile.y / (2 * effectiveTileHeight * scale.x);\n    final iso = _getBlockIsoCache\n      ..setFrom(p)\n      ..sub(_offset)\n      ..sub(position)\n      ..translate(halfTile.x, halfTile.y * multiplier);\n    final cart = isoToCart(iso);\n    final px = (cart.x / halfTile.x - 1).ceil();\n    final py = (cart.y / halfTile.y).ceil();\n    return Block(px, py);\n  }\n\n  final Vector2 _blockPositionCache = Vector2.zero();\n\n  /// Get which block should be rendered on position [p].\n  ///\n  /// This is the opposite of [getBlockRenderPosition].\n  Block getBlockRenderedAt(Vector2 p) {\n    final tile = effectiveTileSize;\n    return getBlock(\n      _blockPositionCache\n        ..setFrom(p)\n        ..translate(\n          (tile.x / 2) * scale.x,\n          (tile.y - effectiveTileHeight - tile.y / 4) * scale.y,\n        ),\n    );\n  }\n\n  /// Sets the block value into the matrix.\n  void setBlockValue(Block pos, int block) {\n    matrix[pos.y][pos.x] = block;\n  }\n\n  /// Gets the block value from the matrix.\n  int blockValue(Block pos) {\n    return matrix[pos.y][pos.x];\n  }\n\n  /// Return whether the matrix contains a block in its bounds.\n  bool containsBlock(Block block) {\n    return block.y >= 0 &&\n        block.y < matrix.length &&\n        block.x >= 0 &&\n        block.x < matrix[block.y].length;\n  }\n\n  void _recomputeSizeAndOffset() {\n    final width = matrix.fold<int>(\n      0,\n      (previousValue, element) => max(previousValue, element.length),\n    );\n    final height = matrix.length;\n\n    size.x = effectiveTileSize.x * width;\n    size.y = effectiveTileSize.y * height / 2 + effectiveTileHeight;\n\n    _offset.x = size.x / 2;\n    _offset.y = effectiveTileHeight;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/component_viewport_margin.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/camera.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/src/game/flame_game.dart';\nimport 'package:flutter/widgets.dart' show EdgeInsets;\nimport 'package:meta/meta.dart';\n\n/// The [ComponentViewportMargin] positions itself by a margin to the edge of\n/// the [Viewport] (or another parent with a `size`) instead of by an absolute\n/// position on the screen or on the game, so if the game is resized the\n/// component will move to keep its margin.\n///\n/// Note that the margin is calculated to the [Anchor], not to the edge of the\n/// component.\n///\n/// If you set the position of the component instead of a margin when\n/// initializing the component, the margin to the edge of the screen from that\n/// position will be used.\n///\n/// Do note that this only works with the old style camera and not the\n/// [CameraComponent].\n// TODO(Lukas): Rename this since it isn't necessarily related to the viewport.\nmixin ComponentViewportMargin<T extends FlameGame>\n    on PositionComponent, HasGameReference<T> {\n  /// Instead of setting a position of the [PositionComponent] that uses\n  /// [ComponentViewportMargin] a margin from the edges of the parent can be\n  /// used instead.\n  EdgeInsets? margin;\n\n  @override\n  @mustCallSuper\n  FutureOr<void> onLoad() async {\n    super.onLoad();\n    assert(parent is ReadOnlySizeProvider, 'The parent must provide a size.');\n    // If margin is not null we will update the position `onGameResize` instead\n    if (margin == null) {\n      final bounds = parent is Viewport\n          ? (parent! as Viewport).virtualSize\n          : (parent! as ReadOnlySizeProvider).size;\n      final topLeft = anchor.toOtherAnchorPosition(\n        position,\n        Anchor.topLeft,\n        scaledSize,\n      );\n      final bottomRight =\n          bounds -\n          anchor.toOtherAnchorPosition(\n            position,\n            Anchor.bottomRight,\n            scaledSize,\n          );\n      margin = EdgeInsets.fromLTRB(\n        topLeft.x,\n        topLeft.y,\n        bottomRight.x,\n        bottomRight.y,\n      );\n    } else {\n      size.addListener(_updateMargins);\n    }\n    _updateMargins();\n  }\n\n  @override\n  @mustCallSuper\n  void onGameResize(Vector2 gameSize) {\n    super.onGameResize(gameSize);\n    if (isMounted) {\n      _updateMargins();\n    }\n  }\n\n  void _updateMargins() {\n    final bounds = parent is Viewport\n        ? (parent! as Viewport).virtualSize\n        : (parent! as ReadOnlySizeProvider).size;\n    final margin = this.margin!;\n    final x = margin.left != 0\n        ? margin.left + scaledSize.x / 2\n        : bounds.x - margin.right - scaledSize.x / 2;\n    final y = margin.top != 0\n        ? margin.top + scaledSize.y / 2\n        : bounds.y - margin.bottom - scaledSize.y / 2;\n    position.setValues(x, y);\n    position = Anchor.center.toOtherAnchorPosition(\n      position,\n      anchor,\n      scaledSize,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/coordinate_transform.dart",
    "content": "import 'package:flame/src/components/core/component.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// Interface to be implemented by components that perform a coordinate change.\n///\n/// Any [Component] that does any coordinate transformation of the canvas during\n/// rendering should consider implementing this interface in order to describe\n/// how the points from the parent's coordinate system relate to the component's\n/// local coordinate system.\n///\n/// This interface assumes that the component performs a \"uniform\" coordinate\n/// transformation, that is, the transform applies to all children of the\n/// component equally. If that is not the case (for example, the component does\n/// different transformations for some of its children), then that component\n/// must implement [Component.componentsAtLocation] method instead.\n///\n/// The two methods of this interface convert between the parent's coordinate\n/// space and the local coordinates. The methods may also return `null`,\n/// indicating that the given cannot be mapped to any local/parent point.\nabstract class CoordinateTransform {\n  Vector2? parentToLocal(Vector2 point);\n\n  Vector2? localToParent(Vector2 point);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/gesture_hitboxes.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:meta/meta.dart';\n\n/// This mixin can be used if you want to use hitboxes to determine whether\n/// a gesture is within the [Component] or not.\nmixin GestureHitboxes on Component {\n  Iterable<ShapeHitbox> get hitboxes => children.query<ShapeHitbox>();\n\n  @override\n  @mustCallSuper\n  Future<void> onLoad() async {\n    super.onLoad();\n    children.register<ShapeHitbox>();\n  }\n\n  @override\n  bool containsPoint(Vector2 point) {\n    return hitboxes.any((hitbox) => hitbox.containsPoint(point));\n  }\n\n  static final _temporaryPoint = Vector2.zero();\n\n  @override\n  bool containsLocalPoint(Vector2 point) {\n    return hitboxes.any(\n      (hitbox) => hitbox.containsLocalPoint(\n        hitbox.parentToLocal(point, output: _temporaryPoint),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/has_ancestor.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flutter/material.dart';\n\n/// A mixin that ensures an ancestor is of the given type [T].\n///\n/// Exposes an [ancestor] field of the given type [T].\nmixin HasAncestor<T extends Component> on Component {\n  /// A reference to an ancestor in the component tree.\n  T get ancestor => _ancestor!;\n  T? _ancestor;\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    if (_ancestor == null) {\n      var c = parent;\n      while (c != null) {\n        if (c is HasAncestor<T>) {\n          _ancestor = c.ancestor;\n          break;\n        } else if (c is T) {\n          _ancestor = c;\n          break;\n        } else {\n          c = c.parent;\n        }\n      }\n    }\n    assert(\n      _ancestor != null,\n      'An ancestor must be of type $T in the component tree',\n    );\n\n    super.onMount();\n  }\n\n  @override\n  @mustCallSuper\n  void onRemove() {\n    super.onRemove();\n    _ancestor = null;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/has_auto_batched_children.dart",
    "content": "import 'dart:math' as math;\nimport 'dart:ui';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame/rendering.dart';\nimport 'package:flame/src/collisions/hitboxes/shape_hitbox.dart';\nimport 'package:flame/src/components/core/component.dart';\nimport 'package:flame/src/components/mixins/has_decorator.dart';\nimport 'package:flame/src/components/mixins/has_paint.dart';\nimport 'package:flame/src/components/mixins/snapshot.dart';\nimport 'package:flame/src/components/position_component.dart';\nimport 'package:flame/src/components/sprite_animation_component.dart';\nimport 'package:flame/src/components/sprite_component.dart';\nimport 'package:flame/src/sprite_batch.dart';\nimport 'package:meta/meta.dart';\n\n/// A mixin that enables children of a [Component] to be automatically batched\n/// into a single [Canvas.drawAtlas] call per shared atlas image per priority\n/// group.\n///\n/// Eligible children must be [SpriteComponent] or [SpriteAnimationComponent]\n/// instances with uniform scale and no extra decorators or snapshot caching.\n/// They may either be leaf nodes or have only [ShapeHitbox] children, which\n/// are ignored for batching. All other children fall back to their individual\n/// [Component.renderTree] calls, and render order (z-order by priority) is\n/// fully preserved.\n///\n/// Usage:\n/// ```dart\n/// class EnemyGroup extends PositionComponent with HasAutoBatchedChildren {}\n/// ```\n///\n/// Toggle batching at runtime:\n/// ```dart\n/// final group = EnemyGroup();\n/// group.batchingEnabled = false; // falls back to individual rendering\n/// ```\nmixin HasAutoBatchedChildren on Component {\n  /// When `false`, every child is rendered individually (no batching).\n  bool batchingEnabled = true;\n\n  // Accumulators keyed by atlas identity hash.\n  final Map<Image, _BatchAccumulator> _accumulators = {};\n\n  // The priority of the child group currently being accumulated.\n  int? _currentPriority;\n\n  /// Renders [child] with batching if eligible, or falls back to individual\n  /// rendering if not.\n  ///\n  /// Batching is performed at priority-group boundaries to preserve render\n  /// order between eligible and non-eligible children. If a child is eligible\n  /// for batching, its render info is extracted and accumulated into a batch\n  /// for its atlas image. If not eligible, any pending batches are flushed\n  /// before rendering the child individually, so that the child renders after\n  /// any already-accumulated sprites (same priority group).\n  @override\n  @protected\n  void renderChild(Canvas canvas, Component child) {\n    if (!batchingEnabled) {\n      super.renderChild(canvas, child);\n      return;\n    }\n\n    // Detect priority-group boundary — flush before crossing it.\n    final childPriority = child.priority;\n    if (_currentPriority != null && childPriority != _currentPriority) {\n      _flushAll(canvas);\n    }\n    _currentPriority = childPriority;\n\n    final batchInfo = _tryGetBatchInfo(child);\n    if (batchInfo != null) {\n      _accumulateChild(child as PositionComponent, batchInfo);\n      if (child.debugMode) {\n        for (final hitbox in child.children.whereType<ShapeHitbox>()) {\n          canvas.save();\n          canvas.transform2D(child.transform);\n          hitbox.renderDebugMode(canvas);\n          canvas.restore();\n        }\n      }\n    } else {\n      // Non-eligible child: flush any pending batch first so that the child\n      // renders after already-accumulated sprites (same priority group).\n      _flushAll(canvas);\n      super.renderChild(canvas, child);\n    }\n  }\n\n  /// Flushes any pending batches after all children are rendered, so they\n  /// render after all non-eligible children (same priority group) as well.\n  ///\n  /// This is necessary to ensure that batches render in the correct order\n  /// relative to non-eligible children, but can be overridden if the flush\n  /// needs to happen at a different time (e.g. before rendering a specific\n  /// child) without breaking batching for the rest of the group.\n  @override\n  @protected\n  void afterChildrenRendered(Canvas canvas) {\n    _flushAll(canvas);\n    _currentPriority = null;\n    super.afterChildrenRendered(canvas);\n  }\n\n  /// Accumulates [child] for batching based on [info].\n  void _accumulateChild(PositionComponent child, _BatchInfo info) {\n    final accumulator = _accumulators[info.image] ??= _BatchAccumulator(\n      info.image,\n    );\n    accumulator.accumulate(\n      info.sourceRect,\n      _toRSTransform(child, info.sourceRect),\n      info.color,\n    );\n  }\n\n  /// Flushes all accumulators to [canvas] and resets them for the next frame.\n  ///\n  /// This is called automatically at priority-group boundaries and after all\n  /// children are rendered, but can also be called manually if needed (e.g. to\n  /// flush before rendering a non-eligible child without breaking batching for\n  /// the rest of the group).\n  void _flushAll(Canvas canvas) {\n    for (final acc in _accumulators.values) {\n      acc.flush(canvas);\n    }\n    // Do NOT clear the map — reuse accumulators with the same images\n    // across frames. Individual batch.clear() is called inside flush().\n  }\n\n  /// Returns the source [Image], [Rect], and tint [Color] for [component] if\n  /// it is eligible for batching, or `null` if it should fall back to\n  /// individual rendering.\n  ///\n  /// Combines type, state, and transform eligibility checks with source\n  /// extraction so the call site never needs to call both separately.\n  static _BatchInfo? _tryGetBatchInfo(Component component) {\n    if (!component.isMounted) {\n      return null;\n    }\n\n    // Decorators other than the built-in Transform2DDecorator are unsupported.\n    if (component is HasDecorator && component.decorator != null) {\n      return null;\n    }\n\n    if (component is! SpriteComponent &&\n        component is! SpriteAnimationComponent) {\n      return null;\n    }\n\n    final positionComponent = component as PositionComponent;\n    final decorator = positionComponent.decorator;\n\n    if (decorator is! Transform2DDecorator || !decorator.isLastDecorator) {\n      return null;\n    }\n\n    // Allow children that produce no visible output in non-debug mode\n    // (e.g. physics hitboxes) — they can be safely skipped during batching.\n    if (positionComponent.children.any((child) => child is! ShapeHitbox)) {\n      return null;\n    }\n\n    // Snapshot caching bypasses normal render — exclude it.\n    if (positionComponent is Snapshot && positionComponent.renderSnapshot) {\n      return null;\n    }\n\n    // Non-uniform scale cannot be expressed as a single RSTransform.\n    if (positionComponent.scale.x != positionComponent.scale.y) {\n      return null;\n    }\n\n    // Complex paint effects can't be expressed per-item; only simple alpha/\n    // color tints (via HasPaint.paint.color) are supported.\n    final hasPaint = component as HasPaint;\n    final paint = hasPaint.paint;\n\n    if (paint.colorFilter != null ||\n        paint.shader != null ||\n        paint.imageFilter != null ||\n        paint.maskFilter != null ||\n        // SpriteBatch's render() always uses BlendMode.srcOver, so any other\n        // blend mode would be ignored and could lead to unexpected results.\n        // We could support batching by blend mode (e.g., using a tuple as the\n        // batch key (Image, BlendMode)), but that would be a more complex\n        // change. For now, it's safer to just exclude non-srcOver blend modes.\n        paint.blendMode != BlendMode.srcOver) {\n      return null;\n    }\n\n    if (hasPaint.paintLayers.length > 1) {\n      return null;\n    }\n\n    // Extract source info — null means the sprite/animation isn't ready yet.\n    final (Image, Rect)? sourceInfo;\n\n    if (component is SpriteComponent) {\n      final sprite = component.sprite;\n\n      if (sprite == null) {\n        return null;\n      }\n\n      sourceInfo = (sprite.image, sprite.src);\n    } else {\n      final ticker = (component as SpriteAnimationComponent).animationTicker;\n\n      if (ticker == null) {\n        return null;\n      }\n\n      final frame = ticker.currentFrame;\n      sourceInfo = (frame.sprite.image, frame.sprite.src);\n    }\n\n    // RSTransform encodes a 2D transform as a single rotation + uniform scale,\n    // meaning scaleX and scaleY must be identical. When rendering a sprite,\n    // the implied scale factors are:\n    //   scaleX = size.x / sourceWidth\n    //   scaleY = size.y / sourceHeight\n    // For these to be equal: size.x / sourceWidth == size.y / sourceHeight\n    //                  i.e.: size.x * sourceHeight == size.y * sourceWidth\n    // We use cross-multiplication (instead of division) to avoid a sourceWidth/sourceHeight\n    // zero-guard, and keep the tolerance (0.5) in absolute pixel-area units\n    // so it stays stable regardless of sprite dimensions.\n    final sourceWidth = sourceInfo.$2.width;\n    final sourceHeight = sourceInfo.$2.height;\n    if (sourceWidth <= 0 ||\n        sourceHeight <= 0 ||\n        (positionComponent.size.x * sourceHeight -\n                    positionComponent.size.y * sourceWidth)\n                .abs() >\n            0.5) {\n      return null;\n    }\n\n    // Convert fully-opaque white to transparent black (SpriteBatch's \"no tint\"\n    // sentinel) — white is the multiplicative identity so it must stay neutral.\n    final rawColor = hasPaint.paint.color;\n    final batchColor = rawColor != const Color(0xFFFFFFFF)\n        ? rawColor\n        : const Color(0x00000000);\n\n    return _BatchInfo(sourceInfo.$1, sourceInfo.$2, batchColor);\n  }\n\n  /// Builds an [RSTransform] for [component] in parent-local coordinate space.\n  ///\n  /// The effective scale is `c.scale.x × (c.size.x / src.width)` — the\n  /// component's own scale combined with the source-to-size stretch factor.\n  /// The anchor is expressed in source-rect coordinates so that\n  /// [PositionComponent.position] maps to where [PositionComponent.anchor]\n  /// lands in parent space.\n  static RSTransform _toRSTransform(PositionComponent component, Rect source) {\n    // Effective (uniform) scale = component scale × size-to-source stretch.\n    final effectiveScale = component.scale.x * component.size.x / source.width;\n    final totalAngle = component.angle + component.nativeAngle;\n    final cosA = math.cos(totalAngle) * effectiveScale;\n    final sinA = math.sin(totalAngle) * effectiveScale;\n\n    // Anchor expressed in source-rect coordinates.\n    final anchorX = component.anchor.x * source.width;\n    final anchorY = component.anchor.y * source.height;\n\n    // position is where the anchor maps to in parent space.\n    return RSTransform(\n      cosA,\n      sinA,\n      component.position.x - cosA * anchorX + sinA * anchorY,\n      component.position.y - sinA * anchorX - cosA * anchorY,\n    );\n  }\n}\n\n/// Internal accumulator that wraps a [SpriteBatch] for a single atlas [Image].\n///\n/// Reused frame-to-frame: cleared and re-populated on each render pass via\n/// [flush].\nclass _BatchAccumulator {\n  _BatchAccumulator(Image atlas) : batch = SpriteBatch(atlas);\n\n  final SpriteBatch batch;\n\n  void accumulate(Rect source, RSTransform transform, Color color) {\n    batch.addTransform(source: source, transform: transform, color: color);\n  }\n\n  /// Flushes all accumulated items to [canvas] and resets for next frame.\n  void flush(Canvas canvas) {\n    if (!batch.isEmpty) {\n      // Always supply an explicit blendMode so that per-item colors never\n      // trigger the bare-String throw inside SpriteBatch.render().\n      batch.render(canvas, blendMode: BlendMode.srcOver);\n      batch.clear();\n    }\n  }\n}\n\n/// Internal struct for batching info extracted from a component.\nclass _BatchInfo {\n  _BatchInfo(this.image, this.sourceRect, this.color);\n\n  final Image image;\n  final Rect sourceRect;\n  final Color color;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/has_decorator.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/components/core/component.dart';\nimport 'package:flame/src/rendering/decorator.dart';\n\n/// [HasDecorator] mixin adds a nullable [decorator] field to a Component. If\n/// this field is set, it will apply the visual effect encapsulated in this\n/// [Decorator] to the component. If the field is not set, then the component\n/// will be rendered normally.\n///\n/// Note that the decorator only affects visual rendering of a component, but\n/// not its perceived size or shape from the point of view of tap events.\n///\n/// See also:\n///  - [Decorator] class for the list of available decorators.\nmixin HasDecorator on Component {\n  Decorator? decorator;\n\n  @override\n  void renderTree(Canvas canvas) {\n    if (decorator == null) {\n      super.renderTree(canvas);\n    } else {\n      decorator!.applyChain(super.renderTree, canvas);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/has_game_ref.dart",
    "content": "import 'package:flame/src/components/core/component.dart';\nimport 'package:flame/src/game/flame_game.dart';\nimport 'package:flame/src/game/mixins/single_game_instance.dart';\n\n/// [HasGameRef] mixin provides property [game] (or [gameRef]), which is the\n/// cached accessor for the top-level game instance.\n///\n/// The type [T] on the mixin is the type of your game class. This type will be\n/// the type of the [game] reference, and the mixin will check at runtime that\n/// the actual type matches the expectation.\n@Deprecated(\n  'Use HasGameReference instead. This mixin will be removed in a future '\n  'version of Flame.',\n)\nmixin HasGameRef<T extends FlameGame> on Component {\n  T? _game;\n\n  /// Reference to the top-level Game instance that owns this component.\n  ///\n  /// This property is accessible in the component's `onLoad` and later. It may\n  /// be accessible earlier too, but only if your game uses the\n  /// [SingleGameInstance] mixin.\n  T get game => _game ??= _findGameAndCheck();\n\n  /// Allows you to set the game instance explicitly. This may be useful in\n  /// tests, or if you're planning to move the component to another game\n  /// instance.\n  set game(T? value) => _game = value;\n\n  /// Equivalent to the [game] property.\n  T get gameRef => game;\n\n  @override\n  FlameGame? findGame() => _game ?? super.findGame();\n\n  T _findGameAndCheck() {\n    final game = findGame();\n    assert(\n      game != null,\n      'Could not find Game instance: the component is detached from the '\n      'component tree',\n    );\n    assert(\n      game! is T,\n      'Found game of type ${game.runtimeType}, while type $T was expected',\n    );\n    return game! as T;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/has_game_reference.dart",
    "content": "import 'package:flame/src/components/core/component.dart';\nimport 'package:flame/src/game/flame_game.dart';\nimport 'package:flame/src/game/mixins/single_game_instance.dart';\n\n/// [HasGameReference] mixin provides property [game], which is the cached\n/// accessor for the top-level game instance.\n///\n/// The type [T] on the mixin is the type of your game class. This type will be\n/// the type of the [game] reference, and the mixin will check at runtime that\n/// the actual type matches the expectation.\nmixin HasGameReference<T extends FlameGame> on Component {\n  T? _game;\n\n  /// Reference to the top-level Game instance that owns this component.\n  ///\n  /// This property is accessible in the component's `onLoad` and later. It may\n  /// be accessible earlier too, but only if your game uses the\n  /// [SingleGameInstance] mixin.\n  T get game => _game ??= _findGameAndCheck();\n\n  /// Allows you to set the game instance explicitly. This may be useful in\n  /// tests, or if you're planning to move the component to another game\n  /// instance.\n  set game(T? value) => _game = value;\n\n  @override\n  FlameGame? findGame() => _game ?? super.findGame();\n\n  T _findGameAndCheck() {\n    final game = findGame();\n    assert(\n      game != null,\n      'Could not find Game instance: the component is detached from the '\n      'component tree',\n    );\n    assert(\n      game! is T,\n      'Found game of type ${game.runtimeType}, while type $T was expected',\n    );\n    return game! as T;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/has_paint.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/src/palette.dart';\nimport 'package:flame/src/rendering/hue_decorator.dart';\nimport 'package:meta/meta.dart';\n\n/// Adds a collection of paints and paint layers to a component\n///\n/// Component will always have a main Paint that can be accessed\n/// by the [paint] attribute and other paints can be manipulated/accessed\n/// using [getPaint], [setPaint] and [deletePaint] by a paintId of generic type\n/// [T], that can be omitted if the component only has one paint.\n/// [paintLayers] paints should be drawn in list order during the render. The\n/// main Paint is the first element.\nmixin HasPaint<T extends Object> on Component\n    implements OpacityProvider, PaintProvider, HueProvider {\n  late final Map<T, Paint> _paints = {};\n\n  @override\n  Paint paint = BasicPalette.white.paint();\n\n  double _hue = 0.0;\n\n  @internal\n  List<Paint>? paintLayersInternal;\n\n  /// Gets a paint from the collection.\n  ///\n  /// Returns the main paint if no [paintId] is provided.\n  Paint getPaint([T? paintId]) {\n    if (paintId == null) {\n      return this.paint;\n    }\n\n    final paint = _paints[paintId];\n\n    if (paint == null) {\n      throw ArgumentError('No Paint found for $paintId');\n    }\n\n    return paint;\n  }\n\n  /// Sets a paint on the collection.\n  void setPaint(T paintId, Paint paint) {\n    _paints[paintId] = paint;\n  }\n\n  /// Removes a paint from the collection.\n  void deletePaint(T paintId) {\n    _paints.remove(paintId);\n  }\n\n  /// List of paints to use (in order) during render.\n  List<Paint> get paintLayers {\n    if (!hasPaintLayers) {\n      return paintLayersInternal = [];\n    }\n    return paintLayersInternal!;\n  }\n\n  set paintLayers(List<Paint> paintLayers) {\n    paintLayersInternal = paintLayers;\n  }\n\n  /// Whether there are any paint layers defined for the component.\n  bool get hasPaintLayers => paintLayersInternal?.isNotEmpty ?? false;\n\n  /// Manipulate the paint to make it fully transparent.\n  void makeTransparent({T? paintId}) {\n    setOpacity(0, paintId: paintId);\n  }\n\n  /// Manipulate the paint to make it fully opaque.\n  void makeOpaque({T? paintId}) {\n    setOpacity(1, paintId: paintId);\n  }\n\n  /// Changes the opacity of the paint.\n  void setOpacity(double opacity, {T? paintId}) {\n    if (opacity < 0 || opacity > 1) {\n      throw ArgumentError('Opacity needs to be between 0 and 1');\n    }\n\n    setColor(\n      getPaint(paintId).color.withValues(alpha: opacity),\n      paintId: paintId,\n    );\n  }\n\n  /// Returns the current opacity.\n  double getOpacity({T? paintId}) {\n    return getPaint(paintId).color.a;\n  }\n\n  /// Changes the opacity of the paint.\n  void setAlpha(int alpha, {T? paintId}) {\n    if (alpha < 0 || alpha > 255) {\n      throw ArgumentError('Alpha needs to be between 0 and 255');\n    }\n\n    setColor(getPaint(paintId).color.withAlpha(alpha), paintId: paintId);\n  }\n\n  /// Returns the current opacity.\n  int getAlpha({T? paintId}) {\n    return getPaint(paintId).color.a ~/ 255;\n  }\n\n  /// Shortcut for changing the color of the paint.\n  void setColor(Color color, {T? paintId}) {\n    getPaint(paintId).color = color;\n  }\n\n  /// Applies a color filter to the paint which will make\n  /// things rendered with the paint looking like it was\n  /// tinted with the given color.\n  void tint(Color color, {T? paintId}) {\n    getPaint(paintId).colorFilter = ColorFilter.mode(color, BlendMode.srcATop);\n  }\n\n  @override\n  double get opacity => paint.color.a;\n\n  @override\n  set opacity(double value) {\n    paint.color = paint.color.withValues(alpha: value);\n    for (final paint in _paints.values) {\n      paint.color = paint.color.withValues(alpha: value);\n    }\n  }\n\n  @override\n  double get hue => _hue;\n\n  @override\n  set hue(double value) {\n    if (_hue == value) {\n      return;\n    }\n    _hue = value;\n    _updateColorFilter();\n  }\n\n  void _updateColorFilter() {\n    final filter = _hue == 0\n        ? null\n        : ColorFilter.matrix(hueRotationMatrix(_hue));\n    paint.colorFilter = filter;\n    for (final paint in _paints.values) {\n      paint.colorFilter = filter;\n    }\n    if (paintLayersInternal != null) {\n      for (final layerPaint in paintLayersInternal!) {\n        layerPaint.colorFilter = filter;\n      }\n    }\n  }\n\n  /// Creates an [OpacityProvider] for given [paintId] and can be used as\n  /// `target` for [OpacityEffect].\n  OpacityProvider opacityProviderOf(T paintId) {\n    return _ProxyOpacityProvider(paintId, this);\n  }\n\n  /// Creates an [OpacityProvider] for given list of [paintIds] and can be\n  /// used as `target` for [OpacityEffect].\n  ///\n  /// When opacities of all the given [paintIds] are not same, this provider\n  /// directly effects opacity of the most opaque paint. Additionally, it\n  /// modifies other paints such that their respective opacity ratio with most\n  /// opaque paint is maintained.\n  ///\n  /// If [paintIds] is null or empty, all the paints are used for creating the\n  /// [OpacityProvider].\n  ///\n  /// Note: Each call results in a new [OpacityProvider] and hence the cached\n  /// opacity ratios are calculated using opacities when this method was called.\n  OpacityProvider opacityProviderOfList({\n    List<T?>? paintIds,\n    bool includeLayers = true,\n  }) {\n    return _MultiPaintOpacityProvider(\n      paintIds ?? (List<T?>.from(_paints.keys)..add(null)),\n      this,\n      includeLayers: includeLayers,\n    );\n  }\n}\n\nclass _ProxyOpacityProvider<T extends Object> implements OpacityProvider {\n  _ProxyOpacityProvider(this.paintId, this.target);\n\n  final T paintId;\n  final HasPaint<T> target;\n\n  @override\n  double get opacity => target.getOpacity(paintId: paintId);\n\n  @override\n  set opacity(double value) => target.setOpacity(value, paintId: paintId);\n}\n\nclass _MultiPaintOpacityProvider<T extends Object> implements OpacityProvider {\n  _MultiPaintOpacityProvider(\n    this.paintIds,\n    this.target, {\n    required this.includeLayers,\n  }) {\n    final maxOpacity = opacity;\n\n    _opacityRatios = [\n      for (final paintId in paintIds)\n        target.getOpacity(paintId: paintId) / maxOpacity,\n    ];\n    _layerOpacityRatios = target.paintLayersInternal\n        ?.map(\n          (paint) => paint.color.a / maxOpacity,\n        )\n        .toList(growable: false);\n  }\n\n  final List<T?> paintIds;\n  final HasPaint<T> target;\n  final bool includeLayers;\n  late final List<double> _opacityRatios;\n  late final List<double>? _layerOpacityRatios;\n\n  @override\n  double get opacity {\n    var maxOpacity = 0.0;\n\n    for (final paintId in paintIds) {\n      maxOpacity = max(target.getOpacity(paintId: paintId), maxOpacity);\n    }\n    if (includeLayers) {\n      final targetLayers = target.paintLayersInternal;\n      if (targetLayers != null) {\n        for (final paint in targetLayers) {\n          maxOpacity = max(paint.color.a, maxOpacity);\n        }\n      }\n    }\n\n    return maxOpacity;\n  }\n\n  @override\n  set opacity(double value) {\n    for (var i = 0; i < paintIds.length; ++i) {\n      target.setOpacity(\n        value * _opacityRatios.elementAt(i),\n        paintId: paintIds.elementAt(i),\n      );\n    }\n    if (includeLayers) {\n      final paintLayersInternal = target.paintLayersInternal;\n      for (var i = 0; i < (paintLayersInternal?.length ?? 0); ++i) {\n        paintLayersInternal![i].color = paintLayersInternal[i].color.withValues(\n          alpha: value * _layerOpacityRatios![i],\n        );\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/has_time_scale.dart",
    "content": "import 'package:flame/src/components/core/component.dart';\n\n/// This mixin allows components to control their speed as compared to the\n/// normal speed. Only framerate independent logic will benefit from [timeScale]\n/// changes.\n///\n/// Note: Modified [timeScale] will be applied to all children as well.\nmixin HasTimeScale on Component {\n  /// The ratio of components tick speed and normal tick speed.\n  /// It defaults to 1.0, which means the component moves normally.\n  /// A value of 0.5 means the component moves half the normal speed\n  /// and a value of 2.0 means the component moves twice as fast.\n  double _timeScale = 1.0;\n\n  /// Returns the current time scale.\n  double get timeScale => _timeScale;\n\n  /// Sets the time scale to given value if it is non-negative.\n  /// Note: Too high values will result in inconsistent gameplay\n  /// and tunneling in physics.\n  set timeScale(double value) {\n    if (value.isNegative) {\n      return;\n    }\n    _timeScale = value;\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt * (parent == null ? _timeScale : 1.0));\n  }\n\n  @override\n  void updateTree(double dt) {\n    super.updateTree(dt * (parent != null ? _timeScale : 1.0));\n  }\n\n  /// Pauses the component by setting the time scale to 0.0.\n  void pause() {\n    timeScale = 0.0;\n  }\n\n  /// Resumes the component by setting the time scale to 1.0 or to the given\n  /// value.\n  void resume({double? newTimeScale}) {\n    if (timeScale == 0.0) {\n      timeScale = newTimeScale ?? 1.0;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/has_visibility.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flutter/material.dart';\n\n/// A mixin that allows a component visibility to be toggled\n/// without removing it from the tree. Visibility affects\n/// the component and all it's children/descendants.\n///\n/// Set [isVisible] to false to prevent the component and all\n/// it's children from being rendered.\n///\n/// The component will still respond as if it is on the tree,\n/// including lifecycle and other events, but will simply\n/// not render itself or it's children.\n///\n/// If you are adding a custom implementation of the\n/// [renderTree] method, make sure to wrap your render code\n/// in a conditional. i.e.:\n/// ```\n/// if (isVisible) {\n///     // Custom render code here\n/// }\n/// ```\nmixin HasVisibility on Component {\n  bool isVisible = true;\n\n  @override\n  void renderTree(Canvas canvas) {\n    if (isVisible) {\n      super.renderTree(canvas);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/has_world.dart",
    "content": "import 'package:collection/collection.dart';\nimport 'package:flame/camera.dart';\nimport 'package:flame/src/components/core/component.dart';\nimport 'package:meta/meta.dart';\n\n/// [HasWorldReference] mixin provides the [world] property, which is the cached\n/// accessor for the world instance that this component belongs to.\n///\n/// The type [T] on the mixin is the type of your world class. This type will be\n/// the type of the [world] reference, and the mixin will check at runtime that\n/// the actual type matches the expectation.\nmixin HasWorldReference<T extends World> on Component {\n  T? _world;\n\n  /// Reference to the [World] instance that this component belongs to.\n  T get world => _world ??= _findWorldAndCheck();\n\n  /// Allows you to set the world instance explicitly.\n  /// This may be useful in tests.\n  @visibleForTesting\n  set world(T? value) => _world = value;\n\n  T? findWorld() {\n    return ancestors(\n          includeSelf: true,\n        ).firstWhereOrNull((ancestor) => ancestor is T)\n        as T?;\n  }\n\n  T _findWorldAndCheck() {\n    final world = findWorld();\n    assert(\n      world != null,\n      'Could not find a World instance of type $T',\n    );\n    return world!;\n  }\n\n  @override\n  void onRemove() {\n    _world = null;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/ignore_events.dart",
    "content": "import 'package:flame/src/components/core/component.dart';\n\n/// This mixin allows a component and all it's descendants to ignore events.\n///\n/// Do note that this will also ignore the component and its descendants in\n/// calls to [Component.componentsAtLocation].\n///\n/// If you want to dynamically use this mixin, you can add it and set\n/// [ignoreEvents] true or false at runtime.\n///\n/// This mixin is to be used when you have a large subtree of components that\n/// shouldn't receive any events and you want to optimize the event handling.\nmixin IgnoreEvents on Component {\n  bool ignoreEvents = true;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/keyboard_handler.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/input.dart';\nimport 'package:flutter/services.dart';\n\n/// A [Component] mixin to add keyboard handling capability to components.\n/// Must be used in components that can only be added to games that are mixed\n/// with [HasKeyboardHandlerComponents].\nmixin KeyboardHandler on Component {\n  bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {\n    return true;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/notifier.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:meta/meta.dart';\n\n/// Makes a component capable of notifying listeners of changes.\n///\n/// Notifier components will automatically notify when\n/// new instances are added or removed to the game instance.\n///\n/// To notify internal changes of a component instance, the component\n/// should call [notifyListeners].\nmixin Notifier on Component {\n  FlameGame get _gameRef {\n    final game = findGame();\n    assert(game != null, \"Notifier can't be used without FlameGame\");\n    return game!;\n  }\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    super.onMount();\n\n    _gameRef.propagateToApplicableNotifiers(this, (notifier) {\n      notifier.add(this);\n    });\n  }\n\n  @override\n  @mustCallSuper\n  void onRemove() {\n    _gameRef.propagateToApplicableNotifiers(this, (notifier) {\n      notifier.remove(this);\n    });\n\n    super.onRemove();\n  }\n\n  /// When called, will notify listeners that a change happened on\n  /// this component's class notifier.\n  void notifyListeners() {\n    _gameRef.propagateToApplicableNotifiers(this, (notifier) {\n      notifier.notify();\n    });\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/parent_is_a.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flutter/material.dart';\n\n/// A mixin that ensures a parent is of the given type [T].\nmixin ParentIsA<T extends Component> on Component {\n  @override\n  T get parent => super.parent! as T;\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    assert(super.parent is T, 'Parent must be of type $T');\n    super.onMount();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/single_child_particle.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/particles/particle.dart';\n\n/// Implements basic behavior for nesting [Particle] instances\n/// into each other.\n///\n/// ```dart\n/// class BehaviorParticle extends Particle with SingleChildParticle {\n///   Particle child;\n///\n///   BehaviorParticle({\n///     required this.child\n///   });\n///\n///   @override\n///   update(double dt) {\n///     // Will ensure that child [Particle] is properly updated\n///     super.update(dt);\n///\n///     // ... Custom behavior\n///   }\n/// }\n/// ```\nmixin SingleChildParticle on Particle {\n  late Particle child;\n\n  @override\n  void setLifespan(double lifespan) {\n    super.setLifespan(lifespan);\n    child.setLifespan(lifespan);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    child.render(canvas);\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    child.update(dt);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/mixins/snapshot.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\n\n/// A mixin that enables caching a component and all its children. If\n/// [renderSnapshot] is set to `true`, the component and its children will be\n/// rendered to a cache. Subsequent renders use the cache, dramatically\n/// improving performance. This is only effective if the component and its\n/// children do not change - i.e. they are not animated and they do not move\n/// around relative to each other.\n///\n/// The [takeSnapshot] and [snapshotAsImage] methods can also be used to take\n/// one-off snapshots for screen-grabs or other purposes.\nmixin Snapshot on PositionComponent {\n  bool _renderSnapshot = true;\n  Picture? _picture;\n\n  /// If [renderSnapshot] is `true` then this component and all its children\n  /// will be rendered once and cached. If [renderSnapshot] is `false`\n  /// then this component will render normally.\n  bool get renderSnapshot => _renderSnapshot;\n  set renderSnapshot(bool value) {\n    if (_renderSnapshot != value) {\n      _renderSnapshot = value;\n      if (_renderSnapshot == true) {\n        _picture = null;\n      }\n    }\n  }\n\n  /// Check if a snapshot exists.\n  bool get hasSnapshot => _picture != null;\n\n  /// Grab the current snapshot. Check it exists first using [hasSnapshot].\n  Picture get snapshot {\n    assert(_picture != null, 'No snapshot has been taken');\n    return _picture!;\n  }\n\n  /// Convert the snapshot to an image with the given [width] and [height].\n  /// Use [transform] to position the snapshot in the image, or to apply other\n  /// transforms before the image is generated.\n  Image snapshotAsImage(int width, int height, {Matrix4? transform}) {\n    assert(_picture != null, 'No snapshot has been taken');\n    if (transform == null) {\n      return _picture!.toImageSync(width, height);\n    } else {\n      final recorder = PictureRecorder();\n      final canvas = Canvas(recorder);\n      canvas.transform32(transform.storage);\n      canvas.drawPicture(_picture!);\n      final picture = recorder.endRecording();\n      return picture.toImageSync(width, height);\n    }\n  }\n\n  /// Immediately take a snapshot and return it. If [renderSnapshot] is true\n  /// then the snapshot is also used for rendering. A snapshot is always taken\n  /// with no transformations (i.e. as if the Snapshot component is at position\n  /// (0, 0) and has no scale or rotation applied).\n  Picture takeSnapshot() {\n    final recorder = PictureRecorder();\n    final canvas = Canvas(recorder);\n    final matrix = transformMatrix.clone();\n    matrix.invert();\n    canvas.transform32(matrix.storage);\n    super.renderTree(canvas);\n    _picture = recorder.endRecording();\n    return _picture!;\n  }\n\n  /// clear the current snapshot. will trigger the creation of a new snapshot\n  /// next time renderTree is called.\n  void clearSnapshot() {\n    _picture = null;\n  }\n\n  @override\n  void renderTree(Canvas canvas) {\n    if (renderSnapshot) {\n      if (_picture == null) {\n        takeSnapshot();\n      }\n      canvas.save();\n      canvas.transform32(transformMatrix.storage);\n      canvas.drawPicture(_picture!);\n      canvas.restore();\n    } else {\n      super.renderTree(canvas);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/nine_tile_box_component.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:meta/meta.dart';\n\nexport '../nine_tile_box.dart';\n\n/// This class is a thin wrapper on top of [NineTileBox] as a component.\nclass NineTileBoxComponent extends PositionComponent with HasPaint {\n  NineTileBox? nineTileBox;\n\n  /// Takes the [NineTileBox] instance to render a box that can grow and shrink\n  /// seamlessly.\n  ///\n  /// It uses the x, y, width and height coordinates from the\n  /// [PositionComponent] to render.\n  NineTileBoxComponent({\n    this.nineTileBox,\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.key,\n  });\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    assert(\n      nineTileBox != null,\n      'The nineTileBox should be set either in the constructor or in onLoad',\n    );\n  }\n\n  @mustCallSuper\n  @override\n  void render(Canvas canvas) {\n    nineTileBox?.drawRect(canvas, size.toRect(), paint);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/parallax_component.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/camera.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/src/cache/images.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\nimport 'package:flame/src/parallax.dart';\nimport 'package:flutter/painting.dart';\nimport 'package:meta/meta.dart';\n\nextension ParallaxComponentExtension on FlameGame {\n  Future<ParallaxComponent> loadParallaxComponent(\n    Iterable<ParallaxData> dataList, {\n    Vector2? baseVelocity,\n    Vector2? velocityMultiplierDelta,\n    ImageRepeat repeat = ImageRepeat.repeatX,\n    Alignment alignment = Alignment.bottomLeft,\n    LayerFill fill = LayerFill.height,\n    Images? images,\n    Vector2? position,\n    Vector2? size,\n    Vector2? scale,\n    double? angle,\n    Anchor? anchor,\n    int? priority,\n    FilterQuality? filterQuality,\n    ComponentKey? key,\n    String? package,\n  }) {\n    return ParallaxComponent.load(\n      dataList,\n      baseVelocity: baseVelocity,\n      velocityMultiplierDelta: velocityMultiplierDelta,\n      repeat: repeat,\n      alignment: alignment,\n      fill: fill,\n      images: images,\n      position: position,\n      size: size,\n      scale: scale,\n      angle: angle,\n      anchor: anchor,\n      priority: priority,\n      filterQuality: filterQuality,\n      key: key,\n      package: package,\n    );\n  }\n}\n\n/// A full parallax, several layers of images drawn out on the screen and each\n/// layer moves with different velocities to give an effect of depth.\n///\n/// Most of the time you want to add the [ParallaxComponent] as a child to the\n/// viewport: `game.camera.viewport.add(parallaxComponent);`, since you want it\n/// to be static to the rest of the game.\nclass ParallaxComponent<T extends FlameGame> extends PositionComponent\n    with HasGameReference<T> {\n  bool isFullscreen = true;\n  Parallax? _parallax;\n\n  Parallax? get parallax => _parallax;\n  set parallax(Parallax? p) {\n    _parallax = p;\n    _parallax?.resize(size);\n  }\n\n  /// Creates a component with an empty parallax which can be set later.\n  ParallaxComponent({\n    Parallax? parallax,\n    super.position,\n    Vector2? size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.key,\n  }) : _parallax = parallax,\n       isFullscreen = size == null && !(parallax?.isSized ?? false),\n       super(\n         size: size ?? ((parallax?.isSized ?? false) ? parallax?.size : null),\n       );\n\n  @mustCallSuper\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    if (!isFullscreen) {\n      return;\n    }\n    final Vector2 newSize;\n    if (parent is Viewport) {\n      newSize = (parent! as Viewport).virtualSize;\n    } else if (parent is ReadOnlySizeProvider) {\n      newSize = (parent! as ReadOnlySizeProvider).size;\n    } else {\n      newSize = game.size;\n    }\n    this.size.setFrom(newSize);\n    parallax?.resize(newSize);\n  }\n\n  @mustCallSuper\n  @override\n  void onMount() {\n    assert(\n      parallax != null,\n      'The parallax needs to be set in either the constructor or in onLoad',\n    );\n  }\n\n  @mustCallSuper\n  @override\n  void update(double dt) {\n    parallax?.update(dt);\n  }\n\n  @mustCallSuper\n  @override\n  void render(Canvas canvas) {\n    parallax?.render(canvas);\n  }\n\n  /// Note that this method only should be used if all of your layers should\n  /// have the same layer arguments (how the images should be repeated, aligned\n  /// and filled), otherwise load the [ParallaxLayer]s individually and use the\n  /// normal constructor.\n  ///\n  /// [load] takes a list of [ParallaxData] of all the images and a size that\n  /// you want to use in the parallax.\n  ///\n  /// Optionally arguments for the [baseVelocity] and [velocityMultiplierDelta]\n  /// can be passed in, [baseVelocity] defines what the base velocity of the\n  /// layers should be and [velocityMultiplierDelta] defines how the velocity\n  /// should change the closer the layer is (`velocityMultiplierDelta ^ n`,\n  /// where `n` is the layer index).\n  /// Arguments for how all the images should repeat ([repeat]),\n  /// which edge it should align with ([alignment]), which axis it should fill\n  /// the image on ([fill]) and [images] which is the image cache that should be\n  /// used can also be passed in.\n  ///\n  /// If no image cache is set, the global flame cache is used.\n  static Future<ParallaxComponent> load(\n    Iterable<ParallaxData> dataList, {\n    Vector2? baseVelocity,\n    Vector2? velocityMultiplierDelta,\n    ImageRepeat repeat = ImageRepeat.repeatX,\n    Alignment alignment = Alignment.bottomLeft,\n    LayerFill fill = LayerFill.height,\n    Images? images,\n    Vector2? position,\n    Vector2? size,\n    Vector2? scale,\n    double? angle,\n    Anchor? anchor,\n    int? priority,\n    FilterQuality? filterQuality,\n    ComponentKey? key,\n    String? package,\n  }) async {\n    return ParallaxComponent(\n      parallax: await Parallax.load(\n        dataList,\n        size: size,\n        baseVelocity: baseVelocity,\n        velocityMultiplierDelta: velocityMultiplierDelta,\n        repeat: repeat,\n        alignment: alignment,\n        fill: fill,\n        images: images,\n        filterQuality: filterQuality,\n        package: package,\n      ),\n      position: position,\n      size: size,\n      scale: scale,\n      angle: angle,\n      anchor: anchor,\n      priority: priority,\n      key: key,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/particle_system_component.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/particles.dart';\n\n/// {@template particle_system_component}\n/// A [PositionComponent] that renders a [Particle] at the designated\n/// position, scaled to have the designated size and rotated to the specified\n/// angle.\n/// {@endtemplate}\nclass ParticleSystemComponent extends PositionComponent {\n  Particle? particle;\n\n  /// {@macro particle_system_component}\n  ParticleSystemComponent({\n    this.particle,\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.priority,\n    super.key,\n  });\n\n  /// Returns progress of the child [Particle].\n  ///\n  /// Could be used by external code if needed.\n  double get progress => particle?.progress ?? 0;\n\n  /// Passes rendering chain down to the inset\n  /// [Particle] within this [Component].\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    particle?.render(canvas);\n  }\n\n  /// Passes update chain to child [Particle].\n  @override\n  void update(double dt) {\n    particle?.update(dt);\n    if (particle?.shouldRemove ?? false) {\n      removeFromParent();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/position_component.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:flame/camera.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/src/anchor.dart';\nimport 'package:flame/src/components/core/component.dart';\nimport 'package:flame/src/components/mixins/coordinate_transform.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\nimport 'package:flame/src/game/notifying_vector2.dart';\nimport 'package:flame/src/game/transform2d.dart';\nimport 'package:flame/src/rendering/decorator.dart';\nimport 'package:flame/src/rendering/transform2d_decorator.dart';\nimport 'package:meta/meta.dart';\n\n/// A [Component] implementation that represents an object that can be\n/// freely moved around the screen, rotated, and scaled.\n///\n/// The [PositionComponent] class has no visual representation of its own\n/// (except in debug mode). It is common, therefore, to derive from this\n/// class, implementing a specific rendering logic. For example:\n/// ```dart\n/// class MyCircle extends PositionComponent {\n///   MyCircle({required double radius, Paint? paint, Vector2? position})\n///     : _radius = radius,\n///       _paint = paint ?? Paint()..color=Color(0xFF80C080),\n///       super(\n///         position: position,\n///         size: Vector2.all(2 * radius),\n///         anchor: Anchor.center,\n///       );\n///\n///   double _radius;\n///   Paint _paint;\n///\n///   @override\n///   void render(Canvas canvas) {\n///     super.render(canvas);\n///     canvas.drawCircle(Offset(_radius, _radius), _radius, _paint);\n///   }\n/// }\n/// ```\n///\n/// The base [PositionComponent] class can also be used as a container\n/// for several other components. In this case, changing the position,\n/// rotating or scaling the [PositionComponent] will affect the whole\n/// group as if it was a single entity.\n///\n/// The main properties of this class is the [transform] (which combines\n/// the [position], [angle] of rotation, and [scale]), the [size], and\n/// the [anchor]. Thus, the [PositionComponent] can be imagined as an\n/// abstract picture whose of a certain [size]. Within that picture\n/// a point at location [anchor] is selected, and that point is designated as\n/// a \"logical center\" of the picture. Then, a sequence of transforms is\n/// applied: the picture is moved so that the anchor becomes at point\n/// [position] on the screen, then the picture is rotated for [angle]\n/// radians around the anchor point, and finally scaled by [scale] also\n/// around the anchor point.\n///\n/// The [size] property of the [PositionComponent] is used primarily for\n/// tap and collision detection. Thus, the [size] should be set equal to\n/// the approximate bounding rectangle of the rendered picture. If you\n/// do not specify the size of a PositionComponent, then it will be\n/// equal to zero and the component won't be able to respond to taps.\nclass PositionComponent extends Component\n    implements\n        AnchorProvider,\n        AngleProvider,\n        PositionProvider,\n        ScaleProvider,\n        SizeProvider,\n        CoordinateTransform {\n  PositionComponent({\n    Vector2? position,\n    Vector2? size,\n    Vector2? scale,\n    double? angle,\n    this.nativeAngle = 0,\n    Anchor? anchor,\n    super.children,\n    super.priority,\n    super.key,\n  }) : transform = Transform2D(),\n       _anchor = anchor ?? Anchor.topLeft,\n       _size = NotifyingVector2.copy(size ?? Vector2.zero()) {\n    decorator = Transform2DDecorator(transform);\n    if (position != null) {\n      transform.position = position;\n    }\n    if (angle != 0) {\n      transform.angle = angle ?? 0;\n    }\n    if (scale != null) {\n      transform.scale = scale;\n    }\n    _size.addListener(_onModifiedSizeOrAnchor);\n    _onModifiedSizeOrAnchor();\n  }\n\n  final Transform2D transform;\n  final NotifyingVector2 _size;\n  Anchor _anchor;\n\n  /// The angle where this component is looking at when it is in\n  /// the default state, i.e. when [angle] is equal to zero.\n  /// For example, a nativeAngle of\n  ///     0 implies up/north direction\n  ///  pi/2 implies right/east direction\n  ///    pi implies down/south direction\n  /// -pi/2 implies left/west direction\n  double nativeAngle;\n\n  /// The decorator is used to apply visual effects to a component.\n  ///\n  /// By default, the [PositionComponent] is equipped with a\n  /// [Transform2DDecorator] which makes sure the component is rendered at a\n  /// proper location on the canvas. It is possible to replace this decorator\n  /// with another one if a different functionality is desired.\n  ///\n  /// A more common use for this field, however, is to apply additional visual\n  /// effects such as tints/shadows/etc using [Decorator.addLast].\n  late Decorator decorator;\n\n  /// The total transformation matrix for the component. This matrix combines\n  /// translation, rotation and scale transforms into a single entity. The\n  /// matrix is cached and gets recalculated only as necessary.\n  Matrix4 get transformMatrix => transform.transformMatrix;\n\n  /// The position of this component's anchor on the screen.\n  @override\n  NotifyingVector2 get position => transform.position;\n  @override\n  set position(Vector2 position) => transform.position = position;\n\n  /// X position of this component's anchor on the screen.\n  double get x => transform.x;\n  set x(double x) => transform.x = x;\n\n  /// Y position of this component's anchor on the screen.\n  double get y => transform.y;\n  set y(double y) => transform.y = y;\n\n  /// Rotation angle (in radians) of the component. The component will be\n  /// rotated around its anchor point in the clockwise direction if the\n  /// angle is positive, or counterclockwise if the angle is negative.\n  @override\n  double get angle => transform.angle;\n  @override\n  set angle(double a) => transform.angle = a.toNormalizedAngle();\n\n  /// The scale factor of this component. The scale can be different along\n  /// the X and Y dimensions. A scale greater than 1 makes the component\n  /// bigger, and less than 1 smaller. The scale can also be negative,\n  /// which results in a mirror reflection along the corresponding axis.\n  @override\n  NotifyingVector2 get scale => transform.scale;\n  @override\n  set scale(Vector2 scale) => transform.scale = scale;\n\n  /// Anchor point for this component. An anchor point describes a point\n  /// within the rectangle of size [size]. This point is considered to\n  /// be the logical \"center\" of the component. This can be visualized\n  /// as the point where Flame \"grabs\" the component. All transforms\n  /// occur around this point: the [position] is where the anchor point\n  /// will end up after the component is translated; the rotation and\n  /// scaling also happen around this anchor point.\n  ///\n  /// The [anchor] of a component can be modified during runtime. When\n  /// this happens, the [position] of the component will remain unchanged,\n  /// which means that visually the component will shift on the screen\n  /// so that its new anchor will be at the same screen coordinates as\n  /// the old anchor was.\n  @override\n  Anchor get anchor => _anchor;\n  @override\n  set anchor(Anchor anchor) {\n    _anchor = anchor;\n    _onModifiedSizeOrAnchor();\n  }\n\n  /// The logical size of the component. The game assumes that this is the\n  /// approximate size of the object that will be drawn on the screen.\n  /// This size will therefore be used for collision detection and tap\n  /// handling.\n  ///\n  /// This property can be reassigned at runtime, although this is not\n  /// recommended. Instead, in order to make the [PositionComponent] larger\n  /// or smaller, change its [scale].\n  @override\n  NotifyingVector2 get size => _size;\n\n  @override\n  set size(Vector2 size) {\n    _size.setFrom(size);\n    if (hasChildren) {\n      for (final child in children) {\n        child.onParentResize(_size);\n      }\n    }\n  }\n\n  /// The width of the component in local coordinates. Note that the object\n  /// may visually appear larger or smaller due to application of [scale].\n  double get width => _size.x;\n  set width(double w) => _size.x = w;\n\n  /// The height of the component in local coordinates. Note that the object\n  /// may visually appear larger or smaller due to application of [scale].\n  double get height => _size.y;\n  set height(double h) => _size.y = h;\n\n  /// The \"physical\" size of the component. This is the size of the\n  /// component as seen from the parent's perspective, and it is equal to\n  /// [size] * [scale]. This is a computed property and cannot be\n  /// modified by the user.\n  Vector2 get scaledSize {\n    return Vector2(width * scale.x.abs(), height * scale.y.abs());\n  }\n\n  /// The resulting size after all the ancestors and the components own scale\n  /// has been applied.\n  Vector2 get absoluteScaledSize {\n    final absoluteScale = this.absoluteScale;\n    return Vector2(\n      width * absoluteScale.x.abs(),\n      height * absoluteScale.y.abs(),\n    );\n  }\n\n  /// The resulting angle after all the ancestors and the components own angles\n  /// and scales have been applied.\n  double get absoluteAngle => _absoluteAngle();\n\n  /// The resulting angle after all the ancestors and the components own angles\n  /// and scales have been applied, but without reflecting the angle\n  /// if the component is flipped.\n  double get absoluteAngleWithoutReflection => _absoluteAngle(reflect: false);\n\n  double _absoluteAngle({bool reflect = true}) {\n    var angle = 0.0;\n    var totalScaleX = 1.0;\n    var totalScaleY = 1.0;\n\n    final ancestorChain = ancestors(includeSelf: true).toList(growable: false)\n      ..reverse();\n\n    for (final ancestor in ancestorChain) {\n      if (ancestor is ReadOnlyScaleProvider) {\n        final ancestorScale = (ancestor as ReadOnlyScaleProvider).scale;\n        totalScaleX *= ancestorScale.x;\n        totalScaleY *= ancestorScale.y;\n        if (ancestorScale.x.isNegative) {\n          angle *= -1;\n        }\n        if (ancestorScale.y.isNegative && reflect) {\n          angle -= math.pi - angle;\n        }\n      }\n\n      if (ancestor is ReadOnlyAngleProvider) {\n        final reflected = totalScaleX.isNegative ^ totalScaleY.isNegative;\n        final localAngle = (ancestor as ReadOnlyAngleProvider).angle;\n        angle += reflected ? -localAngle : localAngle;\n      }\n    }\n\n    return angle.toNormalizedAngle();\n  }\n\n  /// The resulting scale after all the ancestors and the components own scale\n  /// has been applied.\n  Vector2 get absoluteScale => scale.clone()..multiply(_parentAbsoluteScale);\n\n  Vector2 get _parentAbsoluteScale {\n    return ancestors().whereType<ReadOnlyScaleProvider>().fold<Vector2>(\n      Vector2.all(1.0),\n      (totalScale, c) => totalScale..multiply(c.scale),\n    );\n  }\n\n  /// Measure the distance (in parent's coordinate space) between this\n  /// component's anchor and the [other] component's anchor.\n  double distance(PositionComponent other) =>\n      position.distanceTo(other.position);\n\n  //#region Coordinate transformations\n\n  /// Test whether the `point` (given in local coordinates) lies within this\n  /// component. The top and the left borders of the component are inclusive,\n  /// while the bottom and the right borders are exclusive.\n  @override\n  bool containsLocalPoint(Vector2 point) {\n    return (point.x >= 0) &&\n        (point.y >= 0) &&\n        (point.x < _size.x) &&\n        (point.y < _size.y);\n  }\n\n  /// Test whether the `point` (given in global coordinates) lies within this\n  /// component. The top and the left borders of the component are inclusive,\n  /// while the bottom and the right borders are exclusive.\n  @override\n  bool containsPoint(Vector2 point) {\n    return containsLocalPoint(absoluteToLocal(point));\n  }\n\n  @override\n  Vector2 parentToLocal(Vector2 point, {Vector2? output}) =>\n      transform.globalToLocal(point, output: output);\n\n  @override\n  Vector2 localToParent(Vector2 point, {Vector2? output}) =>\n      transform.localToGlobal(point, output: output);\n\n  /// Convert local coordinates of a point [point] inside the component\n  /// into the parent's coordinate space.\n  Vector2 positionOf(Vector2 point) {\n    return transform.localToGlobal(point);\n  }\n\n  /// Similar to [positionOf()], but applies to any anchor point within\n  /// the component.\n  Vector2 positionOfAnchor(Anchor anchor) {\n    if (anchor == _anchor) {\n      return position;\n    }\n    return positionOf(Vector2(anchor.x * size.x, anchor.y * size.y));\n  }\n\n  /// Convert local coordinates of a point [point] inside the component\n  /// into the global (world) coordinate space.\n  Vector2 absolutePositionOf(Vector2 point) {\n    var parentPoint = positionOf(point);\n    var ancestor = parent;\n    while (ancestor != null) {\n      if (ancestor is PositionComponent) {\n        parentPoint = ancestor.positionOf(parentPoint);\n      }\n      ancestor = ancestor.parent;\n    }\n    return parentPoint;\n  }\n\n  /// Similar to [absolutePositionOf()], but applies to any anchor\n  /// point within the component.\n  Vector2 absolutePositionOfAnchor(Anchor anchor) =>\n      absolutePositionOf(Vector2(anchor.x * size.x, anchor.y * size.y));\n\n  /// Transform [point] from the parent's coordinate space into the local\n  /// coordinates. This function is the inverse of [positionOf()].\n  Vector2 toLocal(Vector2 point) => transform.globalToLocal(point);\n\n  /// Transform [point] from the global (world) coordinate space into the\n  /// local coordinates. This function is the inverse of\n  /// [absolutePositionOf()].\n  ///\n  /// This can be used, for example, to detect whether a specific point\n  /// on the screen lies within this [PositionComponent], and where\n  /// exactly it hits.\n  Vector2 absoluteToLocal(Vector2 point) {\n    var c = parent;\n    while (c != null) {\n      if (c is PositionComponent) {\n        return toLocal(c.absoluteToLocal(point));\n      }\n      c = c.parent;\n    }\n    return toLocal(point);\n  }\n\n  /// The top-left corner's position in the parent's coordinates.\n  Vector2 get topLeftPosition => positionOfAnchor(Anchor.topLeft);\n  set topLeftPosition(Vector2 point) {\n    position += point - topLeftPosition;\n  }\n\n  /// The position of the center of the component's bounding rectangle\n  /// in the parent's coordinates.\n  Vector2 get center => positionOfAnchor(Anchor.center);\n  set center(Vector2 point) {\n    position += point - center;\n  }\n\n  /// The [anchor]'s position in absolute (world) coordinates.\n  Vector2 get absolutePosition => absolutePositionOfAnchor(_anchor);\n\n  /// The absolute top left position regardless of whether it is a child or not.\n  Vector2 get absoluteTopLeftPosition =>\n      absolutePositionOfAnchor(Anchor.topLeft);\n\n  /// The absolute center of the component.\n  Vector2 get absoluteCenter => absolutePositionOfAnchor(Anchor.center);\n\n  /// Returns the angle formed by component's orientation vector and a vector\n  /// starting at component's absolute position and ending at [target]. I.e.\n  /// how much the current component need to rotate to face the target. This\n  /// angle is measured in clockwise direction. [target] should be in\n  /// absolute/world coordinate system.\n  ///\n  /// Uses [nativeAngle] to decide the orientation direction of the component.\n  /// See [lookAt] to make the component instantly rotate towards target.\n  ///\n  /// Note: If target coincides with the current component's position, then it\n  /// is treated as being north.\n  double angleTo(Vector2 target) {\n    final direction = target - absolutePosition;\n    if (direction.isZero()) {\n      // If the target coincides with the component's position, we treat it as\n      // being north.\n      return -nativeAngle % tau;\n    }\n\n    final parentAbsoluteScale = _parentAbsoluteScale;\n    final targetAngle = math.atan2(\n      direction.x * scale.x.sign,\n      -direction.y * scale.y.sign,\n    );\n    final angleDifference = targetAngle - absoluteAngle - nativeAngle;\n\n    final hasOddFlips =\n        parentAbsoluteScale.x.isNegative ^\n        parentAbsoluteScale.y.isNegative ^\n        scale.x.isNegative ^\n        scale.y.isNegative;\n    final hasSelfYFlip =\n        !parentAbsoluteScale.y.isNegative && scale.y.isNegative;\n\n    final result =\n        (hasOddFlips ? -1 : 1) * angleDifference +\n        (hasSelfYFlip ? 1 : 0) * math.pi;\n    return result.toNormalizedAngle();\n  }\n\n  /// Rotates/snaps the component to look at the [target].\n  ///\n  /// This method sets the [angle] so that the component's orientation\n  /// vector (as determined by the [nativeAngle]) is pointing at the target.\n  /// [target] should to be in absolute/world coordinate system.\n  ///\n  /// See also: [angleTo]\n  void lookAt(Vector2 target) {\n    angle += angleTo(target);\n  }\n\n  //#endregion\n\n  //#region Mutators\n\n  /// Flip the component horizontally around its anchor point.\n  void flipHorizontally() => transform.flipHorizontally();\n\n  /// Flip the component vertically around its anchor point.\n  void flipVertically() => transform.flipVertically();\n\n  /// Flip the component horizontally around its center line.\n  void flipHorizontallyAroundCenter() {\n    if (_anchor.x != 0.5) {\n      final delta = (1 - 2 * _anchor.x) * width * transform.scale.x;\n      transform.x += delta * math.cos(transform.angle);\n      transform.y += delta * math.sin(transform.angle);\n    }\n    transform.flipHorizontally();\n  }\n\n  /// Flip the component vertically around its center line.\n  void flipVerticallyAroundCenter() {\n    if (anchor.y != 0.5) {\n      final delta = (1 - 2 * _anchor.y) * height * transform.scale.y;\n      transform.x += -delta * math.sin(transform.angle);\n      transform.y += delta * math.cos(transform.angle);\n    }\n    transform.flipVertically();\n  }\n\n  /// Whether it is currently flipped horizontally.\n  bool get isFlippedHorizontally => transform.scale.x.isNegative;\n\n  /// Whether it is currently flipped vertically.\n  bool get isFlippedVertically => transform.scale.y.isNegative;\n\n  //#endregion\n\n  /// Internal handler that must be invoked whenever either the [size]\n  /// or the [anchor] change.\n  void _onModifiedSizeOrAnchor() {\n    transform.offset = Vector2(-_anchor.x * _size.x, -_anchor.y * _size.y);\n  }\n\n  @override\n  void renderDebugMode(Canvas canvas) {\n    final zoom = CameraComponent.currentCamera?.viewfinder.zoom ?? 1.0;\n    super.renderDebugMode(canvas);\n    final precision = debugCoordinatesPrecision;\n    canvas.drawRect(size.toRect(), debugPaint);\n    // draw small cross at the anchor point\n    final p0 = -transform.offset;\n    canvas.drawLine(Offset(p0.x, p0.y - 2), Offset(p0.x, p0.y + 2), debugPaint);\n    canvas.drawLine(Offset(p0.x - 2, p0.y), Offset(p0.x + 2, p0.y), debugPaint);\n    if (precision != null) {\n      // print coordinates at the top-left corner\n      final p1 = absolutePositionOfAnchor(Anchor.topLeft);\n      final x1str = p1.x.toStringAsFixed(precision);\n      final y1str = p1.y.toStringAsFixed(precision);\n      debugTextPaint.render(\n        canvas,\n        'x:$x1str y:$y1str',\n        Vector2(-10 * (precision + 3) / zoom, -15 / zoom),\n      );\n      // print coordinates at the bottom-right corner\n      final p2 = absolutePositionOfAnchor(Anchor.bottomRight);\n      final x2str = p2.x.toStringAsFixed(precision);\n      final y2str = p2.y.toStringAsFixed(precision);\n      debugTextPaint.render(\n        canvas,\n        'x:$x2str y:$y2str',\n        Vector2(size.x - 10 * (precision + 3) / zoom, size.y),\n      );\n    }\n  }\n\n  @override\n  void renderTree(Canvas canvas) {\n    decorator.applyChain(super.renderTree, canvas);\n  }\n\n  @internal\n  @protected\n  void renderTreeWithoutDecorator(Canvas canvas) {\n    super.renderTree(canvas);\n  }\n\n  /// Returns the bounding rectangle for this component.\n  ///\n  /// The bounding rectangle is given in parent's coordinate space, and is\n  /// defined as the smallest axes-aligned rectangle that can fit this\n  /// component. The aspect ratio of the bounding rectangle may be different\n  /// from [size] if the component was scaled and/or rotated.\n  Rect toRect() => _toRectImpl(positionOfAnchor);\n\n  /// The bounding rectangle of the component in global coordinate space.\n  ///\n  /// This is similar to [toRect()], except the rectangle is projected into the\n  /// outermost coordinate frame.\n  Rect toAbsoluteRect() => _toRectImpl(absolutePositionOfAnchor);\n\n  Rect _toRectImpl(Vector2 Function(Anchor point) projector) {\n    final topLeft = projector(Anchor.topLeft);\n    final bottomRight = projector(Anchor.bottomRight);\n    if (angle == 0) {\n      return Rect.fromPoints(topLeft.toOffset(), bottomRight.toOffset());\n    } else {\n      final topRight = projector(Anchor.topRight);\n      final bottomLeft = projector(Anchor.bottomLeft);\n      final xs = [topLeft.x, topRight.x, bottomLeft.x, bottomRight.x]..sort();\n      final ys = [topLeft.y, topRight.y, bottomLeft.y, bottomRight.y]..sort();\n      return Rect.fromLTRB(xs.first, ys.first, xs.last, ys.last);\n    }\n  }\n\n  /// Mutates position and size using the provided [rect] as basis.\n  /// This is a relative rect, same definition that [toRect] use\n  /// (therefore both methods are compatible,\n  /// i.e. setByRect ∘ toRect = identity).\n  void setByRect(Rect rect) {\n    size.setValues(rect.width, rect.height);\n    topLeftPosition = rect.topLeft.toVector2();\n  }\n\n  @override\n  String toString() {\n    // ignore_for_file: no_runtimeType_toString\n    return '''\n$runtimeType(\n  position: ${position.toStringWithMaxPrecision(4)},\n  size: ${size.toStringWithMaxPrecision(4)},\n  angle: $angle,\n  scale: $scale,\n)''';\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/raster_sprite_component.dart",
    "content": "import 'dart:async';\nimport 'dart:ui';\n\nimport 'package:flame/cache.dart';\nimport 'package:flame/components.dart';\nimport 'package:meta/meta.dart';\n\nexport '../sprite.dart';\n\n/// {@template raster_sprite_component}\n/// A [RasterSpriteComponent] is a [SpriteComponent] that\n/// will rasterize its sprite when loaded and will automatically\n/// manage the disposal of the rasterized image when removed.\n/// {@endtemplate}\nclass RasterSpriteComponent extends SpriteComponent {\n  /// {@macro raster_sprite_component}\n  RasterSpriteComponent({\n    required this.baseSprite,\n    this.images,\n    super.autoResize,\n    super.paint,\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.nativeAngle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.bleed,\n    super.key,\n  });\n\n  RasterSpriteComponent.fromImage(\n    Image image, {\n    Vector2? srcPosition,\n    Vector2? srcSize,\n    bool? autoResize,\n    Paint? paint,\n    Vector2? position,\n    Vector2? size,\n    Vector2? scale,\n    double? angle,\n    double nativeAngle = 0,\n    Anchor? anchor,\n    Iterable<Component>? children,\n    int? priority,\n    ComponentKey? key,\n    double? bleed,\n  }) : this(\n         baseSprite: Sprite(\n           image,\n           srcPosition: srcPosition,\n           srcSize: srcSize,\n         ),\n         autoResize: autoResize,\n         paint: paint,\n         position: position,\n         size: size,\n         scale: scale,\n         angle: angle,\n         nativeAngle: nativeAngle,\n         anchor: anchor,\n         children: children,\n         priority: priority,\n         bleed: bleed,\n         key: key,\n       );\n\n  /// The base sprite to be rasterized.\n  final Sprite baseSprite;\n\n  /// The [Images] cache used to store the rasterized image.\n  final Images? images;\n\n  @mustCallSuper\n  @override\n  FutureOr<void> onLoad() async {\n    await super.onLoad();\n    sprite = await baseSprite.rasterize(images: images);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/router/overlay_route.dart",
    "content": "import 'package:flame/src/components/core/component.dart';\nimport 'package:flame/src/components/router/route.dart';\nimport 'package:flame/src/game/game.dart';\nimport 'package:flutter/widgets.dart' hide Route;\nimport 'package:meta/meta.dart';\n\n/// [OverlayRoute] is a class that allows adding/removing game overlays as if\n/// they were ordinary [Route]s.\n///\n/// There are several differences between an [OverlayRoute] and the regular\n/// [Route]s:\n/// - the overlays are always rendered on top of the game canvas, so if you push\n///   a regular route on top of an overlay route, the overlay route would still\n///   be displayed on top.\n/// - the `builder` of an overlay route produces a widget instead of a\n///   component.\nclass OverlayRoute extends Route {\n  /// An overlay route that uses the specified [builder]. This builder will be\n  /// registered with the Game's map of overlay builders when this route is\n  /// first activated.\n  OverlayRoute(OverlayBuilder builder, {super.transparent = true})\n    : _builder = builder,\n      super(null);\n\n  /// An overlay route that corresponds to an overlay that was already declared\n  /// within GameWidget's `overlayBuilderMap`.\n  OverlayRoute.existing({super.transparent = true})\n    : _builder = null,\n      super(null);\n\n  final OverlayBuilder? _builder;\n\n  @internal\n  Game get game => findGame()!;\n\n  @override\n  String get name => super.name!;\n\n  @override\n  Component build() {\n    if (_builder != null) {\n      game.overlays.addEntry(name, _builder);\n    }\n    return Component();\n  }\n\n  @mustCallSuper\n  @override\n  void onPush(Route? previousRoute) {\n    final didAdd = game.overlays.add(name);\n    assert(didAdd, 'An overlay $name was already added before');\n  }\n\n  @mustCallSuper\n  @override\n  void onPop(Route nextRoute) {\n    final didRemove = game.overlays.remove(name);\n    assert(didRemove, 'An overlay $name was already removed');\n  }\n}\n\ntypedef OverlayBuilder = Widget Function(BuildContext context, Game game);\n"
  },
  {
    "path": "packages/flame/lib/src/components/router/route.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/src/components/router/router_component.dart';\nimport 'package:flame/src/effects/effect.dart';\nimport 'package:flame/src/rendering/decorator.dart';\nimport 'package:meta/meta.dart';\n\n/// [Route] is a light-weight component that builds and manages a page.\n///\n/// The \"page\" is a generic concept here: it is any component that comprises a\n/// distinct UI arrangement. Pages are usually full-screen, for example: a\n/// splash-screen page, a loading page, a game menu page, a level selection\n/// page, a character creation page, and so on. Pages may also occupy less than\n/// the full screen. These can be: a confirmation popup, an \"enter name\" dialog\n/// box, a character inventory panel, a UI for dialogue with an NPC, etc.\n///\n/// Most routes are created when the game is initialized, and thus they should\n/// try to be as lightweight as possible. In particular, a [Route] should avoid\n/// any potentially costly initialization operations.\n///\n/// Routes are managed by the [RouterComponent] component.\nclass Route extends PositionComponent\n    with ParentIsA<RouterComponent>, HasTimeScale {\n  Route(\n    Component Function()? builder, {\n    Component Function()? loadingBuilder,\n    this.transparent = false,\n    this.maintainState = true,\n  }) : _builder = builder,\n       _loadingBuilder = loadingBuilder,\n       _renderEffect = Decorator();\n\n  /// If true, then the route below this one will continue to be rendered when\n  /// this route becomes active. If false, then this route is assumed to\n  /// completely obscure any route that would be underneath, and therefore the\n  /// route underneath doesn't need to be rendered.\n  final bool transparent;\n\n  /// If false, the route will not maintain the state of this route's page\n  /// component.  By default, once a route becomes active, the component\n  /// built by the build routine is maintained by the route after the route\n  /// is popped off the stack. Setting [maintainState] to false will drop the\n  /// page component when the route is popped off the stack.\n  final bool maintainState;\n\n  /// The name of the route (set by the [RouterComponent]).\n  String? get name => _name;\n  String? _name;\n  @internal\n  set name(String? value) => _name = value;\n\n  /// The function that will be invoked in order to build the page component\n  /// when this route first becomes active. This function may also be `null`,\n  /// in which case the user must override the [build] method.\n  final Component Function()? _builder;\n\n  /// The function that will build the loading page component, which is shown\n  /// when this route first becomes active, but hasn't fully loaded yet.\n  final Component Function()? _loadingBuilder;\n\n  /// This method is invoked when the route is pushed on top of the\n  /// [RouterComponent]'s stack.\n  ///\n  /// The argument for this method is the route that was on top of the stack\n  /// before the push. It can be null if the current route becomes the first\n  /// element of the navigation stack.\n  void onPush(Route? previousRoute) {}\n\n  /// This method is called when the route is popped off the top of the\n  /// [RouterComponent]'s stack.\n  ///\n  /// The argument for this method is the route that will become the next\n  /// top-most route on the stack. Thus, the argument in [onPop] will always be\n  /// the same as was given previously in [onPush].\n  void onPop(Route nextRoute) {}\n\n  /// Creates the page component managed by this page.\n  ///\n  /// Overriding this method is an alternative to supplying the explicit builder\n  /// function in the constructor.\n  @protected\n  Component build() {\n    assert(\n      _builder != null,\n      'Either provide `builder` in the constructor, or override the build() '\n      'method',\n    );\n    return _builder!();\n  }\n\n  /// Completely stops time for the managed page.\n  ///\n  /// When the time is stopped, the [updateTree] method of the page is not\n  /// called at all, which can save computational resources. However, this also\n  /// means that the lifecycle events on the page will not be processed, and\n  /// therefore no components will be able to be added or removed from the\n  /// page.\n  void stopTime() => timeScale = 0;\n\n  /// Resumes normal time progression for the page, if it was previously slowed\n  /// down or stopped.\n  void resumeTime() => timeScale = 1.0;\n\n  /// Applies the provided [Decorator] to the page.\n  ///\n  /// Render effects should not be confused with regular [Effect]s. Examples of\n  /// the render effects include: whole-page blur, convert into grayscale,\n  /// apply color tint, etc.\n  void addRenderEffect(Decorator effect) => _renderEffect.addLast(effect);\n\n  /// Removes current [Decorator], is any.\n  void removeRenderEffect() => _renderEffect.removeLast();\n\n  //#region Implementation methods\n\n  /// If true, the page must be rendered normally. If false, the page should\n  /// not be rendered, because it is completely obscured by another route which\n  /// is on top of it. This variable is set by the [RouterComponent].\n  @internal\n  bool isRendered = true;\n\n  /// The page that was built and is now owned by this route. This page will\n  /// also be added as a child component.\n  Component? _page;\n\n  /// The loadingPage that was built and is now owned by this route. The\n  /// [_loadingPage] will also be added as a child component.\n  Component? _loadingPage;\n\n  /// Additional visual effect that may be applied to the page during rendering.\n  final Decorator _renderEffect;\n\n  /// Invoked by the [RouterComponent] when this route is pushed to the top\n  /// of the navigation stack.\n  @internal\n  void didPush(Route? previousRoute) {\n    _page ??= build();\n    (_loadingBuilder != null) ? _addLoadingPage() : _page!.addToParent(this);\n    onPush(previousRoute);\n  }\n\n  /// Adds the [_loadingPage] to the parent and invoked by [didPush] , when\n  /// [_loadingBuilder] is specified\n  Future<void> _addLoadingPage() async {\n    (_loadingPage ??= _loadingBuilder!()).addToParent(this);\n    await _loadingPage!.loaded;\n    await add(_page!);\n    await _page!.loaded;\n    _loadingPage!.removeFromParent();\n  }\n\n  /// Invoked by the [RouterComponent] when this route is popped off the top\n  /// of the navigation stack.\n  /// If [maintainState] is false, the page component rendered by this route\n  /// is not retained when the route it popped.\n  @internal\n  void didPop(Route nextRoute) {\n    onPop(nextRoute);\n    if (!maintainState) {\n      _page?.removeFromParent();\n      _page = null;\n    }\n  }\n\n  @override\n  void renderTree(Canvas canvas) {\n    if (isRendered) {\n      _renderEffect.applyChain(super.renderTree, canvas);\n    }\n  }\n\n  @override\n  void updateTree(double dt) {\n    if (timeScale > 0) {\n      super.updateTree(dt);\n    }\n  }\n\n  @override\n  Iterable<Component> componentsAtLocation<T>(\n    T locationContext,\n    List<T>? nestedContexts,\n    T? Function(CoordinateTransform, T) transformContext,\n    bool Function(Component, T) checkContains,\n  ) {\n    if (isRendered) {\n      return super.componentsAtLocation(\n        locationContext,\n        nestedContexts,\n        transformContext,\n        checkContains,\n      );\n    } else {\n      return const Iterable<Component>.empty();\n    }\n  }\n\n  //#endregion\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/router/router_component.dart",
    "content": "import 'package:flame/src/components/core/component.dart';\nimport 'package:flame/src/components/router/overlay_route.dart';\nimport 'package:flame/src/components/router/route.dart';\nimport 'package:flame/src/components/router/value_route.dart';\nimport 'package:meta/meta.dart';\n\n/// [RouterComponent] handles transitions between multiple pages of a game.\n///\n/// The term **page** is used descriptively here: it is any full-screen (or\n/// partial-screen) component. For example: a starting page, a settings page,\n/// the main game world page, and so on. A page can also be any individual piece\n/// of UI, such as a confirmation dialog box, or a user inventory pop-up.\n///\n/// The router doesn't handle the pages directly -- instead, it operates a stack\n/// of [Route]s. Each route, in turn, manages a single page component. However,\n/// routes are lazy: they will only build their pages when they become active.\n///\n/// Internally, the router maintains a stack of Routes. In the beginning,\n/// the stack will contain the [initialRoute]. New routes can be added via the\n/// [pushNamed] method, and removed with [pop]. However, the stack must be\n/// kept non-empty: it is an error to attempt to remove the only remaining route\n/// from the stack.\n///\n/// Routes that are on the stack are mounted as components. When a route is\n/// popped, it is removed from the stack and unmounted. Routes can be either\n/// transparent or opaque. An opaque route prevents all routes below it from\n/// rendering, and also stops pointer events. In addition, routes are able to\n/// stop or slow down time for the pages that they control, or to apply visual\n/// effects (via decorators) to those pages.\nclass RouterComponent extends Component {\n  RouterComponent({\n    required this.initialRoute,\n    required Map<String, Route> routes,\n    Map<String, RouteFactory>? routeFactories,\n    this.onUnknownRoute,\n    super.key,\n    super.priority = 0x7fffffff,\n  }) : _routes = routes,\n       _routeFactories = routeFactories ?? {} {\n    routes.forEach((name, route) => route.name = name);\n  }\n\n  /// Route that will be placed on the stack in the beginning.\n  final String initialRoute;\n\n  /// The stack of all currently active routes. This stack must not be empty\n  /// (it will be populated with the [initialRoute] in the beginning).\n  ///\n  /// The routes in this list are also added to the Router as child\n  /// components. However, due to the fact that children are usually added or\n  /// removed with a delay, there could be temporary discrepancies between this\n  /// list and the list of children.\n  final List<Route> _routeStack = [];\n  @visibleForTesting\n  List<Route> get stack => _routeStack;\n\n  /// The map of all routes known to the Router, each route will have a\n  /// unique name. This map is initialized in the constructor; in addition, any\n  /// routes produced by the [_routeFactories] will also be cached here.\n  Map<String, Route> get routes => _routes;\n  final Map<String, Route> _routes;\n\n  /// Set of functions that are able to resolve routes dynamically.\n  ///\n  /// Route factories will be used to resolve pages with names like\n  /// \"prefix/arg\". For such a name, we will call the factory \"prefix\" with the\n  /// argument \"arg\". The produced route will be cached in the main [_routes]\n  /// map, and then built and mounted normally.\n  final Map<String, RouteFactory> _routeFactories;\n\n  /// Function that will be called to resolve any route names that couldn't be\n  /// resolved via [_routes] or [_routeFactories]. Unlike with routeFactories,\n  /// the route returned by this function will not be cached.\n  final RouteFactory? onUnknownRoute;\n\n  /// Returns the route that is currently at the top of the stack.\n  Route get currentRoute => _routeStack.last;\n\n  /// Returns the route that is below the current topmost route, if it exists.\n  Route? get previousRoute {\n    return _routeStack.length >= 2 ? _routeStack[_routeStack.length - 2] : null;\n  }\n\n  /// Returns whether the current route can be popped.\n  ///\n  /// Returns `true` if there are at least 2 routes in the stack, meaning the\n  /// current route can be popped without removing the last remaining route.\n  /// Returns `false` if there is only one route left, as the router must\n  /// maintain at least one route on the stack.\n  bool canPop() {\n    return _routeStack.length > 1;\n  }\n\n  /// Puts the route [name] on top of the navigation stack.\n  ///\n  /// If the route is already in the stack, it will be simply moved to the top.\n  /// Otherwise the route will be mounted and added at the top. We will also\n  /// initiate building the route's page if it hasn't been built before. If the\n  /// route is already on top of the stack, this method will do nothing.\n  ///\n  /// The method calls the [Route.didPush] callback for the newly activated\n  /// route.\n  void pushNamed(String name, {bool replace = false}) {\n    final route = _resolveRoute(name);\n    final previousRouteArgument = currentRoute;\n    if (route == currentRoute) {\n      return;\n    } else if (replace) {\n      _removeTopRoute(route);\n    }\n    if (_routeStack.contains(route)) {\n      _routeStack.remove(route);\n    } else {\n      add(route);\n    }\n    _routeStack.add(route);\n    _adjustRoutesOrder();\n    route.didPush(previousRouteArgument);\n    _adjustRoutesVisibility();\n  }\n\n  /// Pops the current route and places the route with [name] on top of the\n  /// navigation stack.\n  ///\n  /// If the route is already in the stack, it will simply be moved to the top.\n  /// Otherwise the route will be mounted and added at the top. The route's\n  /// page will also start building if it hasn't been built before. If the\n  /// route is already on top of the stack, this method will do nothing.\n  ///\n  /// This method calls the [Route.didPush] callback for the newly activated\n  /// route and also calls the [Route.didPop] callback for the popped route.\n  void pushReplacementNamed(String name) {\n    pushNamed(name, replace: true);\n  }\n\n  /// Puts a new [route] on top of the navigation stack.\n  ///\n  /// The route may also be given a [name], in which case it will be cached in\n  /// the [routes] map under this name (if there was already a route with the\n  /// same name, it will be overwritten).\n  ///\n  /// The method calls [Route.didPush] for this new route after it is added.\n  void pushRoute(Route route, {String? name, bool replace = false}) {\n    final previousRouteArgument = currentRoute;\n    if (name != null) {\n      route.name = name;\n      _routes[name] = route;\n    }\n    if (replace) {\n      _removeTopRoute(route);\n    }\n    add(route);\n    _routeStack.add(route);\n    _adjustRoutesOrder();\n    route.didPush(previousRouteArgument);\n    _adjustRoutesVisibility();\n  }\n\n  /// Pops the current route and puts a new [route] on top of the navigation\n  /// stack.\n  ///\n  /// The route may also be given a [name], in which case it will be cached in\n  /// the [routes] map under this name (if there was already a route with the\n  /// same name, it will be overwritten).\n  ///\n  /// The method calls [Route.didPush] for this new route after it is added and\n  /// also calls the [Route.didPop] callback for the popped route.\n  void pushReplacement(Route route, {String? name}) {\n    pushRoute(route, name: name, replace: true);\n  }\n\n  /// Puts the overlay route [name] on top of the navigation stack.\n  ///\n  /// If [name] was already registered as a name of an overlay route, then this\n  /// method is equivalent to [pushNamed]. If not, then a new [OverlayRoute]\n  /// will be created based on the overlay with the same name within the root\n  /// game.\n  void pushOverlay(String name) {\n    if (_routes.containsKey(name)) {\n      assert(_routes[name] is OverlayRoute, '\"$name\" is not an overlay route');\n      pushNamed(name);\n    } else {\n      pushRoute(OverlayRoute.existing(), name: name);\n    }\n  }\n\n  /// Pops the current route and puts the overlay route [name] on top of the\n  /// navigation stack.\n  ///\n  /// If [name] was already registered as a name of an overlay route, then this\n  /// method is equivalent to [pushNamed]. If not, then a new [OverlayRoute]\n  /// will be created based on the overlay with the same name within the root\n  /// game.\n  void pushReplacementOverlay(String name) {\n    if (_routes.containsKey(name)) {\n      assert(_routes[name] is OverlayRoute, '\"$name\" is not an overlay route');\n      pushReplacementNamed(name);\n    } else {\n      pushReplacement(OverlayRoute.existing(), name: name);\n    }\n  }\n\n  /// Puts [route] on top of the stack and waits until that route is popped.\n  ///\n  /// More precisely, this method returns a future that can be awaited until\n  /// the route is popped; the value returned by this future is generated by\n  /// the route as the \"return value\". This can be useful for creating dialog\n  /// boxes that take user input and then return it to the user.\n  Future<T> pushAndWait<T>(ValueRoute<T> route) {\n    pushRoute(route);\n    return route.future;\n  }\n\n  /// Removes the topmost route from the stack, and also removes it as a child\n  /// of the Router.\n  ///\n  /// The method calls [Route.didPop] for the route that was removed.\n  ///\n  /// It is an error to attempt to pop the last remaining route on the stack.\n  void pop() {\n    assert(\n      _routeStack.length > 1,\n      'Cannot pop the last route from the Router',\n    );\n    final route = _routeStack.removeLast();\n    _adjustRoutesOrder();\n    _adjustRoutesVisibility();\n    route.didPop(_routeStack.last);\n    route.removeFromParent();\n  }\n\n  /// Removes routes from the top of the stack until reaches the route with the\n  /// given [name].\n  ///\n  /// After this method, the route [name] will be at the top of the stack. An\n  /// error will occur if this method is run when there is no route with the\n  /// specified name on the stack.\n  void popUntilNamed(String name) {\n    while (currentRoute.name != name) {\n      pop();\n    }\n  }\n\n  /// Removes routes from the stack until [route] is removed. This is equivalent\n  /// to [pop] if [route] is currently on top of the stack.\n  void popRoute(Route route) {\n    while (currentRoute != route) {\n      pop();\n    }\n    pop();\n  }\n\n  /// Local method to bypass [pop]'s assert\n  void _removeTopRoute(Route nextRoute) {\n    final route = _routeStack.removeLast();\n    route.didPop(nextRoute);\n    route.removeFromParent();\n  }\n\n  /// Attempts to resolve the route with the given [name] by searching in the\n  /// [_routes] map, or invoking one of the [_routeFactories], or, lastly,\n  /// falling back to the [onUnknownRoute] function. If none of these methods\n  /// is able to produce a valid [Route], an exception will be raised.\n  Route _resolveRoute(String name) {\n    final existingRoute = _routes[name];\n    if (existingRoute != null) {\n      return existingRoute;\n    }\n    if (name.contains('/')) {\n      final i = name.indexOf('/');\n      final factoryName = name.substring(0, i);\n      final factory = _routeFactories[factoryName];\n      if (factory != null) {\n        final argument = name.substring(i + 1);\n        final generatedRoute = factory(argument)..name = name;\n        _routes[name] = generatedRoute;\n        return generatedRoute;\n      }\n    }\n    if (onUnknownRoute != null) {\n      return onUnknownRoute!(name)..name = name;\n    }\n    throw ArgumentError('Route \"$name\" could not be resolved by the Router');\n  }\n\n  void _adjustRoutesOrder() {\n    for (var i = 0; i < _routeStack.length; i++) {\n      _routeStack[i].priority = i;\n    }\n  }\n\n  void _adjustRoutesVisibility() {\n    var render = true;\n    for (var i = _routeStack.length - 1; i >= 0; i--) {\n      _routeStack[i].isRendered = render;\n      render &= _routeStack[i].transparent;\n    }\n  }\n\n  @override\n  void onMount() {\n    super.onMount();\n    final route = _resolveRoute(initialRoute);\n    _routeStack.add(route);\n    add(route);\n    route.didPush(null);\n  }\n}\n\ntypedef RouteFactory = Route Function(String parameter);\n"
  },
  {
    "path": "packages/flame/lib/src/components/router/value_route.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/src/components/router/route.dart';\nimport 'package:flame/src/components/router/router_component.dart';\nimport 'package:meta/meta.dart';\n\n/// [ValueRoute] is a special route that \"returns a value\" when popped.\n///\n/// This class requires to be derived, and therefore it doesn't have the regular\n/// `.builder` function -- override the [build] method instead.\n///\n/// This route is used in conjunction with [RouterComponent.pushAndWait]. The\n/// return value must be supplied using the [completeWith] method from the\n/// derived class. Usually this method will be invoked by the component\n/// constructed in the [build] method.\n///\n/// If the route is popped without invoking [completeWith], then the\n/// [_defaultValue] will be used.\nabstract class ValueRoute<T> extends Route {\n  ValueRoute({required T value, super.transparent})\n    : _defaultValue = value,\n      _completer = Completer<T>(),\n      super(null);\n\n  final T _defaultValue;\n  final Completer<T> _completer;\n\n  /// Future that will complete when this route is popped from the stack.\n  Future<T> get future => _completer.future;\n\n  void completeWith(T value) {\n    _completer.complete(value);\n    parent.popRoute(this);\n  }\n\n  void complete() => completeWith(_defaultValue);\n\n  @mustCallSuper\n  @override\n  void didPop(Route nextRoute) {\n    if (!_completer.isCompleted) {\n      _completer.complete(_defaultValue);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/router/world_route.dart",
    "content": "import 'package:flame/camera.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/rendering.dart';\nimport 'package:meta/meta.dart';\n\n/// [WorldRoute] is a class that allows setting the world that a camera is\n/// looking at.\nclass WorldRoute extends Route {\n  /// A world route that uses the specified [builder]. This builder will be\n  /// registered with the Game's map of world builders when this route is\n  /// first activated.\n  ///\n  /// The [camera] parameter is optional and can be used to set the camera\n  /// that will be used to render the world, if not provided the default camera\n  /// will be used.\n  WorldRoute(this.builder, {this.camera, super.maintainState}) : super(null);\n\n  final World Function() builder;\n  final CameraComponent? camera;\n  late World? _previousWorld;\n  World? world;\n\n  Game get game => findGame()!;\n\n  @override\n  String get name => super.name!;\n\n  @override\n  World build() {\n    if (!maintainState) {\n      world = builder();\n      return world!;\n    } else {\n      return world ??= builder();\n    }\n  }\n\n  @internal\n  @override\n  void didPush(Route? previousRoute) => onPush(previousRoute);\n\n  @mustCallSuper\n  @override\n  void onPush(Route? previousRoute) {\n    assert(\n      camera != null || game is FlameGame,\n      'You need to either provide a camera or use a FlameGame to use the '\n      'WorldRoute',\n    );\n    if (camera != null) {\n      _previousWorld = camera?.world;\n      camera?.world = build();\n    } else {\n      _previousWorld = (game as FlameGame).world;\n      (game as FlameGame).world = build();\n    }\n  }\n\n  @mustCallSuper\n  @override\n  void onPop(Route nextRoute) {\n    if (camera != null) {\n      camera?.world = _previousWorld;\n    } else {\n      (game as FlameGame).world = _previousWorld ?? World();\n    }\n  }\n\n  @override\n  @internal\n  void addRenderEffect(Decorator effect) => UnimplementedError(\n    'WorldRoute does not support render effects',\n  );\n\n  @override\n  @internal\n  void removeRenderEffect() => UnimplementedError(\n    'WorldRoute does not support render effects',\n  );\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/scroll_text_box_component.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/text.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/painting.dart';\n\n/// [ScrollTextBoxComponent] configures the layout and interactivity of a\n/// scrollable text box.\n/// It focuses on the box's size, scrolling mechanics, padding, and alignment,\n/// contrasting with [TextRenderer], which handles text appearance like font and\n/// color.\n///\n/// This component uses [TextBoxComponent] to provide scrollable text\n/// capabilities.\nclass ScrollTextBoxComponent<T extends TextRenderer> extends PositionComponent {\n  late final _ScrollTextBoxComponent<T> _scrollTextBoxComponent;\n  late final ValueNotifier<int> newLineNotifier;\n\n  /// Constructor for [ScrollTextBoxComponent].\n  /// - [size]: Specifies the size of the text box.\n  ///   Must have positive dimensions.\n  /// - [text]: The text content to be displayed.\n  /// - [textRenderer]: Handles the rendering of the text.\n  /// - [boxConfig]: Configuration for the text box appearance.\n  /// - [onComplete]: Callback will be executed after all text is displayed.\n  /// - Other parameters include alignment, pixel ratio, and positioning\n  ///   settings.\n  /// An assertion ensures that the [size] has positive dimensions.\n  ScrollTextBoxComponent({\n    required Vector2 size,\n    String? text,\n    T? textRenderer,\n    TextBoxConfig? boxConfig,\n    Anchor align = Anchor.topLeft,\n    double pixelRatio = 1.0,\n    super.position,\n    super.scale,\n    double angle = 0.0,\n    super.anchor = Anchor.topLeft,\n    super.priority,\n    super.key,\n    List<Component>? children,\n    void Function()? onComplete,\n  }) : assert(\n         size.x > 0 && size.y > 0,\n         'size must have positive dimensions: $size',\n       ),\n       super(size: size) {\n    final marginTop = boxConfig?.margins.top ?? 0;\n    final marginBottom = boxConfig?.margins.bottom ?? 0;\n    final innerMargins = EdgeInsets.fromLTRB(0, marginTop, 0, marginBottom);\n\n    var resolvedBoxConfig = boxConfig ?? const TextBoxConfig();\n    resolvedBoxConfig = TextBoxConfig(\n      timePerChar: resolvedBoxConfig.timePerChar,\n      dismissDelay: resolvedBoxConfig.dismissDelay,\n      growingBox: resolvedBoxConfig.growingBox,\n      maxWidth: size.x,\n      margins: EdgeInsets.fromLTRB(\n        resolvedBoxConfig.margins.left,\n        0,\n        resolvedBoxConfig.margins.right,\n        0,\n      ),\n    );\n    _scrollTextBoxComponent = _ScrollTextBoxComponent<T>(\n      text: text,\n      textRenderer: textRenderer,\n      boxConfig: resolvedBoxConfig,\n      align: align,\n      pixelRatio: pixelRatio,\n      onComplete: onComplete,\n    );\n    newLineNotifier = _scrollTextBoxComponent.newLineNotifier;\n\n    _scrollTextBoxComponent.setOwnerComponent = this;\n    // Integrates the [ClipComponent] for managing\n    // the text box's scrollable area.\n    add(\n      ClipComponent.rectangle(\n        size: size - Vector2(0, innerMargins.vertical),\n        position: Vector2(0, innerMargins.top),\n        angle: angle,\n        scale: scale,\n        priority: priority,\n        children: children,\n      )..add(_scrollTextBoxComponent),\n    );\n  }\n\n  /// Override this method to provide a custom background to the text box.\n  ///\n  /// Note: The background is designed to stretch across the entire scrollable\n  /// area of the text box. This ensures that as the user scrolls through the\n  /// text, the background moves in sync with the text. As an alternative,\n  /// consider adding [ScrollTextBoxComponent] to a [SpriteComponent].\n  void drawBackground(Canvas canvas) {}\n}\n\n/// Private class handling the internal workings of [ScrollTextBoxComponent].\n///\n/// Extends [TextBoxComponent] and incorporates drag callbacks for text\n/// scrolling. It manages the rendering and user interaction for the text within\n/// the box.\nclass _ScrollTextBoxComponent<T extends TextRenderer> extends TextBoxComponent\n    with DragCallbacks {\n  double scrollBoundsY = 0.0;\n\n  late final ClipComponent clipComponent;\n\n  late ScrollTextBoxComponent<TextRenderer> _owner;\n\n  bool _isOnCompleteExecuted = false;\n\n  _ScrollTextBoxComponent({\n    String? text,\n    T? textRenderer,\n    TextBoxConfig? boxConfig,\n    Anchor super.align = Anchor.topLeft,\n    double super.pixelRatio = 1.0,\n    super.position,\n    super.scale,\n    double super.angle = 0.0,\n    super.onComplete,\n  }) : super(\n         text: text ?? '',\n         textRenderer: textRenderer ?? TextPaint(),\n         boxConfig: boxConfig ?? const TextBoxConfig(),\n       );\n\n  @override\n  Future<void> onLoad() {\n    clipComponent = parent! as ClipComponent;\n    newLinePositionNotifier.addListener(() {\n      if (newLinePositionNotifier.value > clipComponent.size.y) {\n        position.y = -newLinePositionNotifier.value + clipComponent.size.y;\n      }\n    });\n    return super.onLoad();\n  }\n\n  @override\n  void update(double dt) {\n    if (!_isOnCompleteExecuted && finished) {\n      _isOnCompleteExecuted = true;\n      scrollBoundsY = clipComponent.size.y - size.y;\n    }\n\n    super.update(dt);\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    if (finished && scrollBoundsY < 0) {\n      position.y += event.localDelta.y;\n      position.y = position.y.clamp(scrollBoundsY, 0);\n    }\n  }\n\n  @override\n  void drawBackground(Canvas canvas) {\n    _owner.drawBackground(canvas);\n  }\n\n  set setOwnerComponent(ScrollTextBoxComponent scrollTextBoxComponent) {\n    _owner = scrollTextBoxComponent;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/spawn_component.dart",
    "content": "import 'dart:async';\nimport 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/math.dart';\n\n/// {@template spawn_component}\n/// The [SpawnComponent] is a non-visual component which can spawn\n/// [PositionComponent]s randomly within a set [area]. If [area] is not set it\n/// will use the size of the nearest ancestor that provides a size.\n/// [period] will set the static time interval for when it will spawn new\n/// components.\n/// If you want to use a non static time interval, use the\n/// [SpawnComponent.periodRange] constructor.\n/// If you want to set the position of the spawned components yourself inside of\n/// the [factory], set [selfPositioning] to true.\n/// You can either provide a factory that returns one component or a\n/// multiFactory which returns a list of components. In this case the amount\n/// parameter will be increased by the number of returned components.\n/// {@endtemplate}\nclass SpawnComponent extends Component {\n  /// {@macro spawn_component}\n  SpawnComponent({\n    required double period,\n    PositionComponent Function(int amount)? factory,\n    List<PositionComponent> Function(int amount)? multiFactory,\n    this.target,\n    this.spawnCount,\n    this.area,\n    this.within = true,\n    this.selfPositioning = false,\n    this.autoStart = true,\n    this.spawnWhenLoaded = false,\n    Random? random,\n    super.key,\n  }) : assert(\n         !(selfPositioning && area != null),\n         \"Don't set an area when you are using selfPositioning=true\",\n       ),\n       assert(\n         (factory != null) ^ (multiFactory != null),\n         'You need to provide either a factory or a multiFactory, not both.',\n       ),\n       _period = period,\n       multiFactory = multiFactory ?? _wrapFactory(factory!),\n       _random = random ?? randomFallback;\n\n  /// Use this constructor if you want your components to spawn within an\n  /// interval time range.\n  /// [minPeriod] will be the minimum amount of time before the next component\n  /// spawns and [maxPeriod] will be the maximum amount of time before it\n  /// spawns.\n  SpawnComponent.periodRange({\n    required double this.minPeriod,\n    required double this.maxPeriod,\n    PositionComponent Function(int amount)? factory,\n    List<PositionComponent> Function(int amount)? multiFactory,\n    this.target,\n    this.spawnCount,\n    this.area,\n    this.within = true,\n    this.selfPositioning = false,\n    this.autoStart = true,\n    this.spawnWhenLoaded = false,\n    Random? random,\n    super.key,\n  }) : assert(\n         !(selfPositioning && area != null),\n         \"Don't set an area when you are using selfPositioning=true\",\n       ),\n       _period =\n           minPeriod +\n           (random ?? randomFallback).nextDouble() * (maxPeriod - minPeriod),\n       multiFactory = multiFactory ?? _wrapFactory(factory!),\n       _random = random ?? randomFallback;\n\n  /// The function used to create a new component to spawn.\n  ///\n  /// [amount] is the amount of components that the [SpawnComponent] has spawned\n  /// so far.\n  ///\n  /// Be aware: internally the component uses a factory that creates a list of\n  /// components.\n  /// If you have set such a factory it was wrapped to create a list. The\n  /// factory getter wraps it again to return the first element of the list and\n  /// fails when the list is empty!\n  PositionComponent Function(int amount) get factory => (int amount) {\n    final result = multiFactory.call(amount);\n    assert(\n      result.isNotEmpty,\n      'The factory call yielded no result, which is required when calling'\n      ' the single result factory',\n    );\n    return result.elementAt(0);\n  };\n\n  set factory(PositionComponent Function(int amount) newFactory) {\n    multiFactory = _wrapFactory(newFactory);\n  }\n\n  static List<PositionComponent> Function(int amount) _wrapFactory(\n    PositionComponent Function(int amount) newFactory,\n  ) {\n    return (int amount) => [newFactory.call(amount)];\n  }\n\n  /// The function used to create new components to spawn.\n  ///\n  /// [amount] is the amount of components that the [SpawnComponent] has spawned\n  /// so far.\n  List<PositionComponent> Function(int amount) multiFactory;\n\n  /// The area where the components should be spawned.\n  Shape? area;\n\n  /// The component that the spawned components should be added to.\n  ///\n  /// If not set, the components will be added to the parent of the\n  /// [SpawnComponent].\n  Component? target;\n\n  /// The amount of components that should be spawned until the [SpawnComponent]\n  /// is removed from its parent.\n  ///\n  /// Do note that it is possible to overshoot the [spawnCount] for one tick if\n  /// the [multiFactory] returns more components than expected.\n  int? spawnCount;\n\n  /// Whether the random point should be within the [area] or along its edges.\n  bool within;\n\n  /// Whether the spawned components positions shouldn't be given a position,\n  /// so that they can continue to have the position that they had after they\n  /// came out of the [factory].\n  bool selfPositioning;\n\n  /// The timer that is used to control when components are spawned.\n  late final Timer timer;\n\n  /// The time between each component is spawned.\n  double get period => _period;\n  set period(double newPeriod) {\n    _period = newPeriod;\n    timer.limit = _period;\n  }\n\n  double _period;\n\n  /// The minimum amount of time that has to pass until the next component is\n  /// spawned.\n  double? minPeriod;\n\n  /// The maximum amount of time that has to pass until the next component is\n  /// spawned.\n  double? maxPeriod;\n\n  /// Whether it is spawning components within a random time frame or at a\n  /// static rate.\n  bool get hasRandomPeriod => minPeriod != null;\n\n  final Random _random;\n\n  /// The amount of spawned components.\n  int amount = 0;\n\n  /// Whether the timer automatically starts or not.\n  final bool autoStart;\n\n  /// Whether the timer should start when the [SpawnComponent] is loaded.\n  final bool spawnWhenLoaded;\n\n  @override\n  FutureOr<void> onLoad() async {\n    if (area == null && !selfPositioning) {\n      final Vector2? maybeProvidedPosition;\n      if (target != null) {\n        if (target is ReadOnlyPositionProvider) {\n          maybeProvidedPosition = (target! as PositionProvider).position;\n        } else {\n          maybeProvidedPosition = Vector2.zero();\n        }\n      } else {\n        maybeProvidedPosition = null;\n      }\n      final targetPosition =\n          maybeProvidedPosition ??\n          ancestors().whereType<PositionProvider>().firstOrNull?.position ??\n          Vector2.zero();\n\n      final Vector2? maybeProvidedSize;\n      if (target != null) {\n        assert(\n          target is ReadOnlySizeProvider,\n          'The SpawnComponent needs a target with a size if area is not '\n          'provided.',\n        );\n        maybeProvidedSize = (target! as PositionProvider).position;\n      } else {\n        maybeProvidedSize = null;\n      }\n      final targetSize =\n          maybeProvidedSize ??\n          ancestors().whereType<ReadOnlySizeProvider>().firstOrNull?.size ??\n          Vector2.zero();\n      assert(\n        !targetSize.isZero(),\n        'The SpawnComponent needs an ancestor or target with a size if area is '\n        'not provided.',\n      );\n      area = Rectangle.fromLTWH(\n        targetPosition.x,\n        targetPosition.y,\n        targetSize.x,\n        targetSize.y,\n      );\n    }\n\n    void updatePeriod() {\n      if (hasRandomPeriod) {\n        period = minPeriod! + _random.nextDouble() * (maxPeriod! - minPeriod!);\n      }\n    }\n\n    final timerComponent = TimerComponent(\n      period: _period,\n      repeat: true,\n      onTick: () {\n        final components = multiFactory(amount);\n        if (!selfPositioning) {\n          for (final component in components) {\n            component.position = area!.randomPoint(\n              random: _random,\n              within: within,\n            );\n          }\n        }\n        (target ?? parent)?.addAll(components);\n        updatePeriod();\n        amount += components.length;\n        if (spawnCount != null && amount >= spawnCount!) {\n          timer.stop();\n          removeFromParent();\n        }\n      },\n      autoStart: autoStart,\n      tickWhenLoaded: spawnWhenLoaded,\n    );\n    timer = timerComponent.timer;\n    add(timerComponent);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/sprite_animation_component.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/src/sprite_animation_ticker.dart';\nimport 'package:meta/meta.dart';\n\nexport '../sprite_animation.dart';\n\nclass SpriteAnimationComponent extends PositionComponent with HasPaint {\n  /// The animation ticker used for updating [animation].\n  SpriteAnimationTicker? _animationTicker;\n\n  /// Returns the animation ticker for current [animation].\n  SpriteAnimationTicker? get animationTicker => _animationTicker;\n\n  /// If the component should be removed once the animation has finished.\n  /// Needs the animation to have `loop = false` to ever remove the component,\n  /// since it will never finish otherwise.\n  bool removeOnFinish;\n\n  /// Whether the animation is paused or playing.\n  bool playing;\n\n  /// Whether to reset the animation when the component is removed from the\n  /// component tree.\n  bool resetOnRemove;\n\n  /// When set to true, the component is auto-resized to match the\n  /// size of current animation sprite.\n  bool _autoResize;\n\n  /// Creates a component with an empty animation which can be set later\n  SpriteAnimationComponent({\n    SpriteAnimation? animation,\n    bool? autoResize,\n    this.removeOnFinish = false,\n    this.playing = true,\n    this.resetOnRemove = false,\n    Paint? paint,\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.nativeAngle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.key,\n  }) : assert(\n         (size == null) == (autoResize ?? size == null),\n         '''If size is set, autoResize should be false or size should be null when autoResize is true.''',\n       ),\n       _autoResize = autoResize ?? size == null,\n       _animationTicker = animation?.createTicker() {\n    if (paint != null) {\n      this.paint = paint;\n    }\n\n    /// Register a listener to differentiate between size modification done by\n    /// external calls v/s the ones done by [_resizeToSprite].\n    size.addListener(_handleAutoResizeState);\n    _resizeToSprite();\n  }\n\n  /// Creates a SpriteAnimationComponent from a [size], an [image] and [data].\n  /// Check [SpriteAnimationData] for more info on the available options.\n  ///\n  /// Optionally [removeOnFinish] can be set to true to have this component be\n  /// auto removed from the FlameGame when the animation is finished.\n  SpriteAnimationComponent.fromFrameData(\n    Image image,\n    SpriteAnimationData data, {\n    bool? autoResize,\n    bool removeOnFinish = false,\n    bool playing = true,\n    bool resetOnRemove = false,\n    Paint? paint,\n    Vector2? position,\n    Vector2? size,\n    Vector2? scale,\n    double? angle,\n    double nativeAngle = 0,\n    Anchor? anchor,\n    Iterable<Component>? children,\n    int? priority,\n    ComponentKey? key,\n  }) : this(\n         animation: SpriteAnimation.fromFrameData(image, data),\n         autoResize: autoResize,\n         removeOnFinish: removeOnFinish,\n         playing: playing,\n         resetOnRemove: resetOnRemove,\n         paint: paint,\n         position: position,\n         size: size,\n         scale: scale,\n         angle: angle,\n         nativeAngle: nativeAngle,\n         anchor: anchor,\n         children: children,\n         priority: priority,\n         key: key,\n       );\n\n  /// Returns current value of auto resize flag.\n  bool get autoResize => _autoResize;\n\n  /// Sets the given value of autoResize flag. Will update the [size]\n  /// to fit srcSize of current sprite if set to  true.\n  set autoResize(bool value) {\n    _autoResize = value;\n    _resizeToSprite();\n  }\n\n  /// This flag helps in detecting if the size modification is done by\n  /// some external call vs [_autoResize]ing code from [_resizeToSprite].\n  bool _isAutoResizing = false;\n\n  /// Returns the current [SpriteAnimation].\n  SpriteAnimation? get animation => _animationTicker?.spriteAnimation;\n\n  /// Sets the given [value] as current [animation].\n  set animation(SpriteAnimation? value) {\n    if (animation != value) {\n      if (value != null) {\n        _animationTicker = value.createTicker();\n      } else {\n        _animationTicker = null;\n      }\n      _resizeToSprite();\n    }\n  }\n\n  @mustCallSuper\n  @override\n  void render(Canvas canvas) {\n    _animationTicker?.getSprite().render(\n      canvas,\n      size: size,\n      overridePaint: paint,\n    );\n  }\n\n  @mustCallSuper\n  @override\n  void update(double dt) {\n    if (playing) {\n      _animationTicker?.update(dt);\n      _resizeToSprite();\n    }\n    if (removeOnFinish && (_animationTicker?.done() ?? false)) {\n      removeFromParent();\n    }\n  }\n\n  /// Updates the size to current [animation] sprite's srcSize if\n  /// [autoResize] is true.\n  void _resizeToSprite() {\n    if (_autoResize) {\n      _isAutoResizing = true;\n\n      final newX = _animationTicker?.getSprite().srcSize.x ?? 0;\n      final newY = _animationTicker?.getSprite().srcSize.y ?? 0;\n\n      // Modify only if changed.\n      if (size.x != newX || size.y != newY) {\n        size.setValues(newX, newY);\n      }\n\n      _isAutoResizing = false;\n    }\n  }\n\n  /// Turns off [_autoResize]ing if a size modification is done by user.\n  void _handleAutoResizeState() {\n    if (_autoResize && (!_isAutoResizing)) {\n      _autoResize = false;\n    }\n  }\n\n  @override\n  void onRemove() {\n    super.onRemove();\n    if (resetOnRemove) {\n      _animationTicker?.reset();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/sprite_animation_group_component.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/src/sprite_animation_ticker.dart';\nimport 'package:flutter/foundation.dart';\n\nexport '../sprite_animation.dart';\n\nclass SpriteAnimationGroupComponent<T> extends PositionComponent with HasPaint {\n  /// Key with the current playing animation\n  T? _current;\n\n  ValueNotifier<T?>? _currentAnimationNotifier;\n\n  /// A [ValueNotifier] that notifies when the current animation changes.\n  ValueNotifier<T?> get currentAnimationNotifier =>\n      _currentAnimationNotifier ??= ValueNotifier<T?>(_current);\n\n  /// Map with the mapping each state to the flag removeOnFinish\n  final Map<T, bool> removeOnFinish;\n\n  /// Map with the available states for this animation group\n  Map<T, SpriteAnimation>? _animations;\n\n  /// Map containing animation tickers for each animation state.\n  Map<T, SpriteAnimationTicker>? _animationTickers;\n\n  /// Whether the animation is paused or playing.\n  bool playing;\n\n  /// When set to true, the component is auto-resized to match the\n  /// size of current animation sprite.\n  bool _autoResize;\n\n  /// Whether the current animation's ticker should reset to the beginning\n  /// when it becomes current.\n  bool autoResetTicker;\n\n  /// Creates a component with an empty animation which can be set later\n  SpriteAnimationGroupComponent({\n    Map<T, SpriteAnimation>? animations,\n    T? current,\n    bool? autoResize,\n    this.playing = true,\n    this.removeOnFinish = const {},\n    this.autoResetTicker = true,\n    Paint? paint,\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.nativeAngle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.key,\n  }) : assert(\n         (size == null) == (autoResize ?? size == null),\n         '''If size is set, autoResize should be false or size should be null when autoResize is true.''',\n       ),\n       _current = current,\n       _animations = animations,\n       _autoResize = autoResize ?? size == null,\n       _animationTickers = animations != null\n           ? Map.fromEntries(\n               animations.entries\n                   .map((e) => MapEntry(e.key, e.value.createTicker()))\n                   .toList(),\n             )\n           : null {\n    if (paint != null) {\n      this.paint = paint;\n    }\n\n    /// Register a listener to differentiate between size modification done by\n    /// external calls v/s the ones done by [_resizeToSprite].\n    size.addListener(_handleAutoResizeState);\n    _resizeToSprite();\n  }\n\n  /// Creates a SpriteAnimationGroupComponent from a [size], an [image] and\n  /// [data].\n  /// Check [SpriteAnimationData] for more info on the available options.\n  ///\n  /// Optionally [removeOnFinish] can be mapped to true to have this component\n  /// be auto removed from the FlameGame when the animation is finished.\n  SpriteAnimationGroupComponent.fromFrameData(\n    Image image,\n    Map<T, SpriteAnimationData> data, {\n    T? current,\n    bool? autoResize,\n    bool playing = true,\n    Map<T, bool> removeOnFinish = const {},\n    bool autoResetTicker = true,\n    Paint? paint,\n    Vector2? position,\n    Vector2? size,\n    Vector2? scale,\n    double? angle,\n    double nativeAngle = 0,\n    Anchor? anchor,\n    int? priority,\n    ComponentKey? key,\n  }) : this(\n         animations: data.map((key, value) {\n           return MapEntry(\n             key,\n             SpriteAnimation.fromFrameData(\n               image,\n               value,\n             ),\n           );\n         }),\n         current: current,\n         autoResize: autoResize,\n         removeOnFinish: removeOnFinish,\n         autoResetTicker: autoResetTicker,\n         playing: playing,\n         paint: paint,\n         position: position,\n         size: size,\n         scale: scale,\n         angle: angle,\n         anchor: anchor,\n         nativeAngle: nativeAngle,\n         priority: priority,\n         key: key,\n       );\n\n  SpriteAnimation? get animation => _animations?[current];\n  SpriteAnimationTicker? get animationTicker => _animationTickers?[current];\n\n  /// Returns the current group state.\n  T? get current => _current;\n\n  /// The group state to given state.\n  ///\n  /// Will update [size] if [autoResize] is true.\n  set current(T? value) {\n    assert(_animations != null, 'Animations not set');\n    assert(\n      _animations!.keys.contains(value),\n      'Animation not found for key: $value',\n    );\n\n    final changed = value != current;\n    _current = value;\n    _resizeToSprite();\n\n    if (changed) {\n      if (autoResetTicker) {\n        animationTicker?.reset();\n      }\n      _currentAnimationNotifier?.value = value;\n    }\n  }\n\n  /// Returns the map of animation state and their corresponding animations.\n  ///\n  /// If you want to change the contents of the map use the animations setter\n  /// and pass in a new map of animations.\n  Map<T, SpriteAnimation>? get animations =>\n      _animations != null ? Map.unmodifiable(_animations!) : null;\n\n  /// Sets the given [value] as new animation state map.\n  set animations(Map<T, SpriteAnimation>? value) {\n    if (_animations != value) {\n      _animations = value;\n\n      _animationTickers = _animations != null\n          ? Map.fromEntries(\n              _animations!.entries\n                  .map((e) => MapEntry(e.key, e.value.createTicker()))\n                  .toList(),\n            )\n          : null;\n      _resizeToSprite();\n    }\n  }\n\n  /// Returns a map containing [SpriteAnimationTicker] for each state.\n  Map<T, SpriteAnimationTicker>? get animationTickers => _animationTickers;\n\n  /// Returns current value of auto resize flag.\n  bool get autoResize => _autoResize;\n\n  /// Sets the given value of autoResize flag.\n  ///\n  /// Will update the [size] to fit srcSize of current animation sprite if set\n  /// to  true.\n  set autoResize(bool value) {\n    _autoResize = value;\n    _resizeToSprite();\n  }\n\n  /// This flag helps in detecting if the size modification is done by\n  /// some external call vs [_autoResize]ing code from [_resizeToSprite].\n  bool _isAutoResizing = false;\n\n  @mustCallSuper\n  @override\n  void render(Canvas canvas) {\n    animationTicker?.getSprite().render(\n      canvas,\n      size: size,\n      overridePaint: paint,\n    );\n  }\n\n  @mustCallSuper\n  @override\n  void update(double dt) {\n    if (playing) {\n      animationTicker?.update(dt);\n      _resizeToSprite();\n    }\n    if ((removeOnFinish[current] ?? false) &&\n        (animationTicker?.done() ?? false)) {\n      removeFromParent();\n    }\n  }\n\n  /// Updates the size to current animation sprite's srcSize if\n  /// [autoResize] is true.\n  void _resizeToSprite() {\n    if (_autoResize) {\n      _isAutoResizing = true;\n\n      final newX = animationTicker?.getSprite().srcSize.x ?? 0;\n      final newY = animationTicker?.getSprite().srcSize.y ?? 0;\n\n      // Modify only if changed.\n      if (size.x != newX || size.y != newY) {\n        size.setValues(newX, newY);\n      }\n\n      _isAutoResizing = false;\n    }\n  }\n\n  /// Turns off [_autoResize]ing if a size modification is done by user.\n  void _handleAutoResizeState() {\n    if (_autoResize && (!_isAutoResizing)) {\n      _autoResize = false;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/sprite_batch_component.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/components/core/component.dart';\nimport 'package:flame/src/sprite_batch.dart';\nimport 'package:meta/meta.dart';\n\nclass SpriteBatchComponent extends Component {\n  SpriteBatch? spriteBatch;\n  BlendMode? blendMode;\n  Rect? cullRect;\n  Paint? paint;\n\n  /// Creates a component with an empty sprite batch which can be set later\n  SpriteBatchComponent({\n    this.spriteBatch,\n    this.blendMode,\n    this.cullRect,\n    this.paint,\n    super.key,\n    super.children,\n    super.priority,\n  });\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    assert(\n      spriteBatch != null,\n      'You have to set spriteBatch in either the constructor or in onLoad',\n    );\n  }\n\n  @mustCallSuper\n  @override\n  void render(Canvas canvas) {\n    spriteBatch?.render(\n      canvas,\n      blendMode: blendMode,\n      cullRect: cullRect,\n      paint: paint,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/sprite_component.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:meta/meta.dart';\n\nexport '../sprite.dart';\n\n/// A [PositionComponent] that renders a single [Sprite] at the designated\n/// position, scaled to have the designated size and rotated to the specified\n/// angle.\n///\n/// This a commonly used subclass of [Component].\nclass SpriteComponent extends PositionComponent with HasPaint {\n  /// When set to true, the component is auto-resized to match the\n  /// size of underlying sprite.\n  bool _autoResize;\n\n  /// The [sprite] to be rendered by this component.\n  Sprite? _sprite;\n\n  /// Creates a component with an empty sprite which can be set later\n  SpriteComponent({\n    Sprite? sprite,\n    bool? autoResize,\n    Paint? paint,\n    super.position,\n    Vector2? size,\n    super.scale,\n    super.angle,\n    super.nativeAngle,\n    super.anchor,\n    super.children,\n    super.priority,\n    this.bleed,\n    super.key,\n  }) : assert(\n         (size == null) == (autoResize ?? size == null),\n         '''If size is set, autoResize should be false or size should be null when autoResize is true.''',\n       ),\n       _autoResize = autoResize ?? size == null,\n       _sprite = sprite,\n       super(size: size ?? sprite?.srcSize) {\n    if (paint != null) {\n      this.paint = paint;\n    }\n\n    /// Register a listener to differentiate between size modification done by\n    /// external calls v/s the ones done by [_resizeToSprite].\n    this.size.addListener(_handleAutoResizeState);\n  }\n\n  SpriteComponent.fromImage(\n    Image image, {\n    Vector2? srcPosition,\n    Vector2? srcSize,\n    bool? autoResize,\n    Paint? paint,\n    Vector2? position,\n    Vector2? size,\n    Vector2? scale,\n    double? angle,\n    double nativeAngle = 0,\n    Anchor? anchor,\n    Iterable<Component>? children,\n    int? priority,\n    ComponentKey? key,\n    double? bleed,\n  }) : this(\n         sprite: Sprite(\n           image,\n           srcPosition: srcPosition,\n           srcSize: srcSize,\n         ),\n         autoResize: autoResize,\n         paint: paint,\n         position: position,\n         size: size,\n         scale: scale,\n         angle: angle,\n         nativeAngle: nativeAngle,\n         anchor: anchor,\n         children: children,\n         priority: priority,\n         bleed: bleed,\n         key: key,\n       );\n\n  /// Returns current value of auto resize flag.\n  bool get autoResize => _autoResize;\n\n  /// Sets the given value of autoResize flag. Will update the [size]\n  /// to fit srcSize of [sprite] if set to  true.\n  set autoResize(bool value) {\n    _autoResize = value;\n    _resizeToSprite();\n  }\n\n  /// This flag helps in detecting if the size modification is done by\n  /// some external call vs [_autoResize]ing code from [_resizeToSprite].\n  bool _isAutoResizing = false;\n\n  /// Returns the current sprite rendered by this component.\n  Sprite? get sprite => _sprite;\n\n  /// Sets the given sprite as the new [sprite] of this component.\n  /// Will update the size if [autoResize] is set to true.\n  set sprite(Sprite? value) {\n    _sprite = value;\n    _resizeToSprite();\n  }\n\n  double? bleed;\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    assert(\n      sprite != null,\n      'You have to set the sprite in either the constructor or in onLoad',\n    );\n  }\n\n  @mustCallSuper\n  @override\n  void render(Canvas canvas) {\n    sprite?.render(\n      canvas,\n      size: size,\n      overridePaint: paint,\n      bleed: bleed,\n    );\n  }\n\n  /// Updates the size [sprite]'s srcSize if [autoResize] is true.\n  void _resizeToSprite() {\n    if (_autoResize) {\n      _isAutoResizing = true;\n\n      final newX = _sprite?.srcSize.x ?? 0;\n      final newY = _sprite?.srcSize.y ?? 0;\n\n      // Modify only if changed.\n      if (size.x != newX || size.y != newY) {\n        size.setValues(newX, newY);\n      }\n\n      _isAutoResizing = false;\n    }\n  }\n\n  /// Turns off [_autoResize]ing if a size modification is done by user.\n  void _handleAutoResizeState() {\n    if (_autoResize && (!_isAutoResizing)) {\n      _autoResize = false;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/sprite_group_component.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flutter/foundation.dart';\n\nexport '../sprite_animation.dart';\n\n/// A [PositionComponent] that can have multiple [Sprite]s and render\n/// the one mapped with the [current] key.\nclass SpriteGroupComponent<T> extends PositionComponent with HasPaint {\n  /// Key for the current sprite.\n  T? _current;\n\n  ValueNotifier<T?>? _currentSpriteNotifier;\n\n  /// A [ValueNotifier] that notifies when the current sprite changes.\n  ValueNotifier<T?> get currentSpriteNotifier =>\n      _currentSpriteNotifier ??= ValueNotifier<T?>(_current);\n\n  /// Map with the available states for this sprite group.\n  Map<T, Sprite>? _sprites;\n\n  /// When set to true, the component is auto-resized to match the\n  /// size of current sprite.\n  bool _autoResize;\n\n  /// Creates a component with an empty animation which can be set later.\n  SpriteGroupComponent({\n    Map<T, Sprite>? sprites,\n    T? current,\n    bool? autoResize,\n    Paint? paint,\n    super.position,\n    Vector2? size,\n    super.scale,\n    super.angle,\n    super.nativeAngle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.key,\n  }) : assert(\n         (size == null) == (autoResize ?? size == null),\n         '''If size is set, autoResize should be false or size should be null when autoResize is true.''',\n       ),\n       _current = current,\n       _sprites = sprites,\n       _autoResize = autoResize ?? size == null,\n       super(size: size ?? sprites?[current]?.srcSize) {\n    if (paint != null) {\n      this.paint = paint;\n    }\n\n    /// Register a listener to differentiate between size modification done by\n    /// external calls v/s the ones done by [_resizeToSprite].\n    this.size.addListener(_handleAutoResizeState);\n  }\n\n  Sprite? get sprite => _sprites?[current];\n\n  /// Returns the current group state.\n  T? get current => _current;\n\n  /// The group state to given state.\n  ///\n  /// Will update [size] if [autoResize] is true.\n  set current(T? value) {\n    assert(_sprites != null, 'Sprites not set');\n    assert(_sprites!.keys.contains(value), 'Sprite not found for key: $value');\n\n    final changed = _current != value;\n    _current = value;\n    _resizeToSprite();\n    if (changed) {\n      _currentSpriteNotifier?.value = value;\n    }\n  }\n\n  /// Returns current value of auto resize flag.\n  bool get autoResize => _autoResize;\n\n  /// Returns the sprites map.\n  ///\n  /// If you want to change the contents of the map use the sprites setter\n  /// and pass in a new map of sprites, or use [updateSprite] to update a\n  /// specific sprite.\n  Map<T, Sprite>? get sprites =>\n      _sprites != null ? Map.unmodifiable(_sprites!) : null;\n\n  /// Sets the given [value] as sprites map.\n  set sprites(Map<T, Sprite>? value) {\n    if (_sprites != value) {\n      _sprites = value;\n      _resizeToSprite();\n    }\n  }\n\n  /// Updates the sprite for the given key.\n  void updateSprite(T key, Sprite sprite) {\n    assert(\n      _sprites != null,\n      'This call can only be made after the sprites map has been initialized, '\n      'which should be done in either the constructor or in onLoad.',\n    );\n    _sprites?[key] = sprite;\n    _resizeToSprite();\n  }\n\n  /// Sets the given value of autoResize flag.\n  ///\n  /// Will update the [size] to fit srcSize of\n  /// current [sprite] if set to  true.\n  set autoResize(bool value) {\n    _autoResize = value;\n    _resizeToSprite();\n  }\n\n  /// This flag helps in detecting if the size modification is done by\n  /// some external call vs [_autoResize]ing code from [_resizeToSprite].\n  bool _isAutoResizing = false;\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    assert(\n      _sprites != null,\n      'You have to set the sprites in either the constructor or in onLoad',\n    );\n    assert(\n      _current != null,\n      'You have to set current in either the constructor or in onLoad',\n    );\n  }\n\n  @mustCallSuper\n  @override\n  void render(Canvas canvas) {\n    sprite?.render(\n      canvas,\n      size: size,\n      overridePaint: paint,\n    );\n  }\n\n  /// Updates the size to current [sprite]'s srcSize if [autoResize] is true.\n  void _resizeToSprite() {\n    if (_autoResize) {\n      _isAutoResizing = true;\n\n      final newX = sprite?.srcSize.x ?? 0;\n      final newY = sprite?.srcSize.y ?? 0;\n\n      // Modify only if changed.\n      if (size.x != newX || size.y != newY) {\n        size.setValues(newX, newY);\n      }\n\n      _isAutoResizing = false;\n    }\n  }\n\n  /// Turns off [_autoResize]ing if a size modification is done by user.\n  void _handleAutoResizeState() {\n    if (_autoResize && (!_isAutoResizing)) {\n      _autoResize = false;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/text_box_component.dart",
    "content": "import 'dart:async';\nimport 'dart:math' as math;\nimport 'dart:math';\nimport 'dart:ui';\n\nimport 'package:collection/collection.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame/text.dart';\nimport 'package:flutter/widgets.dart' hide Image;\nimport 'package:meta/meta.dart';\n\n/// A set of configurations for the [TextBoxComponent] itself, as opposed to\n/// the [TextRenderer], which contains the configuration for how to render the\n/// text only (font size, color, family, etc).\nclass TextBoxConfig {\n  /// Max width this paragraph can take. Lines will be broken trying to respect\n  /// word boundaries in as many lines as necessary.\n  final double maxWidth;\n\n  /// Margins of the text box with respect to the [PositionComponent.size].\n  final EdgeInsets margins;\n\n  /// Defaults to 0. If not zero, the characters will appear one-by-one giving\n  /// a typing effect to the text box, and this will be the delay in seconds\n  /// between each character.\n  final double timePerChar;\n\n  /// Defaults to null. If not null, this component will disappear after this\n  /// many seconds after being fully typed out.\n  final double? dismissDelay;\n\n  /// Only relevant if [timePerChar] is set. If true, the box will start with\n  /// the size to fit the first character and grow as more lines are typed.\n  /// If false, the box will start with the full necessary size from the\n  /// beginning (both width and height).\n  final bool growingBox;\n\n  const TextBoxConfig({\n    this.maxWidth = 200.0,\n    this.margins = const EdgeInsets.all(8.0),\n    this.timePerChar = 0.0,\n    this.dismissDelay,\n    this.growingBox = false,\n  });\n\n  TextBoxConfig copyWith({\n    double? maxWidth,\n    EdgeInsets? margins,\n    double? timePerChar,\n    double? dismissDelay,\n    bool? growingBox,\n  }) {\n    return TextBoxConfig(\n      maxWidth: maxWidth ?? this.maxWidth,\n      margins: margins ?? this.margins,\n      timePerChar: timePerChar ?? this.timePerChar,\n      dismissDelay: dismissDelay ?? this.dismissDelay,\n      growingBox: growingBox ?? this.growingBox,\n    );\n  }\n}\n\nclass TextBoxComponent<T extends TextRenderer> extends TextComponent {\n  static final Paint _imagePaint = BasicPalette.white.paint()\n    ..filterQuality = FilterQuality.medium;\n  TextBoxConfig _boxConfig;\n\n  TextBoxConfig get boxConfig => _boxConfig;\n\n  set boxConfig(TextBoxConfig value) {\n    final oldConfig = _boxConfig;\n    _boxConfig = value;\n    if (oldConfig.maxWidth != value.maxWidth) {\n      // This call is necessary to reflow the text for any change in\n      // TextBoxConfig maxWidth.\n      updateBounds();\n    }\n    redraw();\n  }\n\n  final double pixelRatio;\n\n  @visibleForTesting\n  final List<String> lines = [];\n  double _maxLineWidth = 0.0;\n  late double _lineHeight;\n  late int _totalLines;\n\n  double _lifeTime = 0.0;\n  int? _previousChar;\n\n  @visibleForTesting\n  Image? cache;\n\n  /// Notifies when a new line is rendered.\n  final ValueNotifier<int> newLineNotifier = ValueNotifier<int>(0);\n\n  // Notifies when a new line is rendered with the position of the new line.\n  @internal\n  final ValueNotifier<double> newLinePositionNotifier = ValueNotifier<double>(\n    0,\n  );\n\n  double _currentLinePosition = 0.0;\n  bool _isOnCompleteExecuted = false;\n\n  /// Callback function to be executed after all text is displayed.\n  void Function()? onComplete;\n\n  double get lineHeight => _lineHeight;\n\n  TextBoxComponent({\n    super.text,\n    T? super.textRenderer,\n    TextBoxConfig? boxConfig,\n    Anchor? align,\n    double? pixelRatio,\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n    this.onComplete,\n    super.key,\n  }) : _boxConfig = boxConfig ?? const TextBoxConfig(),\n       _fixedSize = size != null,\n       align = align ?? Anchor.topLeft,\n       pixelRatio =\n           pixelRatio ??\n           PlatformDispatcher.instance.views.first.devicePixelRatio;\n\n  /// Alignment of the text within its bounding box.\n  ///\n  /// This property combines both the horizontal and vertical alignment. For\n  /// example, setting this property to `Align.center` will make the text\n  /// centered inside its box. Similarly, `Align.bottomRight` will render the\n  /// text that's aligned to the right and to the bottom of the box.\n  ///\n  /// Custom alignment anchors are supported too. For example, if this property\n  /// is set to `Anchor(0.1, 0)`, then the text would be positioned such that\n  /// its every line will have 10% of whitespace on the left, and 90% on the\n  /// right. You can use an `AnchorEffect` to make the text gradually transition\n  /// between different alignment values.\n  Anchor align;\n\n  /// If true, the size of the component will remain fixed. If false, the size\n  /// will expand or shrink to the fit the text.\n  ///\n  /// This property is set to true if the user has explicitly specified [size]\n  /// in the constructor.\n  final bool _fixedSize;\n\n  @override\n  set text(String value) {\n    if (text != value) {\n      super.text = value;\n      // This ensures that the component will redraw on next update\n      _previousChar = -1;\n    }\n  }\n\n  @override\n  @mustCallSuper\n  Future<void> onLoad() {\n    return redraw();\n  }\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    if (cache == null) {\n      redraw();\n    }\n  }\n\n  @override\n  @internal\n  void updateBounds() {\n    lines.clear();\n    final maxBoxWidth = _fixedSize ? width : boxConfig.maxWidth;\n    final availableWidth = maxBoxWidth - boxConfig.margins.horizontal;\n\n    // Use Flutter's TextPainter for proper text layout when available.\n    // This correctly handles CJK languages and other scripts that don't\n    // use spaces for word boundaries.\n    if (textRenderer is TextPaint) {\n      _updateBoundsWithTextPainter(availableWidth);\n    } else {\n      // Fallback to the original space-based splitting for other renderers\n      _updateBoundsWordBased(availableWidth);\n    }\n\n    size = _recomputeSize();\n  }\n\n  void _updateBoundsWithTextPainter(double availableWidth) {\n    final textPaint = textRenderer as TextPaint;\n    final textPainter = textPaint.toTextPainter(text);\n    textPainter.layout(maxWidth: availableWidth);\n\n    // Extract lines from the laid out text using line boundaries\n    final lineMetricsList = textPainter.computeLineMetrics();\n    var lineHeight = 0.0;\n    var offset = 0;\n\n    while (offset < text.length) {\n      final range = textPainter.getLineBoundary(TextPosition(offset: offset));\n      var lineText = text.substring(range.start, range.end);\n\n      // Don't include the trailing newline character in the line text\n      if (lineText.endsWith('\\n')) {\n        lineText = lineText.substring(0, lineText.length - 1);\n      }\n\n      lines.add(lineText);\n\n      // Update max width based on actual line width\n      final actualLineMetrics = textRenderer.getLineMetrics(lineText);\n      _updateMaxWidth(actualLineMetrics.width);\n\n      // Move to the next line\n      offset = range.end;\n      if (offset < text.length && text[offset] == '\\n') {\n        offset++; // Skip the newline character\n      }\n    }\n\n    // Get line height from the metrics\n    if (lineMetricsList.isNotEmpty) {\n      lineHeight = lineMetricsList.first.height;\n    }\n\n    _lineHeight = lineHeight;\n    _totalLines = lines.length;\n  }\n\n  void _updateBoundsWordBased(double availableWidth) {\n    var lineHeight = 0.0;\n    for (final word in text.split(' ')) {\n      final wordLines = word.split('\\n');\n      final possibleLine = lines.isEmpty\n          ? wordLines[0]\n          : '${lines.last} ${wordLines[0]}';\n      final metrics = textRenderer.getLineMetrics(possibleLine);\n      lineHeight = max(lineHeight, metrics.height);\n\n      _updateMaxWidth(metrics.width);\n      final bool canAppend;\n      if (metrics.width <= availableWidth) {\n        canAppend = lines.isNotEmpty;\n      } else {\n        canAppend = lines.isNotEmpty && lines.last == '';\n      }\n\n      if (canAppend) {\n        lines.last = '${lines.last} ${wordLines[0]}';\n        wordLines.removeAt(0);\n        if (wordLines.isNotEmpty) {\n          lines.addAll(wordLines);\n        }\n      } else {\n        lines.addAll(wordLines);\n      }\n    }\n    _totalLines = lines.length;\n    _lineHeight = lineHeight;\n  }\n\n  void _updateMaxWidth(double w) {\n    if (w > _maxLineWidth) {\n      _maxLineWidth = w;\n    }\n  }\n\n  double get totalCharTime => text.length * boxConfig.timePerChar;\n\n  bool get finished =>\n      _lifeTime >= totalCharTime + (boxConfig.dismissDelay ?? 0);\n\n  int get _actualTextLength {\n    return lines.map((e) => e.length).sum;\n  }\n\n  /// The index of the character that should be appearing given the fact that\n  /// [TextBoxComponent] can build up its text per character at the rate\n  /// dictated by [TextBoxConfig.timePerChar] in [boxConfig].\n  /// If the timePerChar is 0, then returns the last possible character index.\n  int get currentChar => boxConfig.timePerChar == 0.0\n      ? _actualTextLength\n      : math.min(_lifeTime ~/ boxConfig.timePerChar, _actualTextLength);\n\n  /// The index of the line that [currentChar] belongs to.\n  int get currentLine {\n    var totalCharCount = 0;\n    final cachedCurrentChar = currentChar;\n    for (var i = 0; i < lines.length; i++) {\n      totalCharCount += lines[i].length;\n      if (totalCharCount > cachedCurrentChar) {\n        return i;\n      }\n    }\n    return lines.length - 1;\n  }\n\n  double getLineWidth(String line, int charCount) {\n    return textRenderer\n        .getLineMetrics(\n          line.substring(0, math.min(charCount, line.length)),\n        )\n        .width;\n  }\n\n  Vector2 _recomputeSize() {\n    if (_fixedSize) {\n      return size;\n    } else if (boxConfig.growingBox) {\n      var i = 0;\n      var totalCharCount = 0;\n      final cachedCurrentChar = currentChar;\n      final cachedCurrentLine = currentLine;\n      final textWidth = lines\n          .sublist(0, cachedCurrentLine + 1)\n          .map((line) {\n            final charCount = (i < cachedCurrentLine)\n                ? line.length\n                : (cachedCurrentChar - totalCharCount);\n            totalCharCount += line.length;\n            i++;\n            return getLineWidth(line, charCount);\n          })\n          .reduce(math.max);\n      return Vector2(\n        textWidth + boxConfig.margins.horizontal,\n        _lineHeight * lines.length + boxConfig.margins.vertical,\n      );\n    } else {\n      return Vector2(\n        boxConfig.maxWidth + boxConfig.margins.horizontal,\n        _lineHeight * _totalLines + boxConfig.margins.vertical,\n      );\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    if (cache == null) {\n      return;\n    }\n    canvas.save();\n    canvas.scale(1 / pixelRatio);\n    canvas.drawImage(cache!, Offset.zero, _imagePaint);\n    canvas.restore();\n  }\n\n  Future<Image> _fullRenderAsImage(Vector2 size) {\n    final recorder = PictureRecorder();\n    final scaledSize = size * pixelRatio;\n    final c = Canvas(recorder, scaledSize.toRect());\n    c.scale(pixelRatio);\n    _fullRender(c);\n    return recorder.endRecording().toImageSafe(\n      scaledSize.x.ceil(),\n      scaledSize.y.ceil(),\n    );\n  }\n\n  /// Override this method to provide a custom background to the text box.\n  void drawBackground(Canvas c) {}\n\n  void _fullRender(Canvas canvas) {\n    drawBackground(canvas);\n\n    final nLines = currentLine + 1;\n    final boxWidth = size.x - boxConfig.margins.horizontal;\n    final boxHeight = size.y - boxConfig.margins.vertical;\n    var charCount = 0;\n    for (var i = 0; i < nLines; i++) {\n      var line = lines[i];\n      if (i == nLines - 1) {\n        final nChars = math.min(currentChar - charCount, line.length);\n        line = line.substring(0, nChars);\n      }\n\n      final textElement = textRenderer.format(line);\n      final metrics = textElement.metrics;\n\n      final position = Vector2(\n        boxConfig.margins.left + (boxWidth - metrics.width) * align.x,\n        boxConfig.margins.top +\n            (boxHeight - nLines * _lineHeight) * align.y +\n            i * _lineHeight,\n      );\n      textElement.render(canvas, position);\n      if (position.y > _currentLinePosition) {\n        _currentLinePosition = position.y;\n        newLineNotifier.value = newLineNotifier.value + 1;\n        newLinePositionNotifier.value = _currentLinePosition + _lineHeight;\n      }\n      charCount += lines[i].length;\n    }\n  }\n\n  final Set<Image> cachedToRemove = {};\n\n  Future<void> redraw() async {\n    final newSize = _recomputeSize();\n    final cachedImage = cache;\n    if (cachedImage != null && !cachedToRemove.contains(cachedImage)) {\n      cachedToRemove.add(cachedImage);\n      // Do not dispose of the cached image immediately, since it may have been\n      // sent into the rendering pipeline where it is still pending to be used.\n      // See issue #1618 for details.\n      Future.delayed(const Duration(milliseconds: 100), () {\n        cachedToRemove.remove(cachedImage);\n        if (isMounted) {\n          cachedImage.dispose();\n        }\n      });\n    }\n    cache = await _fullRenderAsImage(newSize);\n    size = newSize;\n  }\n\n  @override\n  void update(double dt) {\n    _lifeTime += dt;\n    if (_previousChar != currentChar) {\n      redraw();\n    }\n    _previousChar = currentChar;\n\n    if (finished) {\n      if (!_isOnCompleteExecuted) {\n        _isOnCompleteExecuted = true;\n        onComplete?.call();\n      }\n      if (boxConfig.dismissDelay != null) {\n        removeFromParent();\n      }\n    }\n  }\n\n  @override\n  @mustCallSuper\n  void onRemove() {\n    super.onRemove();\n    cache?.dispose();\n    cache = null;\n  }\n\n  /// Force [TextBoxComponent] to display [text] in its entirety.\n  ///\n  /// It is possible that in the future, one may want to revert timePerChar or\n  /// even the old [boxConfig] value to its previous value once [onComplete]\n  /// is called. Such a case might be when a new value of [text] is set.\n  /// However, this is non-trivial task, so this implementation is intentionally\n  /// kept simple.\n  /// If this behavior is needed, the user can simply add the code for setting\n  /// [boxConfig] by themselves in [onComplete].\n  void skip() {\n    boxConfig = boxConfig.copyWith(timePerChar: 0);\n  }\n\n  /// Rewind the typewriter effect to start from the first character again.\n  ///\n  /// Useful for reusing this component when changing lines so that the next\n  /// line does not show immediately.\n  /// Also resets the [onComplete] call state which will be called again\n  /// when the line is finished.\n  void resetAnimation() {\n    _lifeTime = 0;\n    _isOnCompleteExecuted = false;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/text_component.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/text.dart';\nimport 'package:flutter/painting.dart';\nimport 'package:meta/meta.dart';\n\nclass TextComponent<T extends TextRenderer> extends PositionComponent {\n  TextComponent({\n    String? text,\n    T? textRenderer,\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.key,\n  }) : _text = text ?? '',\n       _textRenderer = textRenderer ?? TextRendererFactory.createDefault<T>() {\n    updateBounds();\n  }\n\n  String get text => _text;\n  String _text;\n  set text(String text) {\n    if (_text != text) {\n      _text = text;\n      updateBounds();\n    }\n  }\n\n  T get textRenderer => _textRenderer;\n  T _textRenderer;\n  set textRenderer(T textRenderer) {\n    _textRenderer = textRenderer;\n    updateBounds();\n  }\n\n  late InlineTextElement _textElement;\n\n  @internal\n  void updateBounds() {\n    _textElement = _textRenderer.format(_text);\n    final measurements = _textElement.metrics;\n    _textElement.translate(0, measurements.ascent);\n    size.setValues(measurements.width, measurements.height);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    _textElement.draw(canvas);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/text_element_component.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/text.dart';\n\nclass TextElementComponent extends PositionComponent {\n  final Vector2? documentSize;\n  TextElement element;\n\n  TextElementComponent({\n    required this.element,\n    this.documentSize,\n    super.size,\n    super.position,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.key,\n  });\n\n  factory TextElementComponent.fromDocument({\n    required DocumentRoot document,\n    DocumentStyle? style,\n    Vector2? position,\n    Vector2? size,\n    Vector2? scale,\n    double? angle,\n    Anchor? anchor,\n    List<Component>? children,\n    int priority = 0,\n    ComponentKey? key,\n  }) {\n    final effectiveStyle = style ?? DocumentStyle();\n    final effectiveSize = _coalesceSize(effectiveStyle, size);\n    final element = document.format(\n      effectiveStyle,\n      width: effectiveSize.x,\n      height: effectiveSize.y,\n    );\n    final box = element.boundingBox;\n    return TextElementComponent(\n      element: element,\n      position: position,\n      documentSize: effectiveSize,\n      size: box.bottomRight.toVector2(),\n      scale: scale,\n      angle: angle,\n      anchor: anchor,\n      children: children,\n      priority: priority,\n      key: key,\n    );\n  }\n\n  @override\n  void render(Canvas canvas) {\n    element.draw(canvas);\n  }\n\n  static Vector2 _coalesceSize(DocumentStyle style, Vector2? size) {\n    final width = style.width ?? size?.x;\n    final height = style.height ?? size?.y;\n    if (width == null || height == null) {\n      throw ArgumentError('Either style.width or size.x must be provided.');\n    }\n    if ((style.width != null && style.width != width) ||\n        (size?.x != null && size?.x != width)) {\n      throw ArgumentError(\n        'style.width and size.x, if both provided, must match.',\n      );\n    }\n    if ((style.height != null && style.height != height) ||\n        (size?.y != null && size?.y != height)) {\n      throw ArgumentError(\n        'style.height and size.y, if both provided, must match.',\n      );\n    }\n    return Vector2(width, height);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/components/timer_component.dart",
    "content": "import 'dart:async';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:meta/meta.dart';\n\n/// A component that uses a [Timer] instance which you can react to when it has\n/// finished.\nclass TimerComponent extends Component {\n  late final Timer timer;\n  final bool removeOnFinish;\n  final VoidCallback? _onTick;\n  final bool tickWhenLoaded;\n\n  /// Creates a [TimerComponent]\n  ///\n  /// [period] The period of time in seconds that the tick will be called\n  /// [repeat] When true, this will continue running after [period] is reached\n  /// [autoStart] When true, will start upon instantiation (default is true)\n  /// [onTick] When provided, will be called every time [period] is reached.\n  /// This overrides the [onTick] method\n  /// [tickWhenLoaded] When true, will call [onTick] when the component is first\n  /// loaded (default is false).\n  /// [tickCount] The number of time the timer will tick before stopping.\n  /// This is is only used when [repeat] is true. If null,\n  /// the timer will run indefinitely.\n  TimerComponent({\n    required double period,\n    bool repeat = false,\n    bool autoStart = true,\n    this.removeOnFinish = false,\n    VoidCallback? onTick,\n    this.tickWhenLoaded = false,\n    int? tickCount,\n    super.key,\n  }) : _onTick = onTick {\n    timer = Timer(\n      period,\n      repeat: repeat,\n      onTick: this.onTick,\n      autoStart: autoStart,\n      tickCount: tickCount,\n    );\n  }\n\n  @override\n  @mustCallSuper\n  FutureOr<void> onLoad() async {\n    await super.onLoad();\n\n    if (tickWhenLoaded) {\n      onTick();\n    }\n  }\n\n  /// Called every time the [timer] reached a tick.\n  /// The default implementation calls the closure received on the\n  /// constructor and can be overridden to add custom logic.\n  void onTick() {\n    _onTick?.call();\n  }\n\n  @override\n  void update(double dt) {\n    timer.update(dt);\n\n    if (removeOnFinish && timer.finished) {\n      removeFromParent();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/device.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\n\n/// Provides methods for controlling the device (e.g. setting the screen to\n/// full-screen).\n///\n/// To use this class, access it via Flame.device.\nclass Device {\n  void _warnIfDesktop(String source) {\n    assert(() {\n      if (!kIsWeb &&\n          [\n            TargetPlatform.linux,\n            TargetPlatform.macOS,\n            TargetPlatform.windows,\n          ].contains(defaultTargetPlatform)) {\n        // ignore: avoid_print\n        print(\n          'Warning: $source is not supported on desktop platforms. '\n          'It will be a no-op.',\n        );\n      }\n      return true;\n    }());\n  }\n\n  /// Sets the app to be full-screen (no buttons, bar or notifications on top).\n  Future<void> fullScreen() {\n    _warnIfDesktop('fullScreen');\n    return SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);\n  }\n\n  /// Restore the UI mode to the default ([SystemUiMode.edgeToEdge).\n  Future<void> restoreFullscreen() {\n    _warnIfDesktop('restoreFullscreen');\n    return SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);\n  }\n\n  /// Sets the preferred orientation (landscape or portrait) for the app.\n  ///\n  /// When it opens, it will automatically change orientation to the preferred\n  /// one (if possible) depending on the physical orientation of the device.\n  Future<void> setOrientation(DeviceOrientation orientation) {\n    return SystemChrome.setPreferredOrientations(\n      <DeviceOrientation>[orientation],\n    );\n  }\n\n  /// Sets the preferred orientations (landscape left, right, portrait up, or\n  /// down) for the app.\n  ///\n  /// When it opens, it will automatically change orientation to the preferred\n  /// one (if possible) depending on the physical orientation of the device.\n  Future<void> setOrientations(List<DeviceOrientation> orientations) {\n    return SystemChrome.setPreferredOrientations(orientations);\n  }\n\n  /// Sets the preferred orientation of the app to landscape only.\n  ///\n  /// When it opens, it will automatically change orientation to the preferred\n  /// one (if possible).\n  Future<void> setLandscape() {\n    return setOrientations(<DeviceOrientation>[\n      DeviceOrientation.landscapeLeft,\n      DeviceOrientation.landscapeRight,\n    ]);\n  }\n\n  /// Sets the preferred orientation of the app to\n  /// `DeviceOrientation.landscapeLeft` only.\n  ///\n  /// When it opens, it will automatically change orientation to the preferred\n  /// one (if possible).\n  Future<void> setLandscapeLeftOnly() {\n    return setOrientation(DeviceOrientation.landscapeLeft);\n  }\n\n  /// Sets the preferred orientation of the app to\n  /// `DeviceOrientation.landscapeRight` only.\n  ///\n  /// When it opens, it will automatically change orientation to the preferred\n  /// one (if possible).\n  Future<void> setLandscapeRightOnly() {\n    return setOrientation(DeviceOrientation.landscapeRight);\n  }\n\n  /// Sets the preferred orientation of the app to portrait only.\n  ///\n  /// When it opens, it will automatically change orientation to the preferred\n  /// one (if possible).\n  Future<void> setPortrait() {\n    return setOrientations(<DeviceOrientation>[\n      DeviceOrientation.portraitUp,\n      DeviceOrientation.portraitDown,\n    ]);\n  }\n\n  /// Sets the preferred orientation of the app to\n  /// `DeviceOrientation.portraitUp` only.\n  ///\n  /// When it opens, it will automatically change orientation to the preferred\n  /// one (if possible).\n  Future<void> setPortraitUpOnly() {\n    return setOrientation(DeviceOrientation.portraitUp);\n  }\n\n  /// Sets the preferred orientation of the app to\n  /// `DeviceOrientation.portraitDown` only.\n  ///\n  /// When it opens, it will automatically change orientation to the preferred\n  /// one (if possible).\n  Future<void> setPortraitDownOnly() {\n    return setOrientation(DeviceOrientation.portraitDown);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/devtools/connectors/component_count_connector.dart",
    "content": "import 'dart:convert';\nimport 'dart:developer';\n\nimport 'package:flame/src/devtools/dev_tools_connector.dart';\n\n/// The [ComponentCountConnector] is responsible for reporting the component\n/// count of the game to the devtools extension.\nclass ComponentCountConnector extends DevToolsConnector {\n  @override\n  void init() {\n    // Get the amount of components in the tree.\n    registerExtension(\n      'ext.flame_devtools.getComponentCount',\n      (method, parameters) async {\n        var componentCount = 0;\n        game.propagateToChildren((_) {\n          componentCount++;\n          return true;\n        });\n\n        return ServiceExtensionResponse.result(\n          json.encode({\n            'component_count': componentCount,\n          }),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/devtools/connectors/component_snapshot_connector.dart",
    "content": "import 'dart:convert';\nimport 'dart:developer';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/src/devtools/dev_tools_connector.dart';\n\nclass ComponentSnapshotConnector extends DevToolsConnector {\n  @override\n  void init() {\n    registerExtension(\n      'ext.flame_devtools.getComponentSnapshot',\n      (method, parameters) async {\n        Image? image;\n        final id = int.tryParse(parameters['id'] ?? '');\n        game.propagateToChildren<Component>(\n          (c) {\n            if (c.hashCode == id) {\n              final pictureRecorder = PictureRecorder();\n\n              final canvas = Canvas(pictureRecorder);\n\n              // I am not sure how we could calculate the size of a component\n              // that isn't a PositionComponent, so for now we will just use\n              // an arbitrary size.\n              var width = 100;\n              var height = 100;\n\n              if (c is PositionComponent) {\n                width = c.width.toInt();\n                height = c.height.toInt();\n\n                // Translate the canvas so that the component is\n                // drawn at the 0,0\n                canvas.translate(-c.x, -c.y);\n              }\n\n              c.renderTree(canvas);\n\n              final picture = pictureRecorder.endRecording();\n\n              image = picture.toImageSync(width, height);\n\n              return false;\n            }\n            return true;\n          },\n        );\n\n        if (image != null) {\n          final byteData = await image!.toByteData(format: ImageByteFormat.png);\n          final buffer = byteData!.buffer.asUint8List();\n          final snapshot = base64Encode(buffer);\n          return ServiceExtensionResponse.result(\n            json.encode({\n              'id': id,\n              'snapshot': snapshot,\n            }),\n          );\n        }\n\n        return ServiceExtensionResponse.result(\n          json.encode({\n            'id': id,\n            'snapshot': '',\n          }),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/devtools/connectors/component_tree_connector.dart",
    "content": "import 'dart:convert';\nimport 'dart:developer';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/src/devtools/dev_tools_connector.dart';\n\n/// The [ComponentTreeConnector] is responsible for reporting the component\n/// tree of the game to the devtools extension.\nclass ComponentTreeConnector extends DevToolsConnector {\n  @override\n  void init() {\n    // Get the component tree of the game.\n    registerExtension(\n      'ext.flame_devtools.getComponentTree',\n      (method, parameters) async {\n        final componentTree = ComponentTreeNode.fromComponent(game);\n\n        return ServiceExtensionResponse.result(\n          json.encode({\n            'component_tree': componentTree.toJson(),\n          }),\n        );\n      },\n    );\n  }\n}\n\n/// This should only be used internally by the devtools extension.\nclass ComponentTreeNode {\n  final int id;\n  final String name;\n  final String toStringText;\n  final bool isPositionComponent;\n  final List<ComponentTreeNode> children;\n\n  ComponentTreeNode(\n    this.id,\n    this.name,\n    this.toStringText,\n    // ignore: avoid_positional_boolean_parameters\n    this.isPositionComponent,\n    this.children,\n  );\n\n  ComponentTreeNode.fromComponent(Component component)\n    : this(\n        component.hashCode,\n        component.runtimeType.toString(),\n        component.toString(),\n        component is PositionComponent,\n        component.children.map(ComponentTreeNode.fromComponent).toList(),\n      );\n\n  ComponentTreeNode.fromJson(Map<String, dynamic> json)\n    : this(\n        json['id'] as int,\n        json['name'] as String,\n        json['toString'] as String,\n        json['isPositionComponent'] as bool,\n        (json['children'] as List)\n            .map((e) => ComponentTreeNode.fromJson(e as Map<String, dynamic>))\n            .toList(),\n      );\n\n  Map<String, dynamic> toJson() {\n    return {\n      'id': id,\n      'name': name,\n      'toString': toStringText,\n      'isPositionComponent': isPositionComponent,\n      'children': children.map((e) => e.toJson()).toList(),\n    };\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/devtools/connectors/debug_mode_connector.dart",
    "content": "import 'dart:convert';\nimport 'dart:developer';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/src/devtools/dev_tools_connector.dart';\n\n/// The [DebugModeConnector] is responsible for reporting and setting the\n/// `debugMode` of the game from the devtools extension.\nclass DebugModeConnector extends DevToolsConnector {\n  @override\n  void init() {\n    // Get the `debugMode` for a component in the tree.\n    // If no id is provided, the `debugMode` for the entire game will be\n    // returned.\n    registerExtension(\n      'ext.flame_devtools.getDebugMode',\n      (method, parameters) async {\n        final id = int.tryParse(parameters['id'] ?? '') ?? game.hashCode;\n        return ServiceExtensionResponse.result(\n          json.encode({\n            'id': id,\n            'debug_mode': _getDebugMode(id),\n          }),\n        );\n      },\n    );\n\n    // Set the `debugMode` for a component in the tree.\n    // If no id is provided, the `debugMode` will be set for the entire game.\n    registerExtension(\n      'ext.flame_devtools.setDebugMode',\n      (method, parameters) async {\n        final id = int.tryParse(parameters['id'] ?? '');\n        final debugMode = bool.parse(parameters['debug_mode'] ?? 'false');\n        _setDebugMode(debugMode, id: id);\n        return ServiceExtensionResponse.result(\n          json.encode({\n            'id': id,\n            'debug_mode': debugMode,\n          }),\n        );\n      },\n    );\n  }\n\n  bool _getDebugMode(int id) {\n    var debugMode = false;\n    game.propagateToChildren<Component>(\n      (c) {\n        if (c.hashCode == id) {\n          debugMode = c.debugMode;\n          return false;\n        }\n        return true;\n      },\n      includeSelf: true,\n    );\n    return debugMode;\n  }\n\n  void _setDebugMode(bool debugMode, {int? id}) {\n    game.propagateToChildren<Component>(\n      (c) {\n        if (id == null) {\n          c.debugMode = debugMode;\n          return true;\n        } else if (c.hashCode == id) {\n          c.debugMode = debugMode;\n          return false;\n        }\n        return true;\n      },\n      includeSelf: true,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/devtools/connectors/game_loop_connector.dart",
    "content": "import 'dart:convert';\nimport 'dart:developer';\n\nimport 'package:flame/game.dart';\nimport 'package:flame/src/devtools/dev_tools_connector.dart';\nimport 'package:flutter/foundation.dart';\n\n/// The [GameLoopConnector] is responsible for reporting and setting the\n/// pause/running state of the game and stepping the game forwards or backwards\n/// from the devtools extension.\nclass GameLoopConnector extends DevToolsConnector {\n  var _pauseNotifier = ValueNotifier<bool>(true);\n\n  @override\n  void init() {\n    // Get the current `debugMode`.\n    registerExtension(\n      'ext.flame_devtools.getPaused',\n      (method, parameters) async {\n        return ServiceExtensionResponse.result(\n          json.encode({\n            'paused': _pauseNotifier.value,\n          }),\n        );\n      },\n    );\n\n    // Set whether the game should be paused or not.\n    registerExtension(\n      'ext.flame_devtools.setPaused',\n      (method, parameters) async {\n        final shouldPause = bool.parse(parameters['paused'] ?? 'false');\n        _pauseNotifier.value = shouldPause;\n        return ServiceExtensionResponse.result(\n          json.encode({\n            'paused': shouldPause,\n          }),\n        );\n      },\n    );\n\n    // Set whether the game should be paused or not.\n    registerExtension(\n      'ext.flame_devtools.step',\n      (method, parameters) async {\n        final stepTime = double.parse(parameters['step_time'] ?? '0');\n        game.stepEngine(stepTime: stepTime);\n        return ServiceExtensionResponse.result(\n          json.encode({\n            'step_time': stepTime,\n          }),\n        );\n      },\n    );\n  }\n\n  @override\n  void initGame(FlameGame game) {\n    super.initGame(game);\n    _pauseNotifier = ValueNotifier<bool>(game.paused);\n    _pauseNotifier.addListener(() {\n      final newPaused = _pauseNotifier.value;\n      if (newPaused) {\n        game.pauseEngine();\n      } else {\n        game.resumeEngine();\n      }\n    });\n  }\n\n  @override\n  void disposeGame() {\n    _pauseNotifier.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/devtools/connectors/overlay_navigation_connector.dart",
    "content": "import 'dart:convert';\nimport 'dart:developer';\n\nimport 'package:flame/src/devtools/dev_tools_connector.dart';\n\n/// The [OverlayNavigationConnector] is responsible of getting the names of all\n/// registered overlays and navigating to the overlay with the given name.\nclass OverlayNavigationConnector extends DevToolsConnector {\n  @override\n  void init() {\n    // Get the names of all registered overlays\n    registerExtension(\n      'ext.flame_devtools.getOverlays',\n      (method, parameters) async {\n        return ServiceExtensionResponse.result(\n          json.encode({\n            'overlays': game.overlays.registeredOverlays,\n          }),\n        );\n      },\n    );\n\n    // Navigate to the overlay with the given name\n    registerExtension(\n      'ext.flame_devtools.navigateToOverlay',\n      (method, parameters) async {\n        const invalidParamsError = -32602;\n\n        final overlayName = parameters['overlay'];\n\n        if (overlayName == null) {\n          return ServiceExtensionResponse.error(\n            invalidParamsError,\n            'Missing overlay parameter',\n          );\n        }\n\n        if (!game.overlays.registeredOverlays.contains(overlayName)) {\n          return ServiceExtensionResponse.error(\n            invalidParamsError,\n            'Unknown overlay: $overlayName',\n          );\n        }\n\n        game.overlays.clear();\n        game.overlays.add(overlayName);\n        return ServiceExtensionResponse.result(json.encode({'success': true}));\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/devtools/connectors/position_component_attributes_connector.dart",
    "content": "import 'dart:convert';\nimport 'dart:developer';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/src/devtools/dev_tools_connector.dart';\n\nclass PositionComponentAttributesConnector extends DevToolsConnector {\n  @override\n  void init() {\n    registerExtension(\n      'ext.flame_devtools.getPositionComponentAttributes',\n      (method, parameters) async {\n        final id = int.tryParse(parameters['id'] ?? '');\n\n        final positionComponent = findComponent<PositionComponent>(id);\n\n        if (positionComponent != null) {\n          return ServiceExtensionResponse.result(\n            json.encode({\n              'id': id,\n              'x': positionComponent.x,\n              'y': positionComponent.y,\n              'width': positionComponent.width,\n              'height': positionComponent.height,\n              'angle': positionComponent.angle,\n              'scaleX': positionComponent.scale.x,\n              'scaleY': positionComponent.scale.y,\n            }),\n          );\n        } else {\n          return ServiceExtensionResponse.error(\n            ServiceExtensionResponse.extensionError,\n            'No PositionComponent found with id: $id',\n          );\n        }\n      },\n    );\n\n    registerExtension(\n      'ext.flame_devtools.setPositionComponentAttributes',\n      (method, parameters) async {\n        final id = int.tryParse(parameters['id'] ?? '');\n        final attribute = parameters['attribute'];\n\n        final positionComponent = findComponent<PositionComponent>(id);\n\n        if (positionComponent != null) {\n          if (attribute == 'x') {\n            positionComponent.x = double.parse(parameters['value']!);\n          } else if (attribute == 'y') {\n            positionComponent.y = double.parse(parameters['value']!);\n          } else if (attribute == 'width') {\n            positionComponent.width = double.parse(parameters['value']!);\n          } else if (attribute == 'height') {\n            positionComponent.height = double.parse(parameters['value']!);\n          } else if (attribute == 'angle') {\n            positionComponent.angle = double.parse(parameters['value']!);\n          } else if (attribute == 'scaleX') {\n            positionComponent.scale.x = double.parse(parameters['value']!);\n          } else if (attribute == 'scaleY') {\n            positionComponent.scale.y = double.parse(parameters['value']!);\n          } else {\n            return ServiceExtensionResponse.error(\n              ServiceExtensionResponse.extensionError,\n              'Invalid attribute: $attribute',\n            );\n          }\n          return ServiceExtensionResponse.result('Success');\n        } else {\n          return ServiceExtensionResponse.error(\n            ServiceExtensionResponse.extensionError,\n            'No PositionComponent found with id: $id',\n          );\n        }\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/devtools/dev_tools_connector.dart",
    "content": "import 'dart:developer';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/debug.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/foundation.dart';\n\n/// When a [DevToolsConnector] is initialized by the [DevToolsService] it will\n/// call the [init] method the first time, where you should will register\n/// service extensions which makes it possible for the devtools extension to\n/// communicate with your interface. Then the [initGame] method will be called\n/// every time a new game is set in the service.\nabstract class DevToolsConnector {\n  DevToolsConnector() {\n    init();\n  }\n\n  late FlameGame game;\n\n  /// In this method, you should register service extensions using\n  /// [registerExtension] from dart:developer\n  /// (see https://api.flutter.dev/flutter/dart-developer/registerExtension.html).\n  void init();\n\n  @mustCallSuper\n  // ignore: use_setters_to_change_properties\n  void initGame(FlameGame game) {\n    this.game = game;\n  }\n\n  /// Here you can do clean-up before a new game is set in the connector.\n  void disposeGame() {}\n\n  /// Finds a component in the game tree by its id.\n  ///\n  /// Returns the component if found, otherwise null.\n  T? findComponent<T extends Component>(int? id) {\n    T? component;\n    game.propagateToChildren<T>(\n      (c) {\n        if (c.hashCode == id) {\n          component = c;\n          return false;\n        }\n        return true;\n      },\n      includeSelf: true,\n    );\n\n    return component;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/devtools/dev_tools_service.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame/src/devtools/connectors/component_count_connector.dart';\nimport 'package:flame/src/devtools/connectors/component_snapshot_connector.dart';\nimport 'package:flame/src/devtools/connectors/component_tree_connector.dart';\nimport 'package:flame/src/devtools/connectors/debug_mode_connector.dart';\nimport 'package:flame/src/devtools/connectors/game_loop_connector.dart';\nimport 'package:flame/src/devtools/connectors/overlay_navigation_connector.dart';\nimport 'package:flame/src/devtools/connectors/position_component_attributes_connector.dart';\nimport 'package:flame/src/devtools/dev_tools_connector.dart';\n\n/// When [DevToolsService] is initialized by the [FlameGame] it will call\n/// the `init` method for all [DevToolsConnector]s so that they can register\n/// service extensions which are the ones that makes it possible for the\n/// devtools extension to communicate with the game.\n///\n/// Do note that if you have multiple games in your app, only the last one\n/// created will be connected to the devtools. If you want to change it to\n/// another game instance you can call [DevToolsService.initWithGame] with\n/// the game instance that you want to observe.\nclass DevToolsService {\n  DevToolsService._();\n\n  static final instance = DevToolsService._();\n\n  /// Initializes the service with the given game instance.\n  factory DevToolsService.initWithGame(FlameGame game) {\n    instance.initGame(game);\n    return instance;\n  }\n\n  FlameGame? _game;\n  FlameGame get game => _game!;\n\n  /// The list of available connectors, remember to add your connector here if\n  /// you create a new one.\n  final connectors = [\n    DebugModeConnector(),\n    ComponentCountConnector(),\n    ComponentTreeConnector(),\n    GameLoopConnector(),\n    ComponentSnapshotConnector(),\n    PositionComponentAttributesConnector(),\n    OverlayNavigationConnector(),\n  ];\n\n  /// This method is called every time a new game is set in the service and it\n  /// is responsible for calling the [DevToolsConnector.initGame] method in all\n  /// the connectors. It is also responsible for calling\n  /// [DevToolsConnector.disposeGame] of all connectors when a new game is set,\n  /// if there was a game set previously.\n  void initGame(FlameGame game) {\n    if (_game != null) {\n      for (final connector in connectors) {\n        connector.disposeGame();\n      }\n    }\n\n    _game = game;\n    for (final connector in connectors) {\n      connector.initGame(game);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/anchor_by_effect.dart",
    "content": "import 'package:flame/src/anchor.dart';\nimport 'package:flame/src/effects/anchor_effect.dart';\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// An [AnchorEffect] that changes its target's anchor by the specified offset.\n///\n/// This effect will move the anchor in a straight line to a new position that\n/// is at an `offset` from the target's anchor at the start of the effect.\n///\n/// The `controller` can be used to change the timing of the movement: when the\n/// move starts, how fast it is, whether the motion is uniform or not, and so\n/// on. The [EffectController] can even be used to create oscillating or random\n/// motions.\n///\n/// This effect applies incremental changes to the target's position, which\n/// allows it to be combined with other [AnchorEffect]s. When several\n/// [AnchorByEffect]s are applied to the same target simultaneously, the anchor\n/// is moved by the vector sum of offsets from all effects.\nclass AnchorByEffect extends AnchorEffect {\n  AnchorByEffect(\n    Vector2 offset,\n    EffectController controller, {\n    AnchorProvider? target,\n    void Function()? onComplete,\n    super.key,\n  }) : _offset = offset.clone(),\n       super(controller, target, onComplete: onComplete);\n\n  final Vector2 _offset;\n\n  @override\n  void apply(double progress) {\n    final dProgress = progress - previousProgress;\n    target.anchor = Anchor(\n      target.anchor.x + _offset.x * dProgress,\n      target.anchor.y + _offset.y * dProgress,\n    );\n  }\n\n  @override\n  double measure() => _offset.length;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/anchor_effect.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/src/effects/anchor_by_effect.dart';\nimport 'package:flame/src/effects/anchor_to_effect.dart';\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/effect.dart';\nimport 'package:flame/src/effects/effect_target.dart';\nimport 'package:flame/src/effects/measurable_effect.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\n\n/// Base class for effects that affect the `anchor` of their targets.\n///\n/// The main purpose of this class is for type reflection, for example to select\n/// all effects on the target that are of \"anchor\" type.\n///\n/// Factory constructors [AnchorEffect.by] and [AnchorEffect.to] are also\n/// provided for convenience.\nabstract class AnchorEffect extends Effect\n    with EffectTarget<AnchorProvider>\n    implements MeasurableEffect {\n  AnchorEffect(\n    super.controller,\n    AnchorProvider? target, {\n    super.onComplete,\n    super.key,\n  }) {\n    this.target = target;\n  }\n\n  factory AnchorEffect.by(\n    Vector2 offset,\n    EffectController controller, {\n    AnchorProvider? target,\n    void Function()? onComplete,\n    ComponentKey? key,\n  }) => AnchorByEffect(\n    offset,\n    controller,\n    target: target,\n    onComplete: onComplete,\n    key: key,\n  );\n\n  factory AnchorEffect.to(\n    Anchor destination,\n    EffectController controller, {\n    AnchorProvider? target,\n    void Function()? onComplete,\n    ComponentKey? key,\n  }) => AnchorToEffect(\n    destination,\n    controller,\n    target: target,\n    onComplete: onComplete,\n    key: key,\n  );\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/anchor_to_effect.dart",
    "content": "import 'package:flame/src/anchor.dart';\nimport 'package:flame/src/effects/anchor_effect.dart';\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// An effect that moves the target's anchor to the specified value.\n///\n/// The anchor will move in a straight line from the anchor's value at the start\n/// of the effect towards the provided target. The timing of the move is\n/// governed by the [controller].\nclass AnchorToEffect extends AnchorEffect {\n  AnchorToEffect(\n    Anchor destination,\n    EffectController controller, {\n    AnchorProvider? target,\n    void Function()? onComplete,\n    super.key,\n  }) : _destination = destination,\n       super(controller, target, onComplete: onComplete);\n\n  final Anchor _destination;\n  late Vector2 _offset;\n\n  @override\n  void onStart() {\n    _offset = _destination.toVector2() - target.anchor.toVector2();\n  }\n\n  @override\n  void apply(double progress) {\n    final dProgress = progress - previousProgress;\n    target.anchor = Anchor(\n      target.anchor.x + _offset.x * dProgress,\n      target.anchor.y + _offset.y * dProgress,\n    );\n  }\n\n  @override\n  double measure() => _offset.length;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/color_effect.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/src/effects/component_effect.dart';\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flutter/material.dart';\n\n/// Change the color of a component over time.\n///\n/// Due to how this effect is implemented, and how Flutter's [ColorFilter]\n/// class works, this effect can't be mixed with other [ColorEffect]s, when more\n/// than one is added to the component, only the last one will have effect.\nclass ColorEffect extends ComponentEffect<HasPaint> {\n  final String? paintId;\n  final Color color;\n  ColorFilter? _original;\n  late final Tween<double> _tween;\n\n  ColorEffect(\n    this.color,\n    EffectController controller, {\n    double opacityFrom = 0,\n    double opacityTo = 1,\n    this.paintId,\n    void Function()? onComplete,\n    super.key,\n  }) : assert(\n         opacityFrom >= 0 &&\n             opacityFrom <= 1 &&\n             opacityTo >= 0 &&\n             opacityTo <= 1,\n         'Opacity value should be between 0 and 1',\n       ),\n       _tween = Tween(begin: opacityFrom, end: opacityTo),\n       super(controller, onComplete: onComplete);\n\n  @override\n  Future<void> onMount() async {\n    super.onMount();\n\n    _original = target.getPaint(paintId).colorFilter;\n  }\n\n  @override\n  void apply(double progress) {\n    final currentColor = color.withValues(\n      alpha: min(max(_tween.transform(progress), 0), 1),\n    );\n    target.tint(currentColor, paintId: paintId);\n  }\n\n  @override\n  void reset() {\n    super.reset();\n    target.getPaint(paintId).colorFilter = _original;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/combined_effect.dart",
    "content": "import 'package:flame/effects.dart';\n\n/// An effect that runs multiple effects simultaneously.\n///\n/// The [CombinedEffect] is finished when all the effects are finished.\n///\n/// If the `alternate` flag is provided, then all effects will run in the\n/// reverse after they ran forward.\n///\n/// Parameter `repeatCount` will make the combination of effects repeat a\n/// certain number of times. If `alternate` is also true, then the effects will\n/// first run forward, then back, and then repeat this pattern `repeatCount`\n/// times in total.\n///\n/// The flag `infinite = true` makes the combination of effects repeat\n/// infinitely. This is equivalent to setting `repeatCount = infinity`. If both\n/// the `infinite` and the `repeatCount` parameters are given, then `infinite`\n/// takes precedence.\nclass CombinedEffect extends Effect {\n  CombinedEffect(\n    List<Effect> effects, {\n    bool alternate = false,\n    bool infinite = false,\n    int repeatCount = 1,\n    super.onComplete,\n    super.key,\n  }) : assert(effects.isNotEmpty, 'The list of effects cannot be empty'),\n       assert(\n         !(infinite && repeatCount != 1),\n         'Parameters infinite and repeatCount cannot be specified '\n         'simultaneously',\n       ),\n       super(\n         _createController(\n           effects: effects,\n           alternate: alternate,\n           infinite: infinite,\n           repeatCount: repeatCount,\n         ),\n       ) {\n    addAll(effects);\n  }\n\n  @override\n  void onMount() {\n    super.onMount();\n    if (children.isEmpty) {\n      removeFromParent();\n    }\n  }\n\n  @override\n  void apply(double progress) {}\n\n  @override\n  void updateTree(double dt) {\n    update(dt);\n    // Do not update children: the controller will take care of it\n  }\n}\n\nEffectController _createController({\n  required List<Effect> effects,\n  required bool alternate,\n  required bool infinite,\n  required int repeatCount,\n}) {\n  final controller = CombinedEffectController(\n    effects,\n    alternate: alternate,\n  );\n  for (final effect in effects) {\n    effect.removeOnFinish = false;\n  }\n  if (infinite) {\n    return InfiniteEffectController(controller);\n  }\n  if (repeatCount > 1) {\n    return RepeatedEffectController(controller, repeatCount);\n  }\n  return controller;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/component_effect.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/src/effects/effect.dart';\n\n/// Base class for effects that target a [Component] of type [T].\n///\n/// A general abstraction for creating effects targeting [Component]s, currently\n/// used by `SizeEffect`, `OpacityEffect` and `Transform2DEffect`.\nabstract class ComponentEffect<T extends Component> extends Effect {\n  ComponentEffect(\n    super.controller, {\n    super.onComplete,\n    super.key,\n  });\n\n  late T target;\n\n  @override\n  void onMount() {\n    super.onMount();\n    assert(parent != null);\n    var p = parent;\n    while (p is Effect) {\n      p = p.parent;\n    }\n    if (p is T) {\n      target = p;\n    } else {\n      throw UnsupportedError('Can only apply this effect to $T');\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/callback_controller.dart",
    "content": "import 'dart:ui';\nimport 'package:flame/effects.dart';\n\n/// This effect controller invokes the [callback] function and then immediately\n/// finishes. Use it as part of a more complex effect to insert callbacks at\n/// certain parts of that effect.\nclass CallbackController extends DurationEffectController {\n  /// Creates a controller that invokes the given [callback] function.\n  /// The [progress] parameter specifies the progress level of the effect at the\n  /// time when the callback is invoked. It is the responsibility of the user to\n  /// ensure that this progress level is contiguous with respect to the progress\n  /// of the overall effect.\n  CallbackController(this.callback, {required double progress})\n    : _progress = progress,\n      super(0.0);\n\n  final VoidCallback callback;\n  final double _progress;\n\n  @override\n  double get progress => _progress;\n\n  @override\n  bool get completed => true;\n\n  @override\n  double advance(double dt) {\n    callback();\n    return dt;\n  }\n\n  @override\n  double recede(double dt) {\n    callback();\n    return dt;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/combined_effect_controller.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/effect.dart';\n\nclass CombinedEffectController extends EffectController {\n  CombinedEffectController(\n    this.effects, {\n    required this.alternate,\n  }) : _duration = _calculateDuration(effects, alternate),\n       super.empty();\n\n  final List<Effect> effects;\n  final bool alternate;\n  final double _duration;\n\n  double t = 0;\n\n  static double _calculateDuration(List<Effect> effects, bool alternate) {\n    var duration = 0.0;\n    for (final effect in effects) {\n      duration = max(duration, effect.controller.duration ?? 0);\n    }\n    if (alternate) {\n      duration *= 2;\n    }\n    return duration;\n  }\n\n  @override\n  double? get duration => _duration;\n\n  @override\n  double get progress =>\n      alternate ? (_duration - t) / _duration : t / _duration;\n\n  @override\n  bool get completed => t >= _duration;\n\n  @override\n  double advance(double dt) {\n    if (completed) {\n      return dt;\n    }\n    final t0 = t;\n    t += dt;\n\n    if (alternate && t > _duration / 2 && t0 <= _duration / 2) {\n      // Transition from forward to backward\n      final tInForward = _duration / 2 - t0;\n      final tInBackward = t - _duration / 2;\n      for (final effect in effects) {\n        effect.advance(tInForward);\n        if (tInBackward > 0) {\n          effect.recede(tInBackward);\n        }\n      }\n    } else if (!alternate || t <= _duration / 2) {\n      // Forward\n      for (final effect in effects) {\n        final effectDuration = effect.controller.duration ?? 0;\n        if (t0 < effectDuration) {\n          final t1 = min(t, effectDuration);\n          effect.advance(t1 - t0);\n        }\n      }\n    } else {\n      // Backward\n      for (final effect in effects) {\n        final effectDuration = effect.controller.duration ?? 0;\n        final timeInAlt = t0 - _duration / 2;\n        if (timeInAlt < effectDuration) {\n          final t1 = min(t - _duration / 2, effectDuration);\n          effect.recede(t1 - timeInAlt);\n        }\n      }\n    }\n\n    if (t >= _duration) {\n      final surplus = t - _duration;\n      t = _duration;\n      return surplus;\n    }\n    return 0;\n  }\n\n  @override\n  double recede(double dt) {\n    if (t <= 0) {\n      return dt;\n    }\n    final t0 = t;\n    t -= dt;\n\n    if (alternate && t < _duration / 2 && t0 >= _duration / 2) {\n      final tInBackward = t0 - _duration / 2;\n      final tInForward = _duration / 2 - t;\n      for (final effect in effects) {\n        if (tInBackward > 0) {\n          effect.recede(tInBackward);\n        }\n        effect.advance(tInForward);\n      }\n    } else if (!alternate || t >= _duration / 2) {\n      // Backward\n      for (final effect in effects) {\n        final effectDuration = effect.controller.duration ?? 0;\n        final timeInAlt = t0 - _duration / 2;\n        if (timeInAlt < effectDuration) {\n          final t1 = max(t - _duration / 2, 0.0);\n          effect.recede(timeInAlt - t1);\n        }\n      }\n    } else {\n      // Forward\n      for (final effect in effects) {\n        final effectDuration = effect.controller.duration ?? 0;\n        if (t0 < effectDuration) {\n          final t1 = max(t, 0.0);\n          effect.recede(t0 - t1);\n        }\n      }\n    }\n\n    if (t <= 0) {\n      final surplus = -t;\n      t = 0;\n      return surplus;\n    }\n    return 0;\n  }\n\n  @override\n  void setToEnd() {\n    t = _duration;\n    for (final effect in effects) {\n      effect.resetToEnd();\n    }\n  }\n\n  @override\n  void setToStart() {\n    t = 0;\n    for (final effect in effects) {\n      effect.reset();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/curved_effect_controller.dart",
    "content": "import 'package:flame/src/effects/controllers/duration_effect_controller.dart';\nimport 'package:flutter/animation.dart';\n\n/// A controller that grows non-linearly from 0 to 1 following the provided\n/// [curve]. The [duration] cannot be 0.\nclass CurvedEffectController extends DurationEffectController {\n  CurvedEffectController(super.duration, Curve curve)\n    : assert(duration > 0, 'Duration must be positive: $duration'),\n      _curve = curve;\n\n  Curve get curve => _curve;\n  final Curve _curve;\n\n  @override\n  double get progress => _curve.transform(timer / duration);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/delayed_effect_controller.dart",
    "content": "import 'package:flame/effects.dart';\n\n/// An effect controller that waits for [delay] seconds before running the\n/// child controller. While waiting, the progress will be reported at 0.\nclass DelayedEffectController extends EffectController\n    with HasSingleChildEffectController {\n  DelayedEffectController(EffectController child, {required this.delay})\n    : assert(delay >= 0, 'Delay must be non-negative: $delay'),\n      _child = child,\n      _timer = 0,\n      super.empty();\n\n  final EffectController _child;\n  final double delay;\n  double _timer;\n\n  @override\n  EffectController get child => _child;\n\n  @override\n  bool get isRandom => _child.isRandom;\n\n  @override\n  bool get started => _timer == delay;\n\n  @override\n  bool get completed => started && _child.completed;\n\n  @override\n  double get progress => started ? _child.progress : 0;\n\n  @override\n  double? get duration {\n    final childDuration = _child.duration;\n    return childDuration == null ? null : childDuration + delay;\n  }\n\n  @override\n  double advance(double dt) {\n    if (_timer == delay) {\n      return _child.advance(dt);\n    }\n    _timer += dt;\n    if (_timer >= delay) {\n      final t = _child.advance(_timer - delay);\n      _timer = delay;\n      return t;\n    } else {\n      return 0;\n    }\n  }\n\n  @override\n  double recede(double dt) {\n    if (_timer == delay) {\n      _timer -= _child.recede(dt);\n    } else {\n      _timer -= dt;\n    }\n    if (_timer < 0) {\n      final leftoverTime = -_timer;\n      _timer = 0;\n      return leftoverTime;\n    }\n    return 0;\n  }\n\n  @override\n  void setToStart() {\n    _timer = 0;\n    super.setToStart();\n  }\n\n  @override\n  void setToEnd() {\n    _timer = delay;\n    super.setToEnd();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/duration_effect_controller.dart",
    "content": "import 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:meta/meta.dart';\n\n/// Abstract class for an effect controller that has a predefined [duration].\n///\n/// This effect controller cannot be used directly, instead it serves as base\n/// for some other effect controller classes.\n///\n/// The primary functionality offered by this class is the [timer] property,\n/// which keeps track of how much time has passed within this controller. The\n/// effect controller will be considered [completed] when the timer reaches the\n/// [duration] value.\nabstract class DurationEffectController extends EffectController {\n  DurationEffectController(this.duration)\n    : assert(duration >= 0, 'Duration cannot be negative: $duration'),\n      _timer = 0,\n      super.empty();\n\n  double _timer;\n\n  @override\n  double duration;\n\n  @protected\n  double get timer => _timer;\n\n  @override\n  bool get completed => _timer == duration;\n\n  @override\n  double advance(double dt) {\n    _timer += dt;\n    if (_timer > duration) {\n      final leftoverTime = _timer - duration;\n      _timer = duration;\n      return leftoverTime;\n    }\n    return 0;\n  }\n\n  @override\n  double recede(double dt) {\n    _timer -= dt;\n    if (_timer < 0) {\n      final leftoverTime = 0 - _timer;\n      _timer = 0;\n      return leftoverTime;\n    }\n    return 0;\n  }\n\n  @override\n  void setToStart() {\n    _timer = 0;\n  }\n\n  @override\n  void setToEnd() {\n    _timer = duration;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/effect_controller.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/effects/controllers/callback_controller.dart';\nimport 'package:flame/src/effects/controllers/curved_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/delayed_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/infinite_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/linear_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/pause_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/repeated_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/reverse_curved_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/reverse_linear_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/sequence_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/speed_effect_controller.dart';\nimport 'package:flame/src/effects/effect.dart' show Effect;\nimport 'package:flutter/animation.dart' show Curve, Curves;\n\n/// Base \"controller\" class to facilitate animation of effects.\n///\n/// The purpose of an effect controller is to define how an [Effect] or an\n/// animation should progress over time. To facilitate that, this class provides\n/// variable [progress], which will grow from 0.0 to 1.0. The value of 0\n/// corresponds to the beginning of an animation, and the value of 1.0 is\n/// the end of the animation.\n///\n/// The [progress] variable can best be thought of as a \"logical time\". For\n/// example, if you want to animate a certain property from value A to value B,\n/// then you can use [progress] to linearly interpolate between these two\n/// extremes and obtain variable `x = A*(1 - progress) + B*progress`.\n///\n/// The exact behavior of [progress] is determined by subclasses, but the\n/// following general considerations apply:\n///   - the progress can also go in negative direction (i.e. from 1 to 0);\n///   - the progress may oscillate, going from 0 to 1, then back to 0, etc;\n///   - the progress may change over a finite or infinite period of time;\n///   - the value of 0 corresponds to the logical start of an animation;\n///   - the value of 1 is either the end or the \"peak\" of an animation;\n///   - the progress may briefly attain values outside of `[0; 1]` range (for\n///     example if a \"bouncy\" easing curve is applied).\n///\n/// An [EffectController] can be made to run forward in time (`advance()`), or\n/// backward in time (`recede()`).\n///\n/// Unlike the `dart.ui.AnimationController`, this class does not use a `Ticker`\n/// to keep track of time. Instead, it must be pushed through time manually, by\n/// calling the `update()` method within the game loop.\nabstract class EffectController {\n  /// Factory function for producing common [EffectController]s.\n  ///\n  /// In the simplest case, when only `duration` is provided, this will return\n  /// a [LinearEffectController] that grows linearly from 0 to 1 over the period\n  /// of that duration.\n  ///\n  /// More generally, the produced effect controller allows to add a delay\n  /// before the beginning of the animation, to animate both forward and in\n  /// reverse, to iterate several times (or infinitely), to apply an arbitrary\n  /// [curve] making the effect progression non-linear, etc.\n  ///\n  /// In the most general case, the animation proceeds through the following\n  /// steps:\n  ///   1. wait for [startDelay] seconds,\n  ///   2. repeat the following steps [repeatCount] times (or [infinite]ly):\n  ///       a. progress from 0 to 1 over the [duration] seconds,\n  ///       b. wait for [atMaxDuration] seconds,\n  ///       c. progress from 1 to 0 over the [reverseDuration] seconds,\n  ///       d. wait for [atMinDuration] seconds.\n  ///\n  /// Setting parameter [alternate] to true is another way to create a\n  /// controller whose [reverseDuration] is the same as the forward [duration].\n  ///\n  /// As an alternative to specifying durations, you can also provide [speed]\n  /// and [reverseSpeed] parameters, but only for effects where the notion of\n  /// a speed is well-defined (`MeasurableEffect`s).\n  ///\n  /// If the animation is finite and there are no \"backward\" or \"atMin\" stages\n  /// then the animation will complete at `progress == 1`, otherwise it will\n  /// complete at `progress == 0`.\n  ///\n  /// Before [atMaxDuration] and [atMinDuration] a callback function can be\n  /// provided which will be called after the corresponding [progress] has\n  /// finished.\n  factory EffectController({\n    double? duration,\n    double? speed,\n    Curve curve = Curves.linear,\n    double? reverseDuration,\n    double? reverseSpeed,\n    Curve? reverseCurve,\n    bool infinite = false,\n    bool alternate = false,\n    int? repeatCount,\n    double startDelay = 0.0,\n    double atMaxDuration = 0.0,\n    double atMinDuration = 0.0,\n    VoidCallback? onMax,\n    VoidCallback? onMin,\n  }) {\n    assert(\n      (duration ?? 1) >= 0,\n      'Duration cannot be negative: $duration',\n    );\n    assert(\n      (reverseDuration ?? 1) >= 0,\n      'Reverse duration cannot be negative: $reverseDuration',\n    );\n    assert(\n      (duration != null) || (speed != null),\n      'Either duration or speed must be specified',\n    );\n    assert(\n      !((duration != null || reverseDuration != null) &&\n          (speed != null || reverseSpeed != null)),\n      'Both duration and speed arguments cannot be specified at the same time',\n    );\n    assert(\n      (speed ?? 1) > 0,\n      'Speed must be positive: $speed',\n    );\n    assert(\n      (reverseSpeed ?? 1) > 0,\n      'Reverse speed must be positive: $reverseSpeed',\n    );\n    assert(\n      !(infinite && repeatCount != null),\n      'An infinite effect cannot have a repeat count',\n    );\n    assert(\n      (repeatCount ?? 1) > 0,\n      'Repeat count must be positive: $repeatCount',\n    );\n    assert(\n      startDelay >= 0,\n      'Start delay cannot be negative: $startDelay',\n    );\n    assert(\n      atMaxDuration >= 0,\n      'At-max duration cannot be negative: $atMaxDuration',\n    );\n    assert(\n      atMinDuration >= 0,\n      'At-min duration cannot be negative: $atMinDuration',\n    );\n    final items = <EffectController>[];\n\n    // FORWARD\n    final isLinear = curve == Curves.linear;\n    if (isLinear) {\n      items.add(\n        duration != null\n            ? LinearEffectController(duration)\n            : SpeedEffectController(LinearEffectController(0), speed: speed!),\n      );\n    } else {\n      items.add(\n        duration != null\n            ? CurvedEffectController(duration, curve)\n            : SpeedEffectController(\n                CurvedEffectController(1, curve),\n                speed: speed!,\n              ),\n      );\n    }\n\n    // ON MAX CALLBACK\n    if (onMax != null) {\n      items.add(CallbackController(onMax, progress: 1.0));\n    }\n\n    // AT-MAX\n    if (atMaxDuration != 0) {\n      items.add(PauseEffectController(atMaxDuration, progress: 1.0));\n    }\n\n    // REVERSE\n    final hasReverse =\n        alternate || (reverseDuration != null) || (reverseSpeed != null);\n    if (hasReverse) {\n      final reverseIsLinear =\n          reverseCurve == Curves.linear || ((reverseCurve == null) && isLinear);\n      final reverseHasDuration =\n          (reverseDuration != null) ||\n          (reverseSpeed == null && duration != null);\n      if (reverseIsLinear) {\n        items.add(\n          reverseHasDuration\n              ? ReverseLinearEffectController(reverseDuration ?? duration!)\n              : SpeedEffectController(\n                  ReverseLinearEffectController(0),\n                  speed: reverseSpeed ?? speed!,\n                ),\n        );\n      } else {\n        reverseCurve ??= curve.flipped;\n        items.add(\n          reverseHasDuration\n              ? ReverseCurvedEffectController(\n                  reverseDuration ?? duration!,\n                  reverseCurve,\n                )\n              : SpeedEffectController(\n                  ReverseCurvedEffectController(1, reverseCurve),\n                  speed: reverseSpeed ?? speed!,\n                ),\n        );\n      }\n    }\n\n    // ON MIN CALLBACK\n    if (onMin != null) {\n      items.add(CallbackController(onMin, progress: 0.0));\n    }\n\n    // AT-MIN\n    if (atMinDuration != 0) {\n      items.add(PauseEffectController(atMinDuration, progress: 0.0));\n    }\n\n    assert(items.isNotEmpty);\n    var controller = items.length == 1\n        ? items[0]\n        : SequenceEffectController(items);\n    if (infinite) {\n      controller = InfiniteEffectController(controller);\n    }\n    if (repeatCount != null && repeatCount != 1) {\n      controller = RepeatedEffectController(controller, repeatCount);\n    }\n    if (startDelay != 0) {\n      controller = DelayedEffectController(controller, delay: startDelay);\n    }\n    return controller;\n  }\n\n  EffectController.empty();\n\n  /// Will the effect continue to run forever (never completes)?\n  bool get isInfinite => duration == double.infinity;\n\n  /// Is the effect's duration random or fixed?\n  bool get isRandom => false;\n\n  /// Total duration of the effect. If the duration cannot be determined, this\n  /// will return `null`. For an infinite effect the duration is infinity.\n  double? get duration;\n\n  /// Has the effect started running? Some effects use a \"delay\" parameter to\n  /// postpone the start of an animation. This property then tells you whether\n  /// this delay period has already passed.\n  bool get started => true;\n\n  /// Has the effect already finished?\n  ///\n  /// For a finite animation, this property will turn `true` once the animation\n  /// has finished running and the [progress] variable will no longer change\n  /// in the future. For an infinite animation this should always return\n  /// `false`.\n  bool get completed;\n\n  /// The current progress of the effect, a value between 0 and 1.\n  double get progress;\n\n  /// Advances this controller's internal clock by [dt] seconds.\n  ///\n  /// If the controller is still running, the return value will be 0. If it\n  /// already finished, then the return value will be the \"leftover\" part of\n  /// the [dt]. That is, the amount of time [dt] that remains after the\n  /// controller has finished. In all cases, the return value can be positive\n  /// only when `completed == true`.\n  ///\n  /// Normally, this method will be called by the owner of the controller class.\n  /// For example, if the controller is passed to an [Effect] class, then that\n  /// class will take care of calling this method as necessary.\n  double advance(double dt);\n\n  /// Similar to `advance()`, but makes the effect controller move back in time.\n  ///\n  /// If the supplied amount of time [dt] would push the effect past its\n  /// starting point, then the effect stops at the start and the \"leftover\"\n  /// portion of [dt] is returned.\n  double recede(double dt);\n\n  /// Reverts the controller to its initial state, as it was before the start\n  /// of the animation.\n  void setToStart();\n\n  /// Puts the controller into its final \"completed\" state.\n  void setToEnd();\n\n  /// This is called by the [Effect] class when the controller is attached to\n  /// that effect. Controllers with children should propagate this to their\n  /// children.\n  void onMount(Effect parent) {}\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/infinite_effect_controller.dart",
    "content": "import 'package:flame/effects.dart';\n\n/// Effect controller that wraps a [child] effect controller and repeats it\n/// infinitely.\nclass InfiniteEffectController extends EffectController\n    with HasSingleChildEffectController {\n  InfiniteEffectController(EffectController child)\n    : _child = child,\n      super.empty();\n\n  final EffectController _child;\n\n  @override\n  EffectController get child => _child;\n\n  @override\n  bool get completed => false;\n\n  @override\n  double? get duration => double.infinity;\n\n  @override\n  double get progress => child.progress;\n\n  @override\n  bool get isRandom => child.isRandom;\n\n  @override\n  double advance(double dt) {\n    var t = dt;\n    for (;;) {\n      t = child.advance(t);\n      if (t == 0) {\n        break;\n      }\n      child.setToStart();\n    }\n    return 0;\n  }\n\n  @override\n  double recede(double dt) {\n    var t = dt;\n    for (;;) {\n      t = child.recede(t);\n      if (t == 0) {\n        break;\n      }\n      child.setToEnd();\n    }\n    return 0;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/linear_effect_controller.dart",
    "content": "import 'package:flame/src/effects/controllers/duration_effect_controller.dart';\n\n/// A controller that grows linearly from 0 to 1 over [duration] seconds.\n///\n/// The [duration] can also be 0, in which case the effect will jump from 0 to 1\n/// instantaneously.\nclass LinearEffectController extends DurationEffectController {\n  LinearEffectController(super.duration);\n\n  // If duration is 0, `completed` will be true, and division by 0 avoided.\n  @override\n  double get progress => completed ? 1 : (timer / duration);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/mixins/has_single_child_effect_controller.dart",
    "content": "import 'package:flame/effects.dart';\nimport 'package:meta/meta.dart';\n\n/// This mixin must be used with [EffectController]s that wrap a single child\n/// effect controller of type [T].\nmixin HasSingleChildEffectController<T extends EffectController>\n    on EffectController {\n  /// Returns the wrapped child effect controller.\n  T get child;\n\n  @mustCallSuper\n  @override\n  void setToStart() {\n    child.setToStart();\n  }\n\n  @mustCallSuper\n  @override\n  void setToEnd() {\n    child.setToEnd();\n  }\n\n  @mustCallSuper\n  @override\n  void onMount(Effect parent) {\n    child.onMount(parent);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/pause_effect_controller.dart",
    "content": "import 'package:flame/src/effects/controllers/duration_effect_controller.dart';\n\n/// A controller that keeps constant [progress] over [duration] seconds.\n///\n/// Since \"progress\" represents the \"logical time\" of an Effect, keeping the\n/// progress constant over some time is equivalent to freezing in time or\n/// pausing the effect for the prescribed duration.\n///\n/// This controller is best used in combination with other controllers. For\n/// example, you can create a repeated controller where the progress changes\n/// 0->1->0 over a short period of time, then pauses, and this sequence repeats.\nclass PauseEffectController extends DurationEffectController {\n  PauseEffectController(super.duration, {required double progress})\n    : _progress = progress;\n\n  final double _progress;\n\n  @override\n  double get progress => _progress;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/random_effect_controller.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/effects.dart';\n\n/// An [EffectController] that wraps another effect controller [child] and\n/// randomizes its duration after each reset.\n///\n/// This effect controller works best in contexts were it has a chance to be\n/// executed multiple times, such as within a `RepeatedEffectController`, or\n/// `InfiniteEffectController`, etc.\n///\n/// The child's duration is randomized first at construction, and then at each\n/// reset (`setToStart`). Thus, the child has a concrete well-defined duration\n/// at any point in time.\nclass RandomEffectController extends EffectController\n    with HasSingleChildEffectController<DurationEffectController> {\n  RandomEffectController(DurationEffectController child, this.randomGenerator)\n    : assert(!child.isInfinite, 'Child cannot be infinite'),\n      _child = child,\n      super.empty() {\n    _initializeDuration();\n  }\n\n  /// Factory constructor that uses a random variable uniformly distributed on\n  /// `[min, max)`.\n  factory RandomEffectController.uniform(\n    DurationEffectController child, {\n    required double min,\n    required double max,\n    Random? random,\n  }) {\n    assert(min >= 0, 'Min value cannot be negative: $min');\n    assert(min < max, 'Max value must exceed min: max=$max, min=$min');\n    return RandomEffectController(\n      child,\n      _UniformRandomVariable(min, max, random),\n    );\n  }\n\n  /// Factory constructor that employs a random variable distributed\n  /// exponentially with rate parameter `beta`. The produced random values will\n  /// have the average duration of `beta`.\n  factory RandomEffectController.exponential(\n    DurationEffectController child, {\n    required double beta,\n    Random? random,\n  }) {\n    assert(beta > 0, 'Beta must be positive: $beta');\n    return RandomEffectController(\n      child,\n      _ExponentialRandomVariable(beta, random),\n    );\n  }\n\n  final DurationEffectController _child;\n  final RandomVariable randomGenerator;\n\n  @override\n  DurationEffectController get child => _child;\n\n  @override\n  bool get isRandom => true;\n\n  @override\n  bool get completed => child.completed;\n\n  @override\n  double? get duration => child.duration;\n\n  @override\n  double get progress => child.progress;\n\n  @override\n  double advance(double dt) => child.advance(dt);\n\n  @override\n  double recede(double dt) => child.recede(dt);\n\n  @override\n  void setToStart() {\n    super.setToStart();\n    _initializeDuration();\n  }\n\n  void _initializeDuration() {\n    final duration = randomGenerator.nextValue();\n    assert(\n      duration >= 0,\n      'Random generator produced a negative value: $duration',\n    );\n    child.duration = duration;\n  }\n}\n\n/// [RandomVariable] is an object capable of producing random values with the\n/// prescribed distribution function. Each distribution is implemented within\n/// its own derived class.\nabstract class RandomVariable {\n  RandomVariable(Random? random) : _random = random ?? _defaultRandom;\n\n  /// Internal random number generator.\n  final Random _random;\n  static final Random _defaultRandom = Random();\n\n  /// Produces the next value for this random variable.\n  double nextValue();\n}\n\n/// Random variable distributed uniformly between [min] and [max].\nclass _UniformRandomVariable extends RandomVariable {\n  _UniformRandomVariable(this.min, this.max, Random? random) : super(random);\n\n  final double min;\n  final double max;\n\n  @override\n  double nextValue() => _random.nextDouble() * (max - min) + min;\n}\n\n/// Exponentially distributed random variable with rate parameter [beta].\nclass _ExponentialRandomVariable extends RandomVariable {\n  _ExponentialRandomVariable(this.beta, Random? random) : super(random);\n\n  /// Rate parameter of the exponential distribution. This will be the average\n  /// of all returned values\n  final double beta;\n\n  @override\n  double nextValue() => -log(1 - _random.nextDouble()) * beta;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/repeated_effect_controller.dart",
    "content": "import 'package:flame/effects.dart';\n\n/// Effect controller that repeats [child] controller a certain number of times.\n///\n/// The [repeatCount] must be positive, and [child] controller cannot be\n/// infinite. The child controller will be reset after each iteration (except\n/// the last).\nclass RepeatedEffectController extends EffectController\n    with HasSingleChildEffectController {\n  RepeatedEffectController(EffectController child, this.repeatCount)\n    : assert(repeatCount > 0, 'repeatCount must be positive'),\n      assert(!child.isInfinite, 'child cannot be infinite'),\n      _child = child,\n      _remainingCount = repeatCount,\n      super.empty();\n\n  final EffectController _child;\n  final int repeatCount;\n\n  /// How many iterations this controller has remaining. When this reaches 0\n  /// the controller is considered completed.\n  int get remainingIterationsCount => _remainingCount;\n  int _remainingCount;\n\n  @override\n  EffectController get child => _child;\n\n  @override\n  double get progress => child.progress;\n\n  @override\n  bool get completed => _remainingCount == 0;\n\n  @override\n  double? get duration {\n    final d = child.duration;\n    return d == null ? null : d * repeatCount;\n  }\n\n  @override\n  bool get isRandom => child.isRandom;\n\n  @override\n  double advance(double dt) {\n    var t = child.advance(dt);\n    while (t > 0 && _remainingCount > 0) {\n      assert(child.completed);\n      _remainingCount--;\n      if (_remainingCount != 0) {\n        child.setToStart();\n        t = child.advance(t);\n      }\n    }\n    if (_remainingCount == 1 && child.completed) {\n      _remainingCount--;\n    }\n    return t;\n  }\n\n  @override\n  double recede(double dt) {\n    if (_remainingCount == 0 && dt > 0) {\n      // When advancing, we do not reset the child on last iteration. Hence,\n      // if we recede from the end position the remaining count must be\n      // adjusted.\n      _remainingCount = 1;\n      assert(child.completed);\n    }\n    var t = child.recede(dt);\n    while (t > 0 && _remainingCount < repeatCount) {\n      _remainingCount++;\n      child.setToEnd();\n      t = child.recede(t);\n    }\n    return t;\n  }\n\n  @override\n  void setToStart() {\n    _remainingCount = repeatCount;\n    super.setToStart();\n  }\n\n  @override\n  void setToEnd() {\n    _remainingCount = 0;\n    super.setToEnd();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/reverse_curved_effect_controller.dart",
    "content": "import 'package:flame/src/effects/controllers/duration_effect_controller.dart';\nimport 'package:flutter/animation.dart';\n\n/// A controller that grows non-linearly from 1 to 0 following the provided\n/// [curve]. The [duration] cannot be 0.\nclass ReverseCurvedEffectController extends DurationEffectController {\n  ReverseCurvedEffectController(super.duration, Curve curve)\n    : assert(duration > 0, 'Duration must be positive: $duration'),\n      _curve = curve;\n\n  Curve get curve => _curve;\n  final Curve _curve;\n\n  @override\n  double get progress => _curve.transform(1 - timer / duration);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/reverse_linear_effect_controller.dart",
    "content": "import 'package:flame/src/effects/controllers/duration_effect_controller.dart';\n\n/// A controller that grows linearly from 1 to 0 over [duration] seconds.\n///\n/// The [duration] can also be 0, in which case the effect will jump from 1 to 0\n/// instantaneously.\nclass ReverseLinearEffectController extends DurationEffectController {\n  ReverseLinearEffectController(super.duration);\n\n  // If duration is 0, `completed` will be true, and division by 0 avoided.\n  @override\n  double get progress => completed ? 0 : 1 - (timer / duration);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/sequence_effect_controller.dart",
    "content": "import 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/effect.dart';\n\n/// An effect controller that executes a list of other controllers one after\n/// another.\nclass SequenceEffectController extends EffectController {\n  SequenceEffectController(List<EffectController> controllers)\n    : assert(controllers.isNotEmpty, 'List of controllers cannot be empty'),\n      assert(\n        !controllers.any((c) => c.isInfinite),\n        'Children controllers cannot be infinite',\n      ),\n      children = controllers,\n      _currentIndex = 0,\n      super.empty();\n\n  /// Individual controllers in the sequence.\n  final List<EffectController> children;\n\n  /// The index of the controller currently being executed. This starts with 0,\n  /// and by the end it will be equal to `_children.length - 1`. This variable\n  /// is always a valid index within the `_children` list.\n  int _currentIndex;\n\n  @override\n  bool get completed {\n    return _currentIndex == children.length - 1 &&\n        children[_currentIndex].completed;\n  }\n\n  @override\n  double? get duration {\n    var totalDuration = 0.0;\n    for (final controller in children) {\n      final d = controller.duration;\n      if (d == null) {\n        return null;\n      }\n      totalDuration += d;\n    }\n    return totalDuration;\n  }\n\n  @override\n  bool get isRandom => children.any((c) => c.isRandom);\n\n  @override\n  double get progress => children[_currentIndex].progress;\n\n  @override\n  double advance(double dt) {\n    var t = children[_currentIndex].advance(dt);\n    while (t > 0 && _currentIndex < children.length - 1) {\n      _currentIndex++;\n      t = children[_currentIndex].advance(t);\n    }\n    return t;\n  }\n\n  @override\n  double recede(double dt) {\n    var t = children[_currentIndex].recede(dt);\n    while (t > 0 && _currentIndex > 0) {\n      _currentIndex--;\n      t = children[_currentIndex].recede(t);\n    }\n    return t;\n  }\n\n  @override\n  void setToStart() {\n    _currentIndex = 0;\n    children.forEach((c) => c.setToStart());\n  }\n\n  @override\n  void setToEnd() {\n    _currentIndex = children.length - 1;\n    children.forEach((c) => c.setToEnd());\n  }\n\n  @override\n  void onMount(Effect parent) => children.forEach((c) => c.onMount(parent));\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/sine_effect_controller.dart",
    "content": "import 'dart:math' as math;\nimport 'package:flame/geometry.dart';\nimport 'package:flame/src/effects/controllers/duration_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/infinite_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/repeated_effect_controller.dart';\n\n/// This effect controller follows a sine wave.\n///\n/// Use this controller to create effects that exhibit natural-looking harmonic\n/// motion.\n///\n/// Combine with [RepeatedEffectController] or [InfiniteEffectController] in\n/// order to create longer waves.\nclass SineEffectController extends DurationEffectController {\n  SineEffectController({required double period})\n    : assert(period > 0, 'Period must be positive: $period'),\n      super(period);\n\n  @override\n  double get progress {\n    return math.sin(tau * timer / duration);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/speed_effect_controller.dart",
    "content": "import 'package:flame/effects.dart';\nimport 'package:flame/src/effects/measurable_effect.dart';\n\n/// This controller can force execution of an effect at a predefined speed.\n///\n/// For most of the effect controllers, their duration is set by the user in\n/// advance. This controller is different: it knows the target speed at which\n/// the parent effect wants to proceed, communicates with the effect to\n/// determine its total distance, and uses that to calculate the desired\n/// duration for the [child] effect controller.\n///\n/// Some restrictions apply:\n///   - the [speed] cannot be zero (or negative),\n///   - the [child] controller must be a [DurationEffectController],\n///   - the parent effect must be a [MeasurableEffect].\nclass SpeedEffectController extends EffectController\n    with HasSingleChildEffectController<DurationEffectController> {\n  SpeedEffectController(DurationEffectController child, {required this.speed})\n    : assert(speed > 0, 'Speed must be positive: $speed'),\n      _child = child,\n      super.empty();\n\n  final DurationEffectController _child;\n  final double speed;\n  MeasurableEffect? _parentEffect;\n\n  /// Note that this controller's [started] property is true even if the\n  /// controller is not initialized yet. This is because we want the [Effect]\n  /// to run its `onStart()` callback before we initialize the controller\n  /// (which will happen at the first call to `advance()`).\n  bool _initialized = false;\n\n  @override\n  DurationEffectController get child => _child;\n\n  @override\n  bool get isRandom => true;\n\n  @override\n  bool get completed => child.completed;\n\n  /// The duration of the effect.\n  ///\n  /// If this is called before the effect has started, it might not have the\n  /// correct duration when it is later used, for example if you're using a\n  /// [DelayedEffectController] and then the component moves before the delay\n  /// is over.\n  @override\n  double get duration {\n    return _initialized\n        ? child.duration\n        : (_parentEffect?.measure() ?? double.nan) / speed;\n  }\n\n  @override\n  double get progress => child.progress;\n\n  @override\n  double advance(double dt) {\n    if (!_initialized) {\n      child.duration = duration;\n      _initialized = true;\n    }\n    return child.advance(dt);\n  }\n\n  @override\n  double recede(double dt) {\n    final t = child.recede(dt);\n    if (t > 0) {\n      _initialized = false;\n    }\n    return t;\n  }\n\n  @override\n  void setToEnd() {\n    _initialized = false;\n    super.setToEnd();\n  }\n\n  @override\n  void setToStart() {\n    _initialized = false;\n    super.setToStart();\n  }\n\n  @override\n  void onMount(Effect parent) {\n    assert(\n      parent is MeasurableEffect,\n      'SpeedEffectController can only be applied to a MeasurableEffect',\n    );\n    _parentEffect = parent as MeasurableEffect;\n    super.onMount(parent);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/controllers/zigzag_effect_controller.dart",
    "content": "import 'package:flame/src/effects/controllers/duration_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/infinite_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/repeated_effect_controller.dart';\n\n/// This effect controller goes from 0 to 1, then back to 0, then to -1, and\n/// then again to 0.\n///\n/// This is similar to an alternating controller, except that it treats the\n/// starting position as the \"middle ground\", and oscillates around it.\n///\n/// Combine with [RepeatedEffectController] or [InfiniteEffectController] in\n/// order to create longer zigzags.\nclass ZigzagEffectController extends DurationEffectController {\n  ZigzagEffectController({required double period})\n    : assert(period > 0, 'Period must be positive: $period'),\n      _quarterPeriod = period / 4,\n      super(period);\n\n  final double _quarterPeriod;\n\n  @override\n  double get progress {\n    // Assume zigzag's period is 4 units of length. Within that period, there\n    // are 3 linear segments: at first it's y = x, for 0 ≤ x ≤ 1, then it's\n    // y = -x + 2, for 1 ≤ x ≤ 3, and finally it's y = x + (-4), for 3 ≤ x ≤ 4.\n    final x = timer / _quarterPeriod;\n    return switch (x) {\n      <= 1 => x,\n      >= 3 => x - 4,\n      _ => 2 - x,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/effect.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:meta/meta.dart';\n\n/// An [Effect] is a component that changes properties or appearance of another\n/// component over time.\n///\n/// For example, suppose you have an object \"Goo\", and you want to move it\n/// to some other point on the screen. Directly changing that object's position\n/// will cause it to teleport to the new location, which is likely undesired.\n/// A second approach that you can take is to modify Goo's `update()` method\n/// to implement the logic that will move Goo to the new position smoothly.\n/// However, implementing such logic for every component that you may need to\n/// move is cumbersome. A better approach then is to implement that logic as a\n/// separate \"movement\" component that can attach to Foo or to any other\n/// suitable object and cause them to move to the desired location. Such a\n/// separate component responsible for changing other components is an Effect.\n///\n/// Every [Effect] has a [controller], which describes how this effect evolves\n/// over time. The effect also expects to be mounted into the game tree as a\n/// regular component, in order to be able to use its controller.\n///\n/// This class describes an abstract effect. Concrete implementations are\n/// expected to define the `apply()` method, which facilitates the necessary\n/// changes in the effect's target; and also the `reset()` method if they have\n/// non-trivial internal state.\nabstract class Effect extends Component {\n  Effect(\n    this.controller, {\n    this.onComplete,\n    super.key,\n  }) : removeOnFinish = true,\n       _paused = false,\n       _started = false,\n       _finished = false {\n    controller.onMount(this);\n  }\n\n  /// An object that describes how the effect should evolve over time.\n  final EffectController controller;\n\n  /// Whether the effect should be removed from its parent once it is completed,\n  /// true by default.\n  ///\n  /// Setting this to false will cause the effect component to remain in the\n  /// game tree in the \"completed\" state. However, you can `reset()` the effect\n  /// in order to make it run once again.\n  bool removeOnFinish;\n\n  /// Optional callback function to be invoked once the effect completes.\n  void Function()? onComplete;\n\n  /// Boolean indicators of the effect's state, their purpose is to ensure that\n  /// the `onStart()` and `onFinish()` callbacks are called exactly once.\n  bool _started;\n  bool _finished;\n\n  /// The effect's `progress` variable as it was the last time that the\n  /// `apply()` method was called. Mostly used by the derived classes.\n  double get previousProgress => _lastProgress;\n  double _lastProgress = 0;\n\n  /// Whether the effect is paused or not.\n  ///\n  /// By default, the effect will not be paused, even when it is in the\n  /// \"completed\" state. Use methods `pause()` and `resume()` in order to\n  /// control this property. When the effect is paused, it is as if the time\n  /// stops for it.\n  bool get isPaused => _paused;\n  bool _paused;\n\n  /// Pause the effect. The effect will not respond to updates while it is\n  /// paused. Calling `resume()` or `reset()` will un-pause it. Pausing an\n  /// already paused effect is a no-op.\n  void pause() => _paused = true;\n\n  /// Resume updates in a previously paused effect. If the effect is not\n  /// currently paused, this call is a no-op.\n  void resume() => _paused = false;\n\n  Completer<void>? _completer;\n\n  /// A future that completes when the effect is finished.\n  Future<void> get completed {\n    return controller.completed\n        ? Future.value()\n        : (_completer ??= Completer<void>()).future;\n  }\n\n  /// Restore the effect to its original state as it was when the effect was\n  /// just created.\n  ///\n  /// A common use case for this method is to have an effect which is\n  /// permanently attached to its target (i.e. with `removeOnFinish == false`),\n  /// and then periodically resetting this effect each time you need to apply\n  /// it to the target.\n  @mustCallSuper\n  void reset() {\n    controller.setToStart();\n    _paused = false;\n    _started = false;\n    _finished = false;\n    _lastProgress = 0;\n  }\n\n  @mustCallSuper\n  void resetToEnd() {\n    controller.setToEnd();\n    _started = true;\n    _finished = true;\n    _lastProgress = 1;\n  }\n\n  /// Implementation of [Component]'s `update()` method. Derived classes are\n  /// not expected to redefine this.\n  @override\n  void update(double dt) {\n    if (_paused || _finished) {\n      return;\n    }\n    if (!_started && controller.started) {\n      _started = true;\n      onStart();\n    }\n    controller.advance(dt);\n    if (_started) {\n      final progress = controller.progress;\n      apply(progress);\n      _lastProgress = progress;\n    }\n    if (!_finished && controller.completed) {\n      _finished = true;\n      onFinish();\n      if (removeOnFinish) {\n        removeFromParent();\n      }\n    }\n  }\n\n  /// Used for SequenceEffect. This is similar to `update()`, but cannot be\n  /// paused, does not obey [removeOnFinish], and returns the \"leftover time\"\n  /// similar to `EffectController.advance`.\n  @internal\n  double advance(double dt) {\n    if (!_started && controller.started) {\n      _started = true;\n      onStart();\n    }\n    final remainingDt = controller.advance(dt);\n    if (_started) {\n      final progress = controller.progress;\n      apply(progress);\n      _lastProgress = progress;\n    }\n    if (!_finished && controller.completed) {\n      _finished = true;\n      onFinish();\n    }\n    return remainingDt;\n  }\n\n  /// Used for SequenceEffect. This is similar to `update()`, but the effect is\n  /// moved back in time. The callbacks onStart/onFinish will not be called.\n  /// This method returns the \"leftover time\" similar to\n  /// `EffectController.recede`.\n  @internal\n  double recede(double dt) {\n    if (_finished && dt > 0) {\n      _finished = false;\n    }\n    final remainingDt = controller.recede(dt);\n    if (_started) {\n      final progress = controller.progress;\n      apply(progress);\n      _lastProgress = progress;\n    }\n    return remainingDt;\n  }\n\n  //#region API to be implemented by the derived classes\n\n  /// This method is called once when the effect is about to start, but before\n  /// the first call to `apply()`. The notion of \"about to start\" is defined by\n  /// the [controller]: this method is called when `controller.started` property\n  /// first becomes true.\n  ///\n  /// If the effect is reset, its `onStart()` method will be called again when\n  /// the effect is about to start.\n  void onStart() {}\n\n  /// This method is called once when the effect is about to finish, but before\n  /// it is removed from its parent. The notion of \"about to finish\" is defined\n  /// by the [controller]: this method is called when `controller.completed`\n  /// property first becomes true.\n  ///\n  /// If the effect is reset, its [onFinish] method will be called again after\n  /// the effect has finished again.\n  @mustCallSuper\n  void onFinish() {\n    onComplete?.call();\n    _completer?.complete();\n    _completer = null;\n  }\n\n  /// Apply the given [progress] level to the effect's target.\n  ///\n  /// Here [progress] is a variable that is typically in the range from 0 to 1,\n  /// with 0 being the initial state, and 1 the final state of the effect. See\n  /// [EffectController] for details.\n  ///\n  /// This is a main method that MUST be implemented in every derived class.\n  void apply(double progress);\n\n  //#endregion\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/effect_target.dart",
    "content": "import 'package:flame/src/effects/effect.dart';\n\n/// Mixin adds field [target] of type [T] to an [Effect]. The target can be\n/// either set explicitly by the effect class, or acquired automatically from\n/// the effect's parent when mounting.\n///\n/// The type [T] can be either a Component subclass, or one of the property\n/// providers defined in \"provider_interfaces.dart\".\nmixin EffectTarget<T> on Effect {\n  T get target => _target!;\n  T? _target;\n  set target(T? value) {\n    _target = value;\n  }\n\n  @override\n  void onMount() {\n    if (_target == null) {\n      final parent = ancestors().firstWhere((c) => c is! Effect);\n      if (parent is! T) {\n        throw UnsupportedError('Can only apply this effect to $T');\n      }\n      _target = parent as T;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/function_effect.dart",
    "content": "import 'package:flame/src/effects/effect.dart';\nimport 'package:flame/src/effects/effect_target.dart';\n\n/// The `FunctionEffect` class is a very generic Effect that allows you to\n/// do almost anything without having to define a new effect.\n///\n/// It runs a function that takes the target and the progress of the effect and\n/// then the user can decide what to do with that input.\n///\n/// This could for example be used to make game state changes that happen over\n/// time, but that isn't necessarily visual, like most other effects are.\nclass FunctionEffect<T> extends Effect with EffectTarget<T> {\n  FunctionEffect(\n    this.function,\n    super.controller, {\n    super.onComplete,\n    super.key,\n  });\n\n  void Function(T target, double progress) function;\n\n  @override\n  void apply(double progress) {\n    function(target, progress);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/glow_effect.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/effects.dart';\n\n/// Change the MaskFilter on Paint of a component over time.\n///\n/// This effect applies incremental changes to the MaskFilter on Paint of a\n/// component and requires that any other effect or update logic applied to the\n/// same component also used incremental updates.\nclass GlowEffect extends Effect with EffectTarget<PaintProvider> {\n  GlowEffect(\n    this.strength,\n    super.controller, {\n    this.style = BlurStyle.outer,\n    super.key,\n  });\n\n  final BlurStyle style;\n  final double strength;\n\n  @override\n  void apply(double progress) {\n    target.paint.maskFilter = MaskFilter.blur(style, strength * progress);\n  }\n\n  @override\n  void reset() {\n    super.reset();\n    target.paint.maskFilter = null;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/hue_by_effect.dart",
    "content": "import 'package:flame/src/effects/hue_effect.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\n\n/// An effect that changes the hue of a component by a specified angle.\nclass HueByEffect extends HueEffect {\n  HueByEffect(\n    double angle,\n    super.controller, {\n    HueProvider? target,\n    super.onComplete,\n    super.key,\n  }) : _angle = angle {\n    this.target = target;\n  }\n\n  final double _angle;\n\n  @override\n  void apply(double progress) {\n    final dProgress = progress - previousProgress;\n    target.hue += _angle * dProgress;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/hue_effect.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/effect.dart';\nimport 'package:flame/src/effects/effect_target.dart';\nimport 'package:flame/src/effects/hue_by_effect.dart';\nimport 'package:flame/src/effects/hue_to_effect.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\n\n/// An effect that changes the hue of a component over time.\n///\n/// This effect applies incremental changes to the hue property of the target,\n/// and requires that any other effect or update logic applied to the same\n/// target also used incremental updates.\nabstract class HueEffect extends Effect with EffectTarget<HueProvider> {\n  HueEffect(\n    super.controller, {\n    super.onComplete,\n    super.key,\n  });\n\n  factory HueEffect.by(\n    double angle,\n    EffectController controller, {\n    HueProvider? target,\n    void Function()? onComplete,\n    ComponentKey? key,\n  }) {\n    return HueByEffect(\n      angle,\n      controller,\n      target: target,\n      onComplete: onComplete,\n      key: key,\n    );\n  }\n\n  factory HueEffect.to(\n    double angle,\n    EffectController controller, {\n    HueProvider? target,\n    void Function()? onComplete,\n    ComponentKey? key,\n  }) {\n    return HueToEffect(\n      angle,\n      controller,\n      target: target,\n      onComplete: onComplete,\n      key: key,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/hue_to_effect.dart",
    "content": "import 'package:flame/src/effects/hue_effect.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\n\n/// An effect that changes the hue of a component to a specified angle.\nclass HueToEffect extends HueEffect {\n  HueToEffect(\n    double angle,\n    super.controller, {\n    HueProvider? target,\n    super.onComplete,\n    super.key,\n  }) : _destinationAngle = angle,\n       _angle = 0.0 {\n    this.target = target;\n  }\n\n  final double _destinationAngle;\n  double _angle;\n\n  @override\n  void onStart() {\n    _angle = _destinationAngle - target.hue;\n  }\n\n  @override\n  void apply(double progress) {\n    final dProgress = progress - previousProgress;\n    target.hue += _angle * dProgress;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/measurable_effect.dart",
    "content": "import 'package:flame/src/effects/controllers/speed_effect_controller.dart';\n\n/// Interface implemented by several other Effect classes. This interface is\n/// required by [SpeedEffectController]: it allows that controller to compute\n/// the \"length\" of the effect, and divide by the speed in order to get the\n/// desired duration of the effect.\n///\n/// Use this interface for any effect that you want to be compatible with the\n/// [SpeedEffectController].\nabstract class MeasurableEffect {\n  /// Calculate the \"measure\" of the effect, which is the effect's distance\n  /// from min to max progress. The \"measure\" is any property for which the\n  /// notion of _speed_ is well-defined.\n  ///\n  /// This method will be called at the start of the effect, but after the\n  /// Effect's `onStart` callback. It will be called again after the effect is\n  /// reset.\n  double measure();\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/move_along_path_effect.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/move_effect.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\n\n/// This effect will move the target along the specified path, which may\n/// contain curved segments, but must be simply-connected.\n///\n/// If `absolute` is false (default), the `path` argument will be taken as\n/// relative to the target's position at the start of the effect. It is\n/// recommended in this case to have a path that starts at the origin in order\n/// to avoid sudden jumps in the target's position.\n///\n/// If `absolute` flag is true, then the `path` will be assumed to be given in\n/// absolute coordinate space and the target will be placed at the beginning of\n/// the path when the effect starts.\n///\n/// The `oriented` flag controls the direction of the target as it follows the\n/// path. If this flag is false (default), the target keeps its original\n/// orientation. If the flag is true, the target is automatically rotated as it\n/// follows the path so that it is always oriented tangent to the path. When\n/// using this flag, make sure that the effect is applied to a target that\n/// actually supports rotations.\nclass MoveAlongPathEffect extends MoveEffect {\n  MoveAlongPathEffect(\n    Path path,\n    EffectController controller, {\n    bool absolute = false,\n    bool oriented = false,\n    PositionProvider? target,\n    void Function()? onComplete,\n    super.key,\n  }) : _isAbsolute = absolute,\n       _followDirection = oriented,\n       super(\n         controller,\n         target,\n         onComplete: onComplete,\n       ) {\n    final metrics = path.computeMetrics().toList();\n    if (metrics.length != 1) {\n      throw ArgumentError(\n        'Only single-contour paths are allowed in MoveAlongPathEffect',\n      );\n    }\n    _pathMetric = metrics[0];\n    _pathLength = _pathMetric.length;\n    assert(_pathLength > 0);\n  }\n\n  /// If true, the path is considered _absolute_, i.e. the component will be\n  /// put onto the start of the path and then follow that path. If false, the\n  /// path is considered _relative_, i.e. this path is added as an offset to\n  /// the current position of the target.\n  final bool _isAbsolute;\n\n  /// If true, then not only the target's position will follow the path, but\n  /// also the target's angle of rotation.\n  final bool _followDirection;\n\n  /// The path that the target will follow.\n  late final PathMetric _pathMetric;\n\n  /// Pre-computed length of the path.\n  late final double _pathLength;\n\n  /// Position offset that was applied to the target on the previous iteration.\n  /// This is needed in order to make updates to `target.position` incremental\n  /// (which in turn is necessary in order to allow multiple effects to be able\n  /// to apply to the same target simultaneously).\n  late Vector2 _lastOffset;\n\n  /// Target's angle of rotation on the previous iteration.\n  late double _lastAngle;\n\n  @override\n  void onStart() {\n    _lastOffset = Vector2.zero();\n    _lastAngle = 0;\n    final start = _pathMetric.getTangentForOffset(0)!;\n    if (_followDirection) {\n      assert(\n        target is AngleProvider,\n        'An `oriented` MoveAlongPathEffect cannot be applied to a target that '\n        'does not support rotation',\n      );\n      _lastAngle = -start.angle;\n      final targetProvider = target;\n      (targetProvider as AngleProvider).angle = -start.angle;\n      if (targetProvider is PositionComponent) {\n        targetProvider.angle += targetProvider.nativeAngle;\n      }\n    }\n    if (_isAbsolute) {\n      target.position.x = _lastOffset.x = start.position.dx;\n      target.position.y = _lastOffset.y = start.position.dy;\n    }\n  }\n\n  @override\n  void apply(double progress) {\n    final distance = progress * _pathLength;\n    final tangent = _pathMetric.getTangentForOffset(distance)!;\n    final offset = tangent.position;\n    target.position.x += offset.dx - _lastOffset.x;\n    target.position.y += offset.dy - _lastOffset.y;\n    _lastOffset.x = offset.dx;\n    _lastOffset.y = offset.dy;\n    if (_followDirection) {\n      (target as AngleProvider).angle += -tangent.angle - _lastAngle;\n      _lastAngle = -tangent.angle;\n    }\n  }\n\n  @override\n  double measure() => _pathLength;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/move_by_effect.dart",
    "content": "import 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/move_effect.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// A [MoveEffect] that moves its target by the specified offset vector.\n///\n/// This effect will move its target in a straight line to a new position that\n/// is at an `offset` from the target's position at the start of the effect.\n///\n/// The `controller` can be used to change the timing of the movement: when the\n/// move starts, how fast it is, whether the motion is uniform or not, and so\n/// on. The [EffectController] can even be used to create oscillating or random\n/// motions.\n///\n/// This effect applies incremental changes to the target's position, which\n/// allows it to be combined with other [MoveEffect]s. When several\n/// [MoveByEffect]s are applied to the same target simultaneously, the target\n/// is moved by the vector sum of offsets from all effects.\nclass MoveByEffect extends MoveEffect {\n  MoveByEffect(\n    Vector2 offset,\n    EffectController controller, {\n    PositionProvider? target,\n    void Function()? onComplete,\n    super.key,\n  }) : _offset = offset.clone(),\n       super(controller, target, onComplete: onComplete);\n\n  final Vector2 _offset;\n\n  @override\n  void apply(double progress) {\n    final dProgress = progress - previousProgress;\n    target.position += _offset * dProgress;\n  }\n\n  @override\n  double measure() => _offset.length;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/move_effect.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/effect.dart';\nimport 'package:flame/src/effects/effect_target.dart';\nimport 'package:flame/src/effects/measurable_effect.dart';\nimport 'package:flame/src/effects/move_by_effect.dart';\nimport 'package:flame/src/effects/move_to_effect.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\n\n/// Base class for effects that affect the `position` of their targets.\n///\n/// The main purpose of this class is for reflection, for example to select\n/// all effects on the target that are of \"move\" type.\n///\n/// Factory constructors [MoveEffect.by] and [MoveEffect.to] are also provided,\n/// but they may be deprecated in the future.\nabstract class MoveEffect extends Effect\n    with EffectTarget<PositionProvider>\n    implements MeasurableEffect {\n  MoveEffect(\n    super.controller,\n    PositionProvider? target, {\n    super.onComplete,\n    super.key,\n  }) {\n    this.target = target;\n  }\n\n  factory MoveEffect.by(\n    Vector2 offset,\n    EffectController controller, {\n    PositionProvider? target,\n    void Function()? onComplete,\n    ComponentKey? key,\n  }) => MoveByEffect(\n    offset,\n    controller,\n    target: target,\n    onComplete: onComplete,\n    key: key,\n  );\n\n  factory MoveEffect.to(\n    Vector2 destination,\n    EffectController controller, {\n    PositionProvider? target,\n    void Function()? onComplete,\n    ComponentKey? key,\n  }) => MoveToEffect(\n    destination,\n    controller,\n    target: target,\n    onComplete: onComplete,\n    key: key,\n  );\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/move_to_effect.dart",
    "content": "import 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/move_by_effect.dart';\nimport 'package:flame/src/effects/move_effect.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\nimport 'package:meta/meta.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// A [MoveEffect] that moves its target towards the given destination point.\n///\n/// This effect will move its target in a straight line towards the provided\n/// `destination` position. The `controller` can be used to change the timing\n/// of the movement: when it starts, the speed, whether the motion is uniform\n/// or not, and so on. Refer to [EffectController] for details.\n///\n/// This effect applies incremental changes to the target's position, which\n/// allows it to be combined with other [MoveEffect]s. Care must be taken to\n/// compose effects in a sensible way. For example, applying a [MoveToEffect]\n/// towards point A, and simultaneously another [MoveToEffect] towards point B\n/// will end up moving the target towards point A+B. A more interesting\n/// combination of move effects is to have a [MoveToEffect], together with one\n/// or more [MoveByEffect]s that produce oscillating motion.\nclass MoveToEffect extends MoveEffect {\n  MoveToEffect(\n    Vector2 destination,\n    EffectController controller, {\n    PositionProvider? target,\n    void Function()? onComplete,\n    super.key,\n  }) : _destination = destination.clone(),\n       _offset = Vector2.zero(),\n       super(controller, target, onComplete: onComplete);\n\n  final Vector2 _destination;\n  final Vector2 _offset;\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    super.onMount();\n    _offset.setFrom(_destination - target.position);\n  }\n\n  @override\n  @mustCallSuper\n  void onStart() {\n    _offset.setFrom(_destination - target.position);\n  }\n\n  @override\n  void apply(double progress) {\n    final dProgress = progress - previousProgress;\n    target.position += _offset * dProgress;\n  }\n\n  @override\n  double measure() => _offset.length;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/opacity_effect.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\n\n/// Change the opacity of a component over time.\n///\n/// This effect applies incremental changes to the component's opacity, and\n/// requires that any other effect or update logic applied to the same component\n/// also used incremental updates.\nclass OpacityEffect extends Effect with EffectTarget<OpacityProvider> {\n  /// This constructor will set the opacity in relation to it's current opacity\n  /// over time.\n  OpacityEffect.by(\n    double offset,\n    super.controller, {\n    OpacityProvider? target,\n    super.onComplete,\n    super.key,\n  }) : _opacityOffset = offset {\n    this.target = target;\n  }\n\n  /// This constructor will set the opacity to the specified opacity over time.\n  factory OpacityEffect.to(\n    double targetOpacity,\n    EffectController controller, {\n    OpacityProvider? target,\n    void Function()? onComplete,\n    ComponentKey? key,\n  }) {\n    return _OpacityToEffect(\n      targetOpacity,\n      controller,\n      target: target,\n      onComplete: onComplete,\n      key: key,\n    );\n  }\n\n  factory OpacityEffect.fadeIn(\n    EffectController controller, {\n    OpacityProvider? target,\n    void Function()? onComplete,\n    ComponentKey? key,\n  }) {\n    return _OpacityToEffect(\n      1.0,\n      controller,\n      target: target,\n      onComplete: onComplete,\n      key: key,\n    );\n  }\n\n  factory OpacityEffect.fadeOut(\n    EffectController controller, {\n    OpacityProvider? target,\n    void Function()? onComplete,\n    ComponentKey? key,\n  }) {\n    return _OpacityToEffect(\n      0.0,\n      controller,\n      target: target,\n      onComplete: onComplete,\n      key: key,\n    );\n  }\n\n  double _opacityOffset;\n  double _roundingError = 0.0;\n\n  @override\n  void apply(double progress) {\n    final deltaProgress = progress - previousProgress;\n    final currentOpacity = target.opacity + _roundingError;\n    final deltaOpacity = _opacityOffset * deltaProgress;\n    final newOpacity = (currentOpacity + deltaOpacity).clamp(0, 1).toDouble();\n    target.opacity = newOpacity;\n    _roundingError = newOpacity - target.opacity;\n  }\n\n  @override\n  void reset() {\n    super.reset();\n    // We can't accumulate rounding errors between resets because we don't know\n    // if the opacity has been affected by anything else in between.\n    _roundingError = 0.0;\n  }\n}\n\n/// Implementation class for [OpacityEffect.to]\nclass _OpacityToEffect extends OpacityEffect {\n  final double _targetOpacity;\n\n  _OpacityToEffect(\n    this._targetOpacity,\n    EffectController controller, {\n    OpacityProvider? target,\n    void Function()? onComplete,\n    super.key,\n  }) : super.by(\n         0.0,\n         controller,\n         target: target,\n         onComplete: onComplete,\n       );\n\n  @override\n  void onStart() {\n    _opacityOffset = _targetOpacity - target.opacity;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/provider_interfaces.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\n\n/// Interface for a component that can be affected by move effects.\nabstract class PositionProvider implements ReadOnlyPositionProvider {\n  set position(Vector2 value);\n}\n\n/// Interface for a class that has [position] property which can be read but not\n/// modified.\nabstract class ReadOnlyPositionProvider {\n  Vector2 get position;\n}\n\n/// This class allows constructing [PositionProvider]s on the fly, using the\n/// callbacks for the position getter and setter. This class doesn't require\n/// either the getter or the setter, if you do not intend to use those.\nclass PositionProviderImpl implements PositionProvider {\n  PositionProviderImpl({\n    Vector2 Function()? getValue,\n    void Function(Vector2)? setValue,\n  }) : _getter = getValue,\n       _setter = setValue;\n\n  final Vector2 Function()? _getter;\n  final void Function(Vector2)? _setter;\n\n  @override\n  Vector2 get position => _getter!();\n\n  @override\n  set position(Vector2 value) => _setter!(value);\n}\n\n/// Interface for a class that has [scale] property which can be read but not\n/// modified.\nabstract class ReadOnlyScaleProvider {\n  Vector2 get scale;\n}\n\n/// Interface for a component that can be affected by scale effects.\nabstract class ScaleProvider extends ReadOnlyScaleProvider {\n  set scale(Vector2 value);\n}\n\n/// Interface for a class that has [angle] property which can be read but not\n/// modified.\nabstract class ReadOnlyAngleProvider {\n  double get angle;\n}\n\n/// Interface for a component that can be affected by rotation effects.\nabstract class AngleProvider extends ReadOnlyAngleProvider {\n  set angle(double value);\n}\n\n/// Interface for a component that can be affected by anchor effects.\nabstract class AnchorProvider {\n  Anchor get anchor;\n  set anchor(Anchor value);\n}\n\n/// Interface for a class that has [size] property which can be read but not\n/// modified.\nabstract class ReadOnlySizeProvider {\n  Vector2 get size;\n}\n\n/// Interface for a component that can be affected by size effects.\nabstract class SizeProvider extends ReadOnlySizeProvider {\n  set size(Vector2 value);\n}\n\n/// Interface for a component that can be affected by opacity effects.\n/// Value of [opacity] must be in the range of 0-1 (both inclusive).\n///\n/// It is allowed for implementers of this interface to use an integer for\n/// internal representation. In such cases, [opacity] get/set are expected\n/// to perform necessary conversions from integer to double and vice versa.\n/// As a side effect of this, setting the opacity to some `x` value would not\n/// necessarily produce the same `x` when reading it back.\n///\n/// See [HasPaint] for an example implementation.\nabstract class OpacityProvider {\n  double get opacity;\n  set opacity(double value);\n}\n\n/// Interface for a component that can be affected by Paint effects.\n///\n/// See [HasPaint] for an example implementation.\nabstract class PaintProvider {\n  Paint get paint;\n  set paint(Paint value);\n}\n\n/// Interface for a component that can be affected by hue effects.\nabstract class HueProvider {\n  double get hue;\n  set hue(double value);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/remove_effect.dart",
    "content": "import 'package:flame/effects.dart';\n\n/// This simple effect, when attached to a component, will cause that component\n/// to be removed from the game tree after `delay` seconds.\nclass RemoveEffect extends ComponentEffect {\n  RemoveEffect({\n    double delay = 0.0,\n    void Function()? onComplete,\n    super.key,\n  }) : super(\n         LinearEffectController(delay),\n         onComplete: onComplete,\n       );\n\n  @override\n  void apply(double progress) {\n    if (progress == 1) {\n      target.removeFromParent();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/rotate_around_effect.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/src/effects/effect.dart';\nimport 'package:flame/src/effects/effect_target.dart';\nimport 'package:flame/src/effects/measurable_effect.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\n\n/// Rotate a component around [center].\n///\n/// This effect rotates the target around a fixed point in its parent, specified\n/// by [center]. The target's angle is by default aligned to the target's\n/// angle around center, but this can be disabled by setting [alignRotation] to\n/// false.\nclass RotateAroundEffect extends Effect\n    with EffectTarget<PositionProvider>\n    implements MeasurableEffect {\n  RotateAroundEffect(\n    this.angle,\n    super.controller, {\n    required this.center,\n    this.alignRotation = true,\n    super.onComplete,\n    super.key,\n  });\n\n  /// The magnitude of the effect: how much the target should turn as the\n  /// progress goes from 0 to 1.\n  final double angle;\n\n  /// The center (pivot point) of the rotation.\n  final Vector2 center;\n\n  /// Whether the targets angle should be aligned to the target's current\n  /// rotation around [center].\n  final bool alignRotation;\n\n  @override\n  void apply(double progress) {\n    final dProgress = progress - previousProgress;\n    if (alignRotation && target is AngleProvider) {\n      (target as AngleProvider).angle += angle * dProgress;\n    }\n    target.position.rotate(angle * dProgress, center: center);\n  }\n\n  @override\n  double measure() => angle;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/rotate_effect.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/effect.dart';\nimport 'package:flame/src/effects/effect_target.dart';\nimport 'package:flame/src/effects/measurable_effect.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\n\n/// Rotate a component around its anchor.\n///\n/// Two constructors are provided:\n///   - [RotateEffect.by] will rotate the target by the specified `angle`\n///     relative to its orientation at the onset of the effect. For example,\n///     rotating by `angle = tau/4` will turn the component 90° clockwise\n///     relative to its initial direction;\n///   - [RotateEffect.to] will rotate the target to the fixed orientation\n///     specified by the `angle`. For example, rotating to `angle = tau/4` will\n///     turn the component to look East regardless of its initial bearing.\n///\n/// This effect applies incremental changes to the component's angle, and\n/// requires that any other effect or update logic applied to the same component\n/// also used incremental updates.\nclass RotateEffect extends Effect\n    with EffectTarget<AngleProvider>\n    implements MeasurableEffect {\n  RotateEffect.by(\n    double angle,\n    super.controller, {\n    super.onComplete,\n    super.key,\n  }) : _angle = angle;\n\n  factory RotateEffect.to(\n    double angle,\n    EffectController controller, {\n    void Function()? onComplete,\n    ComponentKey? key,\n  }) {\n    return _RotateToEffect(\n      angle,\n      controller,\n      onComplete: onComplete,\n      key: key,\n    );\n  }\n\n  /// The magnitude of the effect: how much the target should turn as the\n  /// progress goes from 0 to 1.\n  double _angle;\n\n  @override\n  void apply(double progress) {\n    final dProgress = progress - previousProgress;\n    target.angle += _angle * dProgress;\n  }\n\n  @override\n  double measure() => _angle;\n}\n\nclass _RotateToEffect extends RotateEffect {\n  _RotateToEffect(\n    double angle,\n    EffectController controller, {\n    void Function()? onComplete,\n    super.key,\n  }) : _destinationAngle = angle,\n       super.by(0, controller, onComplete: onComplete);\n\n  final double _destinationAngle;\n\n  @override\n  void onStart() {\n    _angle = _destinationAngle - target.angle;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/scale_effect.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/effect.dart';\nimport 'package:flame/src/effects/effect_target.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\n\n/// Scale a component.\n///\n/// The following constructors are provided:\n///\n///   - [ScaleEffect.by] will scale the target by the given factor, relative to\n///     its current scale;\n///   - [ScaleEffect.to] will scale the target to the specified scale.\n///\n/// This effect applies incremental changes to the component's scale, and\n/// requires that any other effect or update logic applied to the same component\n/// also used incremental updates.\nclass ScaleEffect extends Effect with EffectTarget<ScaleProvider> {\n  ScaleEffect.by(\n    Vector2 scaleFactor,\n    super.controller, {\n    super.onComplete,\n    super.key,\n  }) : _scaleFactor = scaleFactor.clone();\n\n  factory ScaleEffect.to(\n    Vector2 targetScale,\n    EffectController controller, {\n    void Function()? onComplete,\n    ComponentKey? key,\n  }) => _ScaleToEffect(\n    targetScale,\n    controller,\n    onComplete: onComplete,\n    key: key,\n  );\n\n  final Vector2 _scaleFactor;\n  late Vector2 _scaleDelta;\n\n  @override\n  void onStart() {\n    _scaleDelta = Vector2(\n      target.scale.x * (_scaleFactor.x - 1),\n      target.scale.y * (_scaleFactor.y - 1),\n    );\n  }\n\n  @override\n  void apply(double progress) {\n    final dProgress = progress - previousProgress;\n    target.scale += _scaleDelta * dProgress;\n  }\n}\n\n/// Implementation class for [ScaleEffect.to]\nclass _ScaleToEffect extends ScaleEffect {\n  final Vector2 _targetScale;\n\n  _ScaleToEffect(\n    Vector2 targetScale,\n    EffectController controller, {\n    void Function()? onComplete,\n    super.key,\n  }) : _targetScale = targetScale.clone(),\n       super.by(\n         Vector2.zero(),\n         controller,\n         onComplete: onComplete,\n       );\n\n  @override\n  void onStart() {\n    _scaleDelta = _targetScale - target.scale;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/sequence_effect.dart",
    "content": "import 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/controllers/infinite_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/repeated_effect_controller.dart';\nimport 'package:flame/src/effects/effect.dart';\n\nEffectController _createController({\n  required List<Effect> effects,\n  required bool alternate,\n  required bool infinite,\n  required int repeatCount,\n}) {\n  EffectController ec = _SequenceEffectEffectController(\n    effects,\n    alternate: alternate,\n  );\n  if (infinite) {\n    ec = InfiniteEffectController(ec);\n  } else if (repeatCount > 1) {\n    ec = RepeatedEffectController(ec, repeatCount);\n  }\n  effects.forEach((e) => e.removeOnFinish = false);\n  return ec;\n}\n\n/// Run multiple effects in a sequence, one after another.\n///\n/// The provided effects will be added as child components; however the custom\n/// `updateTree()` implementation ensures that only one of them runs at any\n/// point in time. The flags `paused` or `removeOnFinish` will be ignored for\n/// children effects.\n///\n/// If the `alternate` flag is provided, then the sequence will run in the\n/// reverse after it ran forward.\n///\n/// Parameter `repeatCount` will make the sequence repeat a certain number of\n/// times. If `alternate` is also true, then the sequence will first run\n/// forward, then back, and then repeat this pattern `repeatCount` times in\n/// total.\n///\n/// The flag `infinite = true` makes the sequence repeat infinitely. This is\n/// equivalent to setting `repeatCount = infinity`. If both the `infinite` and\n/// the `repeatCount` parameters are given, then `infinite` takes precedence.\n///\n/// Note that unlike other effects, [SequenceEffect] does not take an\n/// [EffectController] as a parameter. This is because the timing of a sequence\n/// effect depends on the timings of individual effects, and cannot be\n/// represented as a regular effect controller.\nclass SequenceEffect extends Effect {\n  SequenceEffect(\n    List<Effect> effects, {\n    bool alternate = false,\n    bool infinite = false,\n    int repeatCount = 1,\n    super.onComplete,\n    super.key,\n  }) : assert(effects.isNotEmpty, 'The list of effects cannot be empty'),\n       assert(\n         !(infinite && repeatCount != 1),\n         'Parameters infinite and repeatCount cannot be specified '\n         'simultaneously',\n       ),\n       super(\n         _createController(\n           effects: effects,\n           alternate: alternate,\n           infinite: infinite,\n           repeatCount: repeatCount,\n         ),\n       ) {\n    addAll(effects);\n  }\n\n  @override\n  void apply(double progress) {}\n\n  @override\n  void updateTree(double dt) {\n    update(dt);\n    // Do not update children: the controller will take care of it\n  }\n}\n\n/// Helper class that implements the functionality of a [SequenceEffect]. This\n/// class should not be confused with `SequenceEffectController` (which runs\n/// a sequence of effect controllers).\n///\n/// This effect controller does not strictly adheres to the interface of a\n/// proper [EffectController]: in particular, its [progress] is ill-defined.\n/// The provided implementation returns a value proportional to the number of\n/// effects that has already completed, however this is not used anywhere since\n/// `SequenceEffect.apply()` is empty.\nclass _SequenceEffectEffectController extends EffectController {\n  _SequenceEffectEffectController(\n    this.effects, {\n    required this.alternate,\n  }) : super.empty();\n\n  /// The list of children effects.\n  final List<Effect> effects;\n\n  /// If this flag is true, then after the sequence runs to the end, it will\n  /// run again in the reverse order.\n  final bool alternate;\n\n  /// Index of the currently running effect within the [effects] list. If there\n  /// are n effects in total, then this runs as 0, 1, ..., n-1. After that, if\n  /// the effect alternates, then the `_index` continues as -1, -2, ..., -n,\n  /// where -1 is the last effect and -n is the first.\n  int _index = 0;\n\n  /// The effect that is currently being executed.\n  Effect get currentEffect => effects[_index < 0 ? _index + n : _index];\n\n  /// Total number of effects in this sequence.\n  int get n => effects.length;\n\n  @override\n  bool get completed => _completed;\n  bool _completed = false;\n\n  @override\n  double? get duration {\n    var totalDuration = 0.0;\n    for (final effect in effects) {\n      totalDuration += effect.controller.duration ?? 0;\n    }\n    if (alternate) {\n      totalDuration *= 2;\n    }\n    return totalDuration;\n  }\n\n  @override\n  bool get isRandom {\n    return effects.any((e) => e.controller.isRandom);\n  }\n\n  @override\n  double get progress => (_index + 1) / n;\n\n  @override\n  double advance(double dt) {\n    var t = dt;\n    for (;;) {\n      if (_index >= 0) {\n        t = currentEffect.advance(t);\n        if (t > 0) {\n          _index += 1;\n          if (_index == n) {\n            if (alternate) {\n              _index = -1;\n            } else {\n              _index = n - 1;\n              _completed = true;\n              break;\n            }\n          }\n        }\n      } else {\n        t = currentEffect.recede(t);\n        if (t > 0) {\n          _index -= 1;\n          if (_index < -n) {\n            _index = -n;\n            _completed = true;\n            break;\n          }\n        }\n      }\n      if (t == 0) {\n        break;\n      }\n    }\n    return t;\n  }\n\n  @override\n  double recede(double dt) {\n    if (_completed && dt > 0) {\n      _completed = false;\n    }\n    var t = dt;\n    for (;;) {\n      if (_index >= 0) {\n        t = currentEffect.recede(t);\n        if (t > 0) {\n          _index -= 1;\n          if (_index < 0) {\n            _index = 0;\n            break;\n          }\n        }\n      } else {\n        t = currentEffect.advance(t);\n        if (t > 0) {\n          _index += 1;\n          if (_index == 0) {\n            _index = n - 1;\n          }\n        }\n      }\n      if (t == 0) {\n        break;\n      }\n    }\n    return t;\n  }\n\n  @override\n  void setToEnd() {\n    if (alternate) {\n      _index = -n;\n      effects.forEach((e) => e.reset());\n    } else {\n      _index = n - 1;\n      effects.forEach((e) => e.resetToEnd());\n    }\n    _completed = true;\n  }\n\n  @override\n  void setToStart() {\n    _index = 0;\n    _completed = false;\n    effects.forEach((e) => e.reset());\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/size_effect.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/effect.dart';\nimport 'package:flame/src/effects/effect_target.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\n\n/// Change the size of a component over time.\n///\n/// This effect applies incremental changes to the component's size, and\n/// requires that any other effect or update logic applied to the same component\n/// also used incremental updates.\nclass SizeEffect extends Effect with EffectTarget<SizeProvider> {\n  /// This constructor will create an effect that sets the size in relation to\n  /// the [PositionComponent]'s  current size, for example if the [offset] is\n  /// set to `Vector2(10, -10)` and the size of the affected component is\n  /// `Vector2(100, 100)` at the start of the affected the effect will peak when\n  /// the size is `Vector2(110, 90)`, if there is nothing else affecting the\n  /// size at the same time.\n  SizeEffect.by(\n    Vector2 offset,\n    super.controller, {\n    SizeProvider? target,\n    super.onComplete,\n    super.key,\n  }) : _offset = offset.clone() {\n    this.target = target;\n  }\n\n  /// This constructor will create an effect that sets the size to the absolute\n  /// size that is defined by [targetSize].\n  /// For example if the [targetSize] is set to `Vector2(200, 200)` and the size\n  /// of the affected component is `Vector2(100, 100)` at the start of the\n  /// affected the effect will peak when the size is `Vector2(200, 100)`, if\n  /// there is nothing else affecting the size at the same time.\n  factory SizeEffect.to(\n    Vector2 targetSize,\n    EffectController controller, {\n    void Function()? onComplete,\n    ComponentKey? key,\n  }) => _SizeToEffect(\n    targetSize,\n    controller,\n    onComplete: onComplete,\n    key: key,\n  );\n\n  Vector2 _offset;\n\n  @override\n  void apply(double progress) {\n    final dProgress = progress - previousProgress;\n    target.size += _offset * dProgress;\n    target.size.clampScalar(0, double.infinity);\n  }\n}\n\n/// Implementation class for [SizeEffect.to]\nclass _SizeToEffect extends SizeEffect {\n  final Vector2 _targetSize;\n\n  _SizeToEffect(\n    Vector2 targetSize,\n    EffectController controller, {\n    void Function()? onComplete,\n    super.key,\n  }) : _targetSize = targetSize.clone(),\n       super.by(\n         Vector2.zero(),\n         controller,\n         onComplete: onComplete,\n       );\n\n  @override\n  void onStart() {\n    _offset = _targetSize - target.size;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/effects/transform2d_effect.dart",
    "content": "import 'package:flame/src/components/position_component.dart';\nimport 'package:flame/src/effects/component_effect.dart';\nimport 'package:flame/src/game/transform2d.dart';\n\n/// Base class for effects that target a [Transform2D] property.\n///\n/// Examples of effects of this kind include move effects, rotate effects,\n/// shake effects, scale effects, etc. In order to apply such an effect to a\n/// component simply add the effect as a child to that component.\n///\n/// Currently this class only supports being attached to [PositionComponent]s,\n/// but in the future it will be extended to work with any [Transform2D]-based\n/// classes.\nabstract class Transform2DEffect extends ComponentEffect<PositionComponent> {\n  Transform2DEffect(\n    super.controller, {\n    super.onComplete,\n    super.key,\n  });\n\n  late Transform2D transform;\n\n  @override\n  void onMount() {\n    super.onMount();\n    transform = target.transform;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/component_mixins/double_tap_callbacks.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\n\n/// [DoubleTapCallbacks] adds the ability to receive double-tap events in a\n/// component.\n///\n/// In addition to adding this mixin, the component must also implement the\n/// [containsLocalPoint] method.\n///\n/// At present, flutter detects only one double-tap events simultaneously.\n/// This means that if you're double-tapping two [DoubleTapCallbacks] located\n/// far away from each other, only one callback will be fired (or none).\nmixin DoubleTapCallbacks on Component {\n  /// This triggers when the pointer stops contacting the device after the\n  /// second tap.\n  void onDoubleTapUp(DoubleTapEvent event) {}\n\n  /// This triggers immediately after the down event of the second tap.\n  void onDoubleTapDown(DoubleTapDownEvent event) {}\n\n  /// This triggers once the gesture loses the arena if [onDoubleTapDown] has\n  /// previously been triggered.\n  void onDoubleTapCancel(DoubleTapCancelEvent event) {}\n\n  @override\n  void onMount() {\n    super.onMount();\n    final game = findRootGame()!;\n    if (game.findByKey(const DoubleTapDispatcherKey()) == null) {\n      final dispatcher = DoubleTapDispatcher();\n      game.registerKey(const DoubleTapDispatcherKey(), dispatcher);\n      game.add(dispatcher);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/component_mixins/drag_callbacks.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:meta/meta.dart';\n\n/// This mixin can be added to a [Component] allowing it to receive drag events.\n///\n/// In addition to adding this mixin, the component must also implement the\n/// [containsLocalPoint] method -- the component will only register the start of\n/// a drag if the point where the initial touch event has occurred was inside\n/// the component.\n///\n/// This mixin is the replacement of the Draggable mixin.\nmixin DragCallbacks on Component {\n  bool _isDragged = false;\n\n  /// Returns true while the component is being dragged.\n  bool get isDragged => _isDragged;\n\n  /// The user initiated a drag gesture on top of this component.\n  ///\n  /// By default, only one component will receive a drag event. However, setting\n  /// the property `event.continuePropagation` to true will allow the event to\n  /// also reach the components below this one.\n  ///\n  /// Once a component receives the [onDragStart] event, the subsequent events\n  /// [onDragUpdate], [onDragEnd], and [onDragCancel] with the same pointer id\n  /// will be delivered to the same component. If multiple components have\n  /// received the initial [onDragStart] event, then all of them will be\n  /// receiving the follow-up events.\n  @mustCallSuper\n  void onDragStart(DragStartEvent event) {\n    _isDragged = true;\n  }\n\n  /// The user has moved the pointer that initiated the drag gesture.\n  ///\n  /// This event will be delivered to the component(s) that captured the initial\n  /// [onDragStart], even if the point of touch moves outside of the boundaries\n  /// of the component. In the latter case `event.localPosition` will contain a\n  /// NaN point.\n  void onDragUpdate(DragUpdateEvent event) {}\n\n  /// The drag event has ended.\n  ///\n  /// This event will be delivered to the component(s) that captured the initial\n  /// [onDragStart], even if the point of touch moves outside of the boundaries\n  /// of the component.\n  @mustCallSuper\n  void onDragEnd(DragEndEvent event) {\n    _isDragged = false;\n  }\n\n  /// The drag was cancelled.\n  ///\n  /// This is a very rare event, so we provide a default implementation that\n  /// converts it into an [onDragEnd] event.\n  @mustCallSuper\n  void onDragCancel(DragCancelEvent event) => onDragEnd(event.toDragEnd());\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    super.onMount();\n    final game = findRootGame()!;\n    if (game.findByKey(const MultiDragDispatcherKey()) == null) {\n      final dispatcher = MultiDragDispatcher();\n      game.registerKey(const MultiDragDispatcherKey(), dispatcher);\n      game.add(dispatcher);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/component_mixins/hover_callbacks.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:meta/meta.dart';\n\n/// This mixin can be added to a [Component] allowing it to receive hover\n/// events.\n///\n/// In addition to adding this mixin, the component must also implement the\n/// [containsLocalPoint] method -- the component will only be considered\n/// \"hovered\" if the point where the hover event occurred is inside the\n/// component.\n///\n/// This mixin is the replacement of the Hoverable mixin.\nmixin HoverCallbacks on Component implements PointerMoveCallbacks {\n  bool _isHovered = false;\n\n  /// Returns true while the component is being dragged.\n  bool get isHovered => _isHovered;\n\n  void onHoverEnter() {}\n\n  void onHoverExit() {}\n\n  void _doHoverEnter() {\n    _isHovered = true;\n    onHoverEnter();\n  }\n\n  void _doHoverExit() {\n    _isHovered = false;\n    onHoverExit();\n  }\n\n  @override\n  void onPointerMove(PointerMoveEvent event) {\n    final position = event.localPosition;\n    if (containsLocalPoint(position)) {\n      if (!_isHovered) {\n        _doHoverEnter();\n      }\n    } else {\n      if (_isHovered) {\n        _doHoverExit();\n      }\n    }\n  }\n\n  @override\n  void onPointerMoveStop(PointerMoveEvent event) {\n    if (_isHovered) {\n      _doHoverExit();\n    }\n  }\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    super.onMount();\n    PointerMoveCallbacks.onMountHandler(this);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/component_mixins/pointer_move_callbacks.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:meta/meta.dart';\n\n/// This mixin can be added to a [Component] allowing it to receive\n/// pointer movement events.\nmixin PointerMoveCallbacks on Component {\n  void onPointerMove(PointerMoveEvent event) {}\n\n  void onPointerMoveStop(PointerMoveEvent event) {}\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    super.onMount();\n    onMountHandler(this);\n  }\n\n  static void onMountHandler(PointerMoveCallbacks instance) {\n    final game = instance.findRootGame()!;\n    const key = MouseMoveDispatcherKey();\n    if (game.findByKey(key) == null) {\n      final dispatcher = PointerMoveDispatcher();\n      game.registerKey(key, dispatcher);\n      game.add(dispatcher);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/component_mixins/scale_callbacks.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/src/events/flame_game_mixins/scale_dispatcher.dart';\nimport 'package:flutter/foundation.dart';\n\nmixin ScaleCallbacks on Component {\n  bool _isScaling = false;\n\n  /// Returns true while the component is being scaled.\n  bool get isScaling => _isScaling;\n\n  @mustCallSuper\n  void onScaleStart(ScaleStartEvent event) {\n    _isScaling = true;\n  }\n\n  void onScaleUpdate(ScaleUpdateEvent event) {}\n\n  @mustCallSuper\n  void onScaleEnd(ScaleEndEvent event) {\n    _isScaling = false;\n  }\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    super.onMount();\n    final game = findRootGame()!;\n    if (game.findByKey(const ScaleDispatcherKey()) == null) {\n      final dispatcher = ScaleDispatcher();\n      game.registerKey(const ScaleDispatcherKey(), dispatcher);\n      game.add(dispatcher);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/component_mixins/secondary_tap_callbacks.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:meta/meta.dart';\n\n/// This mixin can be added to a [Component] allowing it to receive secondary\n/// tap events (i.e. right mouse clicks).\n///\n/// In addition to adding this mixin, the component must also implement the\n/// [containsLocalPoint] method -- the component will only be considered\n/// \"tapped\" if the point where the tap has occurred is inside the component.\n///\n/// Note that FlameGame _is_ a [Component] and does implement\n/// [containsLocalPoint]; so this can be used at the game level.\nmixin SecondaryTapCallbacks on Component {\n  void onSecondaryTapDown(SecondaryTapDownEvent event) {}\n  void onSecondaryTapUp(SecondaryTapUpEvent event) {}\n  void onSecondaryTapCancel(SecondaryTapCancelEvent event) {}\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    super.onMount();\n    final game = findRootGame()!;\n    if (game.findByKey(const SecondaryTapDispatcherKey()) == null) {\n      final dispatcher = SecondaryTapDispatcher();\n      game.registerKey(const SecondaryTapDispatcherKey(), dispatcher);\n      game.add(dispatcher);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/component_mixins/tap_callbacks.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:meta/meta.dart';\n\n/// This mixin can be added to a [Component] allowing it to receive tap events.\n///\n/// In addition to adding this mixin, the component must also implement the\n/// [containsLocalPoint] method -- the component will only be considered\n/// \"tapped\" if the point where the tap has occurred is inside the component.\n///\n/// Note that FlameGame _is_ a [Component] and does implement\n/// [containsLocalPoint]; so this can be used at the game level.\nmixin TapCallbacks on Component {\n  void onTapDown(TapDownEvent event) {}\n  void onLongTapDown(TapDownEvent event) {}\n  void onTapUp(TapUpEvent event) {}\n  void onTapCancel(TapCancelEvent event) {}\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    super.onMount();\n    final game = findRootGame()!;\n    if (game.findByKey(const MultiTapDispatcherKey()) == null) {\n      final dispatcher = MultiTapDispatcher();\n      game.registerKey(const MultiTapDispatcherKey(), dispatcher);\n      game.add(dispatcher);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/flame_drag_adapter.dart",
    "content": "import 'package:flame/src/events/interfaces/multi_drag_listener.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:meta/meta.dart';\n\n/// Helper class to convert drag API as expected by the\n/// [ImmediateMultiDragGestureRecognizer] into the API expected by Flame's\n/// [MultiDragListener].\n@internal\nclass FlameDragAdapter implements Drag {\n  FlameDragAdapter(this._listener, Offset startPoint) {\n    start(startPoint);\n  }\n\n  final MultiDragListener _listener;\n  late final int _id;\n  static int _globalIdCounter = 0;\n\n  void start(Offset point) {\n    final event = DragStartDetails(\n      sourceTimeStamp: Duration.zero,\n      globalPosition: point,\n      localPosition: _listener.renderBox.globalToLocal(point),\n    );\n    _id = _globalIdCounter++;\n    _listener.handleDragStart(_id, event);\n  }\n\n  @override\n  void update(DragUpdateDetails event) =>\n      _listener.handleDragUpdate(_id, event);\n\n  @override\n  void end(DragEndDetails event) => _listener.handleDragEnd(_id, event);\n\n  @override\n  void cancel() => _listener.handleDragCancel(_id);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/flame_game_mixins/double_tap_dispatcher.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/gestures.dart';\n\nclass DoubleTapDispatcherKey implements ComponentKey {\n  const DoubleTapDispatcherKey();\n\n  @override\n  int get hashCode => 20260645; // 'DoubleTapDispatcherKey' as hashCode\n\n  @override\n  bool operator ==(Object other) =>\n      other is DoubleTapDispatcherKey && other.hashCode == hashCode;\n}\n\n/// [DoubleTapDispatcher] propagates double-tap events to every components in\n/// the component tree that is mixed with [DoubleTapCallbacks]. This will be\n/// attached to the [FlameGame] instance automatically whenever any\n/// [DoubleTapCallbacks] are mounted into the component tree.\nclass DoubleTapDispatcher extends Component with HasGameReference<FlameGame> {\n  final _components = <DoubleTapCallbacks>{};\n\n  void _onDoubleTapDown(DoubleTapDownEvent event) {\n    event.deliverAtPoint(\n      rootComponent: game,\n      eventHandler: (DoubleTapCallbacks component) {\n        _components.add(component..onDoubleTapDown(event));\n      },\n    );\n  }\n\n  void _onDoubleTapUp(DoubleTapEvent event) {\n    for (final component in _components) {\n      component.onDoubleTapUp(event);\n    }\n    _components.clear();\n  }\n\n  void _onDoubleTapCancel(DoubleTapCancelEvent event) {\n    for (final component in _components) {\n      component.onDoubleTapCancel(event);\n    }\n    _components.clear();\n  }\n\n  @override\n  void onMount() {\n    game.gestureDetectors.add(\n      DoubleTapGestureRecognizer.new,\n      (DoubleTapGestureRecognizer instance) {\n        instance.onDoubleTapDown = (details) =>\n            _onDoubleTapDown(DoubleTapDownEvent(game, details));\n        instance.onDoubleTapCancel = () =>\n            _onDoubleTapCancel(DoubleTapCancelEvent());\n        instance.onDoubleTap = () => _onDoubleTapUp(DoubleTapEvent());\n      },\n    );\n  }\n\n  @override\n  void onRemove() {\n    game.gestureDetectors.remove<DoubleTapGestureRecognizer>();\n    game.unregisterKey(const DoubleTapDispatcherKey());\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/flame_game_mixins/multi_drag_dispatcher.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/src/events/flame_drag_adapter.dart';\nimport 'package:flame/src/events/tagged_component.dart';\nimport 'package:flame/src/game/flame_game.dart';\nimport 'package:flame/src/game/game_render_box.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:meta/meta.dart';\n\nclass MultiDragDispatcherKey implements ComponentKey {\n  const MultiDragDispatcherKey();\n\n  @override\n  int get hashCode => 91604879; // 'MultiDragDispatcherKey' as hashCode\n\n  @override\n  bool operator ==(Object other) =>\n      other is MultiDragDispatcherKey && other.hashCode == hashCode;\n}\n\n/// **MultiDragDispatcher** facilitates dispatching of drag events to the\n/// [DragCallbacks] components in the component tree. It will be attached to\n/// the [FlameGame] instance automatically whenever any [DragCallbacks]\n/// components are mounted into the component tree.\nclass MultiDragDispatcher extends Component implements MultiDragListener {\n  /// The record of all components currently being touched.\n  final Set<TaggedComponent<DragCallbacks>> _records = {};\n\n  final _dragUpdateController = StreamController<DragUpdateEvent>.broadcast(\n    sync: true,\n  );\n\n  Stream<DragUpdateEvent> get onUpdate => _dragUpdateController.stream;\n\n  final _dragStartController = StreamController<DragStartEvent>.broadcast(\n    sync: true,\n  );\n\n  Stream<DragStartEvent> get onStart => _dragStartController.stream;\n\n  final _dragEndController = StreamController<DragEndEvent>.broadcast(\n    sync: true,\n  );\n\n  Stream<DragEndEvent> get onEnd => _dragEndController.stream;\n\n  final _dragCancelController = StreamController<DragCancelEvent>.broadcast(\n    sync: true,\n  );\n\n  Stream<DragCancelEvent> get onCancel => _dragCancelController.stream;\n\n  FlameGame get game => parent! as FlameGame;\n\n  /// Called when the user initiates a drag gesture, for example by touching the\n  /// screen and then moving the finger.\n  ///\n  /// The handler propagates the [event] to any component located at the point\n  /// of touch and that uses the [DragCallbacks] mixin. The event will be first\n  /// delivered to the topmost such component, and then propagated to the\n  /// components below only if explicitly requested.\n  ///\n  /// Each [event] has an `event.pointerId` to keep track of multiple touches\n  /// that may occur simultaneously.\n  @mustCallSuper\n  void onDragStart(DragStartEvent event) {\n    event.deliverAtPoint(\n      rootComponent: game,\n      eventHandler: (DragCallbacks component) {\n        _records.add(TaggedComponent(event.pointerId, component));\n        component.onDragStart(event);\n      },\n    );\n  }\n\n  /// Called continuously during the drag as the user moves their finger.\n  ///\n  /// The default handler propagates this event to those components who received\n  /// the initial [onDragStart] event. If the position of the pointer is outside\n  /// of the bounds of the component, then this event will nevertheless be\n  /// delivered, however its `event.localPosition` property will contain NaNs.\n  @mustCallSuper\n  void onDragUpdate(DragUpdateEvent event) {\n    final updated = <TaggedComponent<DragCallbacks>>{};\n    event.deliverAtPoint(\n      rootComponent: game,\n      deliverToAll: true,\n      eventHandler: (DragCallbacks component) {\n        final record = TaggedComponent(event.pointerId, component);\n        if (_records.contains(record)) {\n          component.onDragUpdate(event);\n          updated.add(record);\n        }\n      },\n    );\n    for (final record in _records) {\n      if (record.pointerId == event.pointerId && !updated.contains(record)) {\n        record.component.onDragUpdate(event);\n      }\n    }\n  }\n\n  /// Called when the drag gesture finishes.\n  ///\n  /// The default handler will deliver this event to all components who has\n  /// previously received the corresponding [onDragStart] event and\n  /// [onDragUpdate]s.\n  @mustCallSuper\n  void onDragEnd(DragEndEvent event) {\n    _records.removeWhere((record) {\n      if (record.pointerId == event.pointerId) {\n        record.component.onDragEnd(event);\n        return true;\n      }\n      return false;\n    });\n  }\n\n  @mustCallSuper\n  void onDragCancel(DragCancelEvent event) {\n    _records.removeWhere((record) {\n      if (record.pointerId == event.pointerId) {\n        record.component.onDragCancel(event);\n        return true;\n      }\n      return false;\n    });\n  }\n\n  //#region MultiDragListener API\n\n  @internal\n  @override\n  void handleDragStart(int pointerId, DragStartDetails details) {\n    final event = DragStartEvent(pointerId, game, details);\n    onDragStart(event);\n    _dragStartController.add(event);\n  }\n\n  @internal\n  @override\n  void handleDragUpdate(int pointerId, DragUpdateDetails details) {\n    final event = DragUpdateEvent(pointerId, game, details);\n    onDragUpdate(event);\n    _dragUpdateController.add(event);\n  }\n\n  @internal\n  @override\n  void handleDragEnd(int pointerId, DragEndDetails details) {\n    final event = DragEndEvent(pointerId, details);\n    onDragEnd(event);\n    _dragEndController.add(event);\n  }\n\n  @internal\n  @override\n  void handleDragCancel(int pointerId) {\n    final event = DragCancelEvent(pointerId);\n    onDragCancel(event);\n    _dragCancelController.add(event);\n  }\n\n  //#endregion\n\n  @override\n  void onMount() {\n    game.gestureDetectors.add<ImmediateMultiDragGestureRecognizer>(\n      ImmediateMultiDragGestureRecognizer.new,\n      (ImmediateMultiDragGestureRecognizer instance) {\n        instance.onStart = (Offset point) => FlameDragAdapter(this, point);\n      },\n    );\n  }\n\n  @override\n  void onRemove() {\n    game.gestureDetectors.remove<ImmediateMultiDragGestureRecognizer>();\n    game.unregisterKey(const MultiDragDispatcherKey());\n    _dragUpdateController.close();\n    _dragCancelController.close();\n    _dragStartController.close();\n    _dragEndController.close();\n  }\n\n  @override\n  GameRenderBox get renderBox => game.renderBox;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/flame_game_mixins/multi_tap_dispatcher.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/src/events/tagged_component.dart';\nimport 'package:flame/src/game/flame_game.dart';\nimport 'package:flame/src/game/game_render_box.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:meta/meta.dart';\n\nclass MultiTapDispatcherKey implements ComponentKey {\n  const MultiTapDispatcherKey();\n\n  @override\n  int get hashCode => 401913931; // 'MultiTapDispatcherKey' as hashCode\n\n  @override\n  bool operator ==(Object other) =>\n      other is MultiTapDispatcherKey && other.hashCode == hashCode;\n}\n\nclass MultiTapDispatcher extends Component implements MultiTapListener {\n  /// The record of all components currently being touched.\n  final Set<TaggedComponent<TapCallbacks>> _record = {};\n\n  FlameGame get game => parent! as FlameGame;\n\n  /// Called when the user touches the device screen within the game canvas,\n  /// either with a finger, a stylus, or a mouse.\n  ///\n  /// The handler propagates the [event] to any component located at the point\n  /// of touch and that uses the [TapCallbacks] mixin. The event will be first\n  /// delivered to the topmost such component, and then propagated to the\n  /// components below only if explicitly requested.\n  ///\n  /// Each [event] has an `event.pointerId` to keep track of multiple touches\n  /// that may occur simultaneously.\n  @mustCallSuper\n  void onTapDown(TapDownEvent event) {\n    event.deliverAtPoint(\n      rootComponent: game,\n      eventHandler: (TapCallbacks component) {\n        _record.add(TaggedComponent(event.pointerId, component));\n        component.onTapDown(event);\n      },\n    );\n  }\n\n  /// Called after the user has been touching the screen for [longTapDelay]\n  /// seconds without the tap being cancelled.\n  ///\n  /// This event will be delivered to all the components that previously\n  /// received the `onTapDown` event, and who remain at the point where the user\n  /// is touching the screen.\n  @mustCallSuper\n  void onLongTapDown(TapDownEvent event) {\n    event.deliverAtPoint(\n      rootComponent: game,\n      eventHandler: (TapCallbacks component) {\n        final record = TaggedComponent(event.pointerId, component);\n        if (_record.contains(record)) {\n          component.onLongTapDown(event);\n        }\n      },\n      deliverToAll: true,\n    );\n  }\n\n  /// Called when the user stops touching the device screen within the game\n  /// canvas (and if there was an [onTapDown] event before).\n  ///\n  /// This event is propagated to the components at the point of touch, but only\n  /// if those components have received the `onTapDown` event previously. For\n  /// those components that moved away from the point of touch, an `onTapCancel`\n  /// will be triggered instead.\n  ///\n  /// Note that if a component that was touched moves away from the point of\n  /// touch, then `onTapCancel` will be triggered for this component only when\n  /// the user stops the touch, not when the component moves away.\n  @mustCallSuper\n  void onTapUp(TapUpEvent event) {\n    event.deliverAtPoint(\n      rootComponent: game,\n      eventHandler: (TapCallbacks component) {\n        if (_record.remove(TaggedComponent(event.pointerId, component))) {\n          component.onTapUp(event);\n        }\n      },\n      deliverToAll: true,\n    );\n\n    _tapCancelImpl(event.toTapCancel());\n  }\n\n  /// Called when there was an [onTapDown] event previously, but the [onTapUp]\n  /// can no longer occur.\n  ///\n  /// Usually this happens when the user starts a drag gesture, but could also\n  /// occur for other reasons (e.g. another app coming to the foreground).\n  ///\n  /// This event will be propagated to all components that has previously\n  /// received the `onTapDown` event.\n  @mustCallSuper\n  void onTapCancel(TapCancelEvent event) {\n    _tapCancelImpl(event);\n  }\n\n  void _tapCancelImpl(TapCancelEvent event) {\n    _record.removeWhere((pair) {\n      if (pair.pointerId == event.pointerId) {\n        pair.component.onTapCancel(event);\n        return true;\n      }\n      return false;\n    });\n  }\n\n  //#region MultiTapListener API\n\n  /// The delay (in seconds) after which a tap is considered a long tap.\n  @override\n  double get longTapDelay => TapConfig.longTapDelay;\n\n  @override\n  void handleTap(int pointerId) {}\n\n  @internal\n  @override\n  void handleTapCancel(int pointerId) {\n    onTapCancel(TapCancelEvent(pointerId));\n  }\n\n  @internal\n  @override\n  void handleTapDown(int pointerId, TapDownDetails details) {\n    onTapDown(TapDownEvent(pointerId, game, details));\n  }\n\n  @internal\n  @override\n  void handleTapUp(int pointerId, TapUpDetails details) {\n    onTapUp(TapUpEvent(pointerId, game, details));\n  }\n\n  @internal\n  @override\n  void handleLongTapDown(int pointerId, TapDownDetails details) {\n    onLongTapDown(TapDownEvent(pointerId, game, details));\n  }\n\n  //#endregion\n\n  @override\n  void onMount() {\n    game.gestureDetectors.add<MultiTapGestureRecognizer>(\n      () => MultiTapGestureRecognizer(\n        allowedButtonsFilter: (buttons) => buttons == kPrimaryButton,\n      ),\n      (MultiTapGestureRecognizer instance) {\n        instance.longTapDelay = Duration(\n          milliseconds: (longTapDelay * 1000).toInt(),\n        );\n        instance.onTap = handleTap;\n        instance.onTapDown = handleTapDown;\n        instance.onTapUp = handleTapUp;\n        instance.onTapCancel = handleTapCancel;\n        instance.onLongTapDown = handleLongTapDown;\n      },\n    );\n  }\n\n  @override\n  void onRemove() {\n    game.gestureDetectors.remove<MultiTapGestureRecognizer>();\n    game.unregisterKey(const MultiTapDispatcherKey());\n  }\n\n  @override\n  GameRenderBox get renderBox => game.renderBox;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/flame_game_mixins/pointer_move_dispatcher.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/src/events/tagged_component.dart';\nimport 'package:flame/src/game/flame_game.dart';\nimport 'package:flutter/gestures.dart' as flutter;\nimport 'package:meta/meta.dart';\n\n/// **MouseMoveDispatcher** facilitates dispatching of mouse move events to the\n/// [PointerMoveCallbacks] components in the component tree. It will be attached\n/// to the [FlameGame] instance automatically whenever any\n/// [PointerMoveCallbacks] components are mounted into the component tree.\nclass PointerMoveDispatcher extends Component {\n  /// The record of all components currently being hovered.\n  final Set<TaggedComponent<PointerMoveCallbacks>> _records = {};\n\n  FlameGame get game => parent! as FlameGame;\n\n  @mustCallSuper\n  void onMouseMove(PointerMoveEvent event) {\n    final updated = <TaggedComponent<PointerMoveCallbacks>>{};\n\n    event.deliverAtPoint(\n      rootComponent: game,\n      deliverToAll: true,\n      eventHandler: (PointerMoveCallbacks component) {\n        final tagged = TaggedComponent(event.pointerId, component);\n        _records.add(tagged);\n        updated.add(tagged);\n        component.onPointerMove(event);\n      },\n    );\n\n    final toRemove = <TaggedComponent<PointerMoveCallbacks>>{};\n    for (final record in _records) {\n      if (record.pointerId == event.pointerId && !updated.contains(record)) {\n        // one last \"exit\" event\n        record.component.onPointerMoveStop(event);\n        toRemove.add(record);\n      }\n    }\n    _records.removeAll(toRemove);\n  }\n\n  void _handlePointerMove(flutter.PointerHoverEvent event) {\n    onMouseMove(PointerMoveEvent.fromPointerHoverEvent(game, event));\n  }\n\n  @override\n  void onMount() {\n    game.mouseDetector = _handlePointerMove;\n  }\n\n  @override\n  void onRemove() {\n    game.mouseDetector = null;\n    game.unregisterKey(const MouseMoveDispatcherKey());\n  }\n}\n\nclass MouseMoveDispatcherKey implements ComponentKey {\n  const MouseMoveDispatcherKey();\n\n  @override\n  int get hashCode => 'MouseMoveDispatcherKey'.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      other is MouseMoveDispatcherKey && other.hashCode == hashCode;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/flame_game_mixins/scale_dispatcher.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/src/events/interfaces/scale_listener.dart';\nimport 'package:flame/src/events/tagged_component.dart';\nimport 'package:flame/src/game/game_widget/gesture_detector_builder.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:meta/meta.dart';\n\n/// Defines a line between two pointers on screen.\n///\n/// [_LineBetweenPointers] is an abstraction of a line between two pointers in\n/// contact with the screen. Used to track the rotation and scale of a scaleAabb\n///  gesture.\nclass _LineBetweenPointers {\n  /// Creates a [_LineBetweenPointers]. None of the [pointerStartLocation]\n  /// [pointerEndLocation]  must be null.\n  /// should be different.\n  _LineBetweenPointers({\n    this.pointerStartLocation = Offset.zero,\n    this.pointerEndLocation = Offset.zero,\n  });\n\n  // The location and the id of the pointer that marks the start of the line.\n  final Offset pointerStartLocation;\n\n  // The location and the id of the pointer that marks the end of the line.\n  final Offset pointerEndLocation;\n}\n\n/// Unique key for the [ScaleDispatcher] so the game can identify it.\nclass ScaleDispatcherKey implements ComponentKey {\n  const ScaleDispatcherKey();\n\n  @override\n  int get hashCode => 31650892; // arbitrary unique number\n\n  @override\n  bool operator ==(Object other) =>\n      other is ScaleDispatcherKey && other.hashCode == hashCode;\n}\n\n/// A component that dispatches scale (pinch/zoom) events to components\n/// implementing [ScaleCallbacks]. It will be attached to\n/// the [FlameGame] instance automatically whenever any [ScaleCallbacks]\n/// components are mounted into the component tree.\nclass ScaleDispatcher extends Component implements ScaleListener {\n  /// Records all components currently being scaled, keyed by pointerId.\n  final Set<TaggedComponent<ScaleCallbacks>> _records = {};\n\n  FlameGame get game => parent! as FlameGame;\n\n  /// Store the last drag events\n  DragStartDetails? lastDragStart;\n  DragUpdateDetails? lastDragUpdate;\n  DragEndDetails? lastDragEnd;\n\n  _LineBetweenPointers? _currentLine;\n\n  _LineBetweenPointers? _lineAtFirstUpdate;\n\n  MultiDragDispatcher? _multiDragDispatcher;\n\n  /// Called when the user starts a scale gesture.\n  @mustCallSuper\n  void onScaleStart(ScaleStartEvent event) {\n    event.deliverAtPoint(\n      rootComponent: game,\n      eventHandler: (ScaleCallbacks component) {\n        _records.add(TaggedComponent(event.pointerId, component));\n        component.onScaleStart(event);\n      },\n    );\n  }\n\n  /// Called continuously as the user updates the scale gesture.\n  @mustCallSuper\n  void onScaleUpdate(ScaleUpdateEvent event) {\n    final updated = <TaggedComponent<ScaleCallbacks>>{};\n\n    // Deliver to components under the pointer\n    event.deliverAtPoint(\n      rootComponent: game,\n      deliverToAll: true,\n      eventHandler: (ScaleCallbacks component) {\n        final record = TaggedComponent(event.pointerId, component);\n        if (_records.contains(record)) {\n          component.onScaleUpdate(event);\n          updated.add(record);\n        }\n      },\n    );\n\n    // Also deliver to components that started the scale but weren't under\n    // the pointer this frame\n    // Currently, the id passed to the scale\n    // events is always 0, so maybe it's not relevant.\n    for (final record in _records) {\n      if (record.pointerId == event.pointerId && !updated.contains(record)) {\n        record.component.onScaleUpdate(event);\n      }\n    }\n  }\n\n  /// Called when the scale gesture ends.\n  @mustCallSuper\n  void onScaleEnd(ScaleEndEvent event) {\n    _records.removeWhere((record) {\n      if (record.pointerId == event.pointerId) {\n        record.component.onScaleEnd(event);\n        return true;\n      }\n      return false;\n    });\n  }\n\n  //#region ScaleListener API\n\n  @internal\n  @override\n  void handleScaleStart(ScaleStartDetails details) {\n    onScaleStart(ScaleStartEvent(0, game, details));\n  }\n\n  @internal\n  @override\n  void handleScaleUpdate(ScaleUpdateDetails details) {\n    if (details.pointerCount != 1) {\n      onScaleUpdate(ScaleUpdateEvent(0, game, details));\n      return;\n    }\n\n    final newDetails = _buildNewUpdateDetails(details);\n    if (newDetails != null) {\n      onScaleUpdate(ScaleUpdateEvent(0, game, newDetails));\n    }\n  }\n\n  /// If the user is doing a scale gesture, and we have more\n  /// than one pointer in contact with the screen, we don't have\n  /// anything special to do.\n  /// However, if the user is doing a scale gesture but a single pointer\n  /// is registered (such as when [ImmediateMultiDragGestureRecognizer] is\n  /// added to the [GestureDetectorBuilder] game gesture detectors), then\n  /// the [ScaleUpdateDetails] details won't contain any useful data, so\n  /// we need to rebuild it using data from the drag gesture.\n  ScaleUpdateDetails? _buildNewUpdateDetails(ScaleUpdateDetails details) {\n    if (lastDragUpdate == null) {\n      return null;\n    }\n\n    _currentLine = _LineBetweenPointers(\n      pointerStartLocation: details.focalPoint,\n      pointerEndLocation: lastDragUpdate!.globalPosition,\n    );\n\n    // Register the line between the two pointers when the first update is\n    // triggered. This line will serve as a reference to compute scale\n    // and rotation data.\n    _lineAtFirstUpdate ??= _currentLine;\n\n    // Do we also need to recompute local focal point,\n    // local relative to what ?\n    return ScaleUpdateDetails(\n      focalPoint: _computeFocalPoint(details),\n      rotation: _computeRotationFactor(details),\n      scale: _computeScale(details),\n      verticalScale: _computeVerticalScale(details),\n      horizontalScale: _computeHorizontalScale(details),\n      pointerCount: details.pointerCount,\n      focalPointDelta: details.focalPointDelta,\n      sourceTimeStamp: details.sourceTimeStamp,\n    );\n  }\n\n  /// Compute the focal point of the scale gesture using the one pointer\n  /// focal point (which is just the position of the pointer itself) and\n  /// the position of the last pointer triggering a drag update.\n  Offset _computeFocalPoint(ScaleUpdateDetails details) {\n    if (lastDragUpdate != null) {\n      return details.focalPoint + lastDragUpdate!.globalPosition / 2.0;\n    } else {\n      return details.focalPoint;\n    }\n  }\n\n  /// Compute the rotation of the scale gesture using the initial\n  /// line formed between the two pointers that form the scale gesture,\n  /// and the subsequent lines they form as they move. The rotation factor\n  /// is just the angle in radian between the two lines.\n  double _computeRotationFactor(ScaleUpdateDetails details) {\n    if (lastDragUpdate == null ||\n        _lineAtFirstUpdate == null ||\n        _currentLine == null) {\n      return 0.0;\n    }\n\n    final fx = _lineAtFirstUpdate!.pointerStartLocation.dx;\n    final fy = _lineAtFirstUpdate!.pointerStartLocation.dy;\n    final sx = _lineAtFirstUpdate!.pointerEndLocation.dx;\n    final sy = _lineAtFirstUpdate!.pointerEndLocation.dy;\n\n    final nfx = _currentLine!.pointerStartLocation.dx;\n    final nfy = _currentLine!.pointerStartLocation.dy;\n    final nsx = _currentLine!.pointerEndLocation.dx;\n    final nsy = _currentLine!.pointerEndLocation.dy;\n\n    final angle1 = math.atan2(fy - sy, fx - sx);\n    final angle2 = math.atan2(nfy - nsy, nfx - nsx);\n\n    return angle2 - angle1;\n  }\n\n  /// Compute the scale of the scale gesture using the initial\n  /// line formed between the two pointers that form the scale gesture,\n  /// and the subsequent lines they form as they move. The scale factor\n  /// is just length of current line over length of initial line.\n  double _computeScale(ScaleUpdateDetails details) {\n    if (lastDragUpdate == null ||\n        _currentLine == null ||\n        _lineAtFirstUpdate == null) {\n      return 1.0;\n    }\n\n    final currentLineDistance =\n        (_currentLine!.pointerStartLocation - _currentLine!.pointerEndLocation)\n            .distance;\n\n    final firstLineDistance =\n        (_lineAtFirstUpdate!.pointerStartLocation -\n                _lineAtFirstUpdate!.pointerEndLocation)\n            .distance;\n\n    return currentLineDistance / firstLineDistance;\n  }\n\n  /// Compute the vertical scale of the scale gesture using the initial\n  /// line formed between the two pointers that form the scale gesture,\n  /// and the subsequent lines they form as they move. The scale factor\n  /// is just length of current line vertical part over\n  /// length of initial line part.\n  double _computeVerticalScale(ScaleUpdateDetails details) {\n    if (lastDragUpdate == null ||\n        _currentLine == null ||\n        _lineAtFirstUpdate == null) {\n      return 1.0;\n    }\n\n    final currentLineVerticalDistance =\n        (_currentLine!.pointerStartLocation.dy -\n                _currentLine!.pointerEndLocation.dy)\n            .abs();\n    final firstLineVerticalDistance =\n        (_lineAtFirstUpdate!.pointerStartLocation.dy -\n                _lineAtFirstUpdate!.pointerEndLocation.dy)\n            .abs();\n\n    return currentLineVerticalDistance / firstLineVerticalDistance;\n  }\n\n  /// Compute the vertical scale of the scale gesture using the initial\n  /// line formed between the two pointers that form the scale gesture,\n  /// and the subsequent lines they form as they move. The scale factor\n  /// is just length of current line horizontal part over\n  /// length of initial line part.\n  double _computeHorizontalScale(ScaleUpdateDetails details) {\n    if (lastDragUpdate == null ||\n        _currentLine == null ||\n        _lineAtFirstUpdate == null) {\n      return 1.0;\n    }\n\n    final currentLineHorizontalDistance =\n        (_currentLine!.pointerStartLocation.dx -\n                _currentLine!.pointerEndLocation.dx)\n            .abs();\n    final firstLineHorizontalDistance =\n        (_lineAtFirstUpdate!.pointerStartLocation.dx -\n                _lineAtFirstUpdate!.pointerEndLocation.dx)\n            .abs();\n\n    return currentLineHorizontalDistance / firstLineHorizontalDistance;\n  }\n\n  @internal\n  @override\n  void handleScaleEnd(ScaleEndDetails details) {\n    _currentLine = null;\n    _lineAtFirstUpdate = null;\n    onScaleEnd(ScaleEndEvent(0, details));\n  }\n\n  //#endregion\n\n  @override\n  void onMount() {\n    game.gestureDetectors.add<ScaleGestureRecognizer>(\n      ScaleGestureRecognizer.new,\n      (ScaleGestureRecognizer instance) {\n        instance\n          ..onStart = handleScaleStart\n          ..onUpdate = handleScaleUpdate\n          ..onEnd = handleScaleEnd;\n      },\n    );\n\n    final existingDispatcher = game.findByKey(const MultiDragDispatcherKey());\n    if (existingDispatcher != null) {\n      _attachMultiDragDispatcher(existingDispatcher as MultiDragDispatcher);\n    }\n\n    super.onMount();\n  }\n\n  @override\n  void onChildrenChanged(Component child, ChildrenChangeType type) {\n    super.onChildrenChanged(child, type);\n\n    if (type == ChildrenChangeType.added && child is MultiDragDispatcher) {\n      _attachMultiDragDispatcher(child);\n    }\n  }\n\n  void _attachMultiDragDispatcher(MultiDragDispatcher newDispatcher) {\n    if (_multiDragDispatcher != null) {\n      return;\n    }\n\n    _multiDragDispatcher = newDispatcher;\n    listenToDragDispatcher(newDispatcher);\n  }\n\n  @override\n  void onRemove() {\n    game.gestureDetectors.remove<ScaleGestureRecognizer>();\n    super.onRemove();\n  }\n\n  /// Subscribe to an external MultiDragDispatcher, we need\n  /// this in order to get the data of pointers used by\n  /// [ImmediateMultiDragGestureRecognizer], as it is necessary\n  /// to compute things such as rotation and scale of the scale gesture.\n  void listenToDragDispatcher(MultiDragDispatcher multiDragDispatcher) {\n    multiDragDispatcher.onUpdate.listen((event) {\n      lastDragUpdate = event.raw;\n    });\n    multiDragDispatcher.onStart.listen((event) {\n      lastDragStart = event.raw;\n    });\n    multiDragDispatcher.onEnd.listen((event) {\n      lastDragEnd = event.raw;\n    });\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/flame_game_mixins/secondary_tap_dispatcher.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/gestures.dart';\n\nclass SecondaryTapDispatcherKey implements ComponentKey {\n  const SecondaryTapDispatcherKey();\n\n  @override\n  int get hashCode => 'SecondaryTapDispatcherKey'.hashCode;\n\n  @override\n  bool operator ==(Object other) =>\n      other is SecondaryTapDispatcherKey && other.hashCode == hashCode;\n}\n\n/// [SecondaryTapDispatcher] propagates secondary-tap events (i.e. right mouse\n/// clicks) to every components in the component tree that is mixed with\n/// [SecondaryTapCallbacks]. This will be attached to the [FlameGame] instance\n/// automatically whenever any [SecondaryTapCallbacks] are mounted into the\n/// component tree.\nclass SecondaryTapDispatcher extends Component\n    with HasGameReference<FlameGame> {\n  final _components = <SecondaryTapCallbacks>{};\n\n  void _onSecondaryTapDown(SecondaryTapDownEvent event) {\n    event.deliverAtPoint(\n      rootComponent: game,\n      eventHandler: (SecondaryTapCallbacks component) {\n        _components.add(component..onSecondaryTapDown(event));\n      },\n    );\n  }\n\n  void _onSecondaryTapUp(SecondaryTapUpEvent event) {\n    for (final component in _components) {\n      component.onSecondaryTapUp(event);\n    }\n    _components.clear();\n  }\n\n  void _onSecondaryTapCancel(SecondaryTapCancelEvent event) {\n    for (final component in _components) {\n      component.onSecondaryTapCancel(event);\n    }\n    _components.clear();\n  }\n\n  @override\n  void onMount() {\n    game.gestureDetectors.add(\n      TapGestureRecognizer.new,\n      (TapGestureRecognizer instance) {\n        instance.onSecondaryTapDown = (details) =>\n            _onSecondaryTapDown(SecondaryTapDownEvent(game, details));\n        instance.onSecondaryTapCancel = () =>\n            _onSecondaryTapCancel(SecondaryTapCancelEvent());\n        instance.onSecondaryTapUp = (details) =>\n            _onSecondaryTapUp(SecondaryTapUpEvent(game, details));\n      },\n    );\n  }\n\n  @override\n  void onRemove() {\n    game.gestureDetectors.remove<TapGestureRecognizer>();\n    game.unregisterKey(const SecondaryTapDispatcherKey());\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/game_mixins/multi_touch_drag_detector.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/src/events/flame_drag_adapter.dart';\nimport 'package:flame/src/game/game.dart';\nimport 'package:flutter/gestures.dart';\n\n/// Mixin that can be added to a [Game] allowing it to receive drag events.\n///\n/// The user can override one of the callback methods\n///  - [onDragStart]\n///  - [onDragUpdate]\n///  - [onDragEnd]\n///  - [onDragCancel]\n/// in order to respond to each corresponding event. Those events whose methods\n/// are not overridden are ignored. See [MultiDragListener] for the description\n/// of each individual event.\nmixin MultiTouchDragDetector on Game implements MultiDragListener {\n  void onDragStart(int pointerId, DragStartInfo info) {}\n  void onDragUpdate(int pointerId, DragUpdateInfo info) {}\n  void onDragEnd(int pointerId, DragEndInfo info) {}\n  void onDragCancel(int pointerId) {}\n\n  //#region MultiDragListener API\n  @override\n  void handleDragStart(int pointerId, DragStartDetails details) {\n    onDragStart(pointerId, DragStartInfo.fromDetails(this, details));\n  }\n\n  @override\n  void handleDragUpdate(int pointerId, DragUpdateDetails details) {\n    onDragUpdate(pointerId, DragUpdateInfo.fromDetails(this, details));\n  }\n\n  @override\n  void handleDragEnd(int pointerId, DragEndDetails details) {\n    onDragEnd(pointerId, DragEndInfo.fromDetails(details));\n  }\n\n  @override\n  void handleDragCancel(int pointerId) {\n    onDragCancel(pointerId);\n  }\n\n  //#endregion\n\n  @override\n  void mount() {\n    gestureDetectors.add<ImmediateMultiDragGestureRecognizer>(\n      ImmediateMultiDragGestureRecognizer.new,\n      (ImmediateMultiDragGestureRecognizer instance) {\n        instance.onStart = (Offset point) => FlameDragAdapter(this, point);\n      },\n    );\n    super.mount();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/game_mixins/multi_touch_tap_detector.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/src/events/tap_config.dart';\nimport 'package:flame/src/game/game.dart';\nimport 'package:flutter/gestures.dart';\n\n/// Mixin that can be added to a [Game] allowing it to receive tap events.\n///\n/// The user can override one of the callback methods\n///  - [onTapDown]\n///  - [onLongTapDown]\n///  - [onTapUp]\n///  - [onTapCancel]\n///  - [onTap]\n/// in order to respond to each corresponding event. Those events whose methods\n/// are not overridden are ignored. See [MultiTapListener] for the description\n/// of each individual event.\nmixin MultiTouchTapDetector on Game implements MultiTapListener {\n  void onTapDown(int pointerId, TapDownInfo info) {}\n  void onLongTapDown(int pointerId, TapDownInfo info) {}\n  void onTapUp(int pointerId, TapUpInfo info) {}\n  void onTapCancel(int pointerId) {}\n  void onTap(int pointerId) {}\n\n  //#region MultiTapListener API\n  @override\n  double get longTapDelay => TapConfig.longTapDelay;\n\n  @override\n  void handleTap(int pointerId) => onTap(pointerId);\n\n  @override\n  void handleTapCancel(int pointerId) => onTapCancel(pointerId);\n\n  @override\n  void handleTapDown(int pointerId, TapDownDetails details) {\n    onTapDown(pointerId, TapDownInfo.fromDetails(this, details));\n  }\n\n  @override\n  void handleTapUp(int pointerId, TapUpDetails details) {\n    onTapUp(pointerId, TapUpInfo.fromDetails(this, details));\n  }\n\n  @override\n  void handleLongTapDown(int pointerId, TapDownDetails details) {\n    onLongTapDown(pointerId, TapDownInfo.fromDetails(this, details));\n  }\n\n  //#endregion\n\n  @override\n  void mount() {\n    gestureDetectors.add<MultiTapGestureRecognizer>(\n      MultiTapGestureRecognizer.new,\n      (MultiTapGestureRecognizer instance) {},\n    );\n    super.mount();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/hardware_keyboard_detector.dart",
    "content": "import 'package:flame/src/components/core/component.dart';\nimport 'package:flutter/services.dart';\n\n/// The **HardwareKeyboardDetector** component allows you to directly listen\n/// to events from a hardware keyboard, bypassing the `Focus` widget in Flutter.\n/// It will not listen for events from any on-screen (software) keyboards.\n///\n/// This component can be placed anywhere in the component tree. For example,\n/// it can be attached to the root level of the Game class, or to the player\n/// being controlled. Multiple `HardwareKeyboardDetector` components can coexist\n/// within the same game, and they all will receive the key events.\n///\n/// The component provides the [onKeyEvent] event handler, which can either be\n/// overridden or passed as a parameter in the constructor. This event handler\n/// fires whenever the user presses or releases any key on a keyboard, and also\n/// when a key is being held.\n///\n/// The stream of key events will be normalized by Flutter, meaning that for\n/// every [KeyDownEvent] there will always be the corresponding [KeyUpEvent],\n/// possibly with some [KeyRepeatEvent]s in the middle. Depending on the\n/// platform, some of these events may be \"synthesized\", i.e. created by the\n/// framework artificially in order to preserve the correct event sequence. See\n/// Flutter's [HardwareKeyboard] for more details.\n///\n/// Similar normalization guarantee exists when this component is added to or\n/// removed from the component tree. If the user was holding any keys when the\n/// `HardwareKeyboardDetector` was mounted, then artificial `KeyDownEvent`s\n/// will be fired; if the user was holding keys when this component was removed,\n/// then `KeyUpEvent`s will be synthesized.\n///\n/// Use [pauseKeyEvents] property to temporarily halt/resume the delivery of\n/// [onKeyEvent]s. The events will also stop being delivered when the component\n/// is removed from the component tree.\nclass HardwareKeyboardDetector extends Component {\n  HardwareKeyboardDetector({void Function(KeyEvent)? onKeyEvent})\n    : _onKeyEvent = onKeyEvent;\n\n  final List<PhysicalKeyboardKey> _physicalKeys = [];\n  Set<LogicalKeyboardKey> _logicalKeys = {};\n  bool _pause = true;\n  final void Function(KeyEvent)? _onKeyEvent;\n\n  /// The list of keys that are currently being pressed on the keyboard (or a\n  /// keyboard-like device). The keys are listed in the order in which they\n  /// were pressed, except for the modifier keys which may be listed\n  /// out-of-order on some systems.\n  List<PhysicalKeyboardKey> get physicalKeysPressed => _physicalKeys;\n\n  /// The set of logical keys that are currently being pressed on the keyboard.\n  /// This set corresponds to the [physicalKeysPressed] list, and can be used\n  /// to search for keys in a keyboard-layout-independent way.\n  Set<LogicalKeyboardKey> get logicalKeysPressed => _logicalKeys;\n\n  /// True if the <kbd>Ctrl</kbd> key is currently being pressed.\n  bool get isControlPressed =>\n      _logicalKeys.contains(LogicalKeyboardKey.controlLeft) ||\n      _logicalKeys.contains(LogicalKeyboardKey.controlRight);\n\n  /// True if the <kbd>Shift</kbd> key is currently being pressed.\n  bool get isShiftPressed =>\n      _logicalKeys.contains(LogicalKeyboardKey.shiftLeft) ||\n      _logicalKeys.contains(LogicalKeyboardKey.shiftRight);\n\n  /// True if the <kbd>Alt</kbd> key is currently being pressed.\n  bool get isAltPressed =>\n      _logicalKeys.contains(LogicalKeyboardKey.altLeft) ||\n      _logicalKeys.contains(LogicalKeyboardKey.altRight);\n\n  /// True if <kbd>Num Lock</kbd> currently turned on.\n  bool get isNumLockOn => _hasLock(KeyboardLockMode.numLock);\n\n  /// True if <kbd>Caps Lock</kbd> currently turned on.\n  bool get isCapsLockOn => _hasLock(KeyboardLockMode.capsLock);\n\n  /// True if <kbd>Scroll Lock</kbd> currently turned on.\n  bool get isScrollLockOn => _hasLock(KeyboardLockMode.scrollLock);\n\n  bool _hasLock(KeyboardLockMode key) =>\n      HardwareKeyboard.instance.lockModesEnabled.contains(key);\n\n  /// When `true`, delivery of key events will be suspended.\n  ///\n  /// When this property is set to true, the system generates KeyUp events for\n  /// all keys currently being held, as if the user has released them.\n  /// Conversely, when this property is switched back to `false`, and the user\n  /// was holding some keys at the time, the system will generate KeyDown\n  /// events as if the user just started pressing those buttons.\n  bool get pauseKeyEvents => _pause;\n  set pauseKeyEvents(bool value) {\n    if (value == _pause) {\n      return;\n    }\n    _pause = value;\n    final timeStamp = ServicesBinding.instance.currentSystemFrameTimeStamp;\n    for (final physicalKey in _physicalKeys) {\n      final logicalKey = HardwareKeyboard.instance.lookUpLayout(physicalKey)!;\n      onKeyEvent(\n        (_pause ? KeyUpEvent.new : KeyDownEvent.new)(\n          physicalKey: physicalKey,\n          logicalKey: logicalKey,\n          timeStamp: timeStamp,\n          synthesized: true,\n        ),\n      );\n    }\n  }\n\n  /// Override this event handler if you want to get notified whenever any key\n  /// on a keyboard is pressed, held, or released. The [event] will be one of\n  /// [KeyDownEvent], [KeyRepeatEvent], or [KeyUpEvent], respectively.\n  void onKeyEvent(KeyEvent event) => _onKeyEvent?.call(event);\n\n  /// Internal handler of raw key events.\n  bool _handleKeyEvent(KeyEvent event) {\n    _logicalKeys = HardwareKeyboard.instance.logicalKeysPressed;\n    if (event is KeyDownEvent) {\n      _physicalKeys.add(event.physicalKey);\n    } else if (event is KeyUpEvent) {\n      _physicalKeys.remove(event.physicalKey);\n    }\n    if (!_pause) {\n      onKeyEvent(event);\n    }\n    return true; // handled\n  }\n\n  @override\n  void onMount() {\n    super.onMount();\n    HardwareKeyboard.instance.addHandler(_handleKeyEvent);\n    _physicalKeys.addAll(HardwareKeyboard.instance.physicalKeysPressed);\n    pauseKeyEvents = false;\n  }\n\n  @override\n  void onRemove() {\n    super.onRemove();\n    HardwareKeyboard.instance.removeHandler(_handleKeyEvent);\n    pauseKeyEvents = true;\n    _physicalKeys.clear();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/interfaces/multi_drag_listener.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/src/game/game_render_box.dart';\nimport 'package:flutter/gestures.dart';\n\n/// Interface that must be implemented by a game in order for it to be eligible\n/// to receive events from an [ImmediateMultiDragGestureRecognizer].\n///\n/// Instead of implementing this class directly consider using one of the\n/// prebuilt mixins:\n///  - [MultiTouchDragDetector] for a custom `Game`\nabstract class MultiDragListener {\n  /// The beginning of a drag operation.\n  ///\n  /// If the game is not listening to tap events, this event will occur as soon\n  /// as the user touches the screen. If the game uses both a [MultiTapListener]\n  /// and a [MultiDragListener] simultaneously, then this event will fire once\n  /// the user moves their finger away from the point of the initial touch.\n  void handleDragStart(int pointerId, DragStartDetails details);\n\n  /// The pointer that was touching the screen has moved.\n  ///\n  /// This event occurs frequently during the drag, allowing you to keep track\n  /// of the position of the point of touch as it moves. This event will only\n  /// fire when the point of touch moves, and not when it stays still.\n  void handleDragUpdate(int pointerId, DragUpdateDetails details);\n\n  /// Marks the end of a drag operation.\n  ///\n  /// This event fires when the pointer stops touching the screen. It will fire\n  /// even if the pointer is currently outside of the game widget.\n  void handleDragEnd(int pointerId, DragEndDetails details);\n\n  /// The drag operation is cancelled.\n  ///\n  /// For example, this may happen if the drag was interrupted by a system-\n  /// modal dialog appearing during the drag.\n  void handleDragCancel(int pointerId);\n\n  GameRenderBox get renderBox;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/interfaces/multi_tap_listener.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/src/game/game_render_box.dart';\nimport 'package:flutter/gestures.dart';\n\n/// Interface that must be implemented by a game in order for it to be eligible\n/// to receive events from a [MultiTapGestureRecognizer].\n///\n/// Instead of implementing this class directly consider using one of the\n/// prebuilt mixins:\n///  - [MultiTouchTapDetector] for a custom `Game`\nabstract class MultiTapListener {\n  /// The amount of time before the \"long tap down\" event is triggered.\n  double get longTapDelay;\n\n  /// A tap has occurred.\n  void handleTap(int pointerId);\n\n  /// A pointer has touched the screen.\n  void handleTapDown(int pointerId, TapDownDetails details);\n\n  /// A pointer stopped contacting the screen.\n  void handleTapUp(int pointerId, TapUpDetails details);\n\n  /// A pointer that already triggered [handleTapDown] will not trigger\n  /// [handleTap].\n  void handleTapCancel(int pointerId);\n\n  /// A pointer that has previously triggered [handleTapDown] is still touching\n  /// the screen after [longTapDelay] seconds.\n  void handleLongTapDown(int pointerId, TapDownDetails details);\n\n  GameRenderBox get renderBox;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/interfaces/scale_listener.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flutter/gestures.dart';\n\n/// Interface that must be implemented by a game in order for it to be eligible\n/// to receive events from an [ScaleGestureRecognizer].\n///\n/// Instead of implementing this class directly consider using one of the\n/// prebuilt mixins:\n///  - [MultiTouchDragDetector] for a custom `Game`\nabstract class ScaleListener {\n  /// The beginning of a drag operation.\n  ///\n  /// If the game is not listening to tap events, this event will occur as soon\n  /// as the user touches the screen. If the game uses both a [MultiTapListener]\n  /// and a [MultiDragListener] simultaneously, then this event will fire once\n  /// the user moves their finger away from the point of the initial touch.\n  void handleScaleStart(ScaleStartDetails details);\n\n  /// The pointer that was touching the screen has moved.\n  ///\n  /// This event occurs frequently during the drag, allowing you to keep track\n  /// of the position of the point of touch as it moves. This event will only\n  /// fire when the point of touch moves, and not when it stays still.\n  void handleScaleUpdate(ScaleUpdateDetails details);\n\n  /// Marks the end of a drag operation.\n  ///\n  /// This event fires when the pointer stops touching the screen. It will fire\n  /// even if the pointer is currently outside of the game widget.\n  void handleScaleEnd(ScaleEndDetails details);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/displacement_event.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/src/events/messages/location_context_event.dart';\nimport 'package:flame/src/game/game.dart';\n\n/// Location context for the Displacement Event.\n///\n/// This represents a user event that has a start and end locations, as the\n/// ones that are represented by a [DisplacementEvent].\ntypedef DisplacementContext = ({Vector2 start, Vector2 end});\n\nextension DisplacementContextDelta on DisplacementContext {\n  /// Displacement delta\n  Vector2 get delta => end - start;\n}\n\n/// Base class for events that contain two points on the screen, representing\n/// some sort of movement (from a \"start\" to and \"end\") and having a \"delta\".\n/// These include: drag events and (in the future) pointer move events.\n///\n/// This class includes properties that describe both positions where the event\n/// has occurred (start and end) and the delta (i.e. displacement) represented\n/// by the event.\nabstract class DisplacementEvent<R>\n    extends LocationContextEvent<DisplacementContext, R> {\n  DisplacementEvent(\n    this._game, {\n    required super.raw,\n    required this.deviceStartPosition,\n    required this.deviceEndPosition,\n  });\n  final Game _game;\n\n  /// Event start position in the coordinate space of the device -- either the\n  /// phone, or the browser window, or the app.\n  ///\n  /// If the game runs in a full-screen mode, then this would be equal to the\n  /// [canvasStartPosition]. Otherwise, the [deviceStartPosition] is the\n  /// Flutter-level global position.\n  final Vector2 deviceStartPosition;\n\n  /// Event start position in the coordinate space of the game widget, i.e.\n  /// relative to the game canvas.\n  ///\n  /// This could be considered the Flame-level global position.\n  late final Vector2 canvasStartPosition = _game.convertGlobalToLocalCoordinate(\n    deviceStartPosition,\n  );\n\n  /// Event start position in the local coordinate space of the current\n  /// component.\n  ///\n  /// This property is only accessible when the event is being propagated to\n  /// the components via [deliverAtPoint]. It is an error to try to read this\n  /// property at other times.\n  Vector2 get localStartPosition => renderingTrace.last.start;\n\n  /// Event end position in the coordinate space of the device -- either the\n  /// phone, or the browser window, or the app.\n  ///\n  /// If the game runs in a full-screen mode, then this would be equal to the\n  /// [canvasEndPosition]. Otherwise, the [deviceEndPosition] is the\n  /// Flutter-level global position.\n  final Vector2 deviceEndPosition;\n\n  /// Event end position in the coordinate space of the game widget, i.e.\n  /// relative to the game canvas.\n  ///\n  /// This could be considered the Flame-level global position.\n  late final Vector2 canvasEndPosition = _game.convertGlobalToLocalCoordinate(\n    deviceEndPosition,\n  );\n\n  /// Event end position in the local coordinate space of the current\n  /// component.\n  ///\n  /// This property is only accessible when the event is being propagated to\n  /// the components via [deliverAtPoint]. It is an error to try to read this\n  /// property at other times.\n  Vector2 get localEndPosition => renderingTrace.last.end;\n\n  /// Event delta in the coordinate space of the device -- either the\n  /// phone, or the browser window, or the app.\n  ///\n  /// If the game runs in a full-screen mode, then this would be equal to the\n  /// [canvasDelta]. Otherwise, the [deviceDelta] is the\n  /// Flutter-level global position.\n  late final Vector2 deviceDelta = deviceEndPosition - deviceStartPosition;\n\n  /// Event delta in the coordinate space of the game widget, i.e.\n  /// relative to the game canvas.\n  ///\n  /// This could be considered the Flame-level global position.\n  late final Vector2 canvasDelta = canvasEndPosition - canvasStartPosition;\n\n  /// Event delta in the local coordinate space of the current component.\n  ///\n  /// This property is only accessible when the event is being propagated to\n  /// the components via [deliverAtPoint]. It is an error to try to read this\n  /// property at other times.\n  Vector2 get localDelta => localEndPosition - localStartPosition;\n\n  @override\n  Iterable<Component> collectApplicableChildren({\n    required Component rootComponent,\n  }) {\n    return rootComponent.componentsAtLocation(\n      (\n        start: canvasStartPosition,\n        end: canvasEndPosition,\n      ),\n      renderingTrace,\n      (transform, context) {\n        final start = context.start;\n        final transformedStart = transform.parentToLocal(start);\n\n        final end = context.end;\n        final transformedEnd = transform.parentToLocal(end);\n\n        if (transformedStart == null || transformedEnd == null) {\n          return null;\n        }\n        return (\n          start: transformedStart,\n          end: transformedEnd,\n        );\n      },\n      // we only trigger the drag start if the component check passes, but\n      // as the user drags the cursor it could end up outside the component\n      // bounds; we don't want the event to be lost, so we bypass the check.\n      (component, context) => true,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/double_tap_cancel_event.dart",
    "content": "import 'package:flame/src/events/messages/event.dart';\n\nclass DoubleTapCancelEvent extends Event<void> {\n  DoubleTapCancelEvent() : super(raw: null);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/double_tap_down_event.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/events/messages/position_event.dart';\nimport 'package:flutter/gestures.dart';\n\nclass DoubleTapDownEvent extends PositionEvent<TapDownDetails> {\n  final PointerDeviceKind deviceKind;\n\n  DoubleTapDownEvent(super.game, TapDownDetails details)\n    : deviceKind = details.kind ?? PointerDeviceKind.unknown,\n      super(\n        raw: details,\n        devicePosition: details.globalPosition.toVector2(),\n      );\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/double_tap_event.dart",
    "content": "import 'package:flame/src/events/messages/event.dart';\n\nclass DoubleTapEvent extends Event<void> {\n  DoubleTapEvent() : super(raw: null);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/drag_cancel_event.dart",
    "content": "import 'package:flame/src/events/messages/drag_end_event.dart';\nimport 'package:flame/src/events/messages/drag_start_event.dart';\nimport 'package:flame/src/events/messages/event.dart';\nimport 'package:flutter/gestures.dart';\n\nclass DragCancelEvent extends Event<void> {\n  DragCancelEvent(this.pointerId) : super(raw: null);\n\n  /// The id of the event that has been cancelled. This id corresponds to the\n  /// id of the previous [DragStartEvent].\n  final int pointerId;\n\n  DragEndEvent toDragEnd() => DragEndEvent(pointerId, DragEndDetails());\n\n  @override\n  String toString() => 'DragCancelEvent(pointerId: $pointerId)';\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/drag_end_event.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/events/messages/event.dart';\nimport 'package:flutter/gestures.dart';\n\nclass DragEndEvent extends Event<DragEndDetails> {\n  DragEndEvent(this.pointerId, DragEndDetails details)\n    : velocity = details.velocity.pixelsPerSecond.toVector2(),\n      super(raw: details);\n\n  final int pointerId;\n\n  final Vector2 velocity;\n\n  @override\n  String toString() =>\n      'DragEndEvent(pointerId: $pointerId, velocity: $velocity)';\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/drag_start_event.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/events/messages/drag_end_event.dart';\nimport 'package:flame/src/events/messages/drag_update_event.dart';\nimport 'package:flame/src/events/messages/position_event.dart';\nimport 'package:flutter/gestures.dart';\n\n/// The event propagated through the Flame engine when the user starts a drag\n/// gesture on the game canvas.\n///\n/// This is a [PositionEvent], where the position is the point of touch.\nclass DragStartEvent extends PositionEvent<DragStartDetails> {\n  DragStartEvent(this.pointerId, super.game, DragStartDetails details)\n    : deviceKind = details.kind ?? PointerDeviceKind.unknown,\n      super(\n        raw: details,\n        devicePosition: details.globalPosition.toVector2(),\n      );\n\n  /// The unique identifier of the drag event.\n  ///\n  /// Subsequent [DragUpdateEvent] or [DragEndEvent] will carry the same pointer\n  /// id. This allows distinguishing multiple drags that may occur at the same\n  /// time on the same component.\n  final int pointerId;\n\n  final PointerDeviceKind deviceKind;\n\n  @override\n  String toString() =>\n      'DragStartEvent(canvasPosition: $canvasPosition, '\n      'devicePosition: $devicePosition, '\n      'pointedId: $pointerId, deviceKind: $deviceKind)';\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/drag_update_event.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/events/messages/displacement_event.dart';\nimport 'package:flutter/gestures.dart';\n\nclass DragUpdateEvent extends DisplacementEvent<DragUpdateDetails> {\n  DragUpdateEvent(this.pointerId, super.game, DragUpdateDetails details)\n    : timestamp = details.sourceTimeStamp ?? Duration.zero,\n      super(\n        raw: details,\n        deviceStartPosition: details.globalPosition.toVector2(),\n        deviceEndPosition:\n            details.globalPosition.toVector2() + details.delta.toVector2(),\n      );\n\n  final int pointerId;\n  final Duration timestamp;\n\n  @override\n  String toString() =>\n      'DragUpdateEvent('\n      'devicePosition: $deviceStartPosition, '\n      'canvasPosition: $canvasStartPosition, '\n      'delta: $localDelta, '\n      'pointerId: $pointerId, '\n      'timestamp: $timestamp'\n      ')';\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/event.dart",
    "content": "import 'package:flame/src/components/core/component.dart';\nimport 'package:meta/meta.dart';\n\n/// Base class for a variety of input events, such as tap events, drag events,\n/// keyboard events, etc.\n///\n/// This base class offers only simple common functionality; please see the\n/// concrete [Event] subclasses for the information about each individual event\n/// and the circumstances when they occur.\n///\n/// The type parameter [R] represents the type of the original Flutter raw event\n/// that triggered this Flame event.\nabstract class Event<R> {\n  /// The original Flutter raw event that triggered this Flame event.\n  R raw;\n\n  /// Flag that can be used to indicate that the event was handled by one of the\n  /// components.\n  ///\n  /// This flag is neither set nor read by Flame. Instead, it can be set by the\n  /// user in a component that handles an event, and then read by the user in a\n  /// different component, or at the root Game level.\n  bool handled = false;\n\n  /// If this flag is false (default), the event will be delivered to the first\n  /// component that can handle it. If that component sets this flag to true,\n  /// the event will propagate further down the component tree to other eligible\n  /// components.\n  bool continuePropagation = false;\n\n  Event({required this.raw});\n\n  @internal\n  void deliverToComponents<T extends Component>(\n    Component rootComponent,\n    void Function(T component) eventHandler,\n  ) {\n    for (final child\n        in rootComponent\n            .descendants(reversed: true, includeSelf: true)\n            .whereType<T>()) {\n      continuePropagation = false;\n      eventHandler(child);\n      if (!continuePropagation) {\n        break;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/location_context_event.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/src/events/messages/event.dart';\nimport 'package:meta/meta.dart';\n\n/// A base event that includes a location context, i.e. a position or set of\n/// positions in which the event happens.\n///\n/// The type parameter [C] is the generalization of the representation used to\n/// describe the location instance, such as a [Vector2].\nabstract class LocationContextEvent<C, R> extends Event<R> {\n  /// The stacktrace of coordinates of the event within the components in their\n  /// rendering order.\n  ///\n  /// This represents the stack of transformed versions of the same location\n  /// context -- which represents the event point -- but in the coordinate space\n  /// of each parent component until the root.\n  final List<C> renderingTrace = [];\n\n  LocationContextEvent({required super.raw});\n\n  /// The context in the parent's coordinate space, containing start and end\n  /// points.\n  C? get parentContext {\n    if (renderingTrace.length >= 2) {\n      final lastIndex = renderingTrace.length - 1;\n      return renderingTrace[lastIndex];\n    } else {\n      return null;\n    }\n  }\n\n  @internal\n  Iterable<Component> collectApplicableChildren({\n    required Component rootComponent,\n  });\n\n  @internal\n  void deliverAtPoint<T extends Component>({\n    required Component rootComponent,\n    required void Function(T component) eventHandler,\n    bool deliverToAll = false,\n  }) {\n    final children = collectApplicableChildren(\n      rootComponent: rootComponent,\n    );\n    for (final child in children.whereType<T>()) {\n      continuePropagation = deliverToAll;\n      eventHandler(child);\n      if (!continuePropagation) {\n        CameraComponent.currentCameras.clear();\n        break;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/pointer_move_event.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/src/events/messages/position_event.dart';\nimport 'package:flutter/services.dart';\n\nclass PointerMoveEvent extends PositionEvent<PointerHoverEvent> {\n  PointerMoveEvent(\n    this.pointerId,\n    super.game,\n    PointerHoverEvent rawEvent,\n  ) : timestamp = rawEvent.timeStamp,\n      delta = rawEvent.delta.toVector2(),\n      super(\n        raw: rawEvent,\n        devicePosition: rawEvent.position.toVector2(),\n      );\n\n  final int pointerId;\n  final Duration timestamp;\n  final Vector2 delta;\n\n  static final _nanPoint = Vector2.all(double.nan);\n\n  @override\n  Vector2 get localPosition {\n    return renderingTrace.isEmpty ? _nanPoint : renderingTrace.last;\n  }\n\n  @override\n  String toString() =>\n      'PointerMoveEvent(devicePosition: $devicePosition, '\n      'canvasPosition: $canvasPosition, '\n      'delta: $delta, '\n      'pointerId: $pointerId, timestamp: $timestamp)';\n\n  factory PointerMoveEvent.fromPointerHoverEvent(\n    Game game,\n    PointerHoverEvent event,\n  ) {\n    return PointerMoveEvent(\n      event.pointer,\n      game,\n      event,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/position_event.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/src/events/messages/location_context_event.dart';\nimport 'package:flame/src/game/game.dart';\n\n/// Base class for events that originate at some point on the screen. These\n/// include: tap events, scale events, etc.\n///\n/// This class includes properties that describe the position where the event\n/// has occurred.\nabstract class PositionEvent<R> extends LocationContextEvent<Vector2, R> {\n  PositionEvent(this._game, {required this.devicePosition, required super.raw});\n\n  final Game _game;\n\n  /// Event position in the coordinate space of the device -- either the phone,\n  /// or the browser window, or the app.\n  ///\n  /// If the game runs in a full-screen mode, then this would be equal to the\n  /// [canvasPosition]. Otherwise, the [devicePosition] is the Flutter-level\n  /// global position.\n  final Vector2 devicePosition;\n\n  /// Event position in the coordinate space of the game widget, i.e. relative\n  /// to the game canvas.\n  ///\n  /// This could be considered the Flame-level global position.\n  late final Vector2 canvasPosition = _game.convertGlobalToLocalCoordinate(\n    devicePosition,\n  );\n\n  /// Event position in the local coordinate space of the current component.\n  ///\n  /// This property is only accessible when the event is being propagated to\n  /// the components via [deliverAtPoint]. It is an error to try to read this\n  /// property at other times.\n  Vector2 get localPosition => renderingTrace.last;\n\n  @override\n  Iterable<Component> collectApplicableChildren({\n    required Component rootComponent,\n  }) {\n    return rootComponent.componentsAtPoint(canvasPosition, renderingTrace);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/scale_end_event.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/events/messages/event.dart';\nimport 'package:flutter/gestures.dart';\n\n/// Event propagated through the Flame engine when a scale gesture ends.\nclass ScaleEndEvent extends Event<ScaleEndDetails> {\n  ScaleEndEvent(this.pointerId, ScaleEndDetails details)\n    : velocity = details.velocity.pixelsPerSecond.toVector2(),\n      super(raw: details);\n\n  /// The unique identifier of the scale gesture.\n  final int pointerId;\n\n  /// The velocity of the fingers at the end of the scale gesture.\n  final Vector2 velocity;\n\n  @override\n  String toString() =>\n      'ScaleEndEvent(pointerId: $pointerId, velocity: $velocity)';\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/scale_start_event.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/src/events/messages/position_event.dart';\nimport 'package:flutter/gestures.dart';\n\n/// The event propagated through the Flame engine when the user starts a scale\n/// (pinch/zoom) gesture on the game canvas.\n///\n/// This is a [PositionEvent], where the position is the focal point of the\n///  gesture.\nclass ScaleStartEvent extends PositionEvent<ScaleStartDetails> {\n  ScaleStartEvent(this.pointerId, super.game, ScaleStartDetails details)\n    : deviceKind = details.kind ?? PointerDeviceKind.unknown,\n      super(\n        raw: details,\n        devicePosition: details.focalPoint.toVector2(),\n      );\n\n  /// The unique identifier of the scale event.\n  ///\n  /// Subsequent [ScaleUpdateEvent] or [ScaleEndEvent] will carry the same\n  /// pointer id. This allows distinguishing multiple simultaneous scale\n  /// gestures.\n  ///\n  final int pointerId;\n\n  /// The type of device that initiated the gesture.\n  final PointerDeviceKind deviceKind;\n\n  @override\n  String toString() =>\n      'ScaleStartEvent(canvasPosition: $canvasPosition, '\n      'devicePosition: $devicePosition, '\n      'pointerId: $pointerId, deviceKind: $deviceKind)';\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/scale_update_event.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/events/messages/displacement_event.dart';\nimport 'package:flutter/gestures.dart';\n\n/// Event propagated through the Flame engine when the user updates a scale\n/// (pinch/zoom/rotate) gesture on the game canvas.\nclass ScaleUpdateEvent extends DisplacementEvent<ScaleUpdateDetails> {\n  ScaleUpdateEvent(\n    this.pointerId,\n    super.game,\n    ScaleUpdateDetails details,\n  ) : scale = details.scale,\n      horizontalScale = details.horizontalScale,\n      verticalScale = details.verticalScale,\n      rotation = details.rotation,\n      pointerCount = details.pointerCount,\n      focalPointDelta = details.focalPointDelta.toVector2(),\n      timestamp = details.sourceTimeStamp ?? Duration.zero,\n      super(\n        raw: details,\n        deviceStartPosition: details.focalPoint.toVector2(),\n        deviceEndPosition:\n            details.focalPoint.toVector2() +\n            details.focalPointDelta.toVector2(),\n      );\n\n  /// Unique identifier of this scale gesture (Flame-level)\n  final int pointerId;\n\n  /// The instantaneous 2D scale factor (global)\n  final double scale;\n\n  /// Horizontal-only scale factor\n  final double horizontalScale;\n\n  /// Vertical-only scale factor\n  final double verticalScale;\n\n  /// Rotation delta in radians\n  final double rotation;\n\n  /// Number of fingers detected during this update\n  final int pointerCount;\n\n  /// Movement of the pinch center since last frame\n  final Vector2 focalPointDelta;\n\n  /// Timestamp for ordering/debugging\n  final Duration timestamp;\n\n  @override\n  String toString() =>\n      'ScaleUpdateEvent('\n      'pointerId: $pointerId, '\n      'scale: $scale, '\n      'hScale: $horizontalScale, '\n      'vScale: $verticalScale, '\n      'rotation: $rotation, '\n      'pointerCount: $pointerCount, '\n      'focalPointDelta: $focalPointDelta, '\n      'deviceStartPosition: $deviceStartPosition, '\n      'deviceEndPosition: $deviceEndPosition, '\n      'localDelta: $localDelta, '\n      'timestamp: $timestamp'\n      ')';\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/secondary_tap_cancel_event.dart",
    "content": "import 'package:flame/src/events/messages/event.dart';\nimport 'package:flame/src/events/messages/secondary_tap_down_event.dart';\n\n/// The event propagated through the Flame engine when a secondary tap\n/// (i.e. right mouse button click) on a component is cancelled.\n///\n/// This event may occur for several reasons, such as:\n///  - a secondary tap was converted into a drag event (for a game where\n///    secondary drag events are enabled);\n///  - a secondary tap was cancelled on the game widget itself -- for example,\n///    if another app came into the foreground, or device turned off, etc;\n///  - a secondary tap was cancelled on a particular component because that\n///    component has moved away from the point of contact.\n///\n/// The [SecondaryTapCancelEvent] will only occur if there was a previous\n/// [SecondaryTapDownEvent].\nclass SecondaryTapCancelEvent extends Event<void> {\n  SecondaryTapCancelEvent() : super(raw: null);\n\n  @override\n  String toString() => 'SecondaryTapCancel()';\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/secondary_tap_down_event.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/src/events/messages/position_event.dart';\nimport 'package:flutter/gestures.dart';\n\n/// The event propagated through the Flame engine when the user starts a\n/// secondary touch (i.e. right mouse click) on the game canvas.\n///\n/// This is a [PositionEvent], where the position is the point of touch.\n///\n/// In order for a component to be eligible to receive this event, it must add\n/// the [SecondaryTapCallbacks] mixin.\nclass SecondaryTapDownEvent extends PositionEvent<TapDownDetails> {\n  SecondaryTapDownEvent(super.game, TapDownDetails details)\n    : deviceKind = details.kind ?? PointerDeviceKind.unknown,\n      super(\n        raw: details,\n        devicePosition: details.globalPosition.toVector2(),\n      );\n\n  final PointerDeviceKind deviceKind;\n\n  @override\n  String toString() =>\n      'TapDownEvent(canvasPosition: $canvasPosition, '\n      'devicePosition: $devicePosition, '\n      'deviceKind: $deviceKind)';\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/secondary_tap_up_event.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/events/messages/position_event.dart';\nimport 'package:flame/src/events/messages/secondary_tap_down_event.dart';\nimport 'package:flutter/gestures.dart';\n\n/// The event propagated through the Flame engine when the user stops secondary\n/// touching (i.e. right mouse button) the game canvas.\n///\n/// This is a [PositionEvent], where the position is the point where the touch\n/// has last occurred.\n///\n/// The [SecondaryTapUpEvent] will only occur if there was a previous\n/// [SecondaryTapDownEvent].\nclass SecondaryTapUpEvent extends PositionEvent<TapUpDetails> {\n  SecondaryTapUpEvent(super.game, TapUpDetails details)\n    : deviceKind = details.kind,\n      super(\n        raw: details,\n        devicePosition: details.globalPosition.toVector2(),\n      );\n\n  final PointerDeviceKind deviceKind;\n\n  @override\n  String toString() =>\n      'SecondaryTapUpEvent(canvasPosition: $canvasPosition, '\n      'devicePosition: $devicePosition, '\n      'deviceKind: $deviceKind)';\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/tap_cancel_event.dart",
    "content": "import 'package:flame/src/events/messages/event.dart';\nimport 'package:flame/src/events/messages/tap_down_event.dart';\n\n/// The event propagated through the Flame engine when a tap on a component is\n/// cancelled.\n///\n/// This event may occur for several reasons, such as:\n///  - a tap was converted into a drag event (for a game where drag events are\n///    enabled);\n///  - a tap was cancelled on the game widget itself -- for example if another\n///    app came into the foreground, or device turned off, etc;\n///  - a tap was cancelled on a particular component because that component has\n///    moved away from the point of contact.\n///\n/// The [TapCancelEvent] will only occur if there was a previous [TapDownEvent].\nclass TapCancelEvent extends Event<void> {\n  TapCancelEvent(this.pointerId) : super(raw: null);\n\n  /// The id of the event that has been cancelled. This id corresponds to the\n  /// id of the previous [TapDownEvent].\n  final int pointerId;\n\n  @override\n  String toString() => 'TapCancelEvent(pointerId: $pointerId)';\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/tap_down_event.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/events/component_mixins/tap_callbacks.dart';\nimport 'package:flame/src/events/messages/position_event.dart';\nimport 'package:flame/src/events/messages/tap_cancel_event.dart';\nimport 'package:flame/src/events/messages/tap_up_event.dart';\nimport 'package:flutter/gestures.dart';\n\n/// The event propagated through the Flame engine when the user starts a touch\n/// on the game canvas.\n///\n/// This is a [PositionEvent], where the position is the point of touch.\n///\n/// In order for a component to be eligible to receive this event, it must add\n/// the [TapCallbacks] mixin.\nclass TapDownEvent extends PositionEvent<TapDownDetails> {\n  TapDownEvent(this.pointerId, super.game, TapDownDetails details)\n    : deviceKind = details.kind ?? PointerDeviceKind.unknown,\n      super(\n        raw: details,\n        devicePosition: details.globalPosition.toVector2(),\n      );\n\n  /// The unique identifier of the tap event.\n  ///\n  /// Subsequent [TapUpEvent] or [TapCancelEvent] will carry the same pointer\n  /// id. This allows distinguishing multiple taps that may occur simultaneously\n  /// on the same component.\n  final int pointerId;\n\n  final PointerDeviceKind deviceKind;\n\n  @override\n  String toString() =>\n      'TapDownEvent(canvasPosition: $canvasPosition, '\n      'devicePosition: $devicePosition, '\n      'pointerId: $pointerId, deviceKind: $deviceKind)';\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/messages/tap_up_event.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/events/messages/position_event.dart';\nimport 'package:flame/src/events/messages/tap_cancel_event.dart';\nimport 'package:flame/src/events/messages/tap_down_event.dart';\nimport 'package:flutter/gestures.dart';\n\n/// The event propagated through the Flame engine when the user stops touching\n/// the game canvas.\n///\n/// This is a [PositionEvent], where the position is the point where the touch\n/// has last occurred.\n///\n/// The [TapUpEvent] will only occur if there was a previous [TapDownEvent].\nclass TapUpEvent extends PositionEvent<TapUpDetails> {\n  TapUpEvent(this.pointerId, super.game, TapUpDetails details)\n    : deviceKind = details.kind,\n      super(\n        raw: details,\n        devicePosition: details.globalPosition.toVector2(),\n      );\n\n  /// The id of the previous [TapDownEvent] to which this event corresponds.\n  final int pointerId;\n\n  final PointerDeviceKind deviceKind;\n\n  TapCancelEvent toTapCancel() => TapCancelEvent(pointerId);\n\n  @override\n  String toString() =>\n      'TapUpEvent(canvasPosition: $canvasPosition, '\n      'devicePosition: $devicePosition, '\n      'pointerId: $pointerId, deviceKind: $deviceKind)';\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/tagged_component.dart",
    "content": "import 'package:flame/src/components/core/component.dart';\nimport 'package:flame/src/events/flame_game_mixins/multi_drag_dispatcher.dart';\nimport 'package:flame/src/events/flame_game_mixins/multi_tap_dispatcher.dart';\nimport 'package:meta/meta.dart';\n\n/// [TaggedComponent] is a utility class that represents a pair of a component\n/// and a pointer id.\n///\n/// This class is used by [MultiTapDispatcher] and [MultiDragDispatcher]\n/// to store information about which components were affected by which pointer\n/// event, so that subsequent events can be reliably delivered to the same\n/// components.\n@internal\n@immutable\nclass TaggedComponent<T extends Component> {\n  const TaggedComponent(this.pointerId, this.component);\n  final int pointerId;\n  final T component;\n\n  @override\n  int get hashCode => Object.hash(pointerId, component);\n\n  @override\n  bool operator ==(Object other) {\n    return other is TaggedComponent<T> &&\n        other.pointerId == pointerId &&\n        other.component == component;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/events/tap_config.dart",
    "content": "import 'dart:math';\n\n/// [TapConfig] is used to expose specific configurations\n/// related to the tap dispatcher.\nfinal class TapConfig {\n  TapConfig._();\n\n  static double _longTapDelay = _defaultLongTapDelay;\n\n  /// The delay (in seconds) after which a tap is considered a long tap.\n  static double get longTapDelay => _longTapDelay;\n\n  /// Set the delay (in seconds) after which a tap is considered a long tap.\n  /// Same games may want to change the long tap delay to better\n  /// fit their needs or accessibility requirements.\n  static set longTapDelay(double value) {\n    _longTapDelay = max(_minLongTapDelay, value);\n  }\n\n  /// Min delay to long tap delay is defined below.\n  /// Values too low don't make sense because they\n  /// would be basically a regular tap.\n  static const double _minLongTapDelay = 0.15;\n\n  /// Default long tap delay is 0.3 seconds.\n  static const double _defaultLongTapDelay = 0.3;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/experimental/column_component.dart",
    "content": "import 'package:flame/src/experimental/linear_layout_component.dart';\nimport 'package:flutter/rendering.dart';\n\n/// Warning: Experimental. API and behavior may change.\n///\n/// ColumnComponent is a layout component that arranges its children in a\n/// vertical column.\n///\n/// The [children] are laid out along the vertical axis, with spacing between\n/// them defined by the [gap] parameter.\n/// The alignment of the children along the vertical axis is controlled by\n/// [mainAxisAlignment], while their alignment along the horizontal axis is\n/// controlled by [crossAxisAlignment].\n///\n/// If [size] is non-null, behaves as normal explicit sizing.\n/// If [size] is null, sets the size to the minimum size that containing all\n/// the children. This is similar to setting the [size] to [intrinsicSize], but\n/// distinct in that sizing will respond to changes in children, other\n/// properties, etc...\n///\n/// Example usage:\n/// ```dart\n/// ColumnComponent(\n///   gap: 10.0,\n///   mainAxisAlignment: MainAxisAlignment.center,\n///   crossAxisAlignment: CrossAxisAlignment.start,\n///   children: [\n///     TextComponent('Child 1'),\n///     TextComponent('Child 2'),\n///     TextComponent('Child 3'),\n///   ],\n/// );\n/// ```\nclass ColumnComponent extends LinearLayoutComponent {\n  ColumnComponent({\n    super.key,\n    super.mainAxisAlignment = MainAxisAlignment.start,\n    super.crossAxisAlignment = CrossAxisAlignment.start,\n    super.gap = 0.0,\n    super.size,\n    super.position,\n    super.anchor,\n    super.priority,\n    super.children,\n  }) : super(direction: Direction.vertical);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/experimental/expanded_component.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/experimental.dart';\n\n/// Warning: Experimental. API and behavior may change.\n///\n/// Works similarly to flutter's Expanded widget.\n/// This component must be a direct child of a [LinearLayoutComponent].\n/// While this component does not do much on its own, it allows its parent\n/// [LinearLayoutComponent] to alter its computations and allow it to take up\n/// any free space in the main axis.\n///\n/// If its [parent] [LinearLayoutComponent] shrink-wraps in the main axis, then\n/// this component isn't expanded.\n///\n/// ExpandedComponent never tries to shrink-wrap. It only ever reports\n/// [intrinsicSize] to its parent, and receives sizing information from its\n/// parent.\n///\n/// However, it does need to report to its parent when its child changes size.\n/// This is less important along the main-axis, and more important along the\n/// cross-axis.\n///\n/// Example usage:\n/// ```dart\n/// ColumnComponent(\n///   children: [\n///     ExpandedComponent(\n///       child: TextComponent(text: 'foo'),\n///     );\n///     TextComponent(text: 'bar')\n///   ],\n/// );\n/// ```\nclass ExpandedComponent extends SingleLayoutComponent\n    with ParentIsA<LinearLayoutComponent> {\n  ExpandedComponent({\n    super.key,\n    super.position,\n    super.anchor,\n    super.priority,\n    super.inflateChild = true,\n    super.child,\n  }) : super(size: null);\n\n  @override\n  void setLayoutAxisLength(LayoutAxis axis, double? value) {\n    super.setLayoutAxisLength(axis, value);\n    final child = this.child;\n    if (inflateChild && child != null && value != null) {\n      // We want to set the child's size.\n      if (child is LayoutComponent) {\n        child.setLayoutAxisLength(axis, value);\n      } else {\n        child.size[axis.axisIndex] = value;\n      }\n    }\n  }\n\n  @override\n  void layoutChildren() {\n    resetSize();\n    parent.layoutChildren();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/experimental/geometry/shapes/circle.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/geometry.dart';\nimport 'package:flame/math.dart';\nimport 'package:flame/src/experimental/geometry/shapes/shape.dart';\nimport 'package:flame/src/extensions/vector2.dart';\nimport 'package:flame/src/game/transform2d.dart';\nimport 'package:flame/src/math/random_fallback.dart';\n\n/// The circle with a given [center] and a [radius].\n///\n/// A circle's radius cannot be negative, but it can be zero.\n///\n/// A circle transforms into a circle under any conformal transformation, i.e.\n/// a [Transform2D] that contains translations, rotations, and uniform scaling.\n/// Under a generic projection, a circle would turn into an ellipse, however,\n/// this is currently not implemented.\nclass Circle extends Shape {\n  Circle(Vector2 center, double radius)\n    : assert(radius >= 0, 'Radius cannot be negative: $radius'),\n      _center = center.clone(),\n      _radius = radius;\n\n  @override\n  Vector2 get center => _center;\n  final Vector2 _center;\n\n  double get radius => _radius;\n  double _radius;\n\n  @override\n  Aabb2 get aabb => _aabb ??= _calculateAabb();\n  Aabb2? _aabb;\n\n  Aabb2 _calculateAabb() {\n    return Aabb2.centerAndHalfExtents(_center, Vector2.all(_radius));\n  }\n\n  @override\n  bool get isConvex => true;\n\n  @override\n  double get perimeter => _radius * tau;\n\n  @override\n  Path asPath() {\n    final center = _center.toOffset();\n    return Path()..addOval(Rect.fromCircle(center: center, radius: _radius));\n  }\n\n  @override\n  bool containsPoint(Vector2 point) {\n    return (_tmpResult\n              ..setFrom(point)\n              ..sub(_center))\n            .length2 <=\n        _radius * _radius;\n  }\n\n  @override\n  Shape project(Transform2D transform, [Shape? target]) {\n    if (transform.isConformal) {\n      final newCenter = transform.localToGlobal(_center);\n      final newRadius = transform.scale.x.abs() * _radius;\n      if (target is Circle) {\n        target._center.setFrom(newCenter);\n        target._radius = newRadius;\n        _aabb = null;\n        return target;\n      } else {\n        return Circle(newCenter, newRadius);\n      }\n    }\n    throw UnimplementedError();\n  }\n\n  @override\n  void move(Vector2 offset) {\n    _center.add(offset);\n    _aabb?.min.add(offset);\n    _aabb?.max.add(offset);\n  }\n\n  @override\n  Vector2 support(Vector2 direction) {\n    return direction.normalized()\n      ..scale(_radius)\n      ..add(_center);\n  }\n\n  static final Vector2 _tmpResult = Vector2.zero();\n\n  @override\n  Vector2 nearestPoint(Vector2 point) {\n    if (_radius == 0) {\n      return _center;\n    }\n    return _tmpResult\n      ..setFrom(point)\n      ..sub(_center)\n      ..length = _radius\n      ..add(_center);\n  }\n\n  @override\n  Vector2 randomPoint({Random? random, bool within = true}) {\n    final randomGenerator = random ?? randomFallback;\n    final theta = randomGenerator.nextDouble() * tau;\n    final radius = within ? randomGenerator.nextDouble() * _radius : _radius;\n    final x = radius * cos(theta);\n    final y = radius * sin(theta);\n\n    return Vector2(_center.x + x, _center.y + y);\n  }\n\n  @override\n  String toString() => 'Circle([${_center.x}, ${_center.y}], $_radius)';\n\n  /// Tries to create a Circle that intersects the 3 points, if it exists.\n  ///\n  /// As long as the points are not co-linear, there is always exactly one\n  /// circle intersecting all 3 points.\n  static Circle? fromPoints(Vector2 p1, Vector2 p2, Vector2 p3) {\n    final offset = p2.length2;\n    final bc = (p1.length2 - offset) / 2.0;\n    final cd = (offset - p3.length2) / 2.0;\n    final det = (p1.x - p2.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p2.y);\n    if (det == 0) {\n      return null;\n    }\n\n    final centerX = (bc * (p2.y - p3.y) - cd * (p1.y - p2.y)) / det;\n    final centerY = (cd * (p1.x - p2.x) - bc * (p2.x - p3.x)) / det;\n    final radius = sqrt(pow(p2.x - centerX, 2) + pow(p2.y - centerY, 2));\n\n    return Circle(Vector2(centerX, centerY), radius);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/experimental/geometry/shapes/polygon.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:collection/collection.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/math.dart';\nimport 'package:flame/src/experimental/geometry/shapes/shape.dart';\nimport 'package:flame/src/game/transform2d.dart';\nimport 'package:flame/src/math/tmp_vector2.dart';\n\n/// An arbitrary polygon with 3 or more vertices.\n///\n/// The vertices of the polygon are in the counter-clockwise order, so that the\n/// polygon's interior is in your left-hand side as you traverse the polygon's\n/// vertices.\n///\n/// A polygon can be either convex or not, the [containsPoint] method will work\n/// in both cases, however, the method used with convex polygon is faster.\nclass Polygon extends Shape {\n  /// Constructs the polygon from the given list of vertices.\n  ///\n  /// If the list is not in the counter-clockwise order, then it will be\n  /// reversed in-place.\n  ///\n  /// If the [convex] flag is provided, then it serves as a hint about whether\n  /// the polygon is convex or not. With this flag the user promises that the\n  /// vertices are already in the correct CCW order.\n  Polygon(this._vertices, {bool? convex})\n    : assert(_vertices.length >= 3, 'At least 3 vertices are required') {\n    _initializeEdges();\n    if (convex == null) {\n      _ensureProperOrientation();\n    } else {\n      _convex = convex;\n    }\n  }\n\n  /// The vertices (corners) of the polygon.\n  ///\n  /// The user should treat this list as read-only and not attempt to modify\n  /// either the list itself or individual points.\n  List<Vector2> get vertices => _vertices;\n  final List<Vector2> _vertices;\n\n  /// The edges (sides) of the polygon.\n  ///\n  /// Each i-th edge is equal to the vector difference between the i-th vertex\n  /// and the preceding vertex. The number of edges is always equal to the\n  /// number of vertices.\n  List<Vector2> get edges => _edges;\n  late List<Vector2> _edges;\n  void _initializeEdges() {\n    var previousVertex = _vertices.last;\n    _edges = _vertices\n        .map((Vector2 vertex) {\n          final edge = vertex - previousVertex;\n          previousVertex = vertex;\n          return edge;\n        })\n        .toList(growable: false);\n  }\n\n  /// Checks whether the vertices are listed in the CCW order, and if not\n  /// reverses them. In addition, this method also checks whether the polygon\n  /// is convex and sets the [_convex] flag accordingly.\n  void _ensureProperOrientation() {\n    var nInteriorAngles = 0;\n    var nExteriorAngles = 0;\n    var previousEdge = _edges.last;\n    for (final edge in _edges) {\n      final crossProduct = edge.cross(previousEdge);\n      previousEdge = edge;\n      // A straight angle counts as both internal and external\n      if (crossProduct >= 0) {\n        nInteriorAngles++;\n      }\n      if (crossProduct <= 0) {\n        nExteriorAngles++;\n      }\n    }\n    if (nInteriorAngles < nExteriorAngles) {\n      _reverseVertices();\n      _initializeEdges();\n      nInteriorAngles = nExteriorAngles;\n    }\n    _convex = nInteriorAngles == _vertices.length;\n  }\n\n  /// Reverses the list of vertices in-place.\n  void _reverseVertices() {\n    for (var i = 0, j = _vertices.length - 1; i < j; i++, j--) {\n      final tmp = _vertices[i];\n      _vertices[i] = _vertices[j];\n      _vertices[j] = tmp;\n    }\n  }\n\n  @override\n  bool get isConvex => _convex;\n  late bool _convex;\n\n  @override\n  Vector2 get center => _center ??= _calculateCenter();\n  Vector2? _center;\n  Vector2 _calculateCenter() {\n    final center = Vector2.zero();\n    _vertices.forEach(center.add);\n    return center..scale(1 / _vertices.length);\n  }\n\n  @override\n  double get perimeter => _perimeter ??= _calculatePerimeter();\n  double? _perimeter;\n  double _calculatePerimeter() => edges.map((e) => e.length).sum;\n\n  @override\n  Aabb2 get aabb => _aabb ??= _calculateAabb();\n  Aabb2? _aabb;\n  Aabb2 _calculateAabb() {\n    final aabb = Aabb2.minMax(_vertices.first, _vertices.first);\n    for (final vertex in _vertices) {\n      aabb.hullPoint(vertex);\n    }\n    return aabb;\n  }\n\n  @override\n  Path asPath() {\n    final path = Path()..moveTo(_vertices.last.x, _vertices.last.y);\n    for (final vertex in _vertices) {\n      path.lineTo(vertex.x, vertex.y);\n    }\n    return path..close();\n  }\n\n  @override\n  bool containsPoint(Vector2 point) {\n    if (!aabb.intersectsWithVector2(point)) {\n      return false;\n    }\n    final n = _vertices.length;\n    if (isConvex) {\n      // For a convex polygon, a point is inside if for each edge the cross-\n      // product of that edge and a vector from the edge's origin to the point\n      // is positive or zero.\n      for (var i = 0; i < n; i++) {\n        if ((point - _vertices[i]).cross(_edges[i]) < 0) {\n          return false;\n        }\n      }\n      return true;\n    } else {\n      // For a non-convex polygon, we can verify that a point is inside using\n      // the winding theorem: if a point is inside, then any ray coming out of\n      // that point will intersect the polygon an odd number of times. If the\n      // point is outside, the number of intersections will be even.\n      // In this case we choose a horizontal ray that starts at `point` and\n      // goes towards +∞.\n      final x0 = point.x;\n      final y0 = point.y;\n      var intersectionCount = 0;\n      var j = n - 1; // index of previous vertex\n      for (var i = 0; i < n; i++) {\n        final vi = _vertices[i];\n        final vj = _vertices[j];\n        if (vi.x == x0 && vi.y == y0) {\n          return true;\n        }\n        if ((vi.y == y0 && vi.x > x0) ||\n            (vj.y == y0 && vj.x > x0) ||\n            (vi.y > y0) != (vj.y > y0) &&\n                ((y0 - vj.y) * (vi.x - vj.x) / (vi.y - vj.y) >= (x0 - vj.x))) {\n          intersectionCount++;\n        }\n        j = i;\n      }\n      return intersectionCount.isOdd;\n    }\n  }\n\n  @override\n  Shape project(Transform2D transform, [Shape? target]) {\n    final n = _vertices.length;\n    if (target is Polygon && target.vertices.length == n) {\n      for (var i = 0; i < n; i++) {\n        target._vertices[i].setFrom(transform.localToGlobal(_vertices[i]));\n      }\n      target._initializeEdges();\n      target._convex = _convex;\n      if (transform.hasReflection) {\n        target._reverseVertices();\n        target._initializeEdges();\n      }\n      return target;\n    }\n    final newVertices = _vertices\n        .map((vertex) => transform.localToGlobal(vertex))\n        .toList(growable: false);\n    final convex = transform.hasReflection ? null : _convex;\n    return Polygon(newVertices, convex: convex);\n  }\n\n  @override\n  void move(Vector2 offset) {\n    _vertices.forEach((vertex) => vertex.add(offset));\n    _center?.add(offset);\n    _aabb?.min.add(offset);\n    _aabb?.max.add(offset);\n  }\n\n  @override\n  Vector2 support(Vector2 direction) {\n    var bestVertex = _vertices.first;\n    var bestProduct = bestVertex.dot(direction);\n    for (final vertex in _vertices) {\n      final dotProduct = vertex.dot(direction);\n      if (dotProduct > bestProduct) {\n        bestProduct = dotProduct;\n        bestVertex = vertex;\n      }\n    }\n    return bestVertex;\n  }\n\n  static final Vector2 _tmpResult = Vector2.zero();\n\n  @override\n  Vector2 nearestPoint(Vector2 point) {\n    var shortestDistance2 = double.infinity;\n    for (var i = 0; i < _vertices.length; i++) {\n      final vertex = _vertices[i];\n      final edge = _edges[i];\n      final dotProduct =\n          (point.x - vertex.x) * edge.x + (point.y - vertex.y) * edge.y;\n      final t = (dotProduct / edge.length2).clamp(-1.0, 0.0);\n      final edgePointX = vertex.x + edge.x * t;\n      final edgePointY = vertex.y + edge.y * t;\n      final dx = edgePointX - point.x;\n      final dy = edgePointY - point.y;\n      final distance2 = dx * dx + dy * dy;\n      if (distance2 < shortestDistance2) {\n        shortestDistance2 = distance2;\n        _tmpResult.setValues(edgePointX, edgePointY);\n      }\n    }\n    return _tmpResult;\n  }\n\n  @override\n  String toString() => 'Polygon($vertices)';\n\n  @override\n  Vector2 randomPoint({Random? random, bool within = true}) {\n    final randomGenerator = random ?? randomFallback;\n    if (within) {\n      final result = Vector2.zero();\n      final min = aabb.min;\n      final max = aabb.max;\n\n      while (true) {\n        final randomX = min.x + randomGenerator.nextDouble() * (max.x - min.x);\n        final randomY = min.y + randomGenerator.nextDouble() * (max.y - min.y);\n        result.setValues(randomX, randomY);\n\n        if (containsPoint(result)) {\n          return result;\n        }\n      }\n    } else {\n      return Polygon.randomPointAlongEdges(_vertices, random: randomGenerator);\n    }\n  }\n\n  /// Returns a random point on the [vertices].\n  static Vector2 randomPointAlongEdges(\n    List<Vector2> vertices, {\n    Random? random,\n  }) {\n    final randomGenerator = random ?? randomFallback;\n    final verticesLengths = <double>[];\n    var totalLength = 0.0;\n    for (final (i, startPoint) in vertices.indexed) {\n      final endPoint = vertices[(i + 1) % vertices.length];\n      final length = startPoint.distanceTo(endPoint);\n      verticesLengths.add(length);\n      totalLength += length;\n    }\n    final pointOnEdges = randomGenerator.nextDouble() * totalLength;\n    var vertexIndex = 0;\n    var currentEndPoint = 0.0;\n    late final double localEdgePoint;\n    while (vertexIndex < verticesLengths.length) {\n      final lastEndPoint = currentEndPoint;\n      currentEndPoint += verticesLengths[vertexIndex];\n      if (currentEndPoint >= pointOnEdges) {\n        localEdgePoint = pointOnEdges - lastEndPoint;\n        break;\n      }\n      vertexIndex++;\n    }\n    final startPoint = vertices[vertexIndex];\n    final endPoint = vertices[(vertexIndex + 1) % vertices.length];\n    tmpVector2\n      ..setFrom(endPoint)\n      ..sub(startPoint)\n      ..scaleTo(localEdgePoint);\n    return startPoint + tmpVector2;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/experimental/geometry/shapes/rectangle.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/src/experimental/geometry/shapes/polygon.dart';\nimport 'package:flame/src/experimental/geometry/shapes/shape.dart';\nimport 'package:flame/src/game/transform2d.dart';\nimport 'package:flame/src/math/random_fallback.dart';\n\n/// An axis-aligned rectangle.\n///\n/// This is similar to ui [Rect], except that this class is mutable and\n/// conforms to the [Shape] API.\n///\n/// Unlike with [Rect], the [Rectangle] is always correctly oriented, in the\n/// sense that its left edge is to the left from the right edge, and its top\n/// edge is above the bottom edge.\n///\n/// The edges of a [Rectangle] can also coincide: the left edge can coincide\n/// with the right edge, and the top side with the bottom.\nclass Rectangle extends Shape {\n  /// Constructs the [Rectangle] from left, top, right and bottom edges.\n  ///\n  /// If the edges are given in the wrong order (e.g. `left` is to the right\n  /// from `right`), then they will be swapped.\n  Rectangle.fromLTRB(this._left, this._top, this._right, this._bottom) {\n    if (_left > _right) {\n      final tmp = _left;\n      _left = _right;\n      _right = tmp;\n    }\n    if (_top > _bottom) {\n      final tmp = _top;\n      _top = _bottom;\n      _bottom = tmp;\n    }\n  }\n\n  Rectangle.fromLTWH(double left, double top, double width, double height)\n    : this.fromLTRB(left, top, left + width, top + height);\n\n  /// Constructs a [Rectangle] from two opposite corners. The points can be in\n  /// any disposition to each other.\n  factory Rectangle.fromPoints(Vector2 a, Vector2 b) =>\n      Rectangle.fromLTRB(a.x, a.y, b.x, b.y);\n\n  /// Constructs a [Rectangle] from a [Rect].\n  factory Rectangle.fromRect(Rect rect) =>\n      Rectangle.fromLTRB(rect.left, rect.top, rect.right, rect.bottom);\n\n  /// Constructs a [Rectangle] from a center point and a size.\n  factory Rectangle.fromCenter({\n    required Vector2 center,\n    required Vector2 size,\n  }) {\n    final halfSize = size / 2;\n    return Rectangle.fromPoints(\n      center - halfSize,\n      center + halfSize,\n    );\n  }\n\n  double _left;\n  double _top;\n  double _right;\n  double _bottom;\n\n  double get left => _left;\n  double get right => _right;\n  double get top => _top;\n  double get bottom => _bottom;\n  double get width => _right - _left;\n  double get height => _bottom - _top;\n\n  @override\n  Aabb2 get aabb => _aabb ??= _calculateAabb();\n  Aabb2? _aabb;\n  Aabb2 _calculateAabb() {\n    return Aabb2.minMax(Vector2(_left, _top), Vector2(_right, _bottom));\n  }\n\n  @override\n  bool get isConvex => true;\n\n  @override\n  Vector2 get center => Vector2((_left + _right) / 2, (_top + _bottom) / 2);\n\n  @override\n  double get perimeter => 2 * (width + height);\n\n  double get area => width * height;\n\n  @override\n  Path asPath() {\n    return Path()..addRect(Rect.fromLTRB(_left, _top, _right, _bottom));\n  }\n\n  @override\n  bool containsPoint(Vector2 point) {\n    return point.x >= _left &&\n        point.y >= _top &&\n        point.x <= _right &&\n        point.y <= _bottom;\n  }\n\n  @override\n  Shape project(Transform2D transform, [Shape? target]) {\n    if (transform.isAxisAligned) {\n      final v1 = transform.localToGlobal(Vector2(_left, _top));\n      final v2 = transform.localToGlobal(Vector2(_right, _bottom));\n      final newLeft = min(v1.x, v2.x);\n      final newRight = max(v1.x, v2.x);\n      final newTop = min(v1.y, v2.y);\n      final newBottom = max(v1.y, v2.y);\n      if (target is Rectangle) {\n        target._left = newLeft;\n        target._right = newRight;\n        target._top = newTop;\n        target._bottom = newBottom;\n        target._aabb = null;\n        return target;\n      } else {\n        return Rectangle.fromLTRB(newLeft, newTop, newRight, newBottom);\n      }\n    }\n    throw UnimplementedError();\n  }\n\n  @override\n  void move(Vector2 offset) {\n    _left += offset.x;\n    _right += offset.x;\n    _top += offset.y;\n    _bottom += offset.y;\n    _aabb?.min.add(offset);\n    _aabb?.max.add(offset);\n  }\n\n  @override\n  Vector2 support(Vector2 direction) {\n    final vx = direction.x >= 0 ? _right : _left;\n    final vy = direction.y >= 0 ? _bottom : _top;\n    return Vector2(vx, vy);\n  }\n\n  static final Vector2 _tmpResult = Vector2.zero();\n\n  @override\n  Vector2 nearestPoint(Vector2 point) {\n    return _tmpResult..setValues(\n      (point.x).clamp(_left, _right),\n      (point.y).clamp(_top, _bottom),\n    );\n  }\n\n  Rect toRect() => Rect.fromLTWH(left, top, width, height);\n\n  /// Returns all intersections between this rectangle's edges and the given\n  /// line segment.\n  Set<Vector2> intersections(LineSegment line) {\n    return edges.expand((e) => e.intersections(line)).toSet();\n  }\n\n  @override\n  Vector2 randomPoint({Random? random, bool within = true}) {\n    final randomGenerator = random ?? randomFallback;\n    if (within) {\n      return Vector2(\n        left + randomGenerator.nextDouble() * width,\n        top + randomGenerator.nextDouble() * height,\n      );\n    } else {\n      return Polygon.randomPointAlongEdges(vertices, random: randomGenerator);\n    }\n  }\n\n  /// The 4 edges of this rectangle, returned in a clockwise fashion.\n  List<LineSegment> get edges => [topEdge, rightEdge, bottomEdge, leftEdge];\n\n  LineSegment get topEdge => LineSegment(topLeft, topRight);\n  LineSegment get rightEdge => LineSegment(topRight, bottomRight);\n  LineSegment get bottomEdge => LineSegment(bottomRight, bottomLeft);\n  LineSegment get leftEdge => LineSegment(bottomLeft, topLeft);\n\n  /// The 4 corners of this rectangle, returned in a clockwise fashion.\n  List<Vector2> get vertices => [topLeft, topRight, bottomRight, bottomLeft];\n\n  Vector2 get topLeft => Vector2(left, top);\n  Vector2 get topRight => Vector2(right, top);\n  Vector2 get bottomRight => Vector2(right, bottom);\n  Vector2 get bottomLeft => Vector2(left, bottom);\n\n  @override\n  String toString() => 'Rectangle([$_left, $_top], [$_right, $_bottom])';\n}\n"
  },
  {
    "path": "packages/flame/lib/src/experimental/geometry/shapes/rounded_rectangle.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/geometry.dart';\nimport 'package:flame/math.dart';\nimport 'package:flame/src/experimental/geometry/shapes/shape.dart';\nimport 'package:flame/src/game/transform2d.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// An axis-aligned rectangle with rounded corners.\n///\n/// The rounded parts of the rectangle are symmetrical in x- and y-directions,\n/// and across all corners.\nclass RoundedRectangle extends Shape {\n  /// Constructs a [RoundedRectangle] with left, top, right and bottom edges,\n  /// and the given radius.\n  ///\n  /// If the edges are given in the wrong order (e.g. `left` is to the right\n  /// from `right`), then they will be swapped.\n  ///\n  /// The radius cannot be negative. At the same time, if the radius is too big,\n  /// it will be reduced so that the rounded edge can fit inside the rectangle.\n  /// In other words, the radius will be adjusted to not exceed the half-width\n  /// or half-height of the rectangle.\n  RoundedRectangle.fromLTRBR(\n    this._left,\n    this._top,\n    this._right,\n    this._bottom,\n    this._radius,\n  ) : assert(_radius >= 0, 'Radius cannot be negative: $_radius') {\n    if (_left > _right) {\n      final tmp = _left;\n      _left = _right;\n      _right = tmp;\n    }\n    if (_top > _bottom) {\n      final tmp = _top;\n      _top = _bottom;\n      _bottom = tmp;\n    }\n    if (_radius > (_right - _left) / 2) {\n      _radius = (_right - _left) / 2;\n    }\n    if (_radius > (_bottom - _top) / 2) {\n      _radius = (_bottom - _top) / 2;\n    }\n  }\n\n  factory RoundedRectangle.fromPoints(Vector2 a, Vector2 b, double radius) =>\n      RoundedRectangle.fromLTRBR(a.x, a.y, b.x, b.y, radius);\n\n  /// Constructs a [RoundedRectangle] from ui [RRect].\n  ///\n  /// All corners of the `rrect` must have the same circular radii.\n  factory RoundedRectangle.fromRRect(RRect rrect) {\n    final radius = rrect.brRadiusX;\n    assert(\n      rrect.blRadiusX == radius &&\n          rrect.brRadiusX == radius &&\n          rrect.tlRadiusX == radius &&\n          rrect.trRadiusX == radius &&\n          rrect.blRadiusY == radius &&\n          rrect.brRadiusY == radius &&\n          rrect.tlRadiusY == radius &&\n          rrect.trRadiusY == radius,\n      'Unequal radii in the $rrect',\n    );\n    return RoundedRectangle.fromLTRBR(\n      rrect.left,\n      rrect.top,\n      rrect.right,\n      rrect.bottom,\n      radius,\n    );\n  }\n\n  double _left;\n  double _top;\n  double _right;\n  double _bottom;\n  double _radius;\n\n  double get left => _left;\n  double get right => _right;\n  double get top => _top;\n  double get bottom => _bottom;\n  double get radius => _radius;\n  double get width => _right - _left;\n  double get height => _bottom - _top;\n\n  @override\n  bool get isConvex => true;\n\n  @override\n  double get perimeter => (tau - 8) * _radius + 2 * (width + height);\n\n  @override\n  Vector2 get center => Vector2((_left + _right) / 2, (_top + _bottom) / 2);\n\n  @override\n  Aabb2 get aabb => _aabb ??= _calculateAabb();\n  Aabb2? _aabb;\n  Aabb2 _calculateAabb() =>\n      Aabb2.minMax(Vector2(_left, _top), Vector2(_right, _bottom));\n\n  /// Converts this shape into an [RRect] from dart:ui.\n  RRect asRRect() =>\n      RRect.fromLTRBR(_left, _top, _right, _bottom, Radius.circular(_radius));\n\n  @override\n  Path asPath() => Path()..addRRect(asRRect());\n\n  @override\n  bool containsPoint(Vector2 point) {\n    final x0 = point.x;\n    final y0 = point.y;\n    if (x0 < _left || x0 > _right || y0 < _top || y0 > _bottom) {\n      return false;\n    }\n    final fx = _radius - min(x0 - _left, min(_right - x0, _radius));\n    final fy = _radius - min(y0 - _top, min(_bottom - y0, _radius));\n    return fx * fx + fy * fy <= _radius * _radius;\n  }\n\n  @override\n  Shape project(Transform2D transform, [Shape? target]) {\n    if (transform.isAxisAligned && transform.isConformal) {\n      final v1 = transform.localToGlobal(Vector2(_left, _top));\n      final v2 = transform.localToGlobal(Vector2(_right, _bottom));\n      final newLeft = min(v1.x, v2.x);\n      final newRight = max(v1.x, v2.x);\n      final newTop = min(v1.y, v2.y);\n      final newBottom = max(v1.y, v2.y);\n      final newRadius = transform.scale.x.abs() * _radius;\n      if (target is RoundedRectangle) {\n        target._left = newLeft;\n        target._right = newRight;\n        target._top = newTop;\n        target._bottom = newBottom;\n        target._radius = newRadius;\n        target._aabb = null;\n        return target;\n      } else {\n        return RoundedRectangle.fromLTRBR(\n          newLeft,\n          newTop,\n          newRight,\n          newBottom,\n          newRadius,\n        );\n      }\n    }\n    throw UnimplementedError();\n  }\n\n  @override\n  void move(Vector2 offset) {\n    _left += offset.x;\n    _right += offset.x;\n    _top += offset.y;\n    _bottom += offset.y;\n    _aabb?.min.add(offset);\n    _aabb?.max.add(offset);\n  }\n\n  @override\n  Vector2 support(Vector2 direction) {\n    final result = direction.normalized()..length = _radius;\n    result.x += direction.x >= 0 ? _right - _radius : _left + _radius;\n    result.y += direction.y >= 0 ? _bottom - _radius : _top + _radius;\n    return result;\n  }\n\n  static final Vector2 _tmpResult = Vector2.zero();\n  static final Vector2 _tmpCenter = Vector2.zero();\n\n  @override\n  Vector2 nearestPoint(Vector2 point) {\n    _tmpResult.setValues(\n      point.x.clamp(_left, _right),\n      point.y.clamp(_top, _bottom),\n    );\n    if (containsPoint(_tmpResult)) {\n      return _tmpResult;\n    }\n    _tmpCenter.setValues(\n      _tmpResult.x <= _left + _radius ? _left + _radius : _right - _radius,\n      _tmpResult.y <= _top + _radius ? _top + _radius : _bottom - _radius,\n    );\n    return _tmpResult\n      ..setFrom(point)\n      ..sub(_tmpCenter)\n      ..length = _radius\n      ..add(_tmpCenter);\n  }\n\n  @override\n  String toString() =>\n      'RoundedRectangle([$_left, $_top], [$_right, $_bottom], $_radius)';\n\n  @override\n  Vector2 randomPoint({Random? random, bool within = true}) {\n    assert(\n      within,\n      'It is not possible to get a point only along the edges of a '\n      'rounded rectangle.',\n    );\n    final randomGenerator = random ?? randomFallback;\n    final result = Vector2.zero();\n    final min = aabb.min;\n    final max = aabb.max;\n\n    while (true) {\n      final randomX = min.x + randomGenerator.nextDouble() * (max.x - min.x);\n      final randomY = min.y + randomGenerator.nextDouble() * (max.y - min.y);\n      result.setValues(randomX, randomY);\n\n      if (containsPoint(result)) {\n        return result;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/experimental/geometry/shapes/shape.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/src/experimental/geometry/shapes/circle.dart';\nimport 'package:flame/src/experimental/geometry/shapes/polygon.dart';\nimport 'package:flame/src/experimental/geometry/shapes/rectangle.dart';\nimport 'package:flame/src/experimental/geometry/shapes/rounded_rectangle.dart';\nimport 'package:flame/src/game/transform2d.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// Base class for various 2D geometric primitives defined on a Cartesian\n/// coordinate plane.\n///\n/// Implementations include:\n///   - [Circle]\n///   - [Polygon]\n///   - [Rectangle]\n///   - [RoundedRectangle]\nabstract class Shape {\n  /// True if the shape is \"closed\", in the sense that it has an interior. For\n  /// example, a closed shape can be filled with a paint.\n  bool get isClosed => true;\n\n  /// True if the shape is convex, i.e. a line segment connecting any two points\n  /// of the shape would lie completely within the shape.\n  bool get isConvex;\n\n  /// The length of the shape's boundary. For some more complicated shapes this\n  /// can be computed approximately.\n  double get perimeter;\n\n  /// The central point of the shape.\n  ///\n  /// For some shapes (circle, rectangle) the center is well-defined and\n  /// unambiguous. For some, there could be multiple definitions (triangle,\n  /// polygon), in which case it is up to the component to decide what its\n  /// \"center\" should be.\n  Vector2 get center;\n\n  /// The axis-aligned bounding box of the shape.\n  ///\n  /// Implementations are encouraged to cache the computed Aabb in order to\n  /// avoid repeated recalculations on every game tick.\n  Aabb2 get aabb;\n\n  /// Returns true if the given [point] is inside the shape or on the boundary.\n  bool containsPoint(Vector2 point);\n\n  /// Converts the shape to a [Path] object, suitable for rendering on a canvas.\n  /// If a particular geometric primitive cannot be represented as a [Path]\n  /// faithfully, an approximate path can be returned.\n  Path asPath();\n\n  /// Returns the result of applying an affine transformation to the shape.\n  ///\n  /// Certain shapes may be transformed into shapes of a different kind during\n  /// the projection. For example, a `Circle` may transform into an `Ellipse`,\n  /// and `Rectangle` into a `Polygon`.\n  ///\n  /// If [target] is provided and it has a proper type, then this method should\n  /// modify the target in-place and return it. If [target] is null, or if its\n  /// type is not compatible with the requested [transform], then the method\n  /// should create and return a new [Shape].\n  Shape project(Transform2D transform, [Shape? target]);\n\n  /// Translates the shape by the specified [offset] vector, in-place.\n  ///\n  /// This method is a simpler version of [project], since all shapes can be\n  /// moved without changing the shape type, and with little modifications to\n  /// the internal state.\n  void move(Vector2 offset);\n\n  /// Finds the intersection of this shape with another one, if it exists.\n  // Intersection? intersection(GeometricPrimitive other);\n\n  /// Returns a point on the boundary that is furthest in the given [direction].\n  ///\n  /// In other words, this returns such a point `p` within in the shape for\n  /// which the dot-product `p·direction` is maximal. If multiple such points\n  /// exist, then any one of them can be returned.\n  ///\n  /// The [direction] vector may have length not equal to 1.\n  ///\n  /// This method is only used for convex shapes.\n  Vector2 support(Vector2 direction);\n\n  /// Returns a point on the shape's boundary which is closest to the given\n  /// [point].\n  ///\n  /// The [point] must lie outside of the shape. If there are multiple nearest\n  /// points, any one can be returned.\n  ///\n  /// This method will not modify [point]. At the same time, the caller does\n  /// not get ownership of the returned object: they must treat it as an\n  /// immutable short-lived object.\n  Vector2 nearestPoint(Vector2 point);\n\n  /// Returns a random point within the shape if [within] is true (default) and\n  /// otherwise a point along the edges of the shape.\n  /// Do note that [within]=true also includes the edges.\n  ///\n  /// If [isClosed] is false, the [within] value does not make a difference.\n  Vector2 randomPoint({Random? random, bool within = true});\n}\n"
  },
  {
    "path": "packages/flame/lib/src/experimental/layout_component.dart",
    "content": "import 'package:flame/components.dart';\n\nenum LayoutAxis {\n  x(0),\n  y(1)\n  ;\n\n  const LayoutAxis(this.axisIndex);\n\n  /// Necessary for use with LinearLayoutComponent's Direction\n  final int axisIndex;\n}\n\nabstract class LayoutComponent extends PositionComponent {\n  LayoutComponent({\n    required super.key,\n    required super.position,\n    required Vector2? size,\n    required super.anchor,\n    required super.priority,\n    super.children,\n  }) : _layoutSizeX = size?.x,\n       _layoutSizeY = size?.y {\n    resetSize();\n  }\n\n  double? _layoutSizeX;\n  double? _layoutSizeY;\n\n  double? get layoutSizeX => _layoutSizeX;\n  double? get layoutSizeY => _layoutSizeY;\n\n  /// Sets both layout axes at the same time, and consequently, sets the [size]\n  /// in one go.\n  /// If you intend to only selectively set one axis length at a time, use\n  /// [setLayoutAxisLength].\n  ///\n  /// This is *not* equivalent to calling [setLayoutAxisLength] for both\n  /// axes. Doing so would result size listeners being called twice: once for\n  /// the x axis, and again for the y axis.\n  void setLayoutSize(double? layoutSizeX, double? layoutSizeY) {\n    _layoutSizeX = layoutSizeX;\n    _layoutSizeY = layoutSizeY;\n    resetSize();\n  }\n\n  /// Sets the appropriate layout dimension based on [axis]. This is needed\n  /// because currently there's no other way, at the [LayoutComponent] level,\n  /// to selectively set width or height without setting both.\n  /// e.g. to set Y axis to 100, `setLayoutAxisLength(LayoutAxis.y, 100)`\n  ///\n  /// If you intend to set both axes at the same time, use [setLayoutSize]\n  ///\n  /// This is *not* equivalent to calling [setLayoutSize] with one of the axes\n  /// set to null. Doing so would actually set the axis to the intrinsic length\n  /// of that dimension.\n  void setLayoutAxisLength(LayoutAxis axis, double? value) {\n    // This is necessary because we cannot extend the accessor assignment of\n    // NullableVector2 to trigger some extra functionality\n    // (i.e. setting height/width) when the accessor is set.\n    //\n    // We also cannot use listeners at the moment, because listeners are\n    // triggered when either height/width are set, when we need things to happen\n    // *only* when height or width are set. Current functionality results in\n    // a race condition.\n    switch (axis) {\n      case LayoutAxis.x:\n        _layoutSizeX = value;\n        width = _layoutSizeX ?? intrinsicSize.x;\n      case LayoutAxis.y:\n        _layoutSizeY = value;\n        height = _layoutSizeY ?? intrinsicSize.y;\n    }\n  }\n\n  /// Reset the size of this [LayoutComponent] to either the layout dimensions\n  /// or the [intrinsicSize].\n  void resetSize() {\n    size.setValues(\n      _layoutSizeX ?? intrinsicSize.x,\n      _layoutSizeY ?? intrinsicSize.y,\n    );\n  }\n\n  bool isShrinkWrappedIn(LayoutAxis axis) {\n    return switch (axis) {\n      LayoutAxis.x => layoutSizeX == null,\n      LayoutAxis.y => layoutSizeY == null,\n    };\n  }\n\n  @override\n  void onChildrenChanged(Component child, ChildrenChangeType type) {\n    if (child is! PositionComponent) {\n      return;\n    }\n    if (type == ChildrenChangeType.added) {\n      child.size.addListener(layoutChildren);\n    } else {\n      child.size.removeListener(layoutChildren);\n    }\n    layoutChildren();\n  }\n\n  /// The method to refresh the layout, triggered by various events.\n  /// (e.g. [onChildrenChanged], size changes on both this component and its\n  /// children)\n  ///\n  /// Override this method for any specific layout needs.\n  void layoutChildren();\n\n  /// A helper property to get only the [PositionComponent]s\n  /// among the [children].\n  List<PositionComponent> get positionChildren =>\n      children.whereType<PositionComponent>().toList();\n\n  /// The smallest possible size this component can possibly have, as a\n  /// container of other components, and given whatever constraints any\n  /// subclass of [LayoutComponent] may prescribe.\n  Vector2 get intrinsicSize;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/experimental/linear_layout_component.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/image_composition.dart';\nimport 'package:flutter/rendering.dart';\n\nenum Direction {\n  horizontal,\n  vertical\n  ;\n\n  /// A getter for returning the [LayoutAxis] whose [LayoutAxis.axisIndex] can\n  /// be used with [Vector2]s to get the corresponding axis values.\n  LayoutAxis get mainAxis =>\n      this == Direction.horizontal ? LayoutAxis.x : LayoutAxis.y;\n\n  /// See [mainAxis]\n  LayoutAxis get crossAxis =>\n      this == Direction.horizontal ? LayoutAxis.y : LayoutAxis.x;\n\n  /// A helper for returning the main axis for [vector], given this direction.\n  double mainAxisValue(Vector2 vector) {\n    return vector[mainAxis.axisIndex];\n  }\n\n  /// A helper for returning the cross axis for [vector], given this direction.\n  double crossAxisValue(Vector2 vector) {\n    return vector[crossAxis.axisIndex];\n  }\n}\n\n/// Superclass for linear layouts.\n/// A re-layout is performed when\n///  - [children] are added or removed\n///  - some types of [children] are resized\n///  - the [gap] parameter is changed\n///  - the [size] parameter is changed\n///  - the [mainAxisAlignment] parameter is changed\n///  - the [crossAxisAlignment] parameter is changed\n///\n/// Property interactions and gotchas\n///  - [gap] is ignored when the [mainAxisAlignment] is set to one of:\n///    - [MainAxisAlignment.spaceAround]\n///    - [MainAxisAlignment.spaceBetween]\n///    - [MainAxisAlignment.spaceEvenly]\n///  - [size] can be set to null to activate shrink-wrap mode.\n///    Instead of top-down sizing and layout, LayoutComponent derives its size\n///    from its children via [intrinsicSize]. In this mode, certain layout\n///    options become meaningless. The following are the behaviors you should\n///    note:\n///    - [mainAxisAlignment] acts like [MainAxisAlignment.start], regardless\n///      of its original value.\n///    - [crossAxisAlignment] will cause all children to have the same length\n///      along the cross-axis, equal to the largest child in that axis.\n///    - [ExpandedComponent]s no longer expand, and their size is set to their\n///      [intrinsicSize], which is simply the size of their respective children.\n///  - The existence of an [ExpandedComponent] among the [children]\n///    automatically disables [MainAxisAlignment.spaceAround],\n///    [MainAxisAlignment.spaceBetween], and [MainAxisAlignment.spaceEvenly],\n///    and inter-child spacing will be fully dictated by [gap]. This is because\n///    [ExpandedComponent]s expand to fill the available space.\n///\n/// Notes:\n///  - Currently, [CrossAxisAlignment.baseline] is unsupported, and behaves\n///    exactly like [CrossAxisAlignment.start].\n///  - Because [CrossAxisAlignment.stretch] alters the size of the children,\n///    and there is no uniform interface for getting the inherent sizes of\n///    [PositionComponent]s, using [CrossAxisAlignment.stretch] \"permanently\"\n///    changes the sizes of the children. Subsequent changes to\n///    [crossAxisAlignment] will work with the new sizes of the children.\n///  - When [crossAxisAlignment] is set to [CrossAxisAlignment.stretch], and the\n///    [direction] is set to [Direction.vertical], sets any child\n///    [TextBoxComponent]'s [TextBoxComponent.boxConfig] to a copy such that\n///    [TextBoxConfig.maxWidth] is set to whatever value any other child's width\n///    would be set to.\nabstract class LinearLayoutComponent extends LayoutComponent {\n  LinearLayoutComponent({\n    required super.key,\n    required this.direction,\n    required CrossAxisAlignment crossAxisAlignment,\n    required MainAxisAlignment mainAxisAlignment,\n    required double gap,\n    required super.size,\n    required super.position,\n    required super.priority,\n    required super.anchor,\n    required super.children,\n  }) : _crossAxisAlignment = crossAxisAlignment,\n       _mainAxisAlignment = mainAxisAlignment,\n       _gap = gap;\n\n  factory LinearLayoutComponent.fromDirection(\n    Direction direction, {\n    CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.start,\n    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,\n    double gap = 0.0,\n    Vector2? position,\n    Vector2? size,\n    Iterable<Component> children = const [],\n    ComponentKey? key,\n  }) {\n    switch (direction) {\n      case Direction.horizontal:\n        return RowComponent(\n          crossAxisAlignment: crossAxisAlignment,\n          mainAxisAlignment: mainAxisAlignment,\n          gap: gap,\n          size: size,\n          position: position,\n          children: children,\n          key: key,\n        );\n      case Direction.vertical:\n        return ColumnComponent(\n          crossAxisAlignment: crossAxisAlignment,\n          mainAxisAlignment: mainAxisAlignment,\n          gap: gap,\n          size: size,\n          position: position,\n          children: children,\n          key: key,\n        );\n    }\n  }\n\n  final Direction direction;\n\n  CrossAxisAlignment _crossAxisAlignment;\n\n  CrossAxisAlignment get crossAxisAlignment {\n    return _crossAxisAlignment;\n  }\n\n  set crossAxisAlignment(CrossAxisAlignment value) {\n    _crossAxisAlignment = value;\n    _layoutCrossAxis();\n  }\n\n  MainAxisAlignment _mainAxisAlignment;\n\n  MainAxisAlignment get mainAxisAlignment {\n    if (isShrinkWrappedIn(direction.mainAxis)) {\n      return MainAxisAlignment.start;\n    }\n    return _mainAxisAlignment;\n  }\n\n  set mainAxisAlignment(MainAxisAlignment value) {\n    _mainAxisAlignment = value;\n    _layoutMainAxis();\n  }\n\n  /// This reflects the value set explicitly and naively, without considering\n  /// the various values of [mainAxisAlignment].\n  double _gap;\n\n  /// The gap between components, that will actually be used for positioning.\n  /// If one or more [ExpandedComponent]s exist among the [children], or if\n  /// [mainAxisAlignment] is not a member of [gapOverridingAlignments], then\n  /// this getter simplifies to [_gap].\n  ///\n  /// Otherwise, returns the value of gap based on the expected behavior of\n  /// [mainAxisAlignment].\n  double get gap {\n    // mainAxisAlignment is not an alignment that can override gaps\n    // OR\n    // There are [ExpandedComponent]s among the children\n    if (!gapOverridingAlignments.contains(mainAxisAlignment) ||\n        children.query<ExpandedComponent>().isNotEmpty) {\n      return _gap;\n    }\n\n    final mainAxisVectorIndex = direction.mainAxis.axisIndex;\n    final availableSpace = size[mainAxisVectorIndex];\n    final unoccupiedSpace = availableSpace - _mainAxisOccupiedSpace;\n    final numberOfGaps = switch (mainAxisAlignment) {\n      MainAxisAlignment.spaceEvenly => children.length + 1,\n      MainAxisAlignment.spaceAround => children.length,\n      MainAxisAlignment.spaceBetween => children.length - 1,\n      _ =>\n        // this should never happen because of\n        // the guard at the start of this method.\n        throw Exception('Unexpected call to _gapOverride'),\n    };\n    return unoccupiedSpace / numberOfGaps;\n  }\n\n  set gap(double newGap) {\n    _gap = newGap;\n    layoutChildren();\n  }\n\n  int get numberOfGaps {\n    return switch (mainAxisAlignment) {\n      MainAxisAlignment.spaceEvenly => children.length + 1,\n      MainAxisAlignment.spaceAround => children.length,\n      MainAxisAlignment.spaceBetween => children.length - 1,\n      _ => children.length - 1,\n    };\n  }\n\n  @override\n  void onChildrenChanged(Component child, ChildrenChangeType type) {\n    if (child is! PositionComponent) {\n      return;\n    }\n\n    void childResizeListener() {\n      onChildResize(child);\n    }\n\n    // A child can be added, and indeed, can be later resized.\n    if (type == ChildrenChangeType.added && child is! ExpandedComponent) {\n      child.size.addListener(childResizeListener);\n    } else {\n      child.size.removeListener(childResizeListener);\n    }\n    childResizeListener();\n  }\n\n  void onChildResize(PositionComponent child) {\n    resetSize();\n    if (child is! LayoutComponent) {\n      layoutChildren();\n      return;\n    }\n    if (child.isShrinkWrappedIn(direction.mainAxis)) {\n      _layoutMainAxis();\n    }\n    if (child.isShrinkWrappedIn(direction.crossAxis)) {\n      _layoutCrossAxis();\n    }\n  }\n\n  @override\n  void onMount() {\n    super.onMount();\n    size.addListener(layoutChildren);\n  }\n\n  @override\n  void onRemove() {\n    size.removeListener(layoutChildren);\n  }\n\n  /// Lays out the children along both main and cross axes.\n  @override\n  void layoutChildren() {\n    _layoutMainAxis();\n    _layoutCrossAxis();\n  }\n\n  void _layoutMainAxis() {\n    final mainAxisVectorIndex = direction.mainAxis.axisIndex;\n    final availableSpace = size[mainAxisVectorIndex];\n    final nonExpandingOccupiedSpace = positionChildren.fold<double>(\n      0.0,\n      (sum, child) => child is ExpandedComponent\n          ? sum\n          : sum + child.size[mainAxisVectorIndex],\n    );\n    final gapSpace = numberOfGaps * gap;\n\n    _mainAxisSizing(\n      components: positionChildren,\n      direction: direction,\n      freeSpace: availableSpace - nonExpandingOccupiedSpace - gapSpace,\n    );\n\n    final unoccupiedSpace = availableSpace - _mainAxisOccupiedSpace;\n    final freeSpace = unoccupiedSpace - gapSpace;\n    // If the accessor `[]` operator is implemented for Offset,\n    // can directly work with Offset rather than Vector2.\n    final initialOffsetVector = Vector2.zero();\n    initialOffsetVector[mainAxisVectorIndex] = switch (mainAxisAlignment) {\n      MainAxisAlignment.spaceEvenly => gap,\n      MainAxisAlignment.spaceAround => gap / 2,\n      MainAxisAlignment.spaceBetween => 0,\n      MainAxisAlignment.start => 0,\n      MainAxisAlignment.end => freeSpace,\n      MainAxisAlignment.center => freeSpace / 2,\n    };\n\n    _mainAxisPositioning(\n      components: positionChildren,\n      gap: gap,\n      direction: direction,\n      initialOffset: initialOffsetVector.toOffset(),\n    );\n  }\n\n  /// Expands any [ExpandedComponent] found among [components] to maximize,\n  /// between themselves, the [freeSpace] available along a [direction].\n  void _mainAxisSizing({\n    required List<PositionComponent> components,\n    required Direction direction,\n    required double freeSpace,\n  }) {\n    final expandedComponents = components.whereType<ExpandedComponent>();\n    // Abort if:\n    // Shrink-wrapped (because meaningless to do any main axis calculation)\n    // OR, There isn't any free space to expand\n    // OR, There are no expanded components to grow\n    if (isShrinkWrappedIn(direction.mainAxis) ||\n        freeSpace <= 0 ||\n        expandedComponents.isEmpty) {\n      return;\n    }\n\n    final spacePerExpandedComponent = freeSpace / expandedComponents.length;\n    for (final expandedComponent in expandedComponents) {\n      final layoutAxisLength = switch (direction.mainAxis) {\n        LayoutAxis.x => expandedComponent.layoutSizeX,\n        LayoutAxis.y => expandedComponent.layoutSizeY,\n      };\n      if (layoutAxisLength != spacePerExpandedComponent) {\n        expandedComponent.setLayoutAxisLength(\n          direction.mainAxis,\n          spacePerExpandedComponent,\n        );\n      }\n    }\n  }\n\n  /// Positions [components] linearly starting from [initialOffset], spaced\n  /// [gap] apart, along a [direction].\n  void _mainAxisPositioning({\n    required List<PositionComponent> components,\n\n    /// The gap between [components]\n    required double gap,\n    required Direction direction,\n\n    /// - Zero when [MainAxisAlignment.start] and\n    ///   [MainAxisAlignment.spaceBetween]\n    /// - [LinearLayoutComponent.size] when [MainAxisAlignment.end]\n    /// - Half of free space when [MainAxisAlignment.center]\n    /// - [LinearLayoutComponent.gap] when [MainAxisAlignment.spaceEvenly]\n    /// - Half of [LinearLayoutComponent.gap] when\n    ///   [MainAxisAlignment.spaceAround]\n    required Offset initialOffset,\n\n    /// true if laying out from the end (bottom/right)\n    bool reverse = false,\n  }) {\n    final mainAxisVectorIndex = direction.mainAxis.axisIndex;\n    final componentList = reverse ? components.reversed : components;\n    for (final (index, component) in componentList.indexed) {\n      final previousChild = index > 0\n          ? componentList.elementAt(index - 1)\n          : null;\n      final reference = previousChild == null\n          // Essentially the same as start, but gap is set.\n          ? initialOffset.toVector2()\n          // The \"end\" at any loop other than the first is the previous\n          // child's top left position minus the gap.\n          : previousChild.topLeftPosition +\n                (reverse\n                    ? -Vector2.all(gap)\n                    : previousChild.size + Vector2.all(gap));\n      final positionOffset = reverse ? component.size : Vector2.zero();\n      final newPosition = Vector2.zero();\n      newPosition[mainAxisVectorIndex] =\n          (reference - positionOffset)[mainAxisVectorIndex];\n      component.topLeftPosition.setFrom(newPosition);\n    }\n  }\n\n  void _layoutCrossAxis() {\n    final crossAxisVectorIndex = direction.crossAxis.axisIndex;\n    final positionChildren = this.positionChildren;\n    final newPosition = Vector2.zero();\n    // There is no need to track index because cross axis positioning is\n    // not influenced by sibling components.\n    for (final component in positionChildren) {\n      newPosition.setFrom(component.topLeftPosition);\n      final crossAxisLength = size[crossAxisVectorIndex];\n      final componentCrossAxisLength = component.size[crossAxisVectorIndex];\n      newPosition[crossAxisVectorIndex] = switch (crossAxisAlignment) {\n        CrossAxisAlignment.start => 0,\n        CrossAxisAlignment.end => crossAxisLength - componentCrossAxisLength,\n        CrossAxisAlignment.center =>\n          (crossAxisLength - componentCrossAxisLength) / 2,\n        CrossAxisAlignment.stretch => 0,\n        CrossAxisAlignment.baseline => 0,\n      };\n      component.topLeftPosition.setFrom(newPosition);\n    }\n    _crossAxisSizing(\n      components: positionChildren,\n      crossAxisAlignment: crossAxisAlignment,\n      direction: direction,\n      crossAxisLength: size[crossAxisVectorIndex],\n    );\n  }\n\n  void _crossAxisSizing({\n    required List<PositionComponent> components,\n    required CrossAxisAlignment crossAxisAlignment,\n    required Direction direction,\n    required double crossAxisLength,\n  }) {\n    final crossAxisVectorIndex = direction.crossAxis.axisIndex;\n    for (final component in components) {\n      if (crossAxisAlignment != CrossAxisAlignment.stretch) {\n        continue;\n      }\n      if (component is LayoutComponent) {\n        final layoutAxisLength = switch (direction.crossAxis) {\n          LayoutAxis.x => component.layoutSizeX,\n          LayoutAxis.y => component.layoutSizeY,\n        };\n        // Don't set value if the value is already correct.\n        if (layoutAxisLength != crossAxisLength) {\n          component.setLayoutAxisLength(\n            direction.crossAxis,\n            crossAxisLength,\n          );\n        }\n      } else {\n        // Don't set value if the value is already correct.\n        if (component.size[crossAxisVectorIndex] != crossAxisLength) {\n          component.size[crossAxisVectorIndex] = crossAxisLength;\n        }\n\n        if (direction == Direction.vertical &&\n            component is TextBoxComponent &&\n            component.boxConfig.maxWidth != crossAxisLength) {\n          final originalBoxConfig = component.boxConfig;\n          component.boxConfig = originalBoxConfig.copyWith(\n            maxWidth: crossAxisLength,\n          );\n        }\n      }\n    }\n  }\n\n  /// The total space along the main axis occupied by the [positionChildren]\n  /// without the [gap]s. This is so named because we expect to\n  /// implement crossAxisOccupiedSpace for shrink wrapping.\n  double get _mainAxisOccupiedSpace {\n    return positionChildren.fold(0.0, (sum, child) {\n      // Because ExpandedComponent size can be their expanded state\n      // and thus the occupied space will be inflated.\n      final mainAxisLength = child is ExpandedComponent\n          ? child.intrinsicSize[direction.mainAxis.axisIndex]\n          : child.size[direction.mainAxis.axisIndex];\n      return sum + mainAxisLength;\n    });\n  }\n\n  /// Any positioning done in [layoutChildren] should not affect the\n  /// [intrinsicSize]. This is because all [crossAxisAlignment] transformations\n  /// fall within the largestCrossAxisLength, while [mainAxisAlignment] is\n  /// entirely ignored in all cases where [intrinsicSize] is needed.\n  /// This means that [intrinsicSize] should be used either *before* or at the\n  /// start of [layoutChildren].\n  @override\n  Vector2 get intrinsicSize {\n    final positionChildren = this.positionChildren;\n    final crossAxisVectorIndex = direction.crossAxis.axisIndex;\n    final mainAxisVectorIndex = direction.mainAxis.axisIndex;\n    if (positionChildren.isEmpty) {\n      return Vector2.zero();\n    }\n    final largestCrossAxisLength = positionChildren.fold(0.0, (largest, child) {\n      final crossAxisLength = child is ExpandedComponent\n          ? child.intrinsicSize[crossAxisVectorIndex]\n          : child.size[crossAxisVectorIndex];\n      return max(largest, crossAxisLength);\n    });\n    // This is tricky because it depends on the mainAxisAlignment.\n    // This should only apply when mainAxisAlignment is start, center, or end.\n    // spaceAround, spaceBetween, and spaceEvenly requires the size as a\n    // constraint.\n    final cumulativeMainAxisLength =\n        ((positionChildren.length - 1) * gap) +\n        positionChildren.fold(0.0, (sum, child) {\n          final mainAxisLength = child is ExpandedComponent\n              ? child.intrinsicSize[mainAxisVectorIndex]\n              : child.size[mainAxisVectorIndex];\n          return sum + mainAxisLength;\n        });\n    final out = Vector2.zero();\n    out[mainAxisVectorIndex] = cumulativeMainAxisLength;\n    out[crossAxisVectorIndex] = largestCrossAxisLength;\n    return out;\n  }\n\n  static const gapOverridingAlignments = {\n    MainAxisAlignment.spaceEvenly,\n    MainAxisAlignment.spaceAround,\n    MainAxisAlignment.spaceBetween,\n  };\n}\n"
  },
  {
    "path": "packages/flame/lib/src/experimental/padding_component.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flutter/rendering.dart';\n\n/// A padding component akin to Flutter's Padding widget.\n/// Use [padding] as you would Flutter's counterpart.\n/// While this component is designed to shrink or expand to its child's\n/// dimensions, it is fine to set its size explicitly. The child will simply be\n/// offset by the padding dimensions.\n///\n/// Set the child of this component with [child]. Avoid using [add] directly on\n/// an instance of [PaddingComponent] because its behavior is undefined with\n/// multiple children. It is designed only for one child.\n///\n/// You may set [padding] as well as the [child] after the fact, and it will\n/// cause the layout to refresh.\n///\n/// If [inflateChild] is true, [resetSize] sets the child's size to fill up\n/// available space via [syncChildSize]. If the child is a [LayoutComponent]\n/// descendant, then [resetSize] uses the [LayoutComponent.setLayoutSize].\n///\n/// Example usage:\n/// ```dart\n/// PaddingComponent(\n///   padding: EdgeInsets.all(10),\n///   child: TextComponent(text: 'bar')\n/// );\n/// ```\nclass PaddingComponent extends SingleLayoutComponent {\n  PaddingComponent({\n    super.key,\n    EdgeInsets? padding,\n    super.anchor,\n    super.position,\n    super.priority,\n    super.size,\n    super.inflateChild = false,\n    PositionComponent? child,\n  }) : _padding = padding ?? EdgeInsets.zero,\n       super(child: null) {\n    this.child = child;\n  }\n\n  EdgeInsets _padding;\n\n  EdgeInsets get padding => _padding;\n\n  set padding(EdgeInsets value) {\n    _padding = value;\n    layoutChildren();\n  }\n\n  @override\n  void layoutChildren() {\n    resetSize();\n    final child = this.child;\n    if (child == null) {\n      return;\n    }\n    // Regardless of shrinkwrap or size, top left padding is set.\n    child.topLeftPosition.setFrom(padding.topLeft.toVector2());\n  }\n\n  @override\n  Vector2 get availableSize {\n    return padding.deflateSize(size.toSize()).toVector2();\n  }\n\n  @override\n  Vector2 get intrinsicSize {\n    final childWidth = child?.size.x ?? 0;\n    final childHeight = child?.size.y ?? 0;\n    return Vector2(\n      childWidth + padding.horizontal,\n      childHeight + padding.vertical,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/experimental/raycast_result.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/src/geometry/ray2.dart';\n\n/// The result of a raycasting operation.\n///\n/// Note that the members of this class is heavily re-used. If you want to\n/// keep the result in an object, clone the parts you want, or the whole\n/// [RaycastResult] with [clone].\n///\n/// NOTE: This class might be subject to breaking changes in an upcoming\n/// version, to make it possible to calculate the values lazily.\nclass RaycastResult<T extends Hitbox<T>> {\n  RaycastResult({\n    T? hitbox,\n    Ray2? reflectionRay,\n    Vector2? normal,\n    double? distance,\n    bool isInsideHitbox = false,\n  }) : _isInsideHitbox = isInsideHitbox,\n       _hitbox = hitbox,\n       _reflectionRay = reflectionRay ?? Ray2.zero(),\n       _normal = normal ?? Vector2.zero(),\n       _distance = distance ?? double.maxFinite;\n\n  /// Whether this result has active results in it.\n  ///\n  /// This is used so that the objects in there can continue to live even when\n  /// there is no result from a ray cast.\n  bool get isActive => _hitbox != null;\n\n  /// Whether the origin of the ray was inside the hitbox.\n  bool get isInsideHitbox => _isInsideHitbox;\n  bool _isInsideHitbox;\n\n  T? _hitbox;\n  T? get hitbox => isActive ? _hitbox : null;\n\n  final Ray2 _reflectionRay;\n  Ray2? get reflectionRay => isActive ? _reflectionRay : null;\n\n  Vector2? get intersectionPoint => reflectionRay?.origin;\n\n  double _distance;\n  double? get distance => isActive ? _distance : null;\n\n  final Vector2 _normal;\n  Vector2? get normal => isActive ? _normal : null;\n\n  void reset() => _hitbox = null;\n\n  /// Sets this [RaycastResult]'s objects to the values stored in [other].\n  void setFrom(RaycastResult<T> other) {\n    setWith(\n      hitbox: other.hitbox,\n      reflectionRay: other.reflectionRay,\n      normal: other.normal,\n      distance: other.distance,\n      isInsideHitbox: other.isInsideHitbox,\n    );\n  }\n\n  /// Sets the values of the result from the specified arguments.\n  void setWith({\n    T? hitbox,\n    Ray2? reflectionRay,\n    Vector2? normal,\n    double? distance,\n    bool isInsideHitbox = false,\n  }) {\n    _hitbox = hitbox;\n    if (reflectionRay != null) {\n      _reflectionRay.setFrom(reflectionRay);\n    }\n    if (normal != null) {\n      _normal.setFrom(normal);\n    }\n    _distance = distance ?? double.maxFinite;\n    _isInsideHitbox = isInsideHitbox;\n  }\n\n  RaycastResult<T> clone() {\n    return RaycastResult(\n      hitbox: hitbox,\n      reflectionRay: _reflectionRay.clone(),\n      normal: _normal.clone(),\n      distance: distance,\n      isInsideHitbox: isInsideHitbox,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/experimental/row_component.dart",
    "content": "import 'package:flame/src/experimental/linear_layout_component.dart';\nimport 'package:flutter/rendering.dart';\n\n/// Warning: Experimental. API and behavior may change.\n///\n/// RowComponent is a layout component that arranges its children in a\n/// horizontal row.\n///\n/// The [children] are laid out along the horizontal axis, with spacing between\n/// them defined by the [gap] parameter.\n/// The alignment of the children along the horizontal axis is controlled by\n/// [mainAxisAlignment], while their alignment along the vertical axis is\n/// controlled by [crossAxisAlignment].\n///\n/// If [size] is non-null, behaves as normal explicit sizing.\n/// If [size] is null, sets the size to the minimum size that containing all\n/// the children. This is similar to setting the [size] to [intrinsicSize], but\n/// distinct in that sizing will respond to changes in children, other\n/// properties, etc...\n///\n/// Example usage:\n/// ```dart\n/// RowComponent(\n///   gap: 10.0,\n///   mainAxisAlignment: MainAxisAlignment.center,\n///   crossAxisAlignment: CrossAxisAlignment.start,\n///   children: [\n///     TextComponent('Child 1'),\n///     TextComponent('Child 2'),\n///     TextComponent('Child 3'),\n///   ],\n/// );\n/// ```\nclass RowComponent extends LinearLayoutComponent {\n  RowComponent({\n    super.key,\n    super.mainAxisAlignment = MainAxisAlignment.start,\n    super.crossAxisAlignment = CrossAxisAlignment.start,\n    super.gap = 0.0,\n    super.size,\n    super.position,\n    super.anchor,\n    super.priority,\n    super.children,\n  }) : super(direction: Direction.horizontal);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/experimental/single_layout_component.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/experimental.dart';\n\n/// A common abstract class for [LayoutComponent]s that are designed to work\n/// with only a single [child]. This includes components like\n/// [ExpandedComponent] and [PaddingComponent], and can possibly be used to\n/// refactor AlignComponent.\n///\n/// [inflateChild] is simply a flag that signals whether the underlying layout\n/// machinery should alter its child's size. It's up to this class's subclasses\n/// to make use of this flag.\n///\n/// Setting [child] automatically manages removing the old child from this\n/// component, as well as adding the new child to this component.\nabstract class SingleLayoutComponent extends LayoutComponent {\n  SingleLayoutComponent({\n    required super.key,\n    required super.position,\n    required super.anchor,\n    required super.priority,\n    required super.size,\n    required this.inflateChild,\n    required PositionComponent? child,\n  }) {\n    this.child = child;\n  }\n\n  final bool inflateChild;\n\n  PositionComponent? _child;\n\n  /// The component that will be positioned by this component. The [child] will\n  /// be automatically mounted to the current component.\n  PositionComponent? get child => _child;\n\n  set child(PositionComponent? value) {\n    final oldChild = _child;\n    if (oldChild?.parent == this) {\n      oldChild?.removeFromParent();\n    }\n    _child = value;\n    if (value != null) {\n      add(value);\n    }\n  }\n\n  void syncChildSize() {\n    if (!inflateChild) {\n      return;\n    }\n    final child = this.child;\n    if (child == null) {\n      return;\n    }\n    if (child.size == availableSize) {\n      return;\n    }\n    if (child is LayoutComponent) {\n      child.setLayoutSize(availableSize.x, availableSize.y);\n    } else {\n      child.size = availableSize;\n    }\n  }\n\n  @override\n  void resetSize() {\n    super.resetSize();\n    syncChildSize();\n  }\n\n  Vector2 get availableSize => size;\n\n  @override\n  Vector2 get intrinsicSize => child?.size ?? Vector2.zero();\n}\n"
  },
  {
    "path": "packages/flame/lib/src/extensions/aabb.dart",
    "content": "import 'package:flame/extensions.dart';\n\nextension Aabb2Extension on Aabb2 {\n  /// Creates a [Rect] starting in [min] and going the [max]\n  Rect toRect() => Rect.fromLTRB(min.x, min.y, max.x, max.y);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/extensions/canvas.dart",
    "content": "import 'dart:typed_data' show Float32List;\nimport 'dart:ui';\n\nimport 'package:flame/palette.dart';\nimport 'package:flame/src/cache/matrix_pool.dart' show canvasTransform;\nimport 'package:flame/src/extensions/vector2.dart';\nimport 'package:flame/src/game/transform2d.dart';\n\nexport 'dart:ui' show Canvas;\n\nextension CanvasExtension on Canvas {\n  void scaleVector(Vector2 vector) {\n    scale(vector.x, vector.y);\n  }\n\n  void translateVector(Vector2 vector) {\n    translate(vector.x, vector.y);\n  }\n\n  /// Renders a point as a square of size [size] (default 1 logical pixel) using\n  /// the provided [paint] (default solid magenta).\n  ///\n  /// This is mostly useful for debugging.\n  void renderPoint(\n    Vector2 point, {\n    double size = 1.0,\n    Paint? paint,\n  }) {\n    final rect = (point - Vector2.all(size / 2)) & Vector2.all(size);\n    drawRect(rect, paint ?? BasicPalette.magenta.paint());\n  }\n\n  /// Renders a line between [p1] and [p2] using the provided [paint].\n  ///\n  /// Equivalent to [drawLine] but using [Vector2] instead of [Offset].\n  void renderLine(Vector2 p1, Vector2 p2, Paint paint) {\n    drawLine(p1.toOffset(), p2.toOffset(), paint);\n  }\n\n  /// Utility method to render stuff on a specific place in an isolated way.\n  ///\n  /// Some render methods don't allow to pass a vector.\n  /// This method translate the canvas before rendering your fn.\n  /// The changes are reset after the fn is run.\n  void renderAt(Vector2 p, void Function(Canvas) fn) {\n    save();\n    translateVector(p);\n    fn(this);\n    restore();\n  }\n\n  /// Utility method to render stuff rotated at specific angle.\n  ///\n  /// It rotates the canvas around the center of rotation.\n  /// The changes are reset after the fn is run.\n  void renderRotated(\n    double angle,\n    Vector2 rotationCenter,\n    void Function(Canvas) fn,\n  ) {\n    save();\n    translateVector(rotationCenter);\n    rotate(angle);\n    translateVector(-rotationCenter);\n    fn(this);\n    restore();\n  }\n\n  /// Use the [Transform2D] object to [transform] the canvas.\n  void transform2D(Transform2D transform2D) {\n    transform32(transform2D.transformMatrix.storage);\n  }\n\n  void transform32(Float32List matrix4) {\n    canvasTransform(this, matrix4);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/extensions/color.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flutter/painting.dart' show HSLColor;\n\nexport 'dart:ui' show Color;\n\nextension ColorExtension on Color {\n  /// Darken the shade of the color by the [amount].\n  ///\n  /// [amount] is a double between 0 and 1.\n  ///\n  /// Based on: https://stackoverflow.com/a/60191441.\n  Color darken(double amount) {\n    assert(amount >= 0 && amount <= 1);\n\n    final hsl = HSLColor.fromColor(this);\n    return hsl\n        .withLightness(\n          clampDouble(hsl.lightness * (1 - amount), 0.0, 1.0),\n        )\n        .toColor();\n  }\n\n  /// Brighten the shade of the color by the [amount].\n  ///\n  /// [amount] is a double between 0 and 1.\n  ///\n  /// Based on: https://stackoverflow.com/a/60191441.\n  Color brighten(double amount) {\n    assert(amount >= 0 && amount <= 1);\n\n    final hsl = HSLColor.fromColor(this);\n    return hsl\n        .withLightness(\n          clampDouble(hsl.lightness + (1 - hsl.lightness) * amount, 0.0, 1.0),\n        )\n        .toColor();\n  }\n\n  // used as an example hex color code on the documentation below\n  // cSpell:ignore fccc\n\n  /// Parses an RGB color from a valid hex string (e.g. #1C1C1C).\n  ///\n  /// The `#` is optional.\n  /// The short-hand syntax is support, e.g.: #CCC.\n  /// Lower-case letters are supported.\n  ///\n  /// Examples of valid inputs:\n  /// ccc, CCC, #ccc, #CCC, #c1c1c1, #C1C1C1, c1c1c1, C1C1C1\n  ///\n  /// If the string is not valid, an error is thrown.\n  ///\n  /// Note: if you are hardcoding colors, use Dart's built-in hexadecimal\n  /// literals instead.\n  static Color fromRGBHexString(String hexString) {\n    if (hexString.length == 3 || hexString.length == 4) {\n      return _parseRegex(1, 3, hexString);\n    } else if (hexString.length == 6 || hexString.length == 7) {\n      return _parseRegex(2, 3, hexString);\n    } else {\n      throw 'Invalid format for RGB hex string: $hexString';\n    }\n  }\n\n  /// Parses an ARGB color from a valid hex string (e.g. #1C1C1C).\n  ///\n  /// The `#` is optional.\n  /// The short-hand syntax is support, e.g.: #CCCC.\n  /// Lower-case letters are supported.\n  ///\n  /// Examples of valid inputs:\n  /// fccc, FCCC, #fccc, #FCCC, #ffc1c1c1, #FFC1C1C1, ffc1c1c1, FFC1C1C1\n  ///\n  /// If the string is not valid, an error is thrown.\n  ///\n  /// Note: if you are hardcoding colors, use Dart's built-in hexadecimal\n  /// literals instead.\n  static Color fromARGBHexString(String hexString) {\n    if (hexString.length == 4 || hexString.length == 5) {\n      return _parseRegex(1, 4, hexString);\n    } else if (hexString.length == 8 || hexString.length == 9) {\n      return _parseRegex(2, 4, hexString);\n    } else {\n      throw 'Invalid format for ARGB hex string: $hexString';\n    }\n  }\n\n  static Color _parseRegex(int size, int blocks, String target) {\n    final groups = [for (var i = 1; i <= blocks; i++) i];\n    final regexBlocks = groups.map((_) => '([0-9a-fA-F]{$size})').join();\n    final regex = RegExp('^\\\\#?$regexBlocks\\$');\n    final matcher = regex.firstMatch(target)!;\n    final extracted = matcher\n        .groups(groups)\n        .map((e) => e!)\n        .map((e) => size == 1 ? '$e$e' : e)\n        .map((e) => int.parse(e, radix: 16))\n        .toList();\n    final components = [if (blocks == 3) 255, ...extracted];\n    return Color.fromARGB(\n      components[0],\n      components[1],\n      components[2],\n      components[3],\n    );\n  }\n\n  /// Generates a random [Color] with the set [withAlpha] or the default (1.0).\n  /// You can pass in a random number generator [rng], if omitted the function\n  /// will create a new [Random] object without a seed and use that.\n  /// [base] can be used to get the random colors in only a lighter spectrum, it\n  /// should be between 0 and 256.\n  static Color random({\n    double withAlpha = 1.0,\n    int base = 0,\n    Random? rng,\n  }) {\n    assert(\n      base >= 0 && base <= 256,\n      'The base argument should be in the range 0..256',\n    );\n    rng ??= Random();\n    return Color.fromRGBO(\n      base + (base >= 255 ? 0 : rng.nextInt(256 - base)),\n      base + (base >= 255 ? 0 : rng.nextInt(256 - base)),\n      base + (base >= 255 ? 0 : rng.nextInt(256 - base)),\n      withAlpha,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/extensions/double.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/geometry.dart';\n\nextension DoubleExtension on double {\n  /// Converts +-[infinity] to +-[maxFinite].\n  /// If it is already a finite value, that is returned.\n  double toFinite() {\n    if (this == double.infinity) {\n      return double.maxFinite;\n    } else if (this == -double.infinity) {\n      return -double.maxFinite;\n    } else {\n      return this;\n    }\n  }\n\n  /// This method normalizes the angle in radians to the range [-π, π].\n  ///\n  /// If the angle is already within this range, it is returned unchanged.\n  double toNormalizedAngle() {\n    if (this >= -pi && this <= pi) {\n      return this;\n    }\n    final normalized = this % tau;\n\n    return normalized > pi ? normalized - tau : normalized;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/extensions/fragment_shader.dart",
    "content": "import 'dart:ui' as ui;\n\nimport 'package:flutter/animation.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// This code was originally from the flutter_shaders package.\n/// https://pub.dev/packages/flutter_shaders\n\n/// A helper extension on [ui.FragmentShader] that allows you to set uniforms\n/// in a more convenient way. Without having to manage indices.\n///\n/// Example:\n/// ```dart\n/// shader.setFloatUniforms((setter) {\n///  setter.setFloat(1.0);\n///  setter.setFloats([1.0, 2.0, 3.0]);\n///  setter.setSize(const Size(1.0, 2.0));\n///  setter.setSizes([const Size(1.0, 2.0), const Size(3.0, 4.0)]);\n///  setter.setOffset(const Offset(1.0, 2.0));\n///  setter.setOffsets([const Offset(1.0, 2.0), const Offset(3.0, 4.0)]);\n///  setter.setMatrix(Matrix4.identity());\n///  setter.setMatrices([Matrix4.identity(), Matrix4.identity()]);\n///  setter.setColor(Colors.red);\n///  setter.setColors([Colors.red, Colors.green]);\n/// });\n/// ```\n///\n/// The receiving end of this script should be:\n/// ```glsl\n/// uniform float u0; // 1.0\n/// uniform float[3] uFloats; // float[3](1.0, 2.0, 3.0)\n/// uniform vec2 size; // vec2(1.0, 2.0)\n/// uniform vec2[2] sizes; // vec2[2](vec2(1.0, 2.0), vec2(3.0, 4.0))\n/// uniform vec2 offset; // vec2(1.0, 2.0)\n/// uniform vec2[2] offsets; // vec2[2](vec2(1.0, 2.0), vec2(3.0, 4.0))\n/// uniform mat4 matrix; // mat4(1.0)\n/// uniform mat4[2] matrices; // mat4[2](mat4(1.0), mat4(1.0))\n/// uniform vec4 color; // vec4(1.0, 0.0, 0.0, 1.0)\n/// uniform vec4[2] colors; // vec4[2](vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 1.0, 0.0, 1.0))\n/// ```\nextension FragmentShaderExtension on ui.FragmentShader {\n  /// Sets the uniforms of the shader using the provided [callback].\n  ///\n  /// The [initialIndex] parameter allows you to set the index of the first\n  /// uniform. Defaults to 0.\n  ///\n  /// Returns the index of the last uniform that was set.\n  int setFloatUniforms(\n    ValueSetter<UniformsSetter> callback, {\n    int initialIndex = 0,\n  }) {\n    final setter = UniformsSetter(this, initialIndex);\n    callback(setter);\n    return setter._index;\n  }\n}\n\n/// A helper class that allows you to set uniforms in a more convenient way.\nclass UniformsSetter {\n  UniformsSetter(this.shader, this._index);\n\n  int _index;\n  final ui.FragmentShader shader;\n\n  void setFloat(double value) {\n    shader.setFloat(_index++, value);\n  }\n\n  void setFloats(List<double> values) {\n    for (final value in values) {\n      setFloat(value);\n    }\n  }\n\n  void setSize(Size size) {\n    shader\n      ..setFloat(_index++, size.width)\n      ..setFloat(_index++, size.height);\n  }\n\n  void setSizes(List<Size> sizes) {\n    for (final size in sizes) {\n      setSize(size);\n    }\n  }\n\n  void setColor(Color color, {bool premultiply = false}) {\n    final double multiplier;\n    if (premultiply) {\n      multiplier = color.a;\n    } else {\n      multiplier = 1.0;\n    }\n\n    setFloat(color.r / 255 * multiplier);\n    setFloat(color.g / 255 * multiplier);\n    setFloat(color.b / 255 * multiplier);\n    setFloat(color.a);\n  }\n\n  void setColors(List<Color> colors, {bool premultiply = false}) {\n    for (final color in colors) {\n      setColor(color, premultiply: premultiply);\n    }\n  }\n\n  void setOffset(Offset offset) {\n    shader\n      ..setFloat(_index++, offset.dx)\n      ..setFloat(_index++, offset.dy);\n  }\n\n  void setOffsets(List<Offset> offsets) {\n    for (final offset in offsets) {\n      setOffset(offset);\n    }\n  }\n\n  void setVector(Vector vector) {\n    setFloats(vector.storage);\n  }\n\n  void setVectors(List<Vector> vectors) {\n    for (final vector in vectors) {\n      setVector(vector);\n    }\n  }\n\n  void setMatrix2(Matrix2 matrix2) {\n    setFloats(matrix2.storage);\n  }\n\n  void setMatrix2s(List<Matrix2> matrix2s) {\n    for (final matrix2 in matrix2s) {\n      setMatrix2(matrix2);\n    }\n  }\n\n  void setMatrix3(Matrix3 matrix3) {\n    setFloats(matrix3.storage);\n  }\n\n  void setMatrix3s(List<Matrix3> matrix3s) {\n    for (final matrix3 in matrix3s) {\n      setMatrix3(matrix3);\n    }\n  }\n\n  void setMatrix4(Matrix4 matrix4) {\n    setFloats(matrix4.storage);\n  }\n\n  void setMatrix4s(List<Matrix4> matrix4s) {\n    for (final matrix4 in matrix4s) {\n      setMatrix4(matrix4);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/extensions/image.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\nimport 'dart:ui';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame/palette.dart';\n\nexport 'dart:ui' show Image;\n\nextension ImageExtension on Image {\n  static final Paint _whitePaint = BasicPalette.white.paint();\n\n  /// Converts a raw list of pixel values into an [Image] object.\n  ///\n  /// The pixels must be in the RGBA format, i.e. first 4 bytes encode the red,\n  /// green, blue, and alpha components of the first pixel, next 4 bytes encode\n  /// the next pixel, and so on. The pixels are in the row-major order, meaning\n  /// that first [width] pixels encode the first row of the image, next [width]\n  /// pixels the second row, and so on.\n  static Future<Image> fromPixels(Uint8List pixels, int width, int height) {\n    assert(pixels.length == width * height * 4);\n    final completer = Completer<Image>();\n    decodeImageFromPixels(\n      pixels,\n      width,\n      height,\n      PixelFormat.rgba8888,\n      completer.complete,\n    );\n    return completer.future;\n  }\n\n  /// Helper method to retrieve the pixel data of the image as a [Uint8List].\n  ///\n  /// Pixel order used the [ImageByteFormat.rawRgba] meaning it is: R G B A.\n  Future<Uint8List> pixelsInUint8() async {\n    return (await toByteData())!.buffer.asUint8List();\n  }\n\n  Future<Image> transformPixels(\n    Color Function(Color) transform, {\n    bool reversePremultipliedAlpha = true,\n  }) async {\n    final pixelData = await pixelsInUint8();\n    final newPixelData = Uint8List(pixelData.length);\n\n    for (var i = 0; i < pixelData.length; i += 4) {\n      final r = pixelData[i + 0] / 255;\n      final g = pixelData[i + 1] / 255;\n      final b = pixelData[i + 2] / 255;\n      final a = pixelData[i + 3] / 255;\n\n      final d = a == 0 || !reversePremultipliedAlpha ? 1 : a;\n\n      // Reverse the pre-multiplied alpha.\n      final color = Color.from(\n        alpha: a,\n        red: r / d,\n        green: g / d,\n        blue: b / d,\n      );\n\n      final newColor = a == 0 ? color : transform(color);\n\n      final newR = newColor.r;\n      final newG = newColor.g;\n      final newB = newColor.b;\n\n      // Pre-multiply the alpha back into the new color.\n      newPixelData[i + 0] = (newR * d * 255).round();\n      newPixelData[i + 1] = (newG * d * 255).round();\n      newPixelData[i + 2] = (newB * d * 255).round();\n      newPixelData[i + 3] = pixelData[i + 3];\n    }\n\n    return fromPixels(newPixelData, width, height);\n  }\n\n  /// Returns the bounding [Rect] of the image.\n  Rect getBoundingRect() => Vector2.zero() & size;\n\n  /// Returns a [Vector2] representing the dimensions of this image.\n  Vector2 get size => Vector2Extension.fromInts(width, height);\n\n  /// Change each pixel's color to be darker and return a new [Image].\n  ///\n  /// The [amount] is a double value between 0 and 1.\n  Future<Image> darken(\n    double amount, {\n    bool reversePremultipliedAlpha = true,\n  }) async {\n    assert(amount >= 0 && amount <= 1);\n\n    return transformPixels(\n      (color) => color.darken(amount),\n      reversePremultipliedAlpha: reversePremultipliedAlpha,\n    );\n  }\n\n  /// Change each pixel's color to be brighter and return a new [Image].\n  ///\n  /// The [amount] is a double value between 0 and 1.\n  Future<Image> brighten(\n    double amount, {\n    bool reversePremultipliedAlpha = false,\n  }) async {\n    assert(amount >= 0 && amount <= 1);\n\n    return transformPixels(\n      (color) => color.brighten(amount),\n      reversePremultipliedAlpha: reversePremultipliedAlpha,\n    );\n  }\n\n  /// Resizes this image to the given [newSize].\n  ///\n  /// Keep in mind that is considered an expensive operation and should be\n  /// avoided in the game loop methods. Prefer using it\n  /// in the loading phase of the game or components.\n  Future<Image> resize(Vector2 newSize) async {\n    final recorder = PictureRecorder();\n    Canvas(recorder).drawImageRect(\n      this,\n      getBoundingRect(),\n      newSize.toRect(),\n      _whitePaint,\n    );\n    final picture = recorder.endRecording();\n    final resizedImage = await picture.toImageSafe(\n      newSize.x.toInt(),\n      newSize.y.toInt(),\n    );\n    return resizedImage;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/extensions/list.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/math.dart';\n\nextension ListExtension<E> on List<E> {\n  /// Reverses the list in-place.\n  void reverse() {\n    for (var i = 0, j = length - 1; i < j; i++, j--) {\n      final temp = this[i];\n      this[i] = this[j];\n      this[j] = temp;\n    }\n  }\n\n  /// Returns a random element from the list.\n  E random([Random? random]) {\n    assert(isNotEmpty, \"Can't get a random element from an empty list\");\n    final randomGenerator = random ?? randomFallback;\n    return this[randomGenerator.nextInt(length)];\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/extensions/matrix4.dart",
    "content": "import 'package:vector_math/vector_math.dart';\n\nexport 'package:vector_math/vector_math.dart' hide Colors;\n\nextension Matrix4Extension on Matrix4 {\n  /// A first row and first column value.\n  double get m11 => storage[0];\n\n  /// A first row and second column value.\n  double get m12 => storage[1];\n\n  /// A first row and third column value.\n  double get m13 => storage[2];\n\n  /// A first row and fourth column value.\n  double get m14 => storage[3];\n\n  /// A second row and first column value.\n  double get m21 => storage[4];\n\n  /// A second row and second column value.\n  double get m22 => storage[5];\n\n  /// A second row and third column value.\n  double get m23 => storage[6];\n\n  /// A second row and fourth column value.\n  double get m24 => storage[7];\n\n  /// A third row and first column value.\n  double get m31 => storage[8];\n\n  /// A third row and second column value.\n  double get m32 => storage[9];\n\n  /// A third row and third column value.\n  double get m33 => storage[10];\n\n  /// A third row and fourth column value.\n  double get m34 => storage[11];\n\n  /// A fourth row and first column value.\n  double get m41 => storage[12];\n\n  /// A fourth row and second column value.\n  double get m42 => storage[13];\n\n  /// A fourth row and third column value.\n  double get m43 => storage[14];\n\n  /// A fourth row and fourth column value.\n  double get m44 => storage[15];\n\n  /// Translate this matrix by a [Vector2].\n  @Deprecated(\n    'Use translateByDouble or translateByVector2 instead. '\n    'This will be removed in a Flame 1.32.0.',\n  )\n  void translate2(Vector2 vector) {\n    return translateByDouble(vector.x, vector.y, 0.0, 1.0);\n  }\n\n  /// Transform [position] of type [Vector2] using the transformation defined by\n  /// this.\n  Vector2 transform2(Vector2 position) {\n    return Vector2(\n      (position.x * m11) + (position.y * m21) + m41,\n      (position.x * m12) + (position.y * m22) + m42,\n    );\n  }\n\n  /// Transform a copy of [position] of type [Vector2] using the transformation\n  /// defined by this. If a [out] parameter is supplied, the copy is stored in\n  /// [out].\n  Vector2 transformed2(Vector2 position, [Vector2? out]) {\n    if (out == null) {\n      // ignore: parameter_assignments\n      out = Vector2.copy(position);\n    } else {\n      out.setFrom(position);\n    }\n    return transform2(out);\n  }\n\n  // TODO(spydon): Remove once min version is 3.35.0\n  void translateByDouble(double tx, double ty, double tz, double tw) {\n    final t1 =\n        storage[0] * tx + storage[4] * ty + storage[8] * tz + storage[12] * tw;\n    storage[12] = t1;\n\n    final t2 =\n        storage[1] * tx + storage[5] * ty + storage[9] * tz + storage[13] * tw;\n    storage[13] = t2;\n\n    final t3 =\n        storage[2] * tx + storage[6] * ty + storage[10] * tz + storage[14] * tw;\n    storage[14] = t3;\n\n    final t4 =\n        storage[3] * tx + storage[7] * ty + storage[11] * tz + storage[15] * tw;\n    storage[15] = t4;\n  }\n\n  // TODO(spydon): Remove once min version is 3.35.0\n  void scaleByDouble(double sx, double sy, double sz, double sw) {\n    storage[0] *= sx;\n    storage[1] *= sx;\n    storage[2] *= sx;\n    storage[3] *= sx;\n    storage[4] *= sy;\n    storage[5] *= sy;\n    storage[6] *= sy;\n    storage[7] *= sy;\n    storage[8] *= sz;\n    storage[9] *= sz;\n    storage[10] *= sz;\n    storage[11] *= sz;\n    storage[12] *= sw;\n    storage[13] *= sw;\n    storage[14] *= sw;\n    storage[15] *= sw;\n  }\n\n  // TODO(spydon): Remove once min version is 3.35.0\n  void scaleByVector3(Vector3 vector) {\n    scaleByDouble(\n      vector.x,\n      vector.y,\n      vector.z,\n      1.0,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/extensions/offset.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/src/extensions/vector2.dart';\n\nexport 'dart:ui' show Offset;\n\nextension OffsetExtension on Offset {\n  /// Creates an [Vector2] from the [Offset]\n  Vector2 toVector2() => Vector2(dx, dy);\n\n  /// Creates a [Size] from the [Offset]\n  Size toSize() => Size(dx, dy);\n\n  /// Creates a [Point] from the [Offset]\n  Point toPoint() => Point(dx, dy);\n\n  /// Creates a [Rect] starting in origin and going the [Offset]\n  Rect toRect() => Rect.fromLTWH(0, 0, dx, dy);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/extensions/paint.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/src/extensions/color.dart';\n\nexport 'dart:ui' show Color;\n\nextension PaintExtension on Paint {\n  /// Darken the shade of the [Color] in the [Paint] object by the [amount].\n  ///\n  /// [amount] is a double between 0 and 1.\n  ///\n  /// Based on: https://stackoverflow.com/a/60191441.\n  void darken(double amount) {\n    color = color.darken(amount);\n  }\n\n  /// Brighten the shade of the [Color] in the [Paint] object by the [amount].\n  ///\n  /// [amount] is a double between 0 and 1.\n  ///\n  /// Based on: https://stackoverflow.com/a/60191441.\n  void brighten(double amount) {\n    color = color.brighten(amount);\n  }\n\n  /// Parses an RGB color from a valid hex string (e.g. #1C1C1C).\n  ///\n  /// The `#` is optional.\n  /// The short-hand syntax is support, e.g.: #CCC.\n  /// Lower-case letters are supported.\n  ///\n  /// Examples of valid inputs:\n  /// ccc, CCC, #ccc, #CCC, #c1c1c1, #C1C1C1, c1c1c1, C1C1C1\n  ///\n  /// If the string is not valid, an error is thrown.\n  ///\n  /// Note: if you are hardcoding colors, use Dart's built-in hexadecimal\n  /// literals instead.\n  static Paint fromRGBHexString(String hexString) {\n    final color = ColorExtension.fromRGBHexString(hexString);\n    return Paint()..color = color;\n  }\n\n  // used as an example hex color code on the documentation below\n  // cSpell:ignore fccc\n\n  /// Parses an ARGB color from a valid hex string (e.g. #1C1C1C).\n  ///\n  /// The `#` is optional.\n  /// The short-hand syntax is support, e.g.: #CCCC.\n  /// Lower-case letters are supported.\n  ///\n  /// Examples of valid inputs:\n  /// fccc, FCCC, #fccc, #FCCC, #ffc1c1c1, #FFC1C1C1, ffc1c1c1, FFC1C1C1\n  ///\n  /// If the string is not valid, an error is thrown.\n  ///\n  /// Note: if you are hardcoding colors, use Dart's built-in hexadecimal\n  /// literals instead.\n  static Paint fromARGBHexString(String hexString) {\n    final color = ColorExtension.fromARGBHexString(hexString);\n    return Paint()..color = color;\n  }\n\n  /// Generates a random [Color] in a new [Paint] object with the set\n  /// alpha as [withAlpha] or the default (1.0).\n  /// You can pass in a random number generator [rng], if omitted the function\n  /// will create a new [Random] object without a seed and use that.\n  /// [base] can be used to get the random colors in only a lighter spectrum, it\n  /// should be between 0 and 256.\n  static Paint random({\n    double withAlpha = 1.0,\n    int base = 0,\n    Random? rng,\n  }) {\n    final color = ColorExtension.random(\n      withAlpha: withAlpha,\n      base: base,\n      rng: rng,\n    );\n    return Paint()..color = color;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/extensions/path.dart",
    "content": "import 'dart:typed_data' show Float32List;\nimport 'dart:ui';\n\nimport 'package:flame/src/cache/matrix_pool.dart' show pathTransform;\n\nexport 'dart:ui' show Path;\n\nextension PathExtension on Path {\n  Path transform32(Float32List matrix4) {\n    return pathTransform(this, matrix4);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/extensions/picture.dart",
    "content": "import 'dart:ui';\n\nextension PictureExtension on Picture {\n  /// Converts [Picture] into an [Image] and disposes of the picture.\n  Future<Image> toImageSafe(int width, int height) {\n    return toImage(width, height).then((image) {\n      dispose();\n      return image;\n    });\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/extensions/random.dart",
    "content": "import 'dart:math' show Random;\nimport 'dart:ui';\n\nextension RandomExtension on Random {\n  /// Returns a random boolean based on the given [odds].\n  bool nextBoolean({double odds = 0.5}) {\n    if (odds < 0 || odds > 1) {\n      throw ArgumentError.value(odds, 'odds', 'Must be between 0 and 1');\n    }\n    return nextDouble() < odds;\n  }\n\n  /// Returns a random double between [min] (inclusive) and [max] (exclusive),\n  /// using a linear probability distribution.\n  double nextDoubleBetween(double min, double max) {\n    if (min >= max) {\n      throw ArgumentError.value(max, 'max', 'Must be greater than min');\n    }\n    return min + nextDouble() * (max - min);\n  }\n\n  /// Returns a random integer between [min] (inclusive) and [max] (exclusive),\n  /// using a linear probability distribution.\n  int nextIntBetween(int min, int max) {\n    if (min >= max) {\n      throw ArgumentError.value(max, 'max', 'Must be greater than min');\n    }\n    return min + nextInt(max - min);\n  }\n\n  /// Returns a random color with 1.0 opacity and RGB values between 0 and 255.\n  Color nextColor() {\n    return Color.fromARGB(\n      255,\n      nextIntBetween(0, 256),\n      nextIntBetween(0, 256),\n      nextIntBetween(0, 256),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/extensions/rect.dart",
    "content": "import 'dart:math' show Random, max, min;\nimport 'dart:math' as math;\nimport 'dart:ui';\n\nimport 'package:flame/experimental.dart' as flame show Rectangle;\nimport 'package:flame/geometry.dart';\nimport 'package:flame/src/extensions/matrix4.dart';\nimport 'package:flame/src/extensions/offset.dart';\nimport 'package:flame/src/extensions/vector2.dart';\nimport 'package:flame/src/math/random_fallback.dart';\n\nexport 'dart:ui' show Rect;\n\nextension RectExtension on Rect {\n  /// Creates an [Offset] from this [Rect]\n  Offset toOffset() => Offset(width, height);\n\n  /// Creates a [Vector2] starting in top left and going to (width, height).\n  Vector2 toVector2() => Vector2(width, height);\n\n  /// Converts this [Rect] into a [math.Rectangle].\n  math.Rectangle toMathRectangle() => math.Rectangle(left, top, width, height);\n\n  /// Converts this [Rect] into a [flame.Rectangle].\n  flame.Rectangle toFlameRectangle() =>\n      flame.Rectangle.fromLTWH(left, top, width, height);\n\n  /// Converts this [Rect] into a [RectangleComponent].\n  RectangleComponent toRectangleComponent() {\n    return RectangleComponent.fromRect(this);\n  }\n\n  /// Whether this [Rect] contains a [Vector2] point or not\n  bool containsPoint(Vector2 point) => contains(point.toOffset());\n\n  /// Whether the segment formed by [pointA] and [pointB] intersects this [Rect]\n  bool intersectsSegment(Vector2 pointA, Vector2 pointB) {\n    return min(pointA.x, pointB.x) <= right &&\n        min(pointA.y, pointB.y) <= bottom &&\n        max(pointA.x, pointB.x) >= left &&\n        max(pointA.y, pointB.y) >= top;\n  }\n\n  /// Whether the [LineSegment] intersects the [Rect]\n  bool intersectsLineSegment(LineSegment segment) {\n    return intersectsSegment(segment.from, segment.to);\n  }\n\n  List<Vector2> toVertices() {\n    return [\n      topLeft.toVector2(),\n      topRight.toVector2(),\n      bottomRight.toVector2(),\n      bottomLeft.toVector2(),\n    ];\n  }\n\n  /// Transform Rect using the transformation defined by [matrix].\n  ///\n  /// **Note:** Rotation matrices will increase the size of the [Rect] but they\n  /// will not rotate it as [Rect] does not have any rotational values.\n  ///\n  /// **Note:** Only non-negative scale transforms are allowed, if a negative\n  /// scale is applied it will return a zero-based [Rect].\n  ///\n  /// **Note:** The transformation will always happen from the center of the\n  /// `Rect`.\n  Rect transform(Matrix4 matrix) {\n    // For performance reasons we are using the same logic from\n    // `Matrix4.transform2` but without the extra overhead of creating vectors.\n    return Rect.fromLTRB(\n      (topLeft.dx * matrix.m11) + (topLeft.dy * matrix.m21) + matrix.m41,\n      (topLeft.dx * matrix.m12) + (topLeft.dy * matrix.m22) + matrix.m42,\n      (bottomRight.dx * matrix.m11) +\n          (bottomRight.dy * matrix.m21) +\n          matrix.m41,\n      (bottomRight.dx * matrix.m12) +\n          (bottomRight.dy * matrix.m22) +\n          matrix.m42,\n    );\n  }\n\n  /// Generates a random point within the bounds of this [Rect].\n  Vector2 randomPoint([Random? random]) {\n    final randomGenerator = random ?? randomFallback;\n    return Vector2(\n      left + randomGenerator.nextDouble() * width,\n      top + randomGenerator.nextDouble() * height,\n    );\n  }\n\n  /// Creates a [Rect] that represents the bounds of the list [pts].\n  static Rect getBounds(List<Vector2> pts) {\n    final xPoints = pts.map((e) => e.x);\n    final yPoints = pts.map((e) => e.y);\n\n    final minX = xPoints.reduce(min);\n    final maxX = xPoints.reduce(max);\n    final minY = yPoints.reduce(min);\n    final maxY = yPoints.reduce(max);\n    return Rect.fromPoints(Offset(minX, minY), Offset(maxX, maxY));\n  }\n\n  /// Constructs a [Rect] with a [width] and [height] around the [center] point.\n  static Rect fromCenter({\n    required Vector2 center,\n    required double width,\n    required double height,\n  }) {\n    return Rect.fromLTRB(\n      center.x - width / 2,\n      center.y - height / 2,\n      center.x + width / 2,\n      center.y + height / 2,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/extensions/rectangle.dart",
    "content": "import 'dart:math' show Rectangle;\nimport 'dart:ui' show Rect;\n\nextension RectangleExtension on Rectangle {\n  /// Converts this math [Rectangle] into an ui [Rect].\n  Rect toRect() {\n    return Rect.fromLTWH(\n      left.toDouble(),\n      top.toDouble(),\n      width.toDouble(),\n      height.toDouble(),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/extensions/size.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/src/extensions/vector2.dart';\n\nexport 'dart:ui' show Size;\n\nextension SizeExtension on Size {\n  /// Creates an [Offset] from the [Size]\n  Offset toOffset() => Offset(width, height);\n\n  /// Creates a [Vector2] from the [Size]\n  Vector2 toVector2() => Vector2(width, height);\n\n  /// Creates a [Point] from the [Size]\n  Point toPoint() => Point(width, height);\n\n  /// Creates a [Rect] from the [Size]\n  Rect toRect() => Rect.fromLTWH(0, 0, width, height);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/extensions/vector2.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:vector_math/vector_math.dart';\n\nexport 'package:vector_math/vector_math.dart' hide Colors;\n\nextension Vector2Extension on Vector2 {\n  /// This is a reusable vector that can be used within the [Vector2Extension]\n  /// to avoid creation of new Vector2 instances.\n  ///\n  /// Avoid using this in async extension methods, as it can lead to race\n  /// conditions.\n  static final _reusableVector = Vector2.zero();\n\n  /// Creates an [Offset] from the [Vector2]\n  Offset toOffset() => Offset(x, y);\n\n  /// Creates a [Size] from the [Vector2]\n  Size toSize() => Size(x, y);\n\n  /// Creates a [Point] from the [Vector2]\n  Point toPoint() => Point(x, y);\n\n  /// A rectangle constructor operator.\n  ///\n  /// Combines two [Vector2]s to form a Rect whose top-left coordinate is the\n  /// point given by adding this vector, the left-hand-side operand,\n  /// to the origin, and whose size is the right-hand-side operand.\n  Rect operator &(Vector2 size) => toPositionedRect(size);\n\n  /// Creates a [Rect] starting from (x, y) and having the size of the\n  /// argument [Vector2]\n  Rect toPositionedRect(Vector2 size) => Rect.fromLTWH(x, y, size.x, size.y);\n\n  /// Creates a [Rect] starting in origin and going the [Vector2]\n  Rect toRect() => Rect.fromLTWH(0, 0, x, y);\n\n  /// Linearly interpolate towards another Vector2\n  void lerp(Vector2 to, double t) {\n    setFrom(\n      _reusableVector\n        ..setFrom(to)\n        ..sub(this)\n        ..scale(t)\n        ..add(this),\n    );\n  }\n\n  /// Whether the [Vector2] is the zero vector or not\n  bool isZero() => x == 0 && y == 0;\n\n  /// Whether the [Vector2] is the identity vector or not\n  bool isIdentity() => x == 1 && y == 1;\n\n  /// Distance to [other] vector, using the taxicab (L1) geometry.\n  double taxicabDistanceTo(Vector2 other) {\n    return (x - other.x).abs() + (y - other.y).abs();\n  }\n\n  /// Rotates the [Vector2] with [angle] in radians\n  /// rotates around [center] if it is defined\n  /// In a screen coordinate system (where the y-axis is flipped) it rotates in\n  /// a clockwise fashion\n  /// In a normal coordinate system it rotates in a counter-clockwise fashion\n  void rotate(double angle, {Vector2? center}) {\n    if (isZero() || angle == 0) {\n      // No point in rotating the zero vector or to rotate with 0 as angle\n      return;\n    }\n    if (center == null) {\n      setValues(\n        x * cos(angle) - y * sin(angle),\n        x * sin(angle) + y * cos(angle),\n      );\n    } else {\n      setValues(\n        cos(angle) * (x - center.x) - sin(angle) * (y - center.y) + center.x,\n        sin(angle) * (x - center.x) + cos(angle) * (y - center.y) + center.y,\n      );\n    }\n  }\n\n  /// Changes the [length] of the vector to the length provided, without\n  /// changing direction.\n  ///\n  /// If you try to scale the zero (empty) vector, it will remain unchanged, and\n  /// no error will be thrown.\n  void scaleTo(double newLength) {\n    final l = length;\n    if (l != 0) {\n      scale(newLength.abs() / l);\n    }\n  }\n\n  /// Clamps the [length] of this vector.\n  ///\n  /// This means that if the length is less than [min] the length will be set to\n  /// [min] and if the length is larger than [max], the length will be set to\n  /// [max]. If the length is in between [min] and [max], no changes will be\n  /// made.\n  void clampLength(double min, double max) {\n    final lengthSquared = length2;\n    if (lengthSquared > max * max) {\n      scaleTo(max);\n    } else if (lengthSquared < min * min) {\n      scaleTo(min);\n    }\n  }\n\n  /// Clamps this vector so that it is within or equals to the bounds defined by\n  /// [min] and [max].\n  void clamp(Vector2 min, Vector2 max) {\n    x = x.clamp(min.x, max.x);\n    y = y.clamp(min.y, max.y);\n  }\n\n  /// Clamps this vector so that it is within or equals to the bounds defined by\n  /// ([minX], [maxX]) and ([minY], [maxY]).\n  void clampDouble(double minX, double maxX, double minY, double maxY) {\n    x = x.clamp(minX, maxX);\n    y = y.clamp(minY, maxY);\n  }\n\n  /// Sets both x and y to [value].\n  void setAll(double value) => setValues(value, value);\n\n  /// Project this onto [other].\n  ///\n  /// [other] needs to have a length > 0;\n  /// If [out] is specified, it will be used to provide the result.\n  Vector2 projection(Vector2 other, {Vector2? out}) {\n    assert(other.length2 > 0, 'other needs to have a length > 0');\n    final dotProduct = dot(other);\n    final result = (out?..setFrom(other)) ?? other.clone();\n    return result..scale(dotProduct / other.length2);\n  }\n\n  /// Inverts the vector.\n  void invert() {\n    x *= -1;\n    y *= -1;\n  }\n\n  /// Returns the inverse of this vector.\n  Vector2 inverted() => Vector2(-x, -y);\n\n  /// Translates this Vector2 by [x] and [y].\n  void translate(double x, double y) {\n    setValues(this.x + x, this.y + y);\n  }\n\n  /// Creates a new Vector2 that is the current Vector2 translated by\n  /// [x] and [y].\n  Vector2 translated(double x, double y) {\n    return Vector2(this.x + x, this.y + y);\n  }\n\n  /// Smoothly moves this [Vector2] in the direction [target] by a displacement\n  /// given by a distance [ds] in that direction.\n  ///\n  /// It does not goes beyond target, regardless of [ds], so the final value\n  /// is always [target].\n  ///\n  /// Note: [ds] is the displacement vector in units of the vector space. It is\n  /// **not** a percentage (relative value).\n  void moveToTarget(\n    Vector2 target,\n    double ds,\n  ) {\n    if (this != target) {\n      final diff = _reusableVector\n        ..setFrom(target)\n        ..sub(this);\n\n      if (diff.length < ds) {\n        setFrom(target);\n      } else {\n        diff.scaleTo(ds);\n        setFrom(this + diff);\n      }\n    }\n  }\n\n  /// Signed angle in a coordinate system where the Y-axis is flipped.\n  ///\n  /// Since on a canvas/screen y is smaller the further up you go, instead of\n  /// larger like on a normal coordinate system, to get an angle that is in that\n  /// coordinate system we have to flip the Y-axis of the [Vector2].\n  ///\n  /// Example:\n  /// Up: Vector(0.0, -1.0).screenAngle == 0\n  /// Down: Vector(0.0, 1.0).screenAngle == +-pi\n  /// Left: Vector(-1.0, 0.0).screenAngle == -pi/2\n  /// Right: Vector(-1.0, 0.0).screenAngle == pi/2\n  double screenAngle() => (_reusableVector..setValues(x, y * (-1)))\n      .angleToSigned(Vector2(0.0, 1.0));\n\n  /// Modulo/Remainder\n  Vector2 operator %(Vector2 mod) => Vector2(x % mod.x, y % mod.y);\n\n  /// Stringifies the Vector2 with a maximum precision of [maxPrecision].\n  String toStringWithMaxPrecision(int maxPrecision) {\n    final precision = pow(10, maxPrecision);\n    final truncatedX = (x * precision).truncate() / precision;\n    final truncatedY = (y * precision).truncate() / precision;\n    return 'Vector2($truncatedX, $truncatedY)';\n  }\n\n  /// Create a Vector2 with ints as input\n  static Vector2 fromInts(int x, int y) => Vector2(x.toDouble(), y.toDouble());\n\n  /// Creates a heading [Vector2] with the given angle in radians.\n  static Vector2 fromRadians(double r) => Vector2(0, -1)..rotate(r);\n\n  /// Creates a heading [Vector2] with the given angle in degrees.\n  static Vector2 fromDegrees(double d) => fromRadians(d * degrees2Radians);\n\n  /// Creates a new identity [Vector2] (1.0, 1.0).\n  static Vector2 identity() => Vector2.all(1.0);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/flame.dart",
    "content": "library flame;\n\nimport 'package:flame/src/cache/assets_cache.dart';\nimport 'package:flame/src/cache/images.dart';\nimport 'package:flame/src/device.dart';\nimport 'package:flutter/services.dart';\n\n/// This class holds static references to some useful objects to use in your\n/// game.\n///\n/// You can access shared instances of [AssetsCache], [Images] and [Device].\n/// Most games should need only one instance of each, and should use this class\n/// to manage that reference.\nclass Flame {\n  /// Flame asset bundle, defaults to the root bundle but can be globally\n  /// changed.\n  static AssetBundle bundle = rootBundle;\n\n  /// Access a shared instance of [AssetsCache] class.\n  static AssetsCache assets = AssetsCache();\n\n  /// Access a shared instance of the [Images] class.\n  static Images images = Images();\n\n  /// Access a shared instance of the [Device] class.\n  static Device device = Device();\n}\n"
  },
  {
    "path": "packages/flame/lib/src/game/flame_game.dart",
    "content": "import 'dart:async';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/src/components/core/component_tree_root.dart';\nimport 'package:flame/src/devtools/dev_tools_service.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\nimport 'package:flame/src/game/game.dart';\nimport 'package:flutter/foundation.dart';\n// TODO(spydon): Remove this import when flutter version is updated to 3.35.0\n// ignore: unnecessary_import\nimport 'package:meta/meta.dart';\n\n/// This is a more complete and opinionated implementation of [Game].\n///\n/// [FlameGame] can be extended to add your game logic, or you can keep the\n/// logic in child [Component]s.\n///\n/// This is the recommended base class to use for most games made with Flame.\n/// It is based on the Flame Component System (also known as FCS).\n///\n/// The type parameter [W] allows you to specify a custom [World] subclass so\n/// that the [world] getter returns your specific type without casting. For\n/// example:\n///\n/// ```dart\n/// class MyWorld extends World {\n///   int score = 0;\n/// }\n///\n/// class MyGame extends FlameGame<MyWorld> {\n///   MyGame() : super(world: MyWorld());\n/// }\n/// ```\n///\n/// When [W] is specified, a matching world instance **must** be passed to the\n/// constructor; otherwise, a runtime assertion error is thrown.\nclass FlameGame<W extends World> extends ComponentTreeRoot\n    with Game\n    implements ReadOnlySizeProvider {\n  FlameGame({\n    super.children,\n    W? world,\n    CameraComponent? camera,\n  }) : assert(\n         world != null || W == World,\n         'The generics type $W does not conform to the type of '\n         '${world?.runtimeType ?? 'World'}.',\n       ),\n       _world = world ?? World() as W,\n       _camera = camera ?? CameraComponent() {\n    assert(\n      Component.staticGameInstance == null,\n      '$this instantiated, while another game ${Component.staticGameInstance} '\n      'declares itself to be a singleton',\n    );\n\n    if (kDebugMode) {\n      DevToolsService.initWithGame(this);\n    }\n\n    _camera.world = _world;\n    add(_camera);\n    add(_world);\n  }\n\n  /// The [World] that the [camera] is rendering.\n  /// Inside of this world is where most of your components should be added.\n  ///\n  /// You don't have to add the world to the tree after setting it here, it is\n  /// done automatically.\n  W get world => _world;\n  set world(W newWorld) {\n    if (newWorld == _world) {\n      return;\n    }\n    _world.removeFromParent();\n    camera.world = newWorld;\n    _world = newWorld;\n    if (_world.parent == null) {\n      add(_world);\n    }\n  }\n\n  W _world;\n\n  /// The component that is responsible for rendering your [world].\n  ///\n  /// In this component you can set different viewports, viewfinders, follow\n  /// components, set bounds for where the camera can move etc.\n  ///\n  /// You don't have to add the CameraComponent to the tree after setting it\n  /// here, it is done automatically.\n  ///\n  /// When setting the camera, if it doesn't already have a world it will be\n  /// set to match the game's world.\n  CameraComponent get camera => _camera;\n  set camera(CameraComponent newCameraComponent) {\n    _camera.removeFromParent();\n    _camera = newCameraComponent;\n    if (_camera.parent == null) {\n      add(_camera);\n    }\n    _camera.world ??= world;\n  }\n\n  CameraComponent _camera;\n\n  @internal\n  late final List<ComponentsNotifier> notifiers = [];\n\n  /// This is overwritten to consider the viewport transformation.\n  ///\n  /// Which means that this is the logical size of the game screen area as\n  /// exposed to the canvas after viewport transformations.\n  ///\n  /// This does not match the Flutter widget size; for that see [canvasSize].\n  @override\n  Vector2 get size => camera.viewport.virtualSize;\n\n  @override\n  @internal\n  FutureOr<void> load() async {\n    await super.load();\n    setLoaded();\n  }\n\n  @override\n  @internal\n  void mount() {\n    super.mount();\n    if (_pausedBecauseBackgrounded) {\n      resumeEngine();\n    }\n    setMounted();\n  }\n\n  @override\n  @internal\n  void finalizeRemoval() {\n    super.finalizeRemoval();\n    setRemoved();\n  }\n\n  /// This implementation of render renders each component, making sure the\n  /// canvas is reset for each one.\n  ///\n  /// You can override it further to add more custom behavior.\n  /// Beware of that if you are rendering components without using this method;\n  /// you must be careful to save and restore the canvas to avoid components\n  /// interfering with each others rendering.\n  @override\n  @mustCallSuper\n  void render(Canvas canvas) {\n    if (parent == null) {\n      renderTree(canvas);\n    }\n  }\n\n  @override\n  void renderTree(Canvas canvas) {\n    if (parent != null) {\n      render(canvas);\n    }\n    for (final component in children) {\n      component.renderTree(canvas);\n    }\n  }\n\n  @override\n  @mustCallSuper\n  void update(double dt) {\n    if (parent == null) {\n      updateTree(dt);\n    }\n  }\n\n  @override\n  void updateTree(double dt) {\n    processLifecycleEvents();\n    if (parent != null) {\n      update(dt);\n    }\n    for (final component in children) {\n      component.updateTree(dt);\n    }\n  }\n\n  /// This passes the new size along to every component in the tree via their\n  /// [Component.onGameResize] method, enabling each one to make their decision\n  /// of how to handle the resize event.\n  ///\n  /// It also updates the [size] field of the class to be used by later added\n  /// components and other methods.\n  /// You can override it further to add more custom behavior, but you should\n  /// seriously consider calling the super implementation as well.\n  @override\n  @mustCallSuper\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    // This work-around is needed since the camera has the highest priority and\n    // [size] uses [viewport.virtualSize], so the viewport needs to be updated\n    // first since users will be using `game.size` in their [onGameResize]\n    // methods.\n    camera.viewport.onGameResize(size);\n    // [onGameResize] is declared both in [Component] and in [Game]. Since\n    // there is no way to explicitly call the [Component]'s implementation,\n    // we propagate the event to [FlameGame]'s children manually.\n    handleResize(size);\n    for (final child in children) {\n      child.onParentResize(size);\n    }\n  }\n\n  /// Ensure that all pending tree operations finish.\n  ///\n  /// This is mainly intended for testing purposes: awaiting on this future\n  /// ensures that the game is fully loaded, and that all pending operations\n  /// of adding the components into the tree are fully materialized.\n  ///\n  /// Warning: awaiting on a game that was not fully connected will result in an\n  /// infinite loop. For example, this could occur if you run `x.add(y)` but\n  /// then forget to mount `x` into the game.\n  Future<void> ready() async {\n    var repeat = true;\n    while (repeat) {\n      // Give chance to other futures to execute first\n      await Future<void>.delayed(Duration.zero);\n      repeat = false;\n      processLifecycleEvents();\n      repeat |= hasLifecycleEvents;\n    }\n  }\n\n  /// Whether a point is within the boundaries of the visible part of the game.\n  @override\n  bool containsLocalPoint(Vector2 point) {\n    return point.x >= 0 &&\n        point.y >= 0 &&\n        point.x < canvasSize.x &&\n        point.y < canvasSize.y;\n  }\n\n  @override\n  bool containsEventHandlerAt(Vector2 position) {\n    // Deprecated game-level detector mixins handle events for the entire\n    // game surface, so any in-bounds point is a hit.\n    // ignore: deprecated_member_use_from_same_package\n    if (this is TapDetector ||\n        this is SecondaryTapDetector ||\n        this is TertiaryTapDetector ||\n        this is DoubleTapDetector ||\n        this is LongPressDetector ||\n        this is VerticalDragDetector ||\n        this is HorizontalDragDetector ||\n        this is ForcePressDetector ||\n        this is PanDetector ||\n        this is ScaleDetector ||\n        this is MultiTapListener ||\n        this is MultiTouchDragDetector) {\n      return true;\n    }\n    for (final component in super.componentsAtPoint(position)) {\n      if (component is TapCallbacks ||\n          component is DragCallbacks ||\n          component is DoubleTapCallbacks ||\n          component is ScaleCallbacks ||\n          component is SecondaryTapCallbacks) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /// Returns the current time in seconds with microseconds precision.\n  ///\n  /// This is compatible with the `dt` value used in the [update] method.\n  double currentTime() {\n    return DateTime.now().microsecondsSinceEpoch.toDouble() /\n        Duration.microsecondsPerSecond;\n  }\n\n  /// Returns a [ComponentsNotifier] for the given type [W].\n  ///\n  /// This method handles duplications, so there will never be\n  /// more than one [ComponentsNotifier] for a given type, meaning\n  /// that this method can be called as many times as needed for a type.\n  ComponentsNotifier<T> componentsNotifier<T extends Component>() {\n    for (final notifier in notifiers) {\n      if (notifier is ComponentsNotifier<T>) {\n        return notifier;\n      }\n    }\n    final notifier = ComponentsNotifier<T>(\n      descendants().whereType<T>().toList(),\n    );\n    notifiers.add(notifier);\n    return notifier;\n  }\n\n  @internal\n  void propagateToApplicableNotifiers(\n    Component component,\n    void Function(ComponentsNotifier) callback,\n  ) {\n    for (final notifier in notifiers) {\n      if (notifier.applicable(component)) {\n        callback(notifier);\n      }\n    }\n  }\n\n  /// Whether the game should pause when the app is backgrounded.\n  ///\n  /// On the latest Flutter stable at the time of writing (3.13),\n  /// this is only working on Android and iOS.\n  ///\n  /// Defaults to true.\n  bool pauseWhenBackgrounded = true;\n  bool _pausedBecauseBackgrounded = false;\n\n  @visibleForTesting\n  bool get isPausedOnBackground => _pausedBecauseBackgrounded;\n\n  @override\n  @mustCallSuper\n  void lifecycleStateChange(AppLifecycleState state) {\n    super.lifecycleStateChange(state);\n    switch (state) {\n      case AppLifecycleState.resumed:\n      case AppLifecycleState.inactive:\n        if (_pausedBecauseBackgrounded) {\n          resumeEngine();\n        }\n      case AppLifecycleState.paused:\n      case AppLifecycleState.detached:\n      case AppLifecycleState.hidden:\n        if (pauseWhenBackgrounded && !paused) {\n          pauseEngine();\n          _pausedBecauseBackgrounded = true;\n        }\n    }\n  }\n\n  @override\n  void pauseEngine() {\n    _pausedBecauseBackgrounded = false;\n    super.pauseEngine();\n  }\n\n  @override\n  void resumeEngine() {\n    _pausedBecauseBackgrounded = false;\n    super.resumeEngine();\n  }\n\n  @override\n  @mustCallSuper\n  void onHotReload() {\n    super.onHotReload();\n    handleHotReload();\n  }\n\n  /// Removes all children from the game and clears all caches.\n  ///\n  /// This will call [onRemove] on all components in the tree, clear\n  /// the [images] and [assets] caches, and process all pending\n  /// lifecycle events.\n  ///\n  /// After calling this method, the game is in a clean state with no\n  /// components or cached resources.\n  void dispose() {\n    removeAll(children);\n    processLifecycleEvents();\n    images.clearCache();\n    assets.clearCache();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/game/game.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/cache.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/src/flame.dart';\nimport 'package:flame/src/game/game_render_box.dart';\nimport 'package:flame/src/game/game_widget/gesture_detector_builder.dart';\nimport 'package:flame/src/game/overlay_manager.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:meta/meta.dart';\n\n/// This gives access to a low-level game API, to not build everything from a\n/// low level `FlameGame` should be used.\n///\n/// You can either extend this class, or add it as a mixin.\n///\n/// Methods [update] and [render] need to be implemented in order to connect\n/// your class with the internal game loop.\nabstract mixin class Game {\n  /// The cache of all images loaded into the game. This defaults to the global\n  /// [Flame.images] cache, but you can replace it with a new cache instance if\n  /// needed.\n  Images images = Flame.images;\n\n  /// The cache of all (non-image) assets loaded into the game. This defaults to\n  /// the global [Flame.assets] cache, but you can replace this with another\n  /// instance if needed.\n  AssetsCache assets = Flame.assets;\n\n  /// Used internally by various mixins that need to use gesture detection\n  /// functionality in Flutter.\n  late final GestureDetectorBuilder gestureDetectors = GestureDetectorBuilder(\n    refreshWidget,\n  )..initializeGestures(this);\n\n  /// Set by the PointerMoveDispatcher to receive mouse events from the\n  /// game widget.\n  void Function(PointerHoverEvent event)? get mouseDetector => _mouseDetector;\n  void Function(PointerHoverEvent event)? _mouseDetector;\n  set mouseDetector(void Function(PointerHoverEvent event)? newMouseDetector) {\n    _mouseDetector = newMouseDetector;\n    refreshWidget();\n  }\n\n  /// This should update the state of the game.\n  void update(double dt);\n\n  /// This should render the game.\n  void render(Canvas canvas);\n\n  /// Just a reference back to the render box that is kept up to date by the\n  /// engine.\n  GameRenderBox get renderBox => _gameRenderBox!;\n  GameRenderBox? _gameRenderBox;\n\n  /// Build context set by [GameWidgetState] before the game is fully\n  /// attached to the render tree. This allows components to access the\n  /// build context during [onLoad] and [onMount].\n  @internal\n  BuildContext? widgetBuildContext;\n\n  /// Currently attached build context. Can be null if not attached.\n  BuildContext? get buildContext =>\n      _gameRenderBox?.buildContext ?? widgetBuildContext;\n\n  /// Whether the game widget was attached to the Flutter tree.\n  bool get isAttached => _gameRenderBox != null;\n\n  /// Current size of the game as provided by the framework; it will be null if\n  /// layout has not been computed yet.\n  ///\n  /// Use [size] and [hasLayout] for safe access.\n  Vector2? _size;\n\n  /// This variable ensures that Game's [onLoad] is called no more than once.\n  late final FutureOr<void> _onLoadFuture = onLoad();\n\n  bool _debugOnLoadStarted = false;\n\n  @internal\n  FutureOr<void> load() async {\n    assert(\n      () {\n        _debugOnLoadStarted = true;\n        return true;\n      }(),\n    );\n    return _onLoadFuture;\n  }\n\n  /// To be used for tests that needs to evaluate the game after it has been\n  /// loaded by the game widget.\n  @visibleForTesting\n  FutureOr<void> toBeLoaded() {\n    assert(\n      _debugOnLoadStarted,\n      'Make sure the game has passed to a mounted '\n      'GameWidget before calling toBeLoaded',\n    );\n    return _onLoadFuture;\n  }\n\n  @mustCallSuper\n  @internal\n  void mount() {\n    onMount();\n  }\n\n  @mustCallSuper\n  @internal\n  void finalizeRemoval() {\n    onRemove();\n  }\n\n  /// Current game viewport size, updated every resize via the [onGameResize]\n  /// method hook.\n  Vector2 get size {\n    assertHasLayout();\n    return _size!;\n  }\n\n  Vector2 get canvasSize {\n    assertHasLayout();\n    return _size!;\n  }\n\n  /// Indicates if this game instance is connected to a GameWidget that is live\n  /// in the flutter widget tree.\n  /// Once this is true, the game is ready to have its size used or in the case\n  /// of a FlameGame, to receive components.\n  bool get hasLayout => _size != null;\n\n  /// Whether the game has an event-handling component at the given [position].\n  ///\n  /// Used by [GameRenderBox] for hit testing to determine if pointer events\n  /// should be consumed by the game or passed through to Flutter widgets\n  /// behind it.\n  ///\n  /// The default returns `true`, meaning games that directly extend [Game]\n  /// will catch all events on their entire surface. [FlameGame] overrides this\n  /// to only report a hit when a component with event callbacks\n  /// (e.g. [TapCallbacks]) exists at the given position.\n  bool containsEventHandlerAt(Vector2 position) => true;\n\n  /// Returns the game background color.\n  /// By default it will return a black color.\n  /// It cannot be changed at runtime, because the game widget does not get\n  /// rebuild when this value changes.\n  Color backgroundColor() => const Color(0xFF000000);\n\n  /// This is the resize hook; every time the game widget is resized, this hook\n  /// is called.\n  ///\n  /// The default implementation just sets the new size on the size field\n  @mustCallSuper\n  void onGameResize(Vector2 size) {\n    _size = (_size ?? Vector2.zero())..setFrom(size);\n  }\n\n  @protected\n  void assertHasLayout() {\n    assert(\n      hasLayout,\n      '\"size\" is not ready yet. Did you try to access it on the Game '\n      'constructor? Use the \"onLoad\" method instead.',\n    );\n  }\n\n  /// This is the lifecycle state change hook; every time the game is resumed,\n  /// paused or suspended, this is called.\n  ///\n  /// The default implementation does nothing; override to use the hook.\n  /// Check [AppLifecycleState] for details about the events received.\n  void lifecycleStateChange(AppLifecycleState state) {}\n\n  /// Method to perform late initialization of the [Game] class.\n  ///\n  /// Usually, this method is the main place where you initialize your [Game]\n  /// class. This has several advantages over the traditional constructor:\n  ///   - this method can be `async`;\n  ///   - it is invoked when the size of the game widget is already known.\n  ///\n  /// The default implementation returns `null`, indicating that there is no\n  /// need to await anything. When overriding this method, you have a choice\n  /// whether to create a regular or async function.\n  ///\n  /// If you need an async [onLoad], then make your override return non-nullable\n  /// `Future<void>`:\n  /// ```dart\n  /// @override\n  /// Future<void> onLoad() async {\n  ///   // your code here\n  /// }\n  /// ```\n  ///\n  /// Alternatively, if your [onLoad] function doesn't use any `await`ing, then\n  /// you can declare it as a regular method and then return `null`:\n  /// ```dart\n  /// @override\n  /// Future<void>? onLoad() {\n  ///   // your code here\n  ///   return null;\n  /// }\n  /// ```\n  ///\n  /// The engine ensures that this method will be called exactly once during\n  /// the lifetime of the [Game] instance. Do not call this method manually.\n  FutureOr<void> onLoad() => null;\n\n  void onMount() {}\n\n  /// Marks game as attached to any Flutter widget tree.\n  ///\n  /// Should not be called manually.\n  void attach(PipelineOwner owner, GameRenderBox gameRenderBox) {\n    if (isAttached) {\n      throw UnsupportedError(\n        '''\n      Game attachment error:\n      A game instance can only be attached to one widget at a time.\n      ''',\n      );\n    }\n    _gameRenderBox = gameRenderBox;\n    if (!_isInternalRefresh) {\n      onAttach();\n    }\n    _isInternalRefresh = false;\n  }\n\n  /// Called when the game has been attached. This can be overridden\n  /// to add logic that requires the game to already be attached\n  /// to the widget tree.\n  void onAttach() {}\n\n  /// Marks game as no longer attached to any Flutter widget tree.\n  ///\n  /// Should not be called manually.\n  void detach() {\n    if (!_isInternalRefresh) {\n      onDetach();\n    }\n    _gameRenderBox = null;\n  }\n\n  /// Called when the game is about to be removed from the Flutter widget tree,\n  /// but before it is actually removed. See the docs for an example on how to\n  /// do cleanups to avoid memory leaks.\n  void onRemove() {}\n\n  /// Called when Flutter's hot reload is triggered.\n  ///\n  /// [FlameGame] overrides this to propagate the notification\n  /// to all components in the component tree.\n  void onHotReload() {}\n\n  /// Called when the GameWidget is disposed by Flutter.\n  void onDispose() {}\n\n  /// Called after the game has left the widget tree.\n  /// This can be overridden to add logic that requires the game\n  /// not be on the flutter widget tree anymore.\n  void onDetach() {}\n\n  /// Converts a global coordinate (i.e. w.r.t. the app itself) to a local\n  /// coordinate (i.e. w.r.t. he game widget).\n  /// If the widget occupies the whole app (\"full screen\" games), or is not\n  /// attached to Flutter, this operation is the identity.\n  Vector2 convertGlobalToLocalCoordinate(Vector2 point) {\n    if (!isAttached) {\n      return point.clone();\n    }\n    return _gameRenderBox!.globalToLocal(point.toOffset()).toVector2();\n  }\n\n  /// Converts a local coordinate (i.e. w.r.t. the game widget) to a global\n  /// coordinate (i.e. w.r.t. the app itself).\n  /// If the widget occupies the whole app (\"full screen\" games), or is not\n  /// attached to Flutter, this operation is the identity.\n  Vector2 convertLocalToGlobalCoordinate(Vector2 point) {\n    if (!isAttached) {\n      return point.clone();\n    }\n    return _gameRenderBox!.localToGlobal(point.toOffset()).toVector2();\n  }\n\n  /// Utility method to load and cache the image for a sprite based on its\n  /// options.\n  Future<Sprite> loadSprite(\n    String path, {\n    Vector2? srcSize,\n    Vector2? srcPosition,\n  }) {\n    return Sprite.load(\n      path,\n      srcPosition: srcPosition,\n      srcSize: srcSize,\n      images: images,\n    );\n  }\n\n  /// Utility method to load and cache the image for a sprite animation based on\n  /// its options.\n  Future<SpriteAnimation> loadSpriteAnimation(\n    String path,\n    SpriteAnimationData data,\n  ) {\n    return SpriteAnimation.load(\n      path,\n      data,\n      images: images,\n    );\n  }\n\n  bool _paused = false;\n\n  /// Returns is the engine if currently paused or running\n  bool get paused => _paused;\n\n  /// Pauses or resume the engine\n  set paused(bool value) {\n    _paused = value;\n\n    final gameLoop = _gameRenderBox?.gameLoop;\n    if (gameLoop != null) {\n      if (value) {\n        gameLoop.stop();\n      } else {\n        gameLoop.start();\n      }\n    }\n  }\n\n  /// Pauses the engine game loop execution.\n  void pauseEngine() {\n    _paused = true;\n    _gameRenderBox?.gameLoop?.stop();\n  }\n\n  /// Resumes the engine game loop execution.\n  void resumeEngine() {\n    _paused = false;\n    _gameRenderBox?.gameLoop?.start();\n  }\n\n  /// Steps the engine game loop by one frame. Works only if the engine is in\n  /// paused state. By default step time is assumed to be 1/60th of a second.\n  void stepEngine({double stepTime = 1 / 60}) {\n    if (_paused) {\n      _paused = false;\n      _gameRenderBox?.gameLoop?.step(stepTime);\n      _paused = true;\n    }\n  }\n\n  /// A property that stores an [OverlayManager]\n  ///\n  /// This is useful to render widgets on top of a game, such as a pause menu.\n  /// Overlays can be made visible via [overlays].add or hidden via\n  /// [overlays].remove.\n  ///\n  /// For example:\n  /// ```\n  /// final pauseOverlayIdentifier = 'PauseMenu';\n  /// overlays.add(pauseOverlayIdentifier); // marks 'PauseMenu' to be rendered.\n  /// overlays.remove(pauseOverlayIdentifier); // hides 'PauseMenu'.\n  /// ```\n  late final overlays = OverlayManager(this);\n\n  /// Used to change the mouse cursor of the GameWidget running this game.\n  /// Setting the value to null will make the GameWidget defer the choice\n  /// of the cursor to the closest region available on the tree.\n  MouseCursor get mouseCursor => _mouseCursor;\n  MouseCursor _mouseCursor = MouseCursor.defer;\n\n  set mouseCursor(MouseCursor value) {\n    _mouseCursor = value;\n    refreshWidget();\n  }\n\n  @visibleForTesting\n  final List<VoidCallback> gameStateListeners = [];\n\n  void addGameStateListener(VoidCallback callback) {\n    gameStateListeners.add(callback);\n  }\n\n  void removeGameStateListener(VoidCallback callback) {\n    gameStateListeners.remove(callback);\n  }\n\n  bool _isInternalRefresh = false;\n\n  /// When a Game is attached to a `GameWidget`, this method will force that\n  /// widget to be rebuilt. This can be used when updating any property which is\n  /// implemented within the Flutter tree.\n  ///\n  /// When [isInternalRefresh] is passed as false it will trigger the `onAttach`\n  /// and `onDetach` events; otherwise, those events will not be called.\n  @internal\n  void refreshWidget({bool isInternalRefresh = true}) {\n    _isInternalRefresh = isInternalRefresh;\n    gameStateListeners.forEach((callback) => callback());\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/game/game_loop.dart",
    "content": "import 'package:flutter/scheduler.dart';\n\n/// Internal class that drives the game loop by calling the provided [callback]\n/// function on every Flutter animation frame.\n///\n/// After creating a GameLoop, call `start()` in order to make it actually run.\n/// When a GameLoop object is no longer needed, it must be `dispose()`d.\n///\n/// For example:\n/// ```dart\n/// final gameLoop = GameLoop(onGameLoopTick);\n/// gameLoop.start();\n/// ...\n/// gameLoop.dispose();\n/// ```\nclass GameLoop {\n  GameLoop(this.callback) {\n    _ticker = Ticker(_tick);\n  }\n\n  /// Function to be called on every Flutter rendering frame.\n  ///\n  /// This function takes a single parameter `dt`, which is the amount of time\n  /// passed since the previous invocation of this function. The time is\n  /// measured in seconds, with microsecond precision. The argument will be\n  /// equal to 0 on first invocation of the callback.\n  void Function(double dt) callback;\n\n  /// Total amount of time passed since the game loop was started.\n  ///\n  /// This variable is updated on every rendering frame, just before the\n  /// [callback] is invoked. It will be equal to zero while the game loop is\n  /// stopped. It is also guaranteed to be equal to zero on the first invocation\n  /// of the callback.\n  Duration _previous = Duration.zero;\n\n  /// Internal object responsible for periodically calling the [callback]\n  /// function.\n  late final Ticker _ticker;\n\n  /// This method is periodically invoked by the [_ticker].\n  void _tick(Duration timestamp) {\n    final durationDelta = timestamp - _previous;\n    final dt = durationDelta.inMicroseconds / Duration.microsecondsPerSecond;\n    _previous = timestamp;\n    callback(dt);\n  }\n\n  /// Start running the game loop. The game loop is created in a paused state,\n  /// so this must be called once in order to make the loop running. Calling\n  /// this method again when the game loop already runs is a noop.\n  void start() {\n    if (!_ticker.isActive) {\n      _ticker.start();\n    }\n  }\n\n  /// Stop the game loop. While it is stopped, the time \"freezes\". When the\n  /// game loop is started again, the [callback] will NOT be made aware that\n  /// any amount of time has passed.\n  void stop() {\n    _ticker.stop();\n    _previous = Duration.zero;\n  }\n\n  /// Steps the game loop by the given amount of time while the ticker is\n  /// stopped.\n  void step(double stepTime) {\n    if (!_ticker.isActive) {\n      callback(stepTime);\n    }\n  }\n\n  /// Call this before deleting the [GameLoop] object.\n  ///\n  /// The [GameLoop] will no longer be usable after this method is called. You\n  /// do not have to stop the game loop before disposing of it.\n  void dispose() {\n    _ticker.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/game/game_render_box.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/src/game/game_loop.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:flutter/widgets.dart' hide WidgetBuilder;\n\n/// A [RenderObjectWidget] that renders the [GameRenderBox].\n///\n/// This is the widget that is used by the [GameWidget] to ACTUALLY\n/// render the game.\nclass RenderGameWidget extends LeafRenderObjectWidget {\n  const RenderGameWidget({\n    required this.game,\n    required this.addRepaintBoundary,\n    required this.behavior,\n    super.key,\n  });\n\n  final Game game;\n  final bool addRepaintBoundary;\n  final HitTestBehavior behavior;\n\n  @override\n  RenderBox createRenderObject(BuildContext context) {\n    return GameRenderBox(\n      game,\n      context,\n      isRepaintBoundary: addRepaintBoundary,\n      behavior: behavior,\n    );\n  }\n\n  @override\n  void updateRenderObject(BuildContext context, GameRenderBox renderObject) {\n    renderObject\n      ..game = game\n      ..buildContext = context\n      ..isRepaintBoundary = addRepaintBoundary\n      ..behavior = behavior;\n  }\n}\n\nclass GameRenderBox extends RenderBox with WidgetsBindingObserver {\n  GameRenderBox(\n    this._game,\n    this.buildContext, {\n    required bool isRepaintBoundary,\n    this.behavior = HitTestBehavior.opaque,\n  }) : _isRepaintBoundary = isRepaintBoundary;\n\n  GameLoop? gameLoop;\n\n  BuildContext buildContext;\n\n  Game _game;\n\n  Game get game => _game;\n\n  set game(Game value) {\n    // Identities are equal, no need to update.\n    if (_game == value) {\n      return;\n    }\n\n    if (attached) {\n      _detachGame();\n    }\n\n    _game = value;\n\n    if (attached) {\n      _attachGame(owner!);\n    }\n  }\n\n  bool _isRepaintBoundary;\n\n  set isRepaintBoundary(bool value) {\n    if (_isRepaintBoundary == value) {\n      return;\n    }\n    _isRepaintBoundary = value;\n    markNeedsCompositingBitsUpdate();\n  }\n\n  @override\n  bool get isRepaintBoundary => _isRepaintBoundary;\n\n  HitTestBehavior behavior;\n\n  @override\n  bool get sizedByParent => true;\n\n  @override\n  Size computeDryLayout(BoxConstraints constraints) => constraints.biggest;\n\n  @override\n  void attach(PipelineOwner owner) {\n    super.attach(owner);\n    _attachGame(owner);\n  }\n\n  void _attachGame(PipelineOwner owner) {\n    game.attach(owner, this);\n\n    final gameLoop = this.gameLoop = GameLoop(gameLoopCallback);\n\n    if (!game.paused) {\n      gameLoop.start();\n    }\n\n    _bindLifecycleListener();\n  }\n\n  @override\n  void detach() {\n    super.detach();\n    _detachGame();\n  }\n\n  void _detachGame() {\n    game.detach();\n    gameLoop?.dispose();\n    gameLoop = null;\n    _unbindLifecycleListener();\n  }\n\n  void gameLoopCallback(double dt) {\n    assert(attached);\n    if (!attached) {\n      return;\n    }\n    game.update(dt);\n    markNeedsPaint();\n  }\n\n  @override\n  bool hitTestSelf(Offset position) {\n    if (behavior == HitTestBehavior.opaque) {\n      return true;\n    }\n    return game.containsEventHandlerAt(position.toVector2());\n  }\n\n  @override\n  void paint(PaintingContext context, Offset offset) {\n    context.canvas.save();\n    context.canvas.translate(offset.dx, offset.dy);\n    game.render(context.canvas);\n    context.canvas.restore();\n  }\n\n  void _bindLifecycleListener() {\n    WidgetsBinding.instance.addObserver(this);\n  }\n\n  void _unbindLifecycleListener() {\n    WidgetsBinding.instance.removeObserver(this);\n  }\n\n  @override\n  void didChangeAppLifecycleState(AppLifecycleState state) {\n    game.lifecycleStateChange(state);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/game/game_widget/game_widget.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/src/game/game_render_box.dart';\nimport 'package:flame/src/game/game_widget/gesture_detector_builder.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter/widgets.dart';\n\n/// The **GameWidget** is a Flutter widget which is used to insert a [Game]\n/// instance into the Flutter widget tree.\n///\n/// The `GameWidget` is sufficiently feature-rich to run as the root of your\n/// Flutter application. Thus, the simplest way to use `GameWidget` is like\n/// this:\n/// ```dart\n/// void main() {\n///   runApp(\n///     GameWidget(game: MyGame()),\n///   );\n/// }\n/// ```\n///\n/// At the same time, `GameWidget` is a regular Flutter widget, and can be\n/// inserted arbitrarily deep into the widget tree, including the possibility\n/// of having multiple `GameWidget`s within a single app.\n///\n/// The layout behavior of this widget is that it will expand to fill all\n/// available space. Thus, when used as a root widget it will make the app\n/// full-screen. Inside any other layout widget it will take as much space as\n/// possible.\n///\n/// In addition to hosting a [Game] instance, the `GameWidget` also provides\n/// some structural support, with the following features:\n///\n/// - [loadingBuilder] to display something while the game is loading;\n/// - [errorBuilder] shown if the game throws an error;\n/// - [backgroundBuilder] to draw some decoration behind the game;\n/// - [overlayBuilderMap] to draw one or more widgets on top of the game.\n///\n/// It should be noted that `GameWidget` does not clip the content of its\n/// canvas, which means the game can potentially draw outside of its boundaries\n/// (not always, depending on which camera is used). If this is not desired,\n/// then consider wrapping the widget in Flutter's [ClipRect].\nclass GameWidget<T extends Game> extends StatefulWidget {\n  /// Renders the provided [game] instance.\n  GameWidget({\n    required T this.game,\n    this.textDirection,\n    this.loadingBuilder,\n    this.errorBuilder,\n    this.backgroundBuilder,\n    this.overlayBuilderMap,\n    this.initialActiveOverlays,\n    this.focusNode,\n    this.autofocus = true,\n    this.mouseCursor,\n    this.addRepaintBoundary = true,\n    this.behavior = HitTestBehavior.opaque,\n    super.key,\n  }) : gameFactory = null {\n    _initializeGame(game!);\n  }\n\n  /// A `GameWidget` which will create and own a `Game` instance, using the\n  /// provided [gameFactory].\n  ///\n  /// This constructor can be useful when you want to put `GameWidget` into\n  /// another widget, but would like to avoid the need to store the game's\n  /// instance yourself. For example:\n  /// ```dart\n  /// class MyWidget extends StatelessWidget {\n  ///   @override\n  ///   Widget build(BuildContext context) {\n  ///     return Container(\n  ///       padding: EdgeInsets.all(20),\n  ///       child: GameWidget.controlled(\n  ///         gameFactory: MyGame.new,\n  ///       ),\n  ///     );\n  ///   }\n  /// }\n  /// ```\n  const GameWidget.controlled({\n    required GameFactory<T> this.gameFactory,\n    this.textDirection,\n    this.loadingBuilder,\n    this.errorBuilder,\n    this.backgroundBuilder,\n    this.overlayBuilderMap,\n    this.initialActiveOverlays,\n    this.focusNode,\n    this.autofocus = true,\n    this.mouseCursor,\n    this.addRepaintBoundary = true,\n    this.behavior = HitTestBehavior.opaque,\n    super.key,\n  }) : game = null;\n\n  /// The game instance which this widget will render, if it was provided with\n  /// the default constructor. Otherwise, if the [GameWidget.controlled]\n  /// constructor was used, this will always be `null`.\n  final T? game;\n\n  /// A function that creates a [Game] that this widget will render.\n  final GameFactory<T>? gameFactory;\n\n  /// The text direction to be used in text elements in a game.\n  final TextDirection? textDirection;\n\n  /// Builder to provide a widget which will be displayed while the game is\n  /// loading. By default this is an empty `Container`.\n  final GameLoadingWidgetBuilder? loadingBuilder;\n\n  /// If set, errors during the game loading will be caught and this widget\n  /// will be shown. If not provided, errors are propagated normally.\n  final GameErrorWidgetBuilder? errorBuilder;\n\n  /// Builder to provide a widget tree to be built between the game elements and\n  /// the background color provided via [Game.backgroundColor].\n  final WidgetBuilder? backgroundBuilder;\n\n  /// A collection of widgets that can be displayed over the game's surface.\n  /// These widgets can be turned on-and-off dynamically from within the game\n  /// via the [Game.overlays] property.\n  ///\n  /// ```dart\n  /// void main() {\n  ///   runApp(\n  ///     GameWidget(\n  ///       game: MyGame(),\n  ///       overlayBuilderMap: {\n  ///         'PauseMenu': (context, game) {\n  ///           return Container(\n  ///             color: const Color(0xFF000000),\n  ///             child: Text('A pause menu'),\n  ///           );\n  ///         },\n  ///       },\n  ///     ),\n  ///   );\n  /// }\n  /// ```\n  final Map<String, OverlayWidgetBuilder<T>>? overlayBuilderMap;\n\n  /// The list of overlays that will be shown when the game starts (but after\n  /// it was loaded).\n  final List<String>? initialActiveOverlays;\n\n  /// The [FocusNode] to control the games focus to receive event inputs.\n  /// If omitted, defaults to an internally controlled focus node.\n  final FocusNode? focusNode;\n\n  /// Whether the [focusNode] requests focus once the game is mounted.\n  /// Defaults to true.\n  final bool autofocus;\n\n  /// The shape of the mouse cursor when it is hovering over the game canvas.\n  /// This property can be changed dynamically via [Game.mouseCursor].\n  final MouseCursor? mouseCursor;\n\n  /// Whether the game should assume the behavior of a [RepaintBoundary],\n  /// defaults to `true`.\n  final bool addRepaintBoundary;\n\n  /// How the game widget behaves during hit testing.\n  ///\n  /// - [HitTestBehavior.opaque] (default): the game absorbs all pointer\n  ///   events on its surface, preventing widgets behind it from receiving them.\n  /// - [HitTestBehavior.deferToChild]: the game only intercepts events at\n  ///   positions where a component with event callbacks (e.g. [TapCallbacks])\n  ///   exists. Events at other positions pass through to widgets behind.\n  /// - [HitTestBehavior.translucent]: the game receives events where it has\n  ///   event-handling components, but always allows widgets behind it to be\n  ///   hit-tested as well.\n  final HitTestBehavior behavior;\n\n  /// Renders a [game] in a flutter widget tree alongside widgets overlays.\n  ///\n  /// To use overlays, the game subclass has to be mixed with HasWidgetsOverlay.\n  @override\n  GameWidgetState<T> createState() => GameWidgetState<T>();\n\n  void _initializeGame(T game) {\n    if (mouseCursor != null) {\n      game.mouseCursor = mouseCursor!;\n    }\n    if (overlayBuilderMap != null) {\n      for (final kv in overlayBuilderMap!.entries) {\n        game.overlays.addEntry(\n          kv.key,\n          (ctx, game) => kv.value(ctx, game as T),\n        );\n      }\n    }\n    if (initialActiveOverlays != null) {\n      game.overlays.addAll(initialActiveOverlays!);\n    }\n  }\n}\n\nclass GameWidgetState<T extends Game> extends State<GameWidget<T>> {\n  late T currentGame;\n\n  Future<void> get loaderFuture => _loaderFuture ??= (() async {\n    final game = currentGame;\n    assert(game.hasLayout);\n    await game.load();\n    game.mount();\n    if (!game.paused) {\n      game.update(0);\n    }\n  })();\n\n  Future<void>? _loaderFuture;\n\n  late FocusNode _focusNode;\n\n  /// The number of `build()` functions currently executing.\n  int _buildDepth = 0;\n\n  /// If true, then a fresh build will be scheduled after the current one\n  /// completes. This should only be set to true when the [_buildDepth] is\n  /// non-zero.\n  bool _requiresRebuild = false;\n\n  /// Helper method that arranges to have `_buildDepth > 0` while the [build] is\n  /// executing, and then schedules a re-build if [_requiresRebuild] flag was\n  /// raised during the build.\n  ///\n  /// This is needed because our build function invokes user code, which in turn\n  /// may change some of the [Game]'s properties which would require the\n  /// [GameWidget] to be rebuilt. However, Flutter doesn't allow widgets to be\n  /// marked dirty while they are building. So, this method is needed to avoid\n  /// such a limitation and ensure that the user code can set [Game]'s\n  /// properties freely, and that they will be propagated to the [GameWidget]\n  /// at the earliest opportunity.\n  Widget _protectedBuild(Widget Function() build) {\n    late final Widget result;\n    try {\n      _buildDepth++;\n      result = build();\n    } finally {\n      _buildDepth--;\n    }\n    if (_requiresRebuild && _buildDepth == 0) {\n      Future.microtask(_onGameStateChange);\n    }\n    return result;\n  }\n\n  void _onGameStateChange() {\n    if (_buildDepth > 0) {\n      _requiresRebuild = true;\n    } else {\n      setState(() => _requiresRebuild = false);\n    }\n  }\n\n  void initCurrentGame() {\n    if (widget.game == null) {\n      currentGame = widget.gameFactory!.call();\n      widget._initializeGame(currentGame);\n    } else {\n      currentGame = widget.game!;\n    }\n    initGameStateListener(currentGame, _onGameStateChange);\n    _loaderFuture = null;\n  }\n\n  /// Visible for testing for\n  /// https://github.com/flame-engine/flame/issues/2771.\n  @visibleForTesting\n  static void initGameStateListener(\n    Game currentGame,\n    void Function() onGameStateChange,\n  ) {\n    currentGame.addGameStateListener(onGameStateChange);\n\n    // See https://github.com/flame-engine/flame/issues/2771\n    // for why we aren't using [WidgetsBinding.instance.lifecycleState].\n    currentGame.lifecycleStateChange(AppLifecycleState.resumed);\n  }\n\n  /// [disposeCurrentGame] is called by two flutter events - `didUpdateWidget`\n  /// and `dispose`.  When the parameter [callGameOnDispose] is true, the\n  /// `currentGame`'s `onDispose` method will be called; otherwise, it will not.\n  void disposeCurrentGame({bool callGameOnDispose = false}) {\n    currentGame.removeGameStateListener(_onGameStateChange);\n    currentGame.lifecycleStateChange(AppLifecycleState.paused);\n    currentGame.finalizeRemoval();\n    currentGame.widgetBuildContext = null;\n    if (callGameOnDispose) {\n      currentGame.onDispose();\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    initCurrentGame();\n    _focusNode = widget.focusNode ?? FocusNode();\n    if (widget.autofocus) {\n      _focusNode.requestFocus();\n    }\n  }\n\n  @override\n  void didUpdateWidget(GameWidget<T> oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    if (oldWidget.game != widget.game) {\n      disposeCurrentGame();\n      initCurrentGame();\n    }\n  }\n\n  @override\n  void reassemble() {\n    super.reassemble();\n    currentGame.onHotReload();\n  }\n\n  @override\n  void dispose() {\n    disposeCurrentGame(callGameOnDispose: true);\n    // If we received a focus node from the user, they are responsible\n    // for disposing it\n    if (widget.focusNode == null) {\n      _focusNode.dispose();\n    }\n    super.dispose();\n  }\n\n  KeyEventResult _handleKeyEvent(FocusNode focusNode, KeyEvent event) {\n    final game = currentGame;\n\n    if (!_focusNode.hasPrimaryFocus) {\n      return KeyEventResult.ignored;\n    }\n\n    if (game is KeyboardEvents) {\n      return game.onKeyEvent(\n        event,\n        HardwareKeyboard.instance.logicalKeysPressed,\n      );\n    }\n    return KeyEventResult.handled;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return _protectedBuild(() {\n      Widget? internalGameWidget = RenderGameWidget(\n        game: currentGame,\n        addRepaintBoundary: widget.addRepaintBoundary,\n        behavior: widget.behavior,\n      );\n\n      assert(\n        !(currentGame is MultiTouchDragDetector && currentGame is PanDetector),\n        'WARNING: Both MultiTouchDragDetector and a PanDetector detected. '\n        'The MultiTouchDragDetector will override the PanDetector and it will '\n        'not receive events',\n      );\n\n      internalGameWidget = currentGame.gestureDetectors.build(\n        internalGameWidget,\n      );\n\n      if (hasMouseDetectors(currentGame)) {\n        internalGameWidget = applyMouseDetectors(\n          currentGame,\n          internalGameWidget,\n        );\n      }\n\n      final stackedWidgets = [internalGameWidget];\n      _addBackground(context, stackedWidgets);\n      _addOverlays(context, stackedWidgets);\n\n      // We can use Directionality.maybeOf when that method lands on stable\n      final textDir = widget.textDirection ?? TextDirection.ltr;\n\n      return FocusScope(\n        child: Focus(\n          focusNode: _focusNode,\n          autofocus: widget.autofocus,\n          descendantsAreFocusable: true,\n          onKeyEvent: _handleKeyEvent,\n          child: MouseRegion(\n            cursor: currentGame.mouseCursor,\n            opaque: widget.behavior == HitTestBehavior.opaque,\n            child: Directionality(\n              textDirection: textDir,\n              child: DecoratedBox(\n                decoration: BoxDecoration(\n                  color: currentGame.backgroundColor(),\n                ),\n                child: LayoutBuilder(\n                  builder: (layoutContext, BoxConstraints constraints) {\n                    return _protectedBuild(() {\n                      final size = constraints.biggest.toVector2();\n                      if (size.isZero()) {\n                        return widget.loadingBuilder?.call(context) ??\n                            Container();\n                      }\n                      currentGame.onGameResize(size);\n                      currentGame.widgetBuildContext = layoutContext;\n                      // This should only be called if the game has already been\n                      // loaded (in the case of resizing for example), since\n                      // update otherwise should be called after onMount.\n                      if (!currentGame.paused && currentGame.isAttached) {\n                        currentGame.update(0);\n                      }\n                      return FutureBuilder(\n                        future: loaderFuture,\n                        builder: (_, snapshot) {\n                          if (snapshot.hasError) {\n                            final errorBuilder = widget.errorBuilder;\n                            if (errorBuilder == null) {\n                              throw Error.throwWithStackTrace(\n                                snapshot.error!,\n                                snapshot.stackTrace!,\n                              );\n                            } else {\n                              return errorBuilder(context, snapshot.error!);\n                            }\n                          }\n\n                          if (snapshot.connectionState ==\n                              ConnectionState.done) {\n                            return Stack(children: stackedWidgets);\n                          }\n\n                          return widget.loadingBuilder?.call(context) ??\n                              const SizedBox.expand();\n                        },\n                      );\n                    });\n                  },\n                ),\n              ),\n            ),\n          ),\n        ),\n      );\n    });\n  }\n\n  void _addBackground(BuildContext context, List<Widget> stackWidgets) {\n    if (widget.backgroundBuilder != null) {\n      final backgroundContent = KeyedSubtree(\n        key: ValueKey(widget.game),\n        child: widget.backgroundBuilder!(context),\n      );\n      stackWidgets.insert(0, backgroundContent);\n    }\n  }\n\n  void _addOverlays(BuildContext context, List<Widget> stackWidgets) {\n    stackWidgets.addAll(\n      currentGame.overlays.buildCurrentOverlayWidgets(context),\n    );\n  }\n}\n\ntypedef GameLoadingWidgetBuilder = Widget Function(BuildContext);\n\ntypedef GameErrorWidgetBuilder = Widget Function(BuildContext, Object error);\n\ntypedef OverlayWidgetBuilder<T extends Game> =\n    Widget Function(\n      BuildContext context,\n      T game,\n    );\n\ntypedef GameFactory<T extends Game> = T Function();\n"
  },
  {
    "path": "packages/flame/lib/src/game/game_widget/gesture_detector_builder.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/src/game/game.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/widgets.dart';\n\nclass GestureDetectorBuilder {\n  GestureDetectorBuilder([this._onChange]);\n\n  final Map<Type, GestureRecognizerFactory> _gestures = {};\n  final Map<Type, int> _counters = {};\n  final void Function()? _onChange;\n\n  void add<T extends GestureRecognizer>(\n    T Function() constructor,\n    void Function(T) initializer,\n  ) {\n    final count = _counters[T];\n    if (count == null) {\n      _gestures[T] = GestureRecognizerFactoryWithHandlers<T>(\n        constructor,\n        initializer,\n      );\n      _onChange?.call();\n    }\n    _counters[T] = (count ?? 0) + 1;\n  }\n\n  void remove<T extends GestureRecognizer>() {\n    final count = _counters[T]!;\n    if (count == 1) {\n      _counters.remove(T);\n      _gestures.remove(T);\n      _onChange?.call();\n    } else {\n      _counters[T] = count - 1;\n    }\n  }\n\n  Widget build(Widget child) {\n    if (_gestures.isEmpty) {\n      return child;\n    }\n    return RawGestureDetector(\n      gestures: _gestures,\n      behavior: HitTestBehavior.deferToChild,\n      child: child,\n    );\n  }\n\n  void initializeGestures(Game game) {\n    // support for deprecated detectors\n    // ignore: deprecated_member_use_from_same_package\n    if (game is TapDetector ||\n        game is SecondaryTapDetector ||\n        game is TertiaryTapDetector) {\n      add(\n        TapGestureRecognizer.new,\n        (TapGestureRecognizer instance) {\n          // support for deprecated detectors\n          // ignore: deprecated_member_use_from_same_package\n          if (game is TapDetector) {\n            instance.onTap = game.onTap;\n            instance.onTapCancel = game.onTapCancel;\n            instance.onTapUp = game.handleTapUp;\n            instance.onTapDown = game.handleTapDown;\n          }\n          if (game is SecondaryTapDetector) {\n            instance.onSecondaryTapCancel = game.onSecondaryTapCancel;\n            instance.onSecondaryTapUp = game.handleSecondaryTapUp;\n            instance.onSecondaryTapDown = game.handleSecondaryTapDown;\n          }\n          if (game is TertiaryTapDetector) {\n            instance.onTertiaryTapCancel = game.onTertiaryTapCancel;\n            instance.onTertiaryTapUp = game.handleTertiaryTapUp;\n            instance.onTertiaryTapDown = game.handleTertiaryTapDown;\n          }\n        },\n      );\n    }\n    if (game is DoubleTapDetector) {\n      add(\n        DoubleTapGestureRecognizer.new,\n        (DoubleTapGestureRecognizer instance) {\n          instance.onDoubleTap = game.onDoubleTap;\n          instance.onDoubleTapDown = game.handleDoubleTapDown;\n          instance.onDoubleTapCancel = game.onDoubleTapCancel;\n        },\n      );\n    }\n    if (game is LongPressDetector) {\n      add(\n        LongPressGestureRecognizer.new,\n        (LongPressGestureRecognizer instance) {\n          instance.onLongPress = game.onLongPress;\n          instance.onLongPressStart = game.handleLongPressStart;\n          instance.onLongPressMoveUpdate = game.handleLongPressMoveUpdate;\n          instance.onLongPressEnd = game.handleLongPressEnd;\n          instance.onLongPressUp = game.onLongPressUp;\n          instance.onLongPressCancel = game.onLongPressCancel;\n        },\n      );\n    }\n    if (game is VerticalDragDetector) {\n      add(\n        VerticalDragGestureRecognizer.new,\n        (VerticalDragGestureRecognizer instance) {\n          instance.onDown = game.handleVerticalDragDown;\n          instance.onStart = game.handleVerticalDragStart;\n          instance.onUpdate = game.handleVerticalDragUpdate;\n          instance.onEnd = game.handleVerticalDragEnd;\n          instance.onCancel = game.onVerticalDragCancel;\n        },\n      );\n    }\n    if (game is HorizontalDragDetector) {\n      add(\n        HorizontalDragGestureRecognizer.new,\n        (HorizontalDragGestureRecognizer instance) {\n          instance.onDown = game.handleHorizontalDragDown;\n          instance.onStart = game.handleHorizontalDragStart;\n          instance.onUpdate = game.handleHorizontalDragUpdate;\n          instance.onEnd = game.handleHorizontalDragEnd;\n          instance.onCancel = game.onHorizontalDragCancel;\n        },\n      );\n    }\n    if (game is ForcePressDetector) {\n      add(\n        ForcePressGestureRecognizer.new,\n        (ForcePressGestureRecognizer instance) {\n          instance.onStart = game.handleForcePressStart;\n          instance.onPeak = game.handleForcePressPeak;\n          instance.onUpdate = game.handleForcePressUpdate;\n          instance.onEnd = game.handleForcePressEnd;\n        },\n      );\n    }\n    if (game is PanDetector) {\n      add(\n        PanGestureRecognizer.new,\n        (PanGestureRecognizer instance) {\n          instance.onDown = game.handlePanDown;\n          instance.onStart = game.handlePanStart;\n          instance.onUpdate = game.handlePanUpdate;\n          instance.onEnd = game.handlePanEnd;\n          instance.onCancel = game.onPanCancel;\n        },\n      );\n    }\n    if (game is ScaleDetector) {\n      add(\n        ScaleGestureRecognizer.new,\n        (ScaleGestureRecognizer instance) {\n          instance.onStart = game.handleScaleStart;\n          instance.onUpdate = game.handleScaleUpdate;\n          instance.onEnd = game.handleScaleEnd;\n        },\n      );\n    }\n    if (game is MultiTapListener) {\n      add(\n        MultiTapGestureRecognizer.new,\n        (MultiTapGestureRecognizer instance) {\n          final g = game as MultiTapListener;\n          instance.longTapDelay = Duration(\n            milliseconds: (g.longTapDelay * 1000).toInt(),\n          );\n          instance.onTap = g.handleTap;\n          instance.onTapDown = g.handleTapDown;\n          instance.onTapUp = g.handleTapUp;\n          instance.onTapCancel = g.handleTapCancel;\n          instance.onLongTapDown = g.handleLongTapDown;\n        },\n      );\n    }\n  }\n}\n\nbool hasMouseDetectors(Game game) {\n  return game is MouseMovementDetector ||\n      game is ScrollDetector ||\n      game.mouseDetector != null;\n}\n\nWidget applyMouseDetectors(Game game, Widget child) {\n  final mouseMoveFn = game is MouseMovementDetector ? game.onMouseMove : null;\n  final mouseDetector = game.mouseDetector;\n  return Listener(\n    child: MouseRegion(\n      child: child,\n      onHover: (PointerHoverEvent e) {\n        mouseMoveFn?.call(PointerHoverInfo.fromDetails(game, e));\n        mouseDetector?.call(e);\n      },\n    ),\n    onPointerSignal: (event) =>\n        game is ScrollDetector && event is PointerScrollEvent\n        ? game.onScroll(PointerScrollInfo.fromDetails(game, event))\n        : null,\n  );\n}\n"
  },
  {
    "path": "packages/flame/lib/src/game/mixins/has_performance_tracker.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/game.dart';\n\n/// A mixin that adds performance tracking to a game.\nmixin HasPerformanceTracker on Game {\n  int _updateTime = 0;\n  int _renderTime = 0;\n  final _stopwatch = Stopwatch();\n\n  /// The time it took to update the game in milliseconds.\n  int get updateTime => _updateTime;\n\n  /// The time it took to render the game in milliseconds.\n  int get renderTime => _renderTime;\n\n  @override\n  void update(double dt) {\n    _stopwatch.reset();\n    _stopwatch.start();\n    super.update(dt);\n    _stopwatch.stop();\n    _updateTime = _stopwatch.elapsedMilliseconds;\n  }\n\n  @override\n  void render(Canvas canvas) {\n    _stopwatch.reset();\n    _stopwatch.start();\n    super.render(canvas);\n    _stopwatch.stop();\n    _renderTime = _stopwatch.elapsedMilliseconds;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/game/mixins/keyboard.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\n/// A [FlameGame] mixin that implements [KeyboardEvents] with keyboard event\n/// propagation to components that are mixed with [KeyboardHandler].\n///\n/// Attention: should not be used alongside [KeyboardEvents] in a game subclass.\n/// Using this mixin remove the necessity of [KeyboardEvents].\nmixin HasKeyboardHandlerComponents<W extends World> on FlameGame<W>\n    implements KeyboardEvents {\n  @override\n  @mustCallSuper\n  KeyEventResult onKeyEvent(\n    KeyEvent event,\n    Set<LogicalKeyboardKey> keysPressed,\n  ) {\n    final blockedPropagation = !propagateToChildren<KeyboardHandler>(\n      (KeyboardHandler child) => child.onKeyEvent(event, keysPressed),\n    );\n\n    // If any component received the event, return handled,\n    // otherwise, ignore it.\n    if (blockedPropagation) {\n      return KeyEventResult.handled;\n    }\n    return KeyEventResult.ignored;\n  }\n}\n\n/// A [Game] mixin to make a game subclass sensitive to keyboard events.\n///\n/// Override [onKeyEvent] to customize the keyboard handling behavior.\nmixin KeyboardEvents on Game {\n  KeyEventResult onKeyEvent(\n    KeyEvent event,\n    Set<LogicalKeyboardKey> keysPressed,\n  ) {\n    assert(\n      this is! HasKeyboardHandlerComponents,\n      'A keyboard event was registered by KeyboardEvents for a game also '\n      'mixed with HasKeyboardHandlerComponents. Do not mix with both, '\n      'HasKeyboardHandlerComponents removes the necessity of KeyboardEvents',\n    );\n\n    return KeyEventResult.handled;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/game/mixins/single_game_instance.dart",
    "content": "import 'package:flame/src/components/core/component.dart';\nimport 'package:flame/src/game/game.dart';\n\n/// Mixin that declares a [Game] class as a singleton.\n///\n/// In many applications there is a single [Game] instance within the entire\n/// app. For example, most regular game apps fall within this category. If your\n/// application is structured like that, you can use this mixin to declare your\n/// game class as a \"singleton game\".\n///\n/// The effect of using this mixin is that it allows component lifecycle events\n/// to be resolved faster in certain situations. For example, [Component]'s\n/// `onLoad` will be able to run even if its parent is currently detached.\n///\n/// Using this mixin in applications where there are multiple [Game] instances\n/// attached to multiple GameWidgets, is invalid.\nmixin SingleGameInstance on Game {\n  @override\n  void onMount() {\n    Component.staticGameInstance = this;\n    super.onMount();\n  }\n\n  @override\n  void onRemove() {\n    super.onRemove();\n    Component.staticGameInstance = null;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/game/notifying_vector2.dart",
    "content": "import 'package:flutter/foundation.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// Extension of the standard [Vector2] class, implementing the [ChangeNotifier]\n/// functionality. This allows any interested party to be notified when the\n/// value of this vector changes.\n///\n/// This class can be used as a regular [Vector2] class. However, if you do\n/// subscribe to notifications, don't forget to eventually unsubscribe in\n/// order to avoid resource leaks.\n///\n/// Direct modification of this vector's [storage] is not allowed.\nclass NotifyingVector2 extends Vector2 with ChangeNotifier {\n  factory NotifyingVector2(double x, double y) =>\n      NotifyingVector2.zero()..setValues(x, y);\n\n  NotifyingVector2.zero() : super.zero();\n\n  factory NotifyingVector2.all(double v) => NotifyingVector2.zero()..splat(v);\n\n  factory NotifyingVector2.copy(Vector2 v) =>\n      NotifyingVector2.zero()..setFrom(v);\n\n  @override\n  void setValues(double x_, double y_) {\n    super.setValues(x_, y_);\n    notifyListeners();\n  }\n\n  @override\n  void setFrom(Vector2 other) {\n    super.setFrom(other);\n    notifyListeners();\n  }\n\n  @override\n  void setZero() {\n    super.setZero();\n    notifyListeners();\n  }\n\n  @override\n  void splat(double arg) {\n    super.splat(arg);\n    notifyListeners();\n  }\n\n  @override\n  void operator []=(int i, double v) {\n    super[i] = v;\n    notifyListeners();\n  }\n\n  @override\n  set length(double l) {\n    super.length = l;\n    notifyListeners();\n  }\n\n  @override\n  double normalize() {\n    final l = super.normalize();\n    notifyListeners();\n    return l;\n  }\n\n  @override\n  void postmultiply(Matrix2 arg) {\n    super.postmultiply(arg);\n    notifyListeners();\n  }\n\n  @override\n  void add(Vector2 arg) {\n    super.add(arg);\n    notifyListeners();\n  }\n\n  @override\n  void addScaled(Vector2 arg, double factor) {\n    super.addScaled(arg, factor);\n    notifyListeners();\n  }\n\n  @override\n  void sub(Vector2 arg) {\n    super.sub(arg);\n    notifyListeners();\n  }\n\n  @override\n  void multiply(Vector2 arg) {\n    super.multiply(arg);\n    notifyListeners();\n  }\n\n  @override\n  void divide(Vector2 arg) {\n    super.divide(arg);\n    notifyListeners();\n  }\n\n  @override\n  void scale(double arg) {\n    super.scale(arg);\n    notifyListeners();\n  }\n\n  @override\n  void negate() {\n    super.negate();\n    notifyListeners();\n  }\n\n  @override\n  void absolute() {\n    super.absolute();\n    notifyListeners();\n  }\n\n  @override\n  void clamp(Vector2 min, Vector2 max) {\n    super.clamp(min, max);\n    notifyListeners();\n  }\n\n  @override\n  void clampScalar(double min, double max) {\n    super.clampScalar(min, max);\n    notifyListeners();\n  }\n\n  @override\n  void floor() {\n    super.floor();\n    notifyListeners();\n  }\n\n  @override\n  void ceil() {\n    super.ceil();\n    notifyListeners();\n  }\n\n  @override\n  void round() {\n    super.round();\n    notifyListeners();\n  }\n\n  @override\n  void roundToZero() {\n    super.roundToZero();\n    notifyListeners();\n  }\n\n  @override\n  void copyFromArray(List<double> array, [int offset = 0]) {\n    super.copyFromArray(array, offset);\n    notifyListeners();\n  }\n\n  @override\n  set xy(Vector2 arg) {\n    super.xy = arg;\n    notifyListeners();\n  }\n\n  @override\n  set yx(Vector2 arg) {\n    super.yx = arg;\n    notifyListeners();\n  }\n\n  @override\n  set x(double x) {\n    super.x = x;\n    notifyListeners();\n  }\n\n  @override\n  set y(double y) {\n    super.y = y;\n    notifyListeners();\n  }\n\n  @override\n  Float32List get storage => super.storage.asUnmodifiableView();\n}\n"
  },
  {
    "path": "packages/flame/lib/src/game/overlay_manager.dart",
    "content": "import 'dart:collection';\n\nimport 'package:flame/src/game/game.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:meta/meta.dart';\n\n/// A helper class used to control the visibility of overlays on a [Game]\n/// instance. See [Game.overlays].\n@internal\nclass OverlayManager {\n  OverlayManager(this._game);\n\n  final Game _game;\n  final List<_OverlayData> _activeOverlays = [];\n  final Map<String, OverlayBuilderFunction> _builders = {};\n\n  /// The names of all currently active overlays.\n  UnmodifiableListView<String> get activeOverlays {\n    return UnmodifiableListView(_activeOverlays.map((overlay) => overlay.name));\n  }\n\n  /// The names of all registered overlays\n  UnmodifiableListView<String> get registeredOverlays {\n    return UnmodifiableListView(_builders.keys);\n  }\n\n  /// Returns if the given [overlayName] is active\n  bool isActive(String overlayName) =>\n      _activeOverlays.any((overlay) => overlay.name == overlayName);\n\n  /// Clears all active overlays.\n  void clear() {\n    _activeOverlays.clear();\n    _game.refreshWidget(isInternalRefresh: false);\n  }\n\n  /// Marks the [overlayName] to be rendered.\n  /// [priority] is used to sort widgets for [buildCurrentOverlayWidgets]\n  /// The smaller the priority, the sooner your component will be build.\n  bool add(String overlayName, {int priority = 0}) {\n    final setChanged = _addImpl(priority: priority, name: overlayName);\n    if (setChanged) {\n      _game.refreshWidget(isInternalRefresh: false);\n    }\n    return setChanged;\n  }\n\n  /// Marks [overlayNames] to be rendered.\n  void addAll(Iterable<String> overlayNames) {\n    final initialCount = _activeOverlays.length;\n    overlayNames.forEach((overlayName) => _addImpl(name: overlayName));\n    if (initialCount != _activeOverlays.length) {\n      _game.refreshWidget(isInternalRefresh: false);\n    }\n  }\n\n  bool _addImpl({required String name, int priority = 0}) {\n    assert(\n      _builders.containsKey(name),\n      'Trying to add an unknown overlay \"$name\"',\n    );\n    if (isActive(name)) {\n      return false;\n    }\n    _activeOverlays.add(_OverlayData(priority: priority, name: name));\n    _activeOverlays.sort(_compare);\n    return true;\n  }\n\n  _OverlayData? _getOverlay(String name) {\n    return _activeOverlays.where((overlay) => overlay.name == name).firstOrNull;\n  }\n\n  /// Adds a named overlay builder\n  void addEntry(String name, OverlayBuilderFunction builder) {\n    _builders[name] = builder;\n  }\n\n  /// Hides the [overlayName].\n  bool remove(String overlayName) {\n    final overlay = _getOverlay(overlayName);\n    final hasRemoved = _activeOverlays.remove(overlay);\n    if (hasRemoved) {\n      _game.refreshWidget(isInternalRefresh: false);\n    }\n    return hasRemoved;\n  }\n\n  /// Hides multiple overlays specified in [overlayNames].\n  void removeAll(Iterable<String> overlayNames) {\n    final initialCount = _activeOverlays.length;\n    _activeOverlays.removeWhere(\n      (overlay) => overlayNames.contains(overlay.name),\n    );\n    if (_activeOverlays.length != initialCount) {\n      _game.refreshWidget(isInternalRefresh: false);\n    }\n  }\n\n  /// Marks the [overlayName] to either be rendered or not, based on the\n  /// current state.\n  ///\n  /// [priority] is used to sort widgets for [buildCurrentOverlayWidgets]\n  /// The smaller the priority, the sooner your component will be build\n  /// (see [add] for more details).\n  bool toggle(String overlayName, {int priority = 0}) {\n    if (isActive(overlayName)) {\n      return remove(overlayName);\n    } else {\n      return add(overlayName, priority: priority);\n    }\n  }\n\n  @internal\n  List<Widget> buildCurrentOverlayWidgets(BuildContext context) {\n    final widgets = <Widget>[];\n    for (final overlay in _activeOverlays) {\n      final builder = _builders[overlay.name]!;\n      widgets.add(\n        KeyedSubtree(\n          key: ValueKey(overlay),\n          child: builder(context, _game),\n        ),\n      );\n    }\n    return widgets;\n  }\n\n  /// Comparator function used to sort overlays.\n  int _compare(_OverlayData a, _OverlayData b) {\n    return a.priority - b.priority;\n  }\n}\n\ntypedef OverlayBuilderFunction =\n    Widget Function(\n      BuildContext context,\n      Game game,\n    );\n\n@immutable\nclass _OverlayData {\n  final int priority;\n  final String name;\n\n  const _OverlayData({required this.priority, required this.name});\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is _OverlayData &&\n          runtimeType == other.runtimeType &&\n          priority == other.priority &&\n          name == other.name;\n\n  @override\n  int get hashCode => priority.hashCode ^ name.hashCode;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/game/transform2d.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:flame/geometry.dart' as geometry;\nimport 'package:flame/src/game/notifying_vector2.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// This class describes a generic 2D transform, which is a combination of\n/// translations, rotations, reflections and scaling. These transforms are\n/// combined into a single matrix, that can be either applied to a canvas,\n/// composed with another transform, or used directly to convert coordinates.\n///\n/// The transform can be visualized as 2 reference frames: a \"global\" and\n/// a \"local\". At first, these two reference frames coincide. Then, the\n/// following sequence of transforms is applied:\n///   - translation to point [position];\n///   - rotation by [angle] radians clockwise;\n///   - scaling in X and Y directions by [scale] factors;\n///   - final translation by [offset], in local coordinates.\n///\n/// The class is optimized for repeated use: the transform matrix is cached\n/// and then recalculated only when the underlying properties change. Moreover,\n/// recalculation of the transform is postponed until the matrix is actually\n/// requested by the user. Thus, modifying multiple properties at once does\n/// not incur the penalty of unnecessary recalculations.\n///\n/// This class implements the [ChangeNotifier] API, allowing you to subscribe\n/// for notifications whenever the transform matrix changes. In addition, you\n/// can subscribe to get notified when individual components of the transform\n/// change: [position], [scale], and [offset] (but not [angle]).\nclass Transform2D extends ChangeNotifier {\n  final Matrix4 _transformMatrix;\n  bool _recalculate;\n  bool _isBatchUpdating = false;\n  double _angle;\n  final NotifyingVector2 _position;\n  final NotifyingVector2 _scale;\n  final NotifyingVector2 _offset;\n\n  Transform2D()\n    : _transformMatrix = Matrix4.identity(),\n      _recalculate = true,\n      _angle = 0,\n      _position = NotifyingVector2.zero(),\n      _scale = NotifyingVector2.all(1),\n      _offset = NotifyingVector2.zero() {\n    _position.addListener(_markAsModified);\n    _scale.addListener(_markAsModified);\n    _offset.addListener(_markAsModified);\n  }\n\n  factory Transform2D.copy(Transform2D other) => Transform2D()\n    ..angle = other.angle\n    ..position = other.position\n    ..scale = other.scale\n    ..offset = other.offset;\n\n  /// Clone of this.\n  Transform2D clone() => Transform2D.copy(this);\n\n  /// Set this to the values of the [other] [Transform2D].\n  void setFrom(Transform2D other) {\n    _isBatchUpdating = true;\n    angle = other.angle;\n    position = other.position;\n    scale = other.scale;\n    offset = other.offset;\n    _isBatchUpdating = false;\n    notifyListeners();\n  }\n\n  /// Check whether this transform is equal to [other], up to the given\n  /// [tolerance]. Setting tolerance to zero will check for exact equality.\n  /// Transforms are considered equal if their rotation angles are the same\n  /// or differ by a multiple of 2π, and if all other transform parameters:\n  /// translation, scale, and offset are the same.\n  ///\n  /// The [tolerance] parameter is in absolute units, not relative.\n  bool closeTo(Transform2D other, {double tolerance = 1e-10}) {\n    final deltaAngle = (angle - other.angle) % geometry.tau;\n    assert(deltaAngle >= 0);\n    return (deltaAngle <= tolerance ||\n            deltaAngle >= geometry.tau - tolerance) &&\n        (position.x - other.position.x).abs() <= tolerance &&\n        (position.y - other.position.y).abs() <= tolerance &&\n        (scale.x - other.scale.x).abs() <= tolerance &&\n        (scale.y - other.scale.y).abs() <= tolerance &&\n        (offset.x - other.offset.x).abs() <= tolerance &&\n        (offset.y - other.offset.y).abs() <= tolerance;\n  }\n\n  /// The translation part of the transform. This translation is applied\n  /// relative to the global coordinate space.\n  ///\n  /// The returned vector can be modified by the user, and the changes\n  /// will be propagated back to the transform matrix.\n  NotifyingVector2 get position => _position;\n  set position(Vector2 position) => _position.setFrom(position);\n\n  /// X coordinate of the translation transform.\n  double get x => _position.x;\n  set x(double x) => _position.x = x;\n\n  /// Y coordinate of the translation transform.\n  double get y => _position.y;\n  set y(double y) => _position.y = y;\n\n  /// The rotation part of the transform. This represents rotation around\n  /// the [position] point in clockwise direction by [angle] radians. If\n  /// the angle is negative then the rotation is counterclockwise.\n  double get angle => _angle;\n  set angle(double a) {\n    _angle = a;\n    _markAsModified();\n  }\n\n  /// Similar to [angle], but uses degrees instead of radians.\n  double get angleDegrees => _angle * (360 / geometry.tau);\n  set angleDegrees(double a) {\n    _angle = a * (geometry.tau / 360);\n    _markAsModified();\n  }\n\n  /// The scale part of the transform. The default scale factor is (1, 1),\n  /// a scale greater than 1 corresponds to expansion, and less than 1 is\n  /// contraction. A negative scale is also allowed, and it corresponds\n  /// to a mirror reflection around the corresponding axis.\n  /// Scale factors can be different for X and Y directions.\n  ///\n  /// The returned vector can be modified by the user, and the changes\n  /// will be propagated back to the transform matrix.\n  NotifyingVector2 get scale => _scale;\n  set scale(Vector2 scale) => _scale.setFrom(scale);\n\n  /// Additional offset applied after all other transforms. Unlike other\n  /// transforms, this offset is applied in the local coordinate system.\n  /// For example, an [offset] of (1, 0) describes a shift by 1 unit along\n  /// the X axis, however, this shift is applied after that axis was\n  /// repositioned, rotated and scaled.\n  ///\n  /// The returned vector can be modified by the user, and the changes\n  /// will be properly applied to the transform matrix.\n  NotifyingVector2 get offset => _offset;\n  set offset(Vector2 offset) => _offset.setFrom(offset);\n\n  /// Flip the coordinate system horizontally.\n  void flipHorizontally() {\n    _scale.x = -_scale.x;\n  }\n\n  /// Flip the coordinate system vertically.\n  void flipVertically() {\n    _scale.y = -_scale.y;\n  }\n\n  /// The total transformation matrix for the component. This matrix combines\n  /// translation, rotation, reflection and scale transforms into a single\n  /// entity. The matrix is cached and gets recalculated only as necessary.\n  ///\n  /// The returned matrix must not be modified by the user.\n  Matrix4 get transformMatrix {\n    if (_recalculate) {\n      // The transforms below are equivalent to:\n      //   _transformMatrix = Matrix4.identity()\n      //       .. translate(_position.x, _position.y)\n      //       .. rotateZ(_angle)\n      //       .. scale(_scale.x, _scale.y, 1)\n      //       .. translate(_offset.x, _offset.y);\n      final m = _transformMatrix.storage;\n      final cosA = math.cos(_angle);\n      final sinA = math.sin(_angle);\n      m[0] = cosA * _scale.x;\n      m[1] = sinA * _scale.x;\n      m[4] = -sinA * _scale.y;\n      m[5] = cosA * _scale.y;\n      m[12] = _position.x + m[0] * _offset.x + m[4] * _offset.y;\n      m[13] = _position.y + m[1] * _offset.x + m[5] * _offset.y;\n      _recalculate = false;\n    }\n    return _transformMatrix;\n  }\n\n  set transformMatrix(Matrix4 value) {\n    assert(\n      value.storage[2] == 0 &&\n          value.storage[3] == 0 &&\n          value.storage[6] == 0 &&\n          value.storage[7] == 0 &&\n          value.storage[8] == 0 &&\n          value.storage[9] == 0 &&\n          value.storage[10] == 1 &&\n          value.storage[11] == 0 &&\n          value.storage[14] == 0 &&\n          value.storage[15] == 1,\n      'The provided matrix is not a valid 2D transformation',\n    );\n    _transformMatrix.setFrom(value);\n    _recalculate = false;\n\n    final storage = _transformMatrix.storage;\n    _isBatchUpdating = true;\n    final determinant = storage[0] * storage[5] - storage[1] * storage[4];\n    _scale.x = math.sqrt(storage[0] * storage[0] + storage[1] * storage[1]);\n    if (_scale.x == 0) {\n      _angle = math.atan2(-storage[4], storage[5]);\n      _scale.y = math.sqrt(storage[4] * storage[4] + storage[5] * storage[5]);\n    } else {\n      _angle = math.atan2(storage[1], storage[0]);\n      _scale.y = determinant / _scale.x;\n    }\n\n    _position.x =\n        storage[12] - (storage[0] * _offset.x + storage[4] * _offset.y);\n    _position.y =\n        storage[13] - (storage[1] * _offset.x + storage[5] * _offset.y);\n    _isBatchUpdating = false;\n    notifyListeners();\n  }\n\n  /// Transform [point] from local coordinates into the parent coordinate space.\n  /// Effectively, this function applies the current transform to [point].\n  ///\n  /// Use [output] to send in a Vector2 object that will be used to avoid\n  /// creating a new Vector2 object in this method.\n  Vector2 localToGlobal(Vector2 point, {Vector2? output}) {\n    final m = transformMatrix.storage;\n    final x = m[0] * point.x + m[4] * point.y + m[12];\n    final y = m[1] * point.x + m[5] * point.y + m[13];\n    return (output?..setValues(x, y)) ?? Vector2(x, y);\n  }\n\n  /// Transform [point] from the global coordinate space into the local\n  /// coordinates. Thus, this method performs the inverse of the current\n  /// transform.\n  ///\n  /// If the current transform is degenerate due to one of the scale\n  /// factors being 0, then this method will return a zero vector.\n  ///\n  /// Use [output] to send in a Vector2 object that will be used to avoid\n  /// creating a new Vector2 object in this method.\n  Vector2 globalToLocal(Vector2 point, {Vector2? output}) {\n    // Here we rely on the fact that in the transform matrix only elements\n    // `m[0]`, `m[1]`, `m[4]`, `m[5]`, `m[12]`, and `m[13]` are modified.\n    // This greatly simplifies computation of the inverse matrix.\n    final m = transformMatrix.storage;\n    var det = m[0] * m[5] - m[1] * m[4];\n    if (det != 0) {\n      det = 1 / det;\n    }\n    final x = ((point.x - m[12]) * m[5] - (point.y - m[13]) * m[4]) * det;\n    final y = ((point.y - m[13]) * m[0] - (point.x - m[12]) * m[1]) * det;\n    return (output?..setValues(x, y)) ?? Vector2(x, y);\n  }\n\n  /// Whether the transform represents a pure translation, i.e. a transform of\n  /// the form `(x, y) -> (x + Δx, y + Δy)`.\n  bool get isTranslation {\n    return _angle == 0 && _scale.x == 1 && _scale.y == 1;\n  }\n\n  /// Whether the transform keeps horizontal (vertical) lines as horizontal\n  /// (vertical).\n  bool get isAxisAligned => _angle == 0;\n\n  /// Whether the transform preserves angles. A conformal transformation may\n  /// consist of a translation, rotation, and uniform scaling. A reflection is\n  /// not considered conformal.\n  bool get isConformal => _scale.x == _scale.y;\n\n  /// Whether the transform includes a reflection, i.e. it flips the orientation\n  /// of the coordinate system.\n  bool get hasReflection => _scale.x.sign * _scale.y.sign == -1;\n\n  void _markAsModified() {\n    _recalculate = true;\n    if (!_isBatchUpdating) {\n      notifyListeners();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/geometry/circle_component.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/src/math/solve_quadratic.dart';\n\nclass CircleComponent extends ShapeComponent {\n  /// With this constructor you can create your [CircleComponent] from a radius\n  /// and a position. It will also calculate the bounding rectangle [size] for\n  /// the [CircleComponent].\n  CircleComponent({\n    double? radius,\n    super.position,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.paint,\n    super.paintLayers,\n    super.key,\n  }) : super(size: Vector2.all((radius ?? 0) * 2)) {\n    _updateCenterOffset();\n    size.addListener(_updateCenterOffset);\n  }\n\n  /// With this constructor you define the [CircleComponent] in relation to the\n  /// [parentSize]. For example having a [relation] of 0.5 would create a circle\n  /// that fills half of the [parentSize].\n  CircleComponent.relative(\n    double relation, {\n    required Vector2 parentSize,\n    super.position,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.paint,\n    super.paintLayers,\n    super.children,\n    super.key,\n  }) : super(size: Vector2.all(relation * min(parentSize.x, parentSize.y))) {\n    _updateCenterOffset();\n    size.addListener(_updateCenterOffset);\n  }\n\n  Offset _centerOffset = Offset.zero;\n\n  void _updateCenterOffset() {\n    _centerOffset = Offset(size.x / 2, size.y / 2);\n  }\n\n  /// Get the radius of the circle before scaling.\n  double get radius {\n    return min(size.x, size.y) / 2;\n  }\n\n  /// Set the radius of the circle (and therefore the [size]).\n  set radius(double value) {\n    size.setValues(value * 2, value * 2);\n  }\n\n  // Used to not create new Vector2 objects every time radius is called.\n  final Vector2 _scaledSize = Vector2.zero();\n\n  /// Get the radius of the circle after it has been sized and scaled.\n  double get scaledRadius {\n    _scaledSize\n      ..setFrom(size)\n      ..multiply(absoluteScale);\n    return min(_scaledSize.x, _scaledSize.y) / 2;\n  }\n\n  @override\n  void render(Canvas canvas) {\n    if (renderShape) {\n      if (hasPaintLayers) {\n        for (final paint in paintLayers) {\n          canvas.drawCircle(_centerOffset, radius, paint);\n        }\n      } else {\n        canvas.drawCircle(_centerOffset, radius, paint);\n      }\n    }\n  }\n\n  @override\n  void renderDebugMode(Canvas canvas) {\n    super.renderDebugMode(canvas);\n    canvas.drawCircle(_centerOffset, radius, debugPaint);\n  }\n\n  /// Checks whether the represented circle contains the [point].\n  @override\n  bool containsPoint(Vector2 point) {\n    final scaledRadius = this.scaledRadius;\n    return absoluteCenter.distanceToSquared(point) <\n        scaledRadius * scaledRadius;\n  }\n\n  @override\n  bool containsLocalPoint(Vector2 point) {\n    final radius = size.x / 2;\n    final dx = point.x - radius;\n    final dy = point.y - radius;\n    return dx * dx + dy * dy <= radius * radius;\n  }\n\n  /// Returns the locus of points in which the provided line segment intersects\n  /// the circle.\n  ///\n  /// This can be an empty list (if they don't intersect), one point (if the\n  /// line is tangent) or two points (if the line is secant).\n  /// An edge point of the [lineSegment] that originates on the edge of the\n  /// circle doesn't count as an intersection.\n  List<Vector2> lineSegmentIntersections(\n    LineSegment lineSegment, {\n    double epsilon = double.minPositive,\n  }) {\n    // A point on a line is `from + t*(to - from)`. We're trying to solve the\n    // equation `‖point - center‖² == radius²`. Or, denoting `Δ₂₁ = to - from`\n    // and `Δ₁₀ = from - center`, the equation is `‖t*Δ₂₁ + Δ₁₀‖² == radius²`.\n    // Expanding the norm, this becomes a square equation in `t`:\n    // `t²Δ₂₁² + 2tΔ₂₁Δ₁₀ + Δ₁₀² - radius² == 0`.\n    _delta21\n      ..setFrom(lineSegment.to)\n      ..sub(lineSegment.from); // to - from\n    _delta10\n      ..setFrom(lineSegment.from)\n      ..sub(absoluteCenter); // from - absoluteCenter\n    final a = _delta21.length2;\n    final b = 2 * _delta21.dot(_delta10);\n    final effectiveRadius = scaledRadius;\n    final c = _delta10.length2 - effectiveRadius * effectiveRadius;\n\n    return solveQuadratic(a, b, c)\n        .where((t) => t > 0 && t <= 1)\n        .map((t) => lineSegment.from.clone()..addScaled(_delta21, t))\n        .toList();\n  }\n\n  static final Vector2 _delta21 = Vector2.zero();\n  static final Vector2 _delta10 = Vector2.zero();\n}\n"
  },
  {
    "path": "packages/flame/lib/src/geometry/constants.dart",
    "content": "import 'dart:math';\n\n/// A simpler constant to use for angles than 2pi (well it is 2pi).\n///\n/// For example: tau/2 is 180 degrees, or pi radians.\nconst tau = pi * 2;\n"
  },
  {
    "path": "packages/flame/lib/src/geometry/line.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/extensions.dart';\n\n/// An infinite line on the 2D Cartesian space, represented in the form\n/// of ax + by = c.\n///\n/// If you just want to represent a part of a line, look into LineSegment.\nclass Line {\n  final double a;\n  final double b;\n  final double c;\n\n  const Line(this.a, this.b, this.c);\n\n  Line.fromPoints(Vector2 p1, Vector2 p2)\n    : this(\n        p2.y - p1.y,\n        p1.x - p2.x,\n        p2.y * p1.x - p1.y * p2.x,\n      );\n\n  /// Returns an empty list if there is no intersection\n  /// If the lines are concurrent it returns one point in the list.\n  /// If they coincide it returns an empty list as well\n  List<Vector2> intersections(Line otherLine) {\n    final determinant = a * otherLine.b - otherLine.a * b;\n    if (determinant == 0) {\n      //The lines are parallel (potentially coincides) and have no intersection\n      return [];\n    }\n    return [\n      Vector2(\n        (otherLine.b * c - b * otherLine.c) / determinant,\n        (a * otherLine.c - otherLine.a * c) / determinant,\n      ),\n    ];\n  }\n\n  /// The angle of this line in relation to the x-axis\n  double get angle => atan2(-a, b);\n\n  @override\n  String toString() {\n    final ax = '${a}x';\n    final by = b.isNegative ? '${b}y' : '+${b}y';\n    return '$ax$by=$c';\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/geometry/line_segment.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/geometry/line.dart';\n\n/// A [LineSegment] represent a segment of an infinitely long line, it is the\n/// segment between the [from] and [to] vectors (inclusive).\nclass LineSegment {\n  final Vector2 from;\n  final Vector2 to;\n\n  /// Creates a [LineSegment] given a start ([from]) point and an end ([to])\n  /// point.\n  LineSegment(this.from, this.to);\n\n  /// Creates a [LineSegment] starting at a given a [start] point and following\n  /// a certain [direction] for a given [length].\n  LineSegment.withLength({\n    required Vector2 start,\n    required Vector2 direction,\n    required double length,\n  }) : this(start, start + direction.normalized() * length);\n\n  factory LineSegment.zero() => LineSegment(Vector2.zero(), Vector2.zero());\n\n  /// A unit vector representing the direction of the line segment.\n  Vector2 get direction => (to - from)..normalize();\n\n  /// The length of the line segment.\n  double get length => (to - from).length;\n\n  /// The point in the center of this line segment.\n  Vector2 get midpoint => (from + to)..scale(0.5);\n\n  /// Spreads points evenly along the line segment.\n  /// A number of points [amount] are returned; the edges are not included.\n  List<Vector2> spread(int amount) {\n    if (amount < 0) {\n      throw ArgumentError('Amount must be non-negative');\n    }\n    if (amount == 0) {\n      return [];\n    }\n\n    final step = length / (amount + 1);\n    return [\n      for (var i = 1; i <= amount; i++) from + direction * (i * step),\n    ];\n  }\n\n  /// Returns a new [LineSegment] translated by a given displacement [offset].\n  LineSegment translate(Vector2 offset) {\n    return LineSegment(from + offset, to + offset);\n  }\n\n  /// Returns a new [LineSegment] with same direction but extended by [amount]\n  /// on both sides.\n  LineSegment inflate(double amount) {\n    final direction = this.direction;\n    return LineSegment(from - direction * amount, to + direction * amount);\n  }\n\n  /// Returns a new [LineSegment] with same direction but reduced by [amount]\n  /// on both sides.\n  LineSegment deflate(double amount) {\n    return inflate(-amount);\n  }\n\n  /// Returns an empty list if there are no intersections between the segments\n  /// If the segments are concurrent, the intersecting point is returned as a\n  /// list with a single point\n  List<Vector2> intersections(LineSegment otherSegment) {\n    final result = toLine().intersections(otherSegment.toLine());\n    if (result.isNotEmpty) {\n      // The lines are not parallel\n      final intersection = result.first;\n      if (containsPoint(intersection) &&\n          otherSegment.containsPoint(intersection)) {\n        // The intersection point is on both line segments\n        return result;\n      }\n    } else {\n      // In here we know that the lines are parallel\n      final overlaps = {\n        if (otherSegment.containsPoint(from)) from,\n        if (otherSegment.containsPoint(to)) to,\n        if (containsPoint(otherSegment.from)) otherSegment.from,\n        if (containsPoint(otherSegment.to)) otherSegment.to,\n      };\n      if (overlaps.isNotEmpty) {\n        final sum = Vector2.zero();\n        for (final overlap in overlaps) {\n          sum.add(overlap);\n        }\n        return [sum..scale(1 / overlaps.length)];\n      }\n    }\n    return [];\n  }\n\n  /// Whether the given [point] lies in this line segment.\n  bool containsPoint(Vector2 point, {double epsilon = 0.01}) {\n    final delta = to - from;\n    final crossProduct =\n        (point.y - from.y) * delta.x - (point.x - from.x) * delta.y;\n\n    // compare versus epsilon for floating point values\n    if (crossProduct.abs() > epsilon) {\n      return false;\n    }\n\n    final dotProduct =\n        (point.x - from.x) * delta.x + (point.y - from.y) * delta.y;\n    if (dotProduct < 0) {\n      return false;\n    }\n\n    final squaredLength = from.distanceToSquared(to);\n    if (dotProduct > squaredLength) {\n      return false;\n    }\n\n    return true;\n  }\n\n  bool pointsAt(Line line) {\n    final result = toLine().intersections(line);\n    if (result.isNotEmpty) {\n      final delta = to - from;\n      final intersection = result.first;\n      final intersectionDelta = intersection - to;\n      // Whether the two points [from] and [through] forms a ray that points on\n      // the line represented by this object\n      if (delta.x.sign == intersectionDelta.x.sign &&\n          delta.y.sign == intersectionDelta.y.sign) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  Line toLine() => Line.fromPoints(from, to);\n\n  @override\n  String toString() {\n    return '[$from, $to]';\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/geometry/polygon_component.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:collection/collection.dart';\nimport 'package:flame/cache.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:meta/meta.dart';\n\nclass PolygonComponent extends ShapeComponent {\n  final List<Vector2> _vertices;\n  UnmodifiableListView<Vector2> get vertices => UnmodifiableListView(_vertices);\n  // These lists are used to minimize the amount of objects that are created,\n  // and only change the contained object if the corresponding `ValueCache` is\n  // deemed outdated.\n  late final List<Vector2> _globalVertices;\n  late final List<LineSegment> _lineSegments;\n  final Path _path = Path();\n  final bool shrinkToBounds;\n  final bool manuallyPositioned;\n\n  final _cachedGlobalVertices = ValueCache<List<Vector2>>();\n\n  /// With this constructor you create your [PolygonComponent] from positions in\n  /// anywhere in the 2d-space. It will automatically calculate the [size] of\n  /// the Polygon (the bounding box) if no size it given.\n  PolygonComponent(\n    this._vertices, {\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.paint,\n    super.paintLayers,\n    super.key,\n    bool? shrinkToBounds,\n  }) : assert(\n         _vertices.length > 2,\n         'Number of vertices are too few to create a polygon',\n       ),\n       shrinkToBounds = shrinkToBounds ?? size == null,\n       manuallyPositioned = position != null {\n    refreshVertices(newVertices: _vertices);\n\n    final verticesLength = _vertices.length;\n    _globalVertices = List.generate(\n      verticesLength,\n      (_) => Vector2.zero(),\n      growable: false,\n    );\n    _lineSegments = List.generate(\n      verticesLength,\n      (_) => LineSegment.zero(),\n      growable: false,\n    );\n  }\n\n  /// With this constructor you define the [PolygonComponent] in relation to the\n  /// [parentSize] of the shape.\n  ///\n  /// Example: `[[1.0, 0.0], [0.0, -1.0], [-1.0, 0.0], [0.0, 1.0]]`\n  /// This will form a diamond shape within the bounding size box.\n  /// NOTE: Always define your shape in a counter-clockwise fashion (in the\n  /// screen coordinate system).\n  PolygonComponent.relative(\n    List<Vector2> relation, {\n    required Vector2 parentSize,\n    Vector2? position,\n    Vector2? scale,\n    double? angle,\n    Anchor? anchor,\n    int? priority,\n    Paint? paint,\n    List<Paint>? paintLayers,\n    bool? shrinkToBounds,\n    ComponentKey? key,\n    List<Component>? children,\n  }) : this(\n         normalsToVertices(relation, parentSize),\n         position: position,\n         angle: angle,\n         anchor: anchor,\n         scale: scale,\n         priority: priority,\n         paint: paint,\n         paintLayers: paintLayers,\n         shrinkToBounds: shrinkToBounds,\n         key: key,\n         children: children,\n       );\n\n  @internal\n  static List<Vector2> normalsToVertices(\n    List<Vector2> normals,\n    Vector2 size,\n  ) {\n    final halfSize = size / 2;\n    return normals\n        .map(\n          (v) => v.clone()\n            ..multiply(halfSize)\n            ..add(halfSize),\n        )\n        .toList(growable: false);\n  }\n\n  @protected\n  void refreshVertices({\n    required List<Vector2> newVertices,\n    bool? shrinkToBoundsOverride,\n  }) {\n    assert(\n      newVertices.length == _vertices.length,\n      'A polygon can not change their number of vertices',\n    );\n    // If the list isn't ccw we have to reverse the order in order for\n    // `containsPoint` to work.\n    if (_isClockwise(newVertices)) {\n      newVertices.reverse();\n    }\n    final topLeft = Vector2.zero();\n    topLeft.setFrom(newVertices[0]);\n    for (var i = 0; i < newVertices.length; i++) {\n      final newVertex = newVertices[i];\n      _vertices[i].setFrom(newVertex);\n      topLeft.x = min(topLeft.x, newVertex.x);\n      topLeft.y = min(topLeft.y, newVertex.y);\n    }\n    for (var i = 0; i < newVertices.length; i++) {\n      final newVertex = newVertices[i];\n      _vertices[i].setFrom(newVertex - topLeft);\n    }\n    _path\n      ..reset()\n      ..addPolygon(\n        _vertices.map((p) => p.toOffset()).toList(growable: false),\n        true,\n      );\n    if (shrinkToBoundsOverride ?? shrinkToBounds) {\n      final bounds = _path.getBounds();\n      size.setValues(bounds.width, bounds.height);\n      if (!manuallyPositioned) {\n        position = Anchor.topLeft.toOtherAnchorPosition(topLeft, anchor, size);\n      }\n    }\n  }\n\n  /// gives back the shape vectors multiplied by the size and scale\n  List<Vector2> globalVertices() {\n    final scale = absoluteScale;\n    final shouldReverse = scale.y.isNegative ^ scale.x.isNegative;\n    final angle = absoluteAngle;\n    final position = absoluteTopLeftPosition;\n    if (!_cachedGlobalVertices.isCacheValid<dynamic>(<dynamic>[\n      position,\n      size,\n      scale,\n      angle,\n    ])) {\n      for (var i = 0; i < _vertices.length; i++) {\n        _globalVertices[i].setFrom(absolutePositionOf(_vertices[i]));\n      }\n      if (shouldReverse) {\n        // Since the list will be clockwise we have to reverse it for it to\n        // become counterclockwise.\n        _reverseList(_globalVertices);\n      }\n      _cachedGlobalVertices.updateCache<dynamic>(\n        _globalVertices,\n        <dynamic>[position.clone(), size.clone(), scale.clone(), angle],\n      );\n    }\n    return _cachedGlobalVertices.value!;\n  }\n\n  @override\n  void render(Canvas canvas) {\n    if (renderShape) {\n      if (hasPaintLayers) {\n        for (final paint in paintLayers) {\n          canvas.drawPath(_path, paint);\n        }\n      } else {\n        canvas.drawPath(_path, paint);\n      }\n    }\n  }\n\n  @override\n  void renderDebugMode(Canvas canvas) {\n    super.renderDebugMode(canvas);\n    canvas.drawPath(_path, debugPaint);\n  }\n\n  bool _containsPoint(Vector2 point, List<Vector2> vertices) {\n    // If the size is 0 then it can't contain any points\n    if (size.x == 0 || size.y == 0) {\n      return false;\n    }\n\n    // Count the amount of edges crossed by going left from the point\n    var count = 0;\n    for (var i = 0; i < vertices.length; i++) {\n      final from = vertices[i];\n      final to = vertices[(i + 1) % vertices.length];\n\n      // Skip if the edge is entirely to the right, above or below the point\n      if (from.x > point.x && to.x > point.x ||\n          min(from.y, to.y) > point.y ||\n          max(from.y, to.y) < point.y) {\n        continue;\n      }\n\n      // Get x coordinate of where the edge intersects with the horizontal line\n      double intersectionX;\n      if (from.y == to.y) {\n        intersectionX = min(from.x, to.x);\n      } else {\n        intersectionX =\n            ((point.y - from.y) * (to.x - from.x)) / (to.y - from.y) + from.x;\n      }\n\n      if (intersectionX == point.x) {\n        // If the point is on the edge, return true\n        return true;\n      } else if (intersectionX < point.x) {\n        // Only count one edge if vertex is crossed\n        // Only count if edges cross the line, not just touch it and go back\n        if ((from.y != point.y && to.y != point.y) ||\n            to.y == from.y ||\n            point.y == max(from.y, to.y)) {\n          count++;\n        }\n      }\n    }\n\n    // If the amount of edges crossed is odd, the point is inside the polygon\n    return (count % 2).isOdd;\n  }\n\n  @override\n  bool containsPoint(Vector2 point) {\n    final vertices = globalVertices();\n    return _containsPoint(point, vertices);\n  }\n\n  @override\n  bool containsLocalPoint(Vector2 point) {\n    return _containsPoint(point, _vertices);\n  }\n\n  /// Return all vertices as [LineSegment]s that intersect [rect], if [rect]\n  /// is null return all vertices as [LineSegment]s.\n  List<LineSegment> possibleIntersectionVertices(Rect? rect) {\n    final rectIntersections = <LineSegment>[];\n    if ((rect?.width == 0 || false) ||\n        (rect?.height == 0 || false) ||\n        width == 0 ||\n        height == 0) {\n      return rectIntersections;\n    }\n    final vertices = globalVertices();\n    for (var i = 0; i < vertices.length; i++) {\n      final edge = getEdge(i, vertices: vertices);\n      if (rect?.intersectsSegment(edge.from, edge.to) ?? true) {\n        rectIntersections.add(edge);\n      }\n    }\n    return rectIntersections;\n  }\n\n  LineSegment getEdge(int i, {required List<Vector2> vertices}) {\n    _lineSegments[i].from.setFrom(getVertex(i, vertices: vertices));\n    _lineSegments[i].to.setFrom(getVertex(i + 1, vertices: vertices));\n    return _lineSegments[i];\n  }\n\n  Vector2 getVertex(int i, {List<Vector2>? vertices}) {\n    vertices ??= globalVertices();\n    return vertices[i % vertices.length];\n  }\n\n  void _reverseList(List<Object> list) {\n    for (var i = 0; i < list.length / 2; i++) {\n      final temp = list[i];\n      list[i] = list[list.length - 1 - i];\n      list[list.length - 1 - i] = temp;\n    }\n  }\n\n  bool _isClockwise(List<Vector2> vertices) {\n    var area = 0.0;\n    for (var i = 0; i < vertices.length; i++) {\n      final j = (i + 1) % vertices.length;\n      area += vertices[i].x * vertices[j].y - vertices[j].x * vertices[i].y;\n    }\n    return area >= 0;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/geometry/polygon_ray_intersection.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/geometry.dart';\n\n/// Used to add the [rayIntersection] method to [RectangleHitbox] and\n/// [PolygonHitbox], used by the raytracing and raycasting methods.\nmixin PolygonRayIntersection<T extends ShapeHitbox> on PolygonComponent {\n  late final _temporaryNormal = Vector2.zero();\n\n  /// Returns whether the [RaycastResult] if the [ray] intersects the polygon.\n  ///\n  /// If [out] is defined that is used to populate with the result and then\n  /// returned, to minimize the creation of new objects.\n  RaycastResult<ShapeHitbox>? rayIntersection(\n    Ray2 ray, {\n    RaycastResult<ShapeHitbox>? out,\n  }) {\n    final vertices = globalVertices();\n    var closestDistance = double.infinity;\n    LineSegment? closestSegment;\n    var crossings = 0;\n    var isOverlappingPoint = false;\n    for (var i = 0; i < vertices.length; i++) {\n      final lineSegment = getEdge(i, vertices: vertices);\n      final distance = ray.lineSegmentIntersection(lineSegment);\n      // Using a small value above 0 just because of rounding errors later that\n      // might cause a ray to go in the wrong direction.\n      if (distance != null && distance > 0.0000000001) {\n        crossings++;\n        if (distance < closestDistance) {\n          isOverlappingPoint = false;\n          closestDistance = distance;\n          closestSegment = lineSegment;\n        } else if (distance == closestDistance) {\n          isOverlappingPoint = true;\n        }\n      }\n    }\n    if (crossings > 0) {\n      final intersectionPoint = ray.point(\n        closestDistance,\n        out: out?.intersectionPoint,\n      );\n      // This is \"from\" to \"to\" since it is defined ccw in the canvas\n      // coordinate system\n      _temporaryNormal\n        ..setFrom(closestSegment!.from)\n        ..sub(closestSegment.to);\n      _temporaryNormal\n        ..setValues(_temporaryNormal.y, -_temporaryNormal.x)\n        ..normalize();\n      var isInsideHitbox = false;\n      if (crossings == 1 || isOverlappingPoint) {\n        _temporaryNormal.invert();\n        isInsideHitbox = true;\n      }\n      final reflectionDirection =\n          (out?.reflectionRay?.direction ?? Vector2.zero())\n            ..setFrom(ray.direction)\n            ..reflect(_temporaryNormal);\n      // Reflect() can introduce sub-epsilon drift. Normalize to keep Ray2's\n      // unit-length assertion satisfied.\n      reflectionDirection.normalize();\n\n      final reflectionRay =\n          (out?.reflectionRay?..setWith(\n            origin: intersectionPoint,\n            direction: reflectionDirection,\n          )) ??\n          Ray2(origin: intersectionPoint, direction: reflectionDirection);\n      return (out ?? RaycastResult<ShapeHitbox>())..setWith(\n        hitbox: this as T,\n        reflectionRay: reflectionRay,\n        normal: _temporaryNormal,\n        distance: closestDistance,\n        isInsideHitbox: isInsideHitbox,\n      );\n    }\n    out?.reset();\n    return null;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/geometry/ray2.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:meta/meta.dart';\n\n/// A ray in the 2d plane.\n///\n/// The [direction] should be normalized.\nclass Ray2 {\n  Ray2({required this.origin, required Vector2 direction}) {\n    this.direction = direction;\n  }\n\n  Ray2.zero() : this(origin: Vector2.zero(), direction: Vector2(1, 0));\n\n  /// The point where the ray originates from.\n  Vector2 origin;\n\n  /// The normalized direction of the ray.\n  ///\n  /// The values within the direction object should not be updated manually, use\n  /// the setter instead.\n  Vector2 get direction => _direction;\n  set direction(Vector2 direction) {\n    _direction.setFrom(direction);\n    _updateInverses();\n  }\n\n  final Vector2 _direction = Vector2.zero();\n\n  /// Should be called if the [direction] values are updated within the object\n  /// instead of by the setter.\n  void _updateInverses() {\n    assert(\n      (direction.length2 - 1).abs() < 0.000001,\n      'direction must be normalized',\n    );\n    directionInvX = (1 / direction.x).toFinite();\n    directionInvY = (1 / direction.y).toFinite();\n  }\n\n  // These are the inverse of the direction (the normal), they are used to avoid\n  // a division in [intersectsWithAabb2], since a ray will usually be tried\n  // against many bounding boxes it's good to pre-calculate it, which is done\n  // in the direction setter.\n  @visibleForTesting\n  late double directionInvX;\n  @visibleForTesting\n  late double directionInvY;\n\n  /// Whether the ray intersects the [box] or not.\n  ///\n  /// Rays that originate on the edge of the [box] are considered to be\n  /// intersecting with the box no matter what direction they have.\n  // This uses the Branchless Ray/Bounding box intersection method by Tavian,\n  // but since +-infinity is replaced by +-maxFinite for directionInvX and\n  // directionInvY, rays that originate on an edge will always be considered to\n  // intersect with the aabb, no matter what direction they have.\n  // https://tavianator.com/2011/ray_box.html\n  // https://tavianator.com/2015/ray_box_nan.html\n  bool intersectsWithAabb2(Aabb2 box) {\n    final tx1 = (box.min.x - origin.x) * directionInvX;\n    final tx2 = (box.max.x - origin.x) * directionInvX;\n\n    final ty1 = (box.min.y - origin.y) * directionInvY;\n    final ty2 = (box.max.y - origin.y) * directionInvY;\n\n    final tMin = max(min(tx1, tx2), min(ty1, ty2));\n    final tMax = min(max(tx1, tx2), max(ty1, ty2));\n\n    return tMax >= max(tMin, 0);\n  }\n\n  /// Gives the point at a certain length along the ray.\n  Vector2 point(double length, {Vector2? out}) {\n    return ((out?..setFrom(origin)) ?? origin.clone())\n      ..addScaled(direction, length);\n  }\n\n  static final Vector2 _v1 = Vector2.zero();\n  static final Vector2 _v2 = Vector2.zero();\n  static final Vector2 _v3 = Vector2.zero();\n\n  /// Returns where (length wise) on the ray that the ray intersects the\n  /// [LineSegment] or null if there is no intersection.\n  ///\n  /// A ray that is parallel and overlapping with the [segment] is considered to\n  /// not intersect. This is due to that a single intersection point can't be\n  /// determined and that a [LineSegment] is almost always connected to another\n  /// line segment which will get the intersection on one of its ends instead.\n  double? lineSegmentIntersection(LineSegment segment) {\n    _v1\n      ..setFrom(origin)\n      ..sub(segment.from);\n    _v2\n      ..setFrom(segment.to)\n      ..sub(segment.from);\n    _v3.setValues(-direction.y, direction.x);\n\n    final dot = _v2.dot(_v3);\n    final t1 = _v2.cross(_v1) / dot;\n    final t2 = _v1.dot(_v3) / dot;\n    if (t1 >= 0 && t2 >= 0 && t2 <= 1) {\n      return t1;\n    }\n    return null;\n  }\n\n  /// Deep clones the object, i.e. both [origin] and [direction] are cloned into\n  /// a new [Ray2] object.\n  Ray2 clone() => Ray2(origin: origin.clone(), direction: direction.clone());\n\n  /// Sets the values by copying them from [other].\n  void setFrom(Ray2 other) {\n    setWith(origin: other.origin, direction: other.direction);\n  }\n\n  void setWith({required Vector2 origin, required Vector2 direction}) {\n    this.origin.setFrom(origin);\n    this.direction = direction;\n  }\n\n  @override\n  String toString() => 'Ray2(origin: $origin, direction: $direction)';\n}\n"
  },
  {
    "path": "packages/flame/lib/src/geometry/rectangle_component.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:meta/meta.dart';\n\nclass RectangleComponent extends PolygonComponent {\n  RectangleComponent({\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.paint,\n    super.paintLayers,\n    super.key,\n  }) : super(sizeToVertices(size ?? Vector2.zero(), anchor)) {\n    size.addListener(\n      () => refreshVertices(\n        newVertices: sizeToVertices(size, anchor),\n        shrinkToBoundsOverride: false,\n      ),\n    );\n  }\n\n  RectangleComponent.square({\n    double size = 0,\n    super.position,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.priority,\n    super.paint,\n    super.paintLayers,\n    super.children,\n    super.key,\n  }) : super(sizeToVertices(Vector2.all(size), anchor)) {\n    this.size.addListener(\n      () => refreshVertices(\n        newVertices: sizeToVertices(this.size, anchor),\n        shrinkToBoundsOverride: false,\n      ),\n    );\n  }\n\n  /// With this constructor you define the [RectangleComponent] in relation to\n  /// the `parentSize`. For example having [relation] as of (0.8, 0.5) would\n  /// create a rectangle that fills 80% of the width and 50% of the height of\n  /// `parentSize`.\n  RectangleComponent.relative(\n    Vector2 relation, {\n    required super.parentSize,\n    super.position,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.priority,\n    super.paint,\n    super.paintLayers,\n    super.shrinkToBounds,\n    super.key,\n    super.children,\n  }) : super.relative([\n         relation.clone(),\n         Vector2(relation.x, -relation.y),\n         -relation,\n         Vector2(-relation.x, relation.y),\n       ]) {\n    size.addListener(\n      () => refreshVertices(\n        newVertices: sizeToVertices(size, anchor),\n        shrinkToBoundsOverride: false,\n      ),\n    );\n  }\n\n  /// This factory will create a [RectangleComponent] from a positioned [Rect].\n  factory RectangleComponent.fromRect(\n    Rect rect, {\n    Vector2? scale,\n    double? angle,\n    Anchor anchor = Anchor.topLeft,\n    int? priority,\n    Paint? paint,\n    List<Paint>? paintLayers,\n    ComponentKey? key,\n    List<Component>? children,\n  }) {\n    return RectangleComponent(\n      position: anchor == Anchor.topLeft\n          ? rect.topLeft.toVector2()\n          : Anchor.topLeft.toOtherAnchorPosition(\n              rect.topLeft.toVector2(),\n              anchor,\n              rect.size.toVector2(),\n            ),\n      size: rect.size.toVector2(),\n      scale: scale,\n      angle: angle,\n      anchor: anchor,\n      priority: priority,\n      paint: paint,\n      paintLayers: paintLayers,\n      key: key,\n      children: children,\n    );\n  }\n\n  @protected\n  static List<Vector2> sizeToVertices(\n    Vector2 size,\n    Anchor? componentAnchor,\n  ) {\n    final anchor = componentAnchor ?? Anchor.topLeft;\n    return [\n      Vector2(-size.x * anchor.x, -size.y * anchor.y),\n      Vector2(-size.x * anchor.x, size.y - size.y * anchor.y),\n      Vector2(size.x - size.x * anchor.x, size.y - size.y * anchor.y),\n      Vector2(size.x - size.x * anchor.x, -size.y * anchor.y),\n    ];\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/geometry/shape_component.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\n\n/// A shape can represent any geometrical shape with optionally a size, position\n/// and angle. It can also have an anchor if it shouldn't be rotated around its\n/// center.\n/// A point can be determined to be within of outside of a shape.\nabstract class ShapeComponent extends PositionComponent with HasPaint {\n  ShapeComponent({\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.key,\n    Paint? paint,\n    List<Paint>? paintLayers,\n  }) {\n    this.paint = paint ?? this.paint;\n    // Only read from this.paintLayers if paintLayers not null to prevent\n    // unnecessary creation of the paintLayers list.\n    if (paintLayers != null) {\n      this.paintLayers = paintLayers;\n    }\n  }\n\n  bool renderShape = true;\n\n  /// Whether the shape is solid or hollow.\n  ///\n  /// If it is solid, intersections will occur even if the other component is\n  /// fully enclosed by the other hitbox. The intersection point in such cases\n  /// will be the center of the enclosed [ShapeComponent].\n  /// A hollow shape that is fully enclosed by a solid hitbox will cause an\n  /// intersection result, but not the other way around.\n  ///\n  /// This field is not related to how the shape should be rendered, see\n  /// [Paint.style] for that.\n  bool isSolid = false;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/geometry/shape_intersections.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame/geometry.dart';\n\nabstract class Intersections<\n  T1 extends ShapeComponent,\n  T2 extends ShapeComponent\n> {\n  Set<Vector2> intersect(T1 shapeA, T2 shapeB);\n\n  bool supportsShapes(ShapeComponent shapeA, ShapeComponent shapeB) {\n    return shapeA is T1 && shapeB is T2 || shapeA is T2 && shapeB is T1;\n  }\n\n  Set<Vector2> unorderedIntersect(\n    ShapeComponent shapeA,\n    ShapeComponent shapeB,\n  ) {\n    if (shapeA is T1 && shapeB is T2) {\n      return intersect(shapeA, shapeB);\n    } else if (shapeA is T2 && shapeB is T1) {\n      return intersect(shapeB, shapeA);\n    } else {\n      throw 'Unsupported shapes';\n    }\n  }\n}\n\nclass PolygonPolygonIntersections\n    extends Intersections<PolygonComponent, PolygonComponent> {\n  /// Returns the intersection points of [polygonA] and [polygonB]\n  /// The two polygons are required to be convex\n  /// If they share a segment of a line, both end points and the center point of\n  /// that line segment will be counted as collision points\n  @override\n  Set<Vector2> intersect(\n    PolygonComponent polygonA,\n    PolygonComponent polygonB, {\n    Rect? overlappingRect,\n  }) {\n    final intersectionPoints = <Vector2>{};\n    final intersectionsA = polygonA.possibleIntersectionVertices(\n      overlappingRect,\n    );\n    final intersectionsB = polygonB.possibleIntersectionVertices(\n      overlappingRect,\n    );\n    for (final lineA in intersectionsA) {\n      for (final lineB in intersectionsB) {\n        final lineIntersections = lineA.intersections(lineB);\n        intersectionPoints.addAll(lineIntersections);\n      }\n    }\n    if (intersectionPoints.isEmpty && (polygonA.isSolid || polygonB.isSolid)) {\n      final outerShape = polygonA.containsPoint(polygonB.globalVertices().first)\n          ? polygonA\n          : (polygonB.containsPoint(polygonA.globalVertices().first)\n                ? polygonB\n                : null);\n      if (outerShape != null && outerShape.isSolid) {\n        final innerShape = outerShape == polygonA ? polygonB : polygonA;\n        return {innerShape.absoluteCenter};\n      }\n    }\n    return intersectionPoints;\n  }\n}\n\nclass CirclePolygonIntersections\n    extends Intersections<CircleComponent, PolygonComponent> {\n  @override\n  Set<Vector2> intersect(\n    CircleComponent circle,\n    PolygonComponent polygon, {\n    Rect? overlappingRect,\n  }) {\n    final intersectionPoints = <Vector2>{};\n    final possibleVertices = polygon.possibleIntersectionVertices(\n      overlappingRect,\n    );\n    for (final line in possibleVertices) {\n      intersectionPoints.addAll(circle.lineSegmentIntersections(line));\n    }\n    if (intersectionPoints.isEmpty && (circle.isSolid || polygon.isSolid)) {\n      final outerShape = circle.containsPoint(polygon.globalVertices().first)\n          ? circle\n          : (polygon.containsPoint(circle.absoluteCenter) ? polygon : null);\n      if (outerShape != null && outerShape.isSolid) {\n        final innerShape = outerShape == circle ? polygon : circle;\n        return {innerShape.absoluteCenter};\n      }\n    }\n    return intersectionPoints;\n  }\n}\n\nclass CircleCircleIntersections\n    extends Intersections<CircleComponent, CircleComponent> {\n  @override\n  Set<Vector2> intersect(CircleComponent shapeA, CircleComponent shapeB) {\n    final centerA = shapeA.absoluteCenter;\n    final centerB = shapeB.absoluteCenter;\n    final distance = centerA.distanceTo(centerB);\n    final radiusA = shapeA.scaledRadius;\n    final radiusB = shapeB.scaledRadius;\n    if (distance > radiusA + radiusB) {\n      // Since the circles are too far away from each other to intersect we\n      // return the empty set.\n      return {};\n    } else if (distance < (radiusA - radiusB).abs()) {\n      // When one circle is contained within the other there is only a collision\n      // if the outer circle isn't hollow.\n      final outerShape = radiusA > radiusB ? shapeA : shapeB;\n      if (outerShape.isSolid) {\n        final center = outerShape == shapeA ? centerB : centerA;\n        return {center};\n      } else {\n        return {};\n      }\n    } else if (distance == 0 && radiusA == radiusB) {\n      // The circles are identical and on top of each other, so there are an\n      // infinite number of solutions. Since it is problematic to return a\n      // set of infinite size, we'll return 4 distinct points here.\n      return {\n        shapeA.absoluteCenter + Vector2(radiusA, 0),\n        shapeA.absoluteCenter + Vector2(0, -radiusA),\n        shapeA.absoluteCenter + Vector2(-radiusA, 0),\n        shapeA.absoluteCenter + Vector2(0, radiusA),\n      };\n    } else {\n      // There are definitely collision points if we end up in here.\n      // To calculate these we use the fact that we can form two triangles going\n      // between the center of shapeA, the point in between the shapes which the\n      // intersecting line goes through, and then two different triangles are\n      // formed with the two intersection points as the last corners.\n      // The length to the point in between the circles is first calculated,\n      // this is [lengthA], then we calculate the length of the other cathetus\n      // [lengthB]. Then the [centerPoint] is calculated, which is the point\n      // which the intersecting line goes through in between the shapes.\n      // At this point we know the two first points of the triangles, the center\n      // of [shapeA] and the [centerPoint], the two third points of the\n      // different triangles are the intersection points that we are looking for\n      // and we get those points by calculating the [delta] from the\n      // [centerPoint] to the intersection points.\n      // The result is then [centerPoint] +- [delta].\n      final lengthA =\n          (pow(radiusA, 2) - pow(radiusB, 2) + pow(distance, 2)) /\n          (2 * distance);\n      final lengthB = sqrt((pow(radiusA, 2) - pow(lengthA, 2)).abs());\n      final centerPoint =\n          shapeA.absoluteCenter +\n          (shapeB.absoluteCenter - shapeA.absoluteCenter) * lengthA / distance;\n      final delta = Vector2(\n        lengthB *\n            (shapeB.absoluteCenter.y - shapeA.absoluteCenter.y).abs() /\n            distance,\n        -lengthB *\n            (shapeB.absoluteCenter.x - shapeA.absoluteCenter.x).abs() /\n            distance,\n      );\n      return {\n        centerPoint + delta,\n        centerPoint - delta,\n      };\n    }\n  }\n}\n\nfinal List<Intersections> _intersectionSystems = [\n  CircleCircleIntersections(),\n  CirclePolygonIntersections(),\n  PolygonPolygonIntersections(),\n];\n\nSet<Vector2> intersections(ShapeComponent shapeA, ShapeComponent shapeB) {\n  final intersectionSystem = _intersectionSystems.firstWhere(\n    (system) => system.supportsShapes(shapeA, shapeB),\n    orElse: () {\n      throw 'Unsupported intersection detected between: '\n          '${shapeA.runtimeType} and ${shapeB.runtimeType}';\n    },\n  );\n  return intersectionSystem.unorderedIntersect(shapeA, shapeB);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/gestures/detectors.dart",
    "content": "import 'package:flame/src/game/game.dart';\nimport 'package:flame/src/gestures/events.dart';\nimport 'package:flutter/gestures.dart';\n\n@Deprecated('Use TapCallbacks instead')\nmixin TapDetector on Game {\n  void onTap() {}\n  void onTapCancel() {}\n  void onTapDown(TapDownInfo info) {}\n  void onTapUp(TapUpInfo info) {}\n\n  void handleTapUp(TapUpDetails details) {\n    onTapUp(TapUpInfo.fromDetails(this, details));\n  }\n\n  void handleTapDown(TapDownDetails details) {\n    onTapDown(TapDownInfo.fromDetails(this, details));\n  }\n}\n\nmixin SecondaryTapDetector on Game {\n  void onSecondaryTapDown(TapDownInfo info) {}\n  void onSecondaryTapUp(TapUpInfo info) {}\n  void onSecondaryTapCancel() {}\n\n  void handleSecondaryTapUp(TapUpDetails details) {\n    onSecondaryTapUp(TapUpInfo.fromDetails(this, details));\n  }\n\n  void handleSecondaryTapDown(TapDownDetails details) {\n    onSecondaryTapDown(TapDownInfo.fromDetails(this, details));\n  }\n}\n\nmixin TertiaryTapDetector on Game {\n  void onTertiaryTapDown(TapDownInfo info) {}\n  void onTertiaryTapUp(TapUpInfo info) {}\n  void onTertiaryTapCancel() {}\n\n  void handleTertiaryTapUp(TapUpDetails details) {\n    onTertiaryTapUp(TapUpInfo.fromDetails(this, details));\n  }\n\n  void handleTertiaryTapDown(TapDownDetails details) {\n    onTertiaryTapDown(TapDownInfo.fromDetails(this, details));\n  }\n}\n\nmixin DoubleTapDetector on Game {\n  void onDoubleTap() {}\n  void onDoubleTapCancel() {}\n  void onDoubleTapDown(TapDownInfo info) {}\n\n  void handleDoubleTapDown(TapDownDetails details) {\n    onDoubleTapDown(TapDownInfo.fromDetails(this, details));\n  }\n}\n\nmixin LongPressDetector on Game {\n  void onLongPress() {}\n  void onLongPressStart(LongPressStartInfo info) {}\n  void onLongPressMoveUpdate(LongPressMoveUpdateInfo info) {}\n  void onLongPressUp() {}\n  void onLongPressEnd(LongPressEndInfo info) {}\n  void onLongPressCancel() {}\n\n  void handleLongPressStart(LongPressStartDetails details) {\n    onLongPressStart(LongPressStartInfo.fromDetails(this, details));\n  }\n\n  void handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) {\n    onLongPressMoveUpdate(LongPressMoveUpdateInfo.fromDetails(this, details));\n  }\n\n  void handleLongPressEnd(LongPressEndDetails details) {\n    onLongPressEnd(LongPressEndInfo.fromDetails(this, details));\n  }\n}\n\nmixin VerticalDragDetector on Game {\n  void onVerticalDragDown(DragDownInfo info) {}\n  void onVerticalDragStart(DragStartInfo info) {}\n  void onVerticalDragUpdate(DragUpdateInfo info) {}\n  void onVerticalDragEnd(DragEndInfo info) {}\n  void onVerticalDragCancel() {}\n\n  void handleVerticalDragDown(DragDownDetails details) {\n    onVerticalDragDown(DragDownInfo.fromDetails(this, details));\n  }\n\n  void handleVerticalDragStart(DragStartDetails details) {\n    onVerticalDragStart(DragStartInfo.fromDetails(this, details));\n  }\n\n  void handleVerticalDragUpdate(DragUpdateDetails details) {\n    onVerticalDragUpdate(DragUpdateInfo.fromDetails(this, details));\n  }\n\n  void handleVerticalDragEnd(DragEndDetails details) {\n    onVerticalDragEnd(DragEndInfo.fromDetails(details));\n  }\n}\n\nmixin HorizontalDragDetector on Game {\n  void onHorizontalDragDown(DragDownInfo info) {}\n  void onHorizontalDragStart(DragStartInfo info) {}\n  void onHorizontalDragUpdate(DragUpdateInfo info) {}\n  void onHorizontalDragEnd(DragEndInfo info) {}\n  void onHorizontalDragCancel() {}\n\n  void handleHorizontalDragDown(DragDownDetails details) {\n    onHorizontalDragDown(DragDownInfo.fromDetails(this, details));\n  }\n\n  void handleHorizontalDragStart(DragStartDetails details) {\n    onHorizontalDragStart(DragStartInfo.fromDetails(this, details));\n  }\n\n  void handleHorizontalDragUpdate(DragUpdateDetails details) {\n    onHorizontalDragUpdate(DragUpdateInfo.fromDetails(this, details));\n  }\n\n  void handleHorizontalDragEnd(DragEndDetails details) {\n    onHorizontalDragEnd(DragEndInfo.fromDetails(details));\n  }\n}\n\nmixin ForcePressDetector on Game {\n  void onForcePressStart(ForcePressInfo info) {}\n  void onForcePressPeak(ForcePressInfo info) {}\n  void onForcePressUpdate(ForcePressInfo info) {}\n  void onForcePressEnd(ForcePressInfo info) {}\n\n  void handleForcePressStart(ForcePressDetails details) {\n    onForcePressStart(ForcePressInfo.fromDetails(this, details));\n  }\n\n  void handleForcePressPeak(ForcePressDetails details) {\n    onForcePressPeak(ForcePressInfo.fromDetails(this, details));\n  }\n\n  void handleForcePressUpdate(ForcePressDetails details) {\n    onForcePressUpdate(ForcePressInfo.fromDetails(this, details));\n  }\n\n  void handleForcePressEnd(ForcePressDetails details) {\n    onForcePressEnd(ForcePressInfo.fromDetails(this, details));\n  }\n}\n\nmixin PanDetector on Game {\n  void onPanDown(DragDownInfo info) {}\n  void onPanStart(DragStartInfo info) {}\n  void onPanUpdate(DragUpdateInfo info) {}\n  void onPanEnd(DragEndInfo info) {}\n  void onPanCancel() {}\n\n  void handlePanDown(DragDownDetails details) {\n    onPanDown(DragDownInfo.fromDetails(this, details));\n  }\n\n  void handlePanStart(DragStartDetails details) {\n    onPanStart(DragStartInfo.fromDetails(this, details));\n  }\n\n  void handlePanUpdate(DragUpdateDetails details) {\n    onPanUpdate(DragUpdateInfo.fromDetails(this, details));\n  }\n\n  void handlePanEnd(DragEndDetails details) {\n    onPanEnd(DragEndInfo.fromDetails(details));\n  }\n}\n\nmixin ScaleDetector on Game {\n  void onScaleStart(ScaleStartInfo info) {}\n  void onScaleUpdate(ScaleUpdateInfo info) {}\n  void onScaleEnd(ScaleEndInfo info) {}\n\n  void handleScaleStart(ScaleStartDetails details) {\n    onScaleStart(ScaleStartInfo.fromDetails(this, details));\n  }\n\n  void handleScaleUpdate(ScaleUpdateDetails details) {\n    onScaleUpdate(ScaleUpdateInfo.fromDetails(this, details));\n  }\n\n  void handleScaleEnd(ScaleEndDetails details) {\n    onScaleEnd(ScaleEndInfo.fromDetails(details));\n  }\n}\n\nmixin MouseMovementDetector on Game {\n  void onMouseMove(PointerHoverInfo info) {}\n}\n\nmixin ScrollDetector on Game {\n  void onScroll(PointerScrollInfo info) {}\n}\n"
  },
  {
    "path": "packages/flame/lib/src/gestures/events.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/game/game.dart';\nimport 'package:flutter/gestures.dart';\n\n/// [EventPosition] converts position based events to three different coordinate\n/// systems (global, local and game).\n///\n/// global: coordinate system relative to the entire app; same as\n/// `globalPosition` in Flutter.\n/// widget: coordinate system relative to the GameWidget widget; same as\n/// `localPosition` in Flutter.\nclass EventPosition {\n  final Game _game;\n  final Offset _globalPosition;\n\n  /// Coordinates of the event relative to the whole screen\n  late final Vector2 global = _globalPosition.toVector2();\n\n  /// Coordinates of the event relative to the game widget position/size\n  late final Vector2 widget = _game.convertGlobalToLocalCoordinate(global);\n\n  EventPosition(this._game, this._globalPosition);\n}\n\n/// [EventDelta] converts deltas based events to two different values\n/// (game and global).\n///\n/// [global]: this is the raw value received by the event without any scale\n/// applied to it; this is always the same as local because Flutter doesn't\n/// apply any scaling.\nclass EventDelta {\n  final Offset _delta;\n\n  /// Raw value relative to the game transformations\n  late final Vector2 global = _delta.toVector2();\n\n  EventDelta(this._delta);\n}\n\n/// BaseInfo is the base class for Flame's input events.\n/// This base class just wraps Flutter's [raw] attribute.\nabstract class BaseInfo<T> {\n  final T raw;\n\n  BaseInfo(this.raw);\n}\n\n/// A more specialized wrapper of Flame's base class for input events.\n/// It adds the [eventPosition] field and is used by all position based\n/// events on Flame.\nabstract class PositionInfo<T> extends BaseInfo<T> {\n  final Game _game;\n  final Offset _globalPosition;\n\n  late final eventPosition = EventPosition(_game, _globalPosition);\n\n  PositionInfo(\n    this._game,\n    this._globalPosition,\n    T raw,\n  ) : super(raw);\n}\n\nclass TapDownInfo extends PositionInfo<TapDownDetails> with _HandledField {\n  TapDownInfo.fromDetails(\n    Game game,\n    TapDownDetails raw,\n  ) : super(game, raw.globalPosition, raw);\n}\n\nclass TapUpInfo extends PositionInfo<TapUpDetails> with _HandledField {\n  TapUpInfo.fromDetails(\n    Game game,\n    TapUpDetails raw,\n  ) : super(game, raw.globalPosition, raw);\n}\n\nclass LongPressStartInfo extends PositionInfo<LongPressStartDetails> {\n  LongPressStartInfo.fromDetails(\n    Game game,\n    LongPressStartDetails raw,\n  ) : super(game, raw.globalPosition, raw);\n}\n\nclass LongPressEndInfo extends PositionInfo<LongPressEndDetails> {\n  late final Vector2 velocity = raw.velocity.pixelsPerSecond.toVector2();\n\n  LongPressEndInfo.fromDetails(\n    Game game,\n    LongPressEndDetails raw,\n  ) : super(game, raw.globalPosition, raw);\n}\n\nclass LongPressMoveUpdateInfo extends PositionInfo<LongPressMoveUpdateDetails> {\n  LongPressMoveUpdateInfo.fromDetails(\n    Game game,\n    LongPressMoveUpdateDetails raw,\n  ) : super(game, raw.globalPosition, raw);\n}\n\nclass ForcePressInfo extends PositionInfo<ForcePressDetails> {\n  late final double pressure = raw.pressure;\n\n  ForcePressInfo.fromDetails(\n    Game game,\n    ForcePressDetails raw,\n  ) : super(game, raw.globalPosition, raw);\n}\n\nclass PointerScrollInfo extends PositionInfo<PointerScrollEvent> {\n  late final EventDelta scrollDelta = EventDelta(raw.scrollDelta);\n\n  PointerScrollInfo.fromDetails(\n    Game game,\n    PointerScrollEvent raw,\n  ) : super(game, raw.position, raw);\n}\n\nclass PointerHoverInfo extends PositionInfo<PointerHoverEvent>\n    with _HandledField {\n  PointerHoverInfo.fromDetails(\n    Game game,\n    PointerHoverEvent raw,\n  ) : super(game, raw.position, raw);\n}\n\nclass DragDownInfo extends PositionInfo<DragDownDetails> {\n  DragDownInfo.fromDetails(\n    Game game,\n    DragDownDetails raw,\n  ) : super(game, raw.globalPosition, raw);\n}\n\nclass DragStartInfo extends PositionInfo<DragStartDetails> with _HandledField {\n  DragStartInfo.fromDetails(\n    Game game,\n    DragStartDetails raw,\n  ) : super(game, raw.globalPosition, raw);\n}\n\nclass DragUpdateInfo extends PositionInfo<DragUpdateDetails>\n    with _HandledField {\n  late final EventDelta delta = EventDelta(raw.delta);\n\n  DragUpdateInfo.fromDetails(\n    Game game,\n    DragUpdateDetails raw,\n  ) : super(game, raw.globalPosition, raw);\n}\n\nclass DragEndInfo extends BaseInfo<DragEndDetails> with _HandledField {\n  late final Vector2 velocity = raw.velocity.pixelsPerSecond.toVector2();\n  double? get primaryVelocity => raw.primaryVelocity;\n\n  DragEndInfo.fromDetails(super.raw);\n}\n\nclass ScaleStartInfo extends PositionInfo<ScaleStartDetails> {\n  int get pointerCount => raw.pointerCount;\n\n  ScaleStartInfo.fromDetails(\n    Game game,\n    ScaleStartDetails raw,\n  ) : super(game, raw.focalPoint, raw);\n}\n\nclass ScaleEndInfo extends BaseInfo<ScaleEndDetails> {\n  late final EventDelta velocity = EventDelta(raw.velocity.pixelsPerSecond);\n\n  int get pointerCount => raw.pointerCount;\n\n  ScaleEndInfo.fromDetails(super.raw);\n}\n\nclass ScaleUpdateInfo extends PositionInfo<ScaleUpdateDetails> {\n  int get pointerCount => raw.pointerCount;\n  double get rotation => raw.rotation;\n  late final EventDelta delta = EventDelta(raw.focalPointDelta);\n  late final EventDelta scale = EventDelta(\n    Offset(raw.horizontalScale, raw.verticalScale),\n  );\n\n  ScaleUpdateInfo.fromDetails(\n    Game game,\n    ScaleUpdateDetails raw,\n  ) : super(game, raw.focalPoint, raw);\n}\n\nmixin _HandledField {\n  bool handled = false;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/image_composition.dart",
    "content": "import 'dart:async';\nimport 'dart:ui';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flutter/foundation.dart';\n\nexport '../extensions.dart';\n\n/// The [ImageComposition] allows for composing multiple images onto a single\n/// image.\n///\n/// **Note:** Composing images is a heavy async operation and should not be\n/// called inside the game loop.\nclass ImageComposition {\n  ImageComposition({\n    this.defaultBlendMode = BlendMode.srcOver,\n    this.defaultAntiAlias = false,\n  });\n\n  /// The values that will be used to compose the image\n  final List<_Fragment> _composes = [];\n\n  /// The [defaultBlendMode] can be used to change how each image will be\n  /// blended onto the composition. Defaults to [BlendMode.srcOver].\n  final BlendMode defaultBlendMode;\n\n  /// The [defaultAntiAlias] can be used to if each image will be anti aliased.\n  final bool defaultAntiAlias;\n\n  /// Add an image to the [ImageComposition].\n  ///\n  /// The [image] will be added at the given [position] on the composition.\n  ///\n  /// An optional [source] can be used to only add the data that is within the\n  /// [source] of the [image].\n  ///\n  /// An optional [angle] (in radians) can be used to rotate the image when it\n  /// gets added to the composition. It will be rotated in a clock-wise\n  /// direction around the [anchor].\n  ///\n  /// By default the [anchor] will be the [source].width and [source].height\n  /// divided by `2`.\n  ///\n  /// [isAntiAlias] can be used to if the [image] will be anti aliased. Defaults\n  /// to [defaultAntiAlias].\n  ///\n  /// The [blendMode] can be used to change how the [image] will be blended onto\n  /// the composition. Defaults to [defaultBlendMode].\n  void add(\n    Image image,\n    Vector2 position, {\n    Rect? source,\n    double angle = 0,\n    Vector2? anchor,\n    bool? isAntiAlias,\n    BlendMode? blendMode,\n  }) {\n    final imageRect = image.getBoundingRect();\n    source ??= imageRect;\n    anchor ??= source.toVector2() / 2;\n    blendMode ??= defaultBlendMode;\n    isAntiAlias ??= defaultAntiAlias;\n\n    assert(\n      imageRect.topLeft <= source.topLeft &&\n          imageRect.bottomRight >= source.bottomRight,\n      'Source rect should fit within the image',\n    );\n\n    _composes.add(\n      _Fragment(\n        image,\n        position,\n        source,\n        angle,\n        anchor,\n        blendMode,\n        antiAlias: isAntiAlias,\n      ),\n    );\n  }\n\n  void clear() => _composes.clear();\n\n  /// Compose all the images into a single composition.\n  Future<Image> compose() {\n    final result = _composeCore();\n\n    return result.picture.toImageSafe(\n      result.width,\n      result.height,\n    );\n  }\n\n  /// Compose all the images into a single composition.\n  ///\n  /// A sync version of [compose] function. Read [Picture.toImageSync] for\n  /// detailed description of possible benefits in performance\n  Image composeSync() {\n    final result = _composeCore();\n    return result.picture.toImageSync(result.width, result.height);\n  }\n\n  _ComposeResult _composeCore() {\n    // Rect used to determine how big the output image will be.\n    var outputRect = Rect.zero;\n    final recorder = PictureRecorder();\n    final canvas = Canvas(recorder);\n\n    for (final compose in _composes) {\n      final image = compose.image;\n      final position = compose.position;\n      final source = compose.source;\n      final rotation = compose.angle;\n      final anchor = compose.anchor;\n      final isAntiAlias = compose.antiAlias;\n      final blendMode = compose.blendMode;\n      final destination = Rect.fromLTWH(0, 0, source.width, source.height);\n      final realDest = destination.translate(position.x, position.y);\n\n      canvas\n        ..save()\n        ..translateVector(position)\n        ..translateVector(anchor)\n        ..rotate(rotation)\n        ..translateVector(-anchor)\n        ..drawImageRect(\n          image,\n          source,\n          destination,\n          Paint()\n            ..blendMode = blendMode\n            ..isAntiAlias = isAntiAlias,\n        )\n        ..restore();\n\n      // Expand the output so it can be used later on when the output image gets\n      // created.\n      outputRect = outputRect.expandToInclude(realDest);\n    }\n\n    final picture = recorder.endRecording();\n    return _ComposeResult(\n      picture: picture,\n      width: outputRect.width.toInt(),\n      height: outputRect.height.toInt(),\n    );\n  }\n}\n\n@immutable\nclass _ComposeResult {\n  const _ComposeResult({\n    required this.picture,\n    required this.width,\n    required this.height,\n  });\n\n  final Picture picture;\n  final int width;\n  final int height;\n}\n\nclass _Fragment {\n  _Fragment(\n    this.image,\n    this.position,\n    this.source,\n    this.angle,\n    this.anchor,\n    this.blendMode, {\n    required this.antiAlias,\n  });\n\n  /// The image that will be composed.\n  final Image image;\n\n  /// The position where the [image] will be composed.\n  final Vector2 position;\n\n  /// The source on the [image] that will be composed.\n  final Rect source;\n\n  /// The angle (in radians) used to rotate the [image] around it's [anchor].\n  final double angle;\n\n  /// The point around which the [image] will be rotated\n  /// (defaults to the centre of the [source]).\n  final Vector2 anchor;\n\n  final bool antiAlias;\n\n  /// The [BlendMode] that will be used when composing the [image].\n  final BlendMode blendMode;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/layers/layer.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/layers/processors.dart';\nimport 'package:meta/meta.dart';\n\nabstract class Layer {\n  List<LayerProcessor> preProcessors = [];\n  List<LayerProcessor> postProcessors = [];\n\n  Picture? _picture;\n\n  PictureRecorder? _recorder;\n  Canvas? _canvas;\n\n  @mustCallSuper\n  void render(Canvas canvas, {double x = 0.0, double y = 0.0}) {\n    final picture = _picture;\n    if (picture == null) {\n      return;\n    }\n\n    canvas.save();\n    canvas.translate(x, y);\n\n    for (final p in preProcessors) {\n      p.process(picture, canvas);\n    }\n    canvas.drawPicture(picture);\n    for (final p in postProcessors) {\n      p.process(picture, canvas);\n    }\n    canvas.restore();\n  }\n\n  Canvas get canvas {\n    assert(\n      _canvas != null,\n      'Layer is not ready for rendering, call beginRendering first',\n    );\n    return _canvas!;\n  }\n\n  void beginRendering() {\n    _recorder = PictureRecorder();\n    _canvas = Canvas(_recorder!);\n  }\n\n  void finishRendering() {\n    _picture = _recorder?.endRecording();\n\n    _recorder = null;\n    _canvas = null;\n  }\n\n  void drawLayer();\n}\n\nabstract class PreRenderedLayer extends Layer {\n  PreRenderedLayer() {\n    reRender();\n  }\n\n  void reRender() {\n    beginRendering();\n    drawLayer();\n    finishRendering();\n  }\n}\n\nabstract class DynamicLayer extends Layer {\n  @override\n  void render(Canvas canvas, {double x = 0.0, double y = 0.0}) {\n    beginRendering();\n    drawLayer();\n    finishRendering();\n\n    super.render(canvas, x: x, y: y);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/layers/processors.dart",
    "content": "import 'dart:ui';\n\nabstract class LayerProcessor {\n  void process(Picture pic, Canvas canvas);\n}\n\nclass ShadowProcessor extends LayerProcessor {\n  final Paint _shadowPaint;\n\n  final Offset offset;\n\n  ShadowProcessor({\n    this.offset = const Offset(10, 10),\n    double opacity = 0.9,\n    Color color = const Color(0xFF000000),\n  }) : _shadowPaint = Paint()\n         ..colorFilter = ColorFilter.mode(\n           color.withValues(alpha: opacity),\n           BlendMode.srcATop,\n         );\n\n  @override\n  void process(Picture pic, Canvas canvas) {\n    canvas.saveLayer(Rect.largest, _shadowPaint);\n    canvas.translate(offset.dx, offset.dy);\n    canvas.drawPicture(pic);\n    canvas.restore();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/layout/align_component.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\nimport 'package:flutter/widgets.dart';\n\n/// **AlignComponent** is a layout component that positions its child within\n/// itself using relative placement. It is similar to Flutter's [Align] widget.\n///\n/// The component requires a single [child], which will be the target of this\n/// component's alignment. Of course, other children can be added to this\n/// component too, but only the initial [child] will be aligned.\n///\n/// The [alignment] parameter describes where the child should be placed within\n/// the current component. For example, if the [alignment] is `Anchor.center`,\n/// then the child will be centered.\n///\n/// Normally, this component's size will match the size of its parent. However,\n/// if you provide properties [widthFactor] or [heightFactor], then the size of\n/// this component in that direction will be equal to the size of the child\n/// times the corresponding factor. For example, if you set [heightFactor] to\n/// 1 then the width of this component will be equal to the width of the parent,\n/// but the height will match the height of the child.\n///\n/// ```dart\n/// AlignComponent(\n///   child: TextComponent('hello'),\n///   alignment: Anchor.centerLeft,\n/// );\n/// ```\n///\n/// By default, the child's anchor is set equal to the [alignment] value. This\n/// achieves traditional alignment behavior: for example, the center of the\n/// child will be placed at the center of the current component, or bottom\n/// right corner of the child can be placed in the bottom right corner of the\n/// component. However, it is also possible to achieve more extravagant\n/// placement by giving the child a different anchor and setting\n/// [keepChildAnchor] to true. For example, if you set `alignment` to\n/// `topCenter`, and child's anchor to `bottomCenter`, then the child will\n/// effectively be placed above the current component:\n/// ```dart\n/// PlayerSprite().add(\n///   AlignComponent(\n///     child: HealthBar()..anchor = Anchor.bottomCenter,\n///     alignment: Anchor.topCenter,\n///     keepChildAnchor: true,\n///   ),\n/// );\n/// ```\nclass AlignComponent extends PositionComponent {\n  /// Creates a component that keeps its [child] positioned according to the\n  /// [alignment] within this component's bounding box.\n  ///\n  /// More precisely, the child will be placed at [alignment] relative position\n  /// within the current component's bounding box. The child's anchor will also\n  /// be set to the [alignment], unless [keepChildAnchor] parameter is true.\n  AlignComponent({\n    PositionComponent? child,\n    Anchor alignment = Anchor.topLeft,\n    this.widthFactor,\n    this.heightFactor,\n    this.keepChildAnchor = false,\n    super.priority,\n  }) {\n    this.alignment = alignment;\n    this.child = child;\n  }\n\n  PositionComponent? _child;\n\n  /// The component that will be positioned by this component. The [child] will\n  /// be automatically mounted to the current component.\n  PositionComponent? get child => _child;\n\n  set child(PositionComponent? value) {\n    if (_child?.parent == this) {\n      _child?.removeFromParent();\n    }\n    _child = value;\n    _child?.parent = this;\n    _updateChildAnchor();\n    _updateChildPosition();\n  }\n\n  late Anchor _alignment;\n\n  /// How the [child] will be positioned within the current component.\n  ///\n  /// Note: unlike Flutter's [Alignment], the top-left corner of the component\n  /// has relative coordinates `(0, 0)`, while the bottom-right corner has\n  /// coordinates `(1, 1)`.\n  Anchor get alignment => _alignment;\n\n  set alignment(Anchor value) {\n    _alignment = value;\n    _updateChildAnchor();\n    _updateChildPosition();\n  }\n\n  /// If `null`, then the component's width will be equal to the width of the\n  /// parent. Otherwise, the width will be equal to the child's width multiplied\n  /// by this factor.\n  final double? widthFactor;\n\n  /// If `null`, then the component's height will be equal to the height of the\n  /// parent. Otherwise, the height will be equal to the child's height\n  /// multiplied by this factor.\n  final double? heightFactor;\n\n  /// If `false` (default), then the child's `anchor` will be kept equal to the\n  /// [alignment] value. If `true`, then the [child] will be allowed to have\n  /// its own `anchor` value independent from the parent.\n  final bool keepChildAnchor;\n\n  @override\n  set size(Vector2 value) {\n    throw UnsupportedError('The size of AlignComponent cannot be set directly');\n  }\n\n  @override\n  void onMount() {\n    assert(\n      parent is ReadOnlySizeProvider,\n      \"An AlignComponent's parent must have a size\",\n    );\n  }\n\n  @override\n  void onParentResize(Vector2 maxSize) {\n    if (_child != null) {\n      super.size = Vector2(\n        widthFactor == null ? maxSize.x : _child!.size.x * widthFactor!,\n        heightFactor == null ? maxSize.y : _child!.size.y * heightFactor!,\n      );\n    }\n    _updateChildPosition();\n  }\n\n  @mustCallSuper\n  @override\n  void onChildrenChanged(Component child, ChildrenChangeType type) {\n    if (_child?.parent != this) {\n      this.child = null;\n    }\n  }\n\n  void _updateChildPosition() {\n    _child?.position = Vector2(size.x * alignment.x, size.y * alignment.y);\n  }\n\n  void _updateChildAnchor() {\n    if (!keepChildAnchor) {\n      _child?.anchor = _alignment;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/math/block.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:meta/meta.dart';\n\n/// This is just a pair of [int, int].\n///\n/// Represents a position in a 2d-matrix or tilemap.\n@immutable\nclass Block {\n  /// x coordinate in the matrix.\n  final int x;\n\n  /// y coordinate in the matrix.\n  final int y;\n\n  const Block(this.x, this.y);\n\n  const Block.zero() : this(0, 0);\n\n  Block.roundFromVector2(Vector2 position)\n    : this(position.x.round(), position.y.round());\n\n  Block.floorFromVector2(Vector2 position)\n    : this(position.x.floor(), position.y.floor());\n\n  Block.ceilFromVector2(Vector2 position)\n    : this(position.x.ceil(), position.y.ceil());\n\n  Block operator +(Block direction) {\n    return Block(x + direction.x, y + direction.y);\n  }\n\n  Block operator -(Block other) {\n    return Block(x - other.x, y - other.y);\n  }\n\n  Vector2 operator *(double scalar) {\n    return Vector2(x * scalar, y * scalar);\n  }\n\n  @override\n  String toString() => '($x, $y)';\n\n  Vector2 toVector2() => Vector2Extension.fromInts(x, y);\n\n  @override\n  bool operator ==(Object other) {\n    if (other is! Block) {\n      return false;\n    }\n    return other.x == x && other.y == y;\n  }\n\n  @override\n  int get hashCode => Object.hash(x, y);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/math/random_fallback.dart",
    "content": "import 'dart:math';\n\n/// When you don't care about what [Random] object you have and don't want to\n/// create an unnecessary object you can use this pre-created object.\nfinal Random randomFallback = Random();\n"
  },
  {
    "path": "packages/flame/lib/src/math/solve_cubic.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/geometry.dart';\nimport 'package:flame/src/math/solve_quadratic.dart';\n\n/// Solves cubic equation `ax³ + bx² + cx + d == 0`.\n///\n/// Depending on the coefficients, either 1 or 3 real solutions may be returned.\n/// In degenerate cases, when some of the roots of the equation coincide, we\n/// return all such roots without deduplication.\n///\n/// If coefficient [a] is equal to zero, then we solve the equation as a\n/// quadratic one (see [solveQuadratic]).\nList<double> solveCubic(double a, double b, double c, double d) {\n  if (a == 0) {\n    return solveQuadratic(b, c, d);\n  }\n  if (b == 0) {\n    return _solveDepressedCubic(c / a, d / a);\n  } else {\n    final s = b / (3 * a);\n    final p = c / a - 3 * s * s;\n    final q = d / a - (p + s * s) * s;\n    return _solveDepressedCubic(p, q).map((t) => t - s).toList();\n  }\n}\n\n/// Solves cubic equation `x³ + px + q == 0`.\nList<double> _solveDepressedCubic(double p, double q) {\n  final discriminant = q * q / 4 + p * p * p / 27;\n  // If the discriminant is very close to zero, then we will treat this as if\n  // it was equal to zero.\n  if (discriminant.abs() < discriminantEpsilon) {\n    final x1 = _cubicRoot(q / 2);\n    final x2 = -2 * x1;\n    return [x1, x1, x2];\n  } else if (discriminant > 0) {\n    final w = _cubicRoot(q.abs() / 2 + sqrt(discriminant));\n    return [(p / (3 * w) - w) * q.sign];\n  } else {\n    final f = 2 * sqrt(-p / 3);\n    final v = acos(3 * q / (f * p)) / 3;\n    final x0 = f * cos(v);\n    final x1 = f * cos(v - 1 / 3 * tau);\n    final x2 = f * cos(v - 2 / 3 * tau);\n    return [x0, x1, x2];\n  }\n}\n\ndouble _cubicRoot(double x) {\n  // Note: `pow(x, y)` function cannot handle negative values of `x`\n  if (x >= 0) {\n    return pow(x, 1 / 3).toDouble();\n  } else {\n    return -pow(-x, 1 / 3).toDouble();\n  }\n}\n\nconst discriminantEpsilon = 1e-15;\n"
  },
  {
    "path": "packages/flame/lib/src/math/solve_quadratic.dart",
    "content": "import 'dart:math';\n\n/// Solves quadratic equation `ax² + bx + c == 0`.\n///\n/// Depending on the coefficients, either 0 or 2 solutions may be returned. If\n/// the equation's determinant is zero, then its 2 roots are equal. In this case\n/// we still return them as two solutions.\n///\n/// If coefficient [a] is equal to zero, then we solve the equation as linear,\n/// in which case exactly one solution is returned. If in this case [b] is also\n/// zero, then the produced solution will be either Infinity or NaN depending\n/// on the value of [c].\nList<double> solveQuadratic(double a, double b, double c) {\n  if (a == 0) {\n    return [-c / b];\n  }\n  final det = b * b - 4 * a * c;\n  if (det >= 0) {\n    final sqrtDet = sqrt(det);\n    return [(-b - sqrtDet) / (2 * a), (-b + sqrtDet) / (2 * a)];\n  } else {\n    return [];\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/math/tmp_vector2.dart",
    "content": "import 'package:meta/meta.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// Use internally when you need a temporary [Vector2] object but don't want to\n/// instantiate a new one due to performance.\n@internal\nfinal Vector2 tmpVector2 = Vector2.zero();\n"
  },
  {
    "path": "packages/flame/lib/src/nine_tile_box.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/extensions/vector2.dart';\nimport 'package:flame/src/palette.dart';\nimport 'package:flame/src/sprite.dart';\nimport 'package:meta/meta.dart';\n\n/// This allows you to create a rectangle textured with a 9-sliced image.\n///\n/// How it works is that you have a template image in a 3x3 grid, made up of 9\n/// tiles, and a new rectangle can be draw by keeping the 4 corners, expanding\n/// the 4 sides only in the direction in which they are located and expanding\n/// the center in both directions.\n/// That allows you to have non distorted borders.\nclass NineTileBox {\n  static final _whitePaint = BasicPalette.white.paint();\n\n  /// The sprite used to render the box, must be a 3x3 grid of square tiles.\n  final Sprite sprite;\n\n  /// The size of each tile in the source sprite image.\n  final int tileSize;\n\n  /// The size each tile becomes when rendered\n  /// (optionally used to scale the src image).\n  late int destTileSize;\n\n  @visibleForTesting\n  late Rect center;\n\n  late final Rect _dst;\n\n  /// Creates a nine-box instance.\n  ///\n  /// [sprite] is the 3x3 grid and [tileSize] is the size of each tile.\n  /// The src sprite must be a square of size 3*[tileSize].\n  ///\n  /// If [tileSize] is not provided, the width of the sprite is assumed as the\n  /// size. Otherwise the width and height properties of the sprite are ignored.\n  ///\n  /// If [destTileSize] is not provided, the evaluated [tileSize] is used\n  /// instead (so no scaling happens).\n  NineTileBox(this.sprite, {int? tileSize, int? destTileSize})\n    : tileSize = tileSize ?? sprite.src.width ~/ 3 {\n    this.destTileSize = destTileSize ?? this.tileSize;\n    final centerEdge = this.tileSize.toDouble();\n    center = Rect.fromLTWH(centerEdge, centerEdge, centerEdge, centerEdge);\n    _dst = Rect.fromLTWH(0, 0, this.destTileSize * 3, this.destTileSize * 3);\n  }\n\n  /// Creates a nine-box instance with the specified grid size\n  ///\n  /// A nine-box is a grid with 3 rows and 3 columns. The outer-most columns,\n  /// [leftWidth] and [rightWidth], are a fixed-width. As the nine-box is\n  /// resized, those columns remain fixed-width and the center column stretches\n  /// to take up the remaining space. In the same way, the outer-most rows,\n  /// [topHeight] and [bottomHeight], are a fixed-height. As the nine-box is\n  /// resized, those rows remain fixed-height and the center row stretches\n  /// to take up the remaining space.\n  NineTileBox.withGrid(\n    this.sprite, {\n    double leftWidth = 0.0,\n    double rightWidth = 0.0,\n    double topHeight = 0.0,\n    double bottomHeight = 0.0,\n  }) : tileSize = sprite.src.width ~/ 3 {\n    destTileSize = tileSize;\n    center = Rect.fromLTWH(0, 0, sprite.src.width, sprite.src.height);\n    _dst = Rect.fromLTWH(0, 0, sprite.src.width, sprite.src.height);\n    setGrid(\n      leftWidth: leftWidth,\n      rightWidth: rightWidth,\n      topHeight: topHeight,\n      bottomHeight: bottomHeight,\n    );\n  }\n\n  /// Set different sizes for each of the fixed size rows and columns\n  ///\n  /// A nine-box is a grid with 3 rows and 3 columns. The outer-most columns,\n  /// [leftWidth] and [rightWidth], are a fixed-width. As the nine-box is\n  /// resized, those columns remain fixed-width and the center column stretches\n  /// to take up the remaining space. In the same way, the outer-most rows,\n  /// [topHeight] and [bottomHeight], are a fixed-height. As the nine-box is\n  /// resized, those rows remain fixed-height and the center row stretches\n  /// to take up the remaining space.\n  ///\n  /// Any widths or heights that are not specified remain unchanged.\n  void setGrid({\n    double? leftWidth,\n    double? rightWidth,\n    double? topHeight,\n    double? bottomHeight,\n  }) {\n    if (leftWidth != null && rightWidth != null) {\n      assert(\n        leftWidth + rightWidth <= sprite.src.width,\n        'The left and right columns ($leftWidth + $rightWidth) do '\n        'not fit in the width of the sprite (${sprite.src.width})',\n      );\n    } else if (leftWidth != null) {\n      assert(\n        leftWidth <= center.right,\n        'The left column ($leftWidth) is too large '\n        '(max ${center.right})',\n      );\n    } else if (rightWidth != null) {\n      assert(\n        rightWidth + center.left <= sprite.src.width,\n        'The right column ($rightWidth) is too large '\n        '(max ${sprite.src.width - center.left})',\n      );\n    }\n    if (topHeight != null && bottomHeight != null) {\n      assert(\n        topHeight + bottomHeight <= sprite.src.height,\n        'The top and bottom rows ($topHeight + $bottomHeight) do not fit '\n        'in the height of the sprite (${sprite.src.height})',\n      );\n    } else if (topHeight != null) {\n      assert(\n        topHeight <= center.bottom,\n        'The top row ($topHeight) is too large '\n        '(max ${center.bottom})',\n      );\n    } else if (bottomHeight != null) {\n      assert(\n        bottomHeight + center.top <= sprite.src.height,\n        'The bottom row ($bottomHeight) is too large '\n        '(max ${sprite.src.height - center.top})',\n      );\n    }\n\n    final left = leftWidth ?? center.left;\n    final top = topHeight ?? center.top;\n    late final double right;\n    if (rightWidth == null) {\n      right = center.right;\n    } else {\n      right = sprite.src.width - rightWidth;\n    }\n    late final double bottom;\n    if (bottomHeight == null) {\n      bottom = center.bottom;\n    } else {\n      bottom = sprite.src.height - bottomHeight;\n    }\n    center = Rect.fromLTRB(\n      left,\n      top,\n      right,\n      bottom,\n    );\n  }\n\n  /// Renders this nine box with the dimensions provided by [dst].\n  void drawRect(Canvas c, [Rect? dst, Paint? overridePaint]) {\n    c.drawImageNine(\n      sprite.image,\n      center,\n      dst ?? _dst,\n      overridePaint ?? _whitePaint,\n    );\n  }\n\n  /// Renders this nine box as a rectangle at [position] with size [size].\n  void draw(Canvas c, Vector2 position, Vector2 size, [Paint? overridePaint]) {\n    c.drawImageNine(\n      sprite.image,\n      center,\n      Rect.fromLTWH(position.x, position.y, size.x, size.y),\n      overridePaint ?? _whitePaint,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/palette.dart",
    "content": "import 'dart:ui';\n\nclass PaletteEntry {\n  final Color color;\n\n  Paint paint() => Paint()..color = color;\n\n  const PaletteEntry(this.color);\n\n  PaletteEntry withAlpha(int alpha) {\n    return PaletteEntry(color.withAlpha(alpha));\n  }\n\n  PaletteEntry withRed(int red) {\n    return PaletteEntry(color.withRed(red));\n  }\n\n  PaletteEntry withGreen(int green) {\n    return PaletteEntry(color.withGreen(green));\n  }\n\n  PaletteEntry withBlue(int blue) {\n    return PaletteEntry(color.withBlue(blue));\n  }\n}\n\nclass BasicPalette {\n  static const PaletteEntry transparent = PaletteEntry(Color(0x00FFFFFF));\n  static const PaletteEntry white = PaletteEntry(Color(0xFFFFFFFF));\n  static const PaletteEntry black = PaletteEntry(Color(0xFF000000));\n  static const PaletteEntry red = PaletteEntry(Color(0xFFFF0000));\n  static const PaletteEntry green = PaletteEntry(Color(0xFF00FF00));\n  static const PaletteEntry blue = PaletteEntry(Color(0xFF0000FF));\n  static const PaletteEntry magenta = PaletteEntry(Color(0xFFFF00FF));\n  static const PaletteEntry brown = PaletteEntry(Color(0xFFA52A2A));\n  static const PaletteEntry cyan = PaletteEntry(Color(0xFF00FFFF));\n  static const PaletteEntry darkBlue = PaletteEntry(Color(0xFF000080));\n  static const PaletteEntry darkGray = PaletteEntry(Color(0xFFA9A9A9));\n  static const PaletteEntry darkGreen = PaletteEntry(Color(0xFF006400));\n  static const PaletteEntry darkPink = PaletteEntry(Color(0xFFFFC0CB));\n  static const PaletteEntry darkRed = PaletteEntry(Color(0xFFB22222));\n  static const PaletteEntry gray = PaletteEntry(Color(0xFF808080));\n  static const PaletteEntry lime = PaletteEntry(Color(0xFF32CD32));\n  static const PaletteEntry lightBlue = PaletteEntry(Color(0xFF00BFFF));\n  static const PaletteEntry lightGreen = PaletteEntry(Color(0xFF7CFC00));\n  static const PaletteEntry lightGray = PaletteEntry(Color(0xFFD3D3D3));\n  static const PaletteEntry lightOrange = PaletteEntry(Color(0xFFFF8C00));\n  static const PaletteEntry lightPink = PaletteEntry(Color(0xFFFFB6C1));\n  static const PaletteEntry lightRed = PaletteEntry(Color(0xFFFFA07A));\n  static const PaletteEntry orange = PaletteEntry(Color(0xFFFFA500));\n  static const PaletteEntry pink = PaletteEntry(Color(0xFFFF69B4));\n  static const PaletteEntry purple = PaletteEntry(Color(0xFF800080));\n  static const PaletteEntry teal = PaletteEntry(Color(0xFF008080));\n  static const PaletteEntry yellow = PaletteEntry(Color(0xFFFFFF00));\n}\n"
  },
  {
    "path": "packages/flame/lib/src/parallax.dart",
    "content": "import 'dart:async';\nimport 'dart:ui';\n\nimport 'package:collection/collection.dart';\nimport 'package:flame/cache.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/src/flame.dart';\nimport 'package:flame/src/sprite_animation.dart';\nimport 'package:flame/src/sprite_animation_ticker.dart';\nimport 'package:flutter/painting.dart';\n\nextension ParallaxExtension on Game {\n  Future<Parallax> loadParallax(\n    List<ParallaxData> dataList, {\n    Vector2? size,\n    Vector2? baseVelocity,\n    Vector2? velocityMultiplierDelta,\n    ImageRepeat repeat = ImageRepeat.repeatX,\n    Alignment alignment = Alignment.bottomLeft,\n    LayerFill fill = LayerFill.height,\n    FilterQuality? filterQuality,\n    String? package,\n  }) {\n    return Parallax.load(\n      dataList,\n      size: size,\n      baseVelocity: baseVelocity,\n      velocityMultiplierDelta: velocityMultiplierDelta,\n      repeat: repeat,\n      alignment: alignment,\n      fill: fill,\n      images: images,\n      filterQuality: filterQuality,\n      package: package,\n    );\n  }\n\n  Future<ParallaxImage> loadParallaxImage(\n    String path, {\n    ImageRepeat repeat = ImageRepeat.repeatX,\n    Alignment alignment = Alignment.bottomLeft,\n    LayerFill fill = LayerFill.height,\n    FilterQuality? filterQuality,\n    String? package,\n  }) {\n    return ParallaxImage.load(\n      path,\n      repeat: repeat,\n      alignment: alignment,\n      fill: fill,\n      images: images,\n      filterQuality: filterQuality,\n      package: package,\n    );\n  }\n\n  Future<ParallaxAnimation> loadParallaxAnimation(\n    String path,\n    SpriteAnimationData animationData, {\n    ImageRepeat repeat = ImageRepeat.repeatX,\n    Alignment alignment = Alignment.bottomLeft,\n    LayerFill fill = LayerFill.height,\n    FilterQuality? filterQuality,\n    String? package,\n  }) {\n    return ParallaxAnimation.load(\n      path,\n      animationData,\n      repeat: repeat,\n      alignment: alignment,\n      fill: fill,\n      images: images,\n      filterQuality: filterQuality,\n      package: package,\n    );\n  }\n\n  Future<ParallaxLayer> loadParallaxLayer(\n    ParallaxData data, {\n    ImageRepeat repeat = ImageRepeat.repeatX,\n    Alignment alignment = Alignment.bottomLeft,\n    LayerFill fill = LayerFill.height,\n    Vector2? velocityMultiplier,\n    FilterQuality? filterQuality,\n    String? package,\n  }) {\n    return ParallaxLayer.load(\n      data,\n      velocityMultiplier: velocityMultiplier,\n      repeat: repeat,\n      alignment: alignment,\n      fill: fill,\n      images: images,\n      filterQuality: filterQuality,\n      package: package,\n    );\n  }\n}\n\nabstract class ParallaxRenderer {\n  /// If and how the image should be repeated on the canvas\n  final ImageRepeat repeat;\n\n  /// How to align the image in relation to the screen\n  final Alignment alignment;\n\n  /// How to fill the screen with the image, always proportionally scaled.\n  final LayerFill fill;\n\n  final FilterQuality filterQuality;\n\n  ParallaxRenderer({\n    ImageRepeat? repeat,\n    Alignment? alignment,\n    LayerFill? fill,\n    FilterQuality? filterQuality,\n  }) : repeat = repeat ?? ImageRepeat.repeatX,\n       alignment = alignment ?? Alignment.bottomLeft,\n       fill = fill ?? LayerFill.height,\n       filterQuality = filterQuality ?? FilterQuality.low;\n\n  void update(double dt);\n\n  Image get image;\n}\n\n/// Specifications with a path to an image and how it should be drawn in\n/// relation to the device screen\nclass ParallaxImage extends ParallaxRenderer {\n  /// The image\n  final Image _image;\n\n  ParallaxImage(\n    this._image, {\n    super.repeat,\n    super.alignment,\n    super.fill,\n    super.filterQuality,\n  });\n\n  /// Takes a path of an image, and optionally arguments for how the image\n  /// should repeat ([repeat]), which edge it should align with ([alignment]),\n  /// which axis it should fill the image on ([fill]) and [images] which is the\n  /// image cache that should be used. If no image cache is set, the global\n  /// flame cache is used.\n  static Future<ParallaxImage> load(\n    String path, {\n    ImageRepeat repeat = ImageRepeat.repeatX,\n    Alignment alignment = Alignment.bottomLeft,\n    LayerFill fill = LayerFill.height,\n    Images? images,\n    FilterQuality? filterQuality,\n    String? package,\n  }) async {\n    images ??= Flame.images;\n    return ParallaxImage(\n      await images.load(path, package: package),\n      repeat: repeat,\n      alignment: alignment,\n      fill: fill,\n      filterQuality: filterQuality,\n    );\n  }\n\n  @override\n  Image get image => _image;\n\n  @override\n  void update(double dt) {\n    // noop\n  }\n}\n\n/// Specifications with a SpriteAnimation and how it should be drawn in\n/// relation to the device screen\nclass ParallaxAnimation extends ParallaxRenderer {\n  final SpriteAnimationTicker _animationTicker;\n\n  /// The animation's frames pre-rendered into images so it can be used in the\n  /// parallax.\n  final List<Image> _prerenderedFrames;\n\n  ParallaxAnimation(\n    SpriteAnimation animation,\n    this._prerenderedFrames, {\n    super.repeat,\n    super.alignment,\n    super.fill,\n    super.filterQuality,\n  }) : _animationTicker = animation.createTicker();\n\n  /// Takes a path of an image, a SpriteAnimationData, and optionally arguments\n  /// for how the image should repeat ([repeat]), which edge it should align\n  /// with ([alignment]), which axis it should fill the image on ([fill]) and\n  /// [images] which is the image cache that should be used. If no image cache\n  /// is set, the global flame cache is used.\n  ///\n  /// _IMPORTANT_: This method pre render all the frames of the animation into\n  /// image instances so it can be used inside the parallax. Just keep that in\n  /// mind when using animations in parallax, the over use of it, or the use\n  /// of big animations (be it in number of frames or the size of the images)\n  /// can lead to high use of memory.\n  static Future<ParallaxAnimation> load(\n    String path,\n    SpriteAnimationData animationData, {\n    ImageRepeat repeat = ImageRepeat.repeatX,\n    Alignment alignment = Alignment.bottomLeft,\n    LayerFill fill = LayerFill.height,\n    Images? images,\n    FilterQuality? filterQuality,\n    String? package,\n  }) async {\n    images ??= Flame.images;\n\n    final animation = await SpriteAnimation.load(\n      path,\n      animationData,\n      images: images,\n      package: package,\n    );\n    final prerenderedFrames = animation.frames\n        .map((frame) => frame.sprite.toImageSync())\n        .toList();\n\n    return ParallaxAnimation(\n      animation,\n      prerenderedFrames,\n      repeat: repeat,\n      alignment: alignment,\n      fill: fill,\n      filterQuality: filterQuality,\n    );\n  }\n\n  @override\n  Image get image => _prerenderedFrames[_animationTicker.currentIndex];\n\n  @override\n  void update(double dt) {\n    _animationTicker.update(dt);\n  }\n}\n\n/// Represents one layer in the parallax, draws out an image on a canvas in the\n/// manner specified by the parallaxImage.\nclass ParallaxLayer {\n  final ParallaxRenderer parallaxRenderer;\n  late Vector2 velocityMultiplier;\n  late Rect _paintArea;\n  final Vector2 _scroll = Vector2.zero();\n  late Vector2 _imageSize;\n  double _scale = 1.0;\n\n  /// [parallaxRenderer] is the representation of the renderer with data of how\n  /// the layer should behave.\n  /// [velocityMultiplier] will be used to determine the velocity of the layer\n  /// by multiplying the [Parallax.baseVelocity] with the [velocityMultiplier].\n  ParallaxLayer(\n    this.parallaxRenderer, {\n    Vector2? velocityMultiplier,\n  }) : velocityMultiplier = velocityMultiplier ?? Vector2.all(1.0);\n\n  Vector2 currentOffset() => _scroll;\n\n  void resize(Vector2 size) {\n    double scale(LayerFill fill) {\n      return switch (fill) {\n        LayerFill.height => parallaxRenderer.image.height / size.y,\n        LayerFill.width => parallaxRenderer.image.width / size.x,\n        _ => _scale,\n      };\n    }\n\n    _scale = scale(parallaxRenderer.fill);\n\n    // The image size so that it fulfills the LayerFill parameter\n    _imageSize = parallaxRenderer.image.size / _scale;\n\n    // Number of images that can fit on the canvas plus one\n    // to have something to scroll to without leaving canvas empty\n    final count = Vector2.all(1) + (size.clone()..divide(_imageSize));\n\n    // Percentage of the image size that will overflow\n    final overflow = ((_imageSize.clone()..multiply(count)) - size)\n      ..divide(_imageSize);\n\n    // Align image to correct side of the screen\n    final alignment = parallaxRenderer.alignment;\n\n    final marginX = alignment.x * overflow.x / 2 + overflow.x / 2;\n    final marginY = alignment.y * overflow.y / 2 + overflow.y / 2;\n\n    _scroll.setValues(marginX, marginY);\n\n    // Size of the area to paint the images on\n    final paintSize = count..multiply(_imageSize);\n    _paintArea = paintSize.toRect();\n  }\n\n  // Used to avoid creating new Vector2 objects in the update-loop.\n  final _delta = Vector2.zero();\n\n  void update(Vector2 delta, double dt) {\n    parallaxRenderer.update(dt);\n    // Scale the delta so that images that are larger don't scroll faster\n    _delta\n      ..setFrom(delta)\n      ..divide(_imageSize);\n    _scroll.add(_delta);\n    switch (parallaxRenderer.repeat) {\n      case ImageRepeat.repeat:\n        _scroll.setValues(_scroll.x % 1, _scroll.y % 1);\n      case ImageRepeat.repeatX:\n        _scroll.setValues(_scroll.x % 1, _scroll.y);\n      case ImageRepeat.repeatY:\n        _scroll.setValues(_scroll.x, _scroll.y % 1);\n      case ImageRepeat.noRepeat:\n    }\n\n    _paintArea = Rect.fromLTWH(\n      -_scroll.x * _imageSize.x,\n      -_scroll.y * _imageSize.y,\n      _paintArea.width,\n      _paintArea.height,\n    );\n  }\n\n  void render(Canvas canvas) {\n    if (_paintArea.isEmpty) {\n      return;\n    }\n    paintImage(\n      canvas: canvas,\n      image: parallaxRenderer.image,\n      rect: _paintArea,\n      repeat: parallaxRenderer.repeat,\n      scale: _scale,\n      alignment: parallaxRenderer.alignment,\n      filterQuality: parallaxRenderer.filterQuality,\n    );\n  }\n\n  /// Takes a data of a parallax renderer, and optionally arguments for how it\n  /// should repeat ([repeat]), which edge it should align with ([alignment]),\n  /// which axis it should fill the image on ([fill]) and [images] which is the\n  /// image cache that should be used. If no image cache is set, the global\n  /// flame cache is used.\n  static Future<ParallaxLayer> load(\n    ParallaxData data, {\n    Vector2? velocityMultiplier,\n    ImageRepeat repeat = ImageRepeat.repeatX,\n    Alignment alignment = Alignment.bottomLeft,\n    LayerFill fill = LayerFill.height,\n    Images? images,\n    FilterQuality? filterQuality,\n    String? package,\n  }) async {\n    return ParallaxLayer(\n      await data.load(\n        repeat,\n        alignment,\n        fill,\n        images,\n        filterQuality,\n        package: package,\n      ),\n      velocityMultiplier: velocityMultiplier,\n    );\n  }\n}\n\n/// How to fill the screen with the image, always proportionally scaled.\nenum LayerFill { height, width, none }\n\nabstract class ParallaxData {\n  Future<ParallaxRenderer> load(\n    ImageRepeat repeat,\n    Alignment alignment,\n    LayerFill fill,\n    Images? images,\n    FilterQuality? filterQuality, {\n    String? package,\n  });\n}\n\n/// Contains the fields and logic to load a [ParallaxImage].\nclass ParallaxImageData extends ParallaxData {\n  final String path;\n\n  ParallaxImageData(this.path);\n\n  @override\n  Future<ParallaxRenderer> load(\n    ImageRepeat repeat,\n    Alignment alignment,\n    LayerFill fill,\n    Images? images,\n    FilterQuality? filterQuality, {\n    String? package,\n  }) {\n    return ParallaxImage.load(\n      path,\n      repeat: repeat,\n      alignment: alignment,\n      fill: fill,\n      images: images,\n      filterQuality: filterQuality,\n      package: package,\n    );\n  }\n}\n\n/// Contains the fields and logic to load a [ParallaxAnimation].\nclass ParallaxAnimationData extends ParallaxData {\n  final String path;\n  final SpriteAnimationData animationData;\n\n  ParallaxAnimationData(this.path, this.animationData);\n\n  @override\n  Future<ParallaxRenderer> load(\n    ImageRepeat repeat,\n    Alignment alignment,\n    LayerFill fill,\n    Images? images,\n    FilterQuality? filterQuality, {\n    String? package,\n  }) {\n    return ParallaxAnimation.load(\n      path,\n      animationData,\n      repeat: repeat,\n      alignment: alignment,\n      fill: fill,\n      images: images,\n      filterQuality: filterQuality,\n      package: package,\n    );\n  }\n}\n\n/// A full parallax, several layers of images drawn out on the screen and each\n/// layer moves with different velocities to give an effect of depth.\nclass Parallax {\n  late Vector2 baseVelocity;\n  late Rect _clipRect;\n  final List<ParallaxLayer> layers;\n\n  bool isSized = false;\n  late final Vector2 _size;\n\n  /// Do not modify this directly, since the layers won't be resized if you do.\n  Vector2 get size => _size;\n\n  set size(Vector2 newSize) {\n    resize(newSize);\n  }\n\n  Parallax(\n    this.layers, {\n    Vector2? size,\n    Vector2? baseVelocity,\n  }) {\n    this.baseVelocity = baseVelocity ?? Vector2.zero();\n    if (size != null) {\n      resize(size);\n    }\n  }\n\n  /// The base offset of the parallax, can be used in an outer update loop\n  /// if you want to transition the parallax to a certain position.\n  Vector2 currentOffset() => layers[0].currentOffset();\n\n  /// If the `ParallaxComponent` isn't used your own wrapper needs to call this\n  /// on creation.\n  void resize(Vector2 newSize) {\n    if (!isSized) {\n      _size = Vector2.zero();\n    }\n    if (newSize != _size || !isSized) {\n      _size.setFrom(newSize);\n      _clipRect = _size.toRect();\n      layers.forEach((layer) => layer.resize(_size));\n    }\n    isSized |= true;\n  }\n\n  // Used to avoid creating new Vector2 objects in the update-loop.\n  final _delta = Vector2.zero();\n\n  void update(double dt) {\n    for (final layer in layers) {\n      layer.update(\n        _delta\n          ..setFrom(baseVelocity)\n          ..multiply(layer.velocityMultiplier)\n          ..scale(dt),\n        dt,\n      );\n    }\n  }\n\n  /// Note that this method only should be used if all of your layers should\n  /// have the same layer arguments (how the images should be repeated, aligned\n  /// and filled), otherwise load the [ParallaxLayer]s individually and use the\n  /// normal constructor.\n  ///\n  /// [load] takes a list of paths to all the images that you want to use in the\n  /// parallax.\n  /// Optionally arguments for the [baseVelocity] and [velocityMultiplierDelta]\n  /// can be passed in, [baseVelocity] defines what the base velocity of the\n  /// layers should be and [velocityMultiplierDelta] defines how the velocity\n  /// should change the closer the layer is (`velocityMultiplierDelta ^ n`,\n  /// where `n` is the layer index).\n  /// Arguments for how all the images should repeat ([repeat]),\n  /// which edge it should align with ([alignment]), which axis it should fill\n  /// the image on ([fill]) and [images] which is the image cache that should be\n  /// used can also be passed in.\n  /// If no image cache is set, the global flame cache is used.\n  static Future<Parallax> load(\n    Iterable<ParallaxData> dataList, {\n    Vector2? size,\n    Vector2? baseVelocity,\n    Vector2? velocityMultiplierDelta,\n    ImageRepeat repeat = ImageRepeat.repeatX,\n    Alignment alignment = Alignment.bottomLeft,\n    LayerFill fill = LayerFill.height,\n    Images? images,\n    FilterQuality? filterQuality,\n    String? package,\n  }) async {\n    final velocityDelta = velocityMultiplierDelta ?? Vector2.all(1.0);\n    final layers = await Future.wait<ParallaxLayer>(\n      dataList.mapIndexed((depth, data) async {\n        final velocityMultiplier = List.filled(depth, velocityDelta)\n            .fold<Vector2>(\n              velocityDelta,\n              (previousValue, delta) => previousValue.clone()..multiply(delta),\n            );\n        final renderer = await data.load(\n          repeat,\n          alignment,\n          fill,\n          images,\n          filterQuality,\n          package: package,\n        );\n        return ParallaxLayer(\n          renderer,\n          velocityMultiplier: velocityMultiplier,\n        );\n      }),\n    );\n    return Parallax(\n      layers,\n      size: size,\n      baseVelocity: baseVelocity,\n    );\n  }\n\n  void render(Canvas canvas, {Vector2? position}) {\n    canvas.save();\n    if (position != null) {\n      canvas.translateVector(position);\n    }\n    canvas.clipRect(_clipRect);\n    layers.forEach((layer) {\n      canvas.save();\n      layer.render(canvas);\n      canvas.restore();\n    });\n    canvas.restore();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/particles/accelerated_particle.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/components/mixins/single_child_particle.dart';\nimport 'package:flame/src/particles/curved_particle.dart';\nimport 'package:flame/src/particles/particle.dart';\n\n/// A particle that serves as a container for basic acceleration physics.\n///\n/// [speed] is logical px per second.\n///\n/// ```dart\n/// AcceleratedParticle(\n///   speed: Vector2(0, 100), // is 100 logical px/s down.\n///   acceleration: Vector2(-40, 0) // will accelerate to the left at rate of 40 px/s\n/// )\n/// ```\nclass AcceleratedParticle extends CurvedParticle with SingleChildParticle {\n  @override\n  Particle child;\n\n  final Vector2 acceleration;\n  Vector2 speed;\n  Vector2 position;\n\n  AcceleratedParticle({\n    required this.child,\n    Vector2? acceleration,\n    Vector2? speed,\n    Vector2? position,\n    super.lifespan,\n  }) : acceleration = acceleration ?? Vector2.zero(),\n       position = position ?? Vector2.zero(),\n       speed = speed ?? Vector2.zero();\n\n  @override\n  void render(Canvas canvas) {\n    canvas.save();\n    canvas.translateVector(position);\n    super.render(canvas);\n    canvas.restore();\n  }\n\n  @override\n  void update(double dt) {\n    speed.addScaled(acceleration, dt);\n    position\n      ..addScaled(speed, dt)\n      ..addScaled(acceleration, -dt * dt * 0.5);\n    super.update(dt);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/particles/circle_particle.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/particles/particle.dart';\n\n/// Plain circle with no other behaviors.\n///\n/// Consider composing this with other [Particle]s to achieve needed effects.\nclass CircleParticle extends Particle {\n  final Paint paint;\n  final double radius;\n\n  CircleParticle({\n    required this.paint,\n    this.radius = 10.0,\n    super.lifespan,\n  });\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawCircle(Offset.zero, radius, paint);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/particles/component_particle.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/components/core/component.dart';\nimport 'package:flame/src/extensions/vector2.dart';\nimport 'package:flame/src/particles/particle.dart';\n\nclass ComponentParticle extends Particle {\n  final Component component;\n  final Vector2? size;\n  final Paint? overridePaint;\n\n  ComponentParticle({\n    required this.component,\n    this.size,\n    this.overridePaint,\n    super.lifespan,\n  });\n\n  @override\n  void render(Canvas canvas) {\n    component.render(canvas);\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    component.update(dt);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/particles/composed_particle.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/particles/particle.dart';\n\n/// A single [Particle] which manages multiple children\n/// by proxying all lifecycle hooks.\n///\n/// [applyLifespanToChildren] if true, then [children] will have the same\n/// lifespan as parent [ComposedParticle]\nclass ComposedParticle extends Particle {\n  final List<Particle> children;\n  final bool applyLifespanToChildren;\n\n  ComposedParticle({\n    required this.children,\n    super.lifespan,\n    this.applyLifespanToChildren = true,\n  });\n\n  @override\n  void setLifespan(double lifespan) {\n    super.setLifespan(lifespan);\n\n    if (applyLifespanToChildren) {\n      for (final child in children) {\n        child.setLifespan(lifespan);\n      }\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    for (final child in children) {\n      child.render(canvas);\n    }\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    children.removeWhere((particle) => particle.shouldRemove);\n\n    for (final child in children) {\n      child.update(dt);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/particles/computed_particle.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/particles/particle.dart';\n\n/// A function which should render desired contents\n/// onto a given canvas. External state needed for\n/// rendering should be stored elsewhere, so that this delegate could use it\ntypedef ParticleRenderDelegate = void Function(Canvas c, Particle particle);\n\n/// An abstract [Particle] container which delegates rendering outside\n/// Allows to implement very interesting scenarios from scratch.\nclass ComputedParticle extends Particle {\n  // A delegate function which will be called\n  // to render particle on each frame\n  ParticleRenderDelegate renderer;\n\n  ComputedParticle({\n    required this.renderer,\n    super.lifespan,\n  });\n\n  @override\n  void render(Canvas canvas) {\n    renderer(canvas, this);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/particles/curved_particle.dart",
    "content": "import 'package:flame/src/particles/particle.dart';\nimport 'package:flutter/animation.dart';\n\n/// A [Particle] which applies certain [Curve] for\n/// easing or other purposes to its [progress] getter.\nclass CurvedParticle extends Particle {\n  final Curve curve;\n\n  CurvedParticle({\n    this.curve = Curves.linear,\n    super.lifespan,\n  });\n\n  @override\n  double get progress => curve.transform(super.progress);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/particles/image_particle.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/extensions/vector2.dart';\nimport 'package:flame/src/particles/particle.dart';\n\n/// A [Particle] which renders given [Image] on a [Canvas] image is centered.\n/// If any other behavior is needed, consider using ComputedParticle.\nclass ImageParticle extends Particle {\n  /// dart.ui [Image] to draw\n  Image image;\n  Paint paint;\n\n  late Rect src;\n  late Rect dest;\n\n  ImageParticle({\n    required this.image,\n    Paint? paint,\n    Vector2? size,\n    super.lifespan,\n  }) : paint = paint ?? Paint() {\n    final srcWidth = image.width.toDouble();\n    final srcHeight = image.height.toDouble();\n    final destWidth = size?.x ?? srcWidth;\n    final destHeight = size?.y ?? srcHeight;\n\n    src = Rect.fromLTWH(0, 0, srcWidth, srcHeight);\n    dest = Rect.fromLTWH(\n      -destWidth / 2,\n      -destHeight / 2,\n      destWidth,\n      destHeight,\n    );\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawImageRect(image, src, dest, paint);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/particles/moving_particle.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/components/mixins/single_child_particle.dart';\nimport 'package:flame/src/particles/curved_particle.dart';\nimport 'package:flame/src/particles/particle.dart';\n\n/// Statically move given child [Particle] by given [Vector2].\n///\n/// If you're looking to move the child, consider the [MovingParticle].\nclass MovingParticle extends CurvedParticle with SingleChildParticle {\n  @override\n  Particle child;\n\n  final Vector2 from;\n  final Vector2 to;\n\n  MovingParticle({\n    required this.child,\n    required this.to,\n    Vector2? from,\n    super.lifespan,\n    super.curve,\n  }) : from = from ?? Vector2.zero();\n\n  /// Used to avoid creating new [Vector2] objects in [update].\n  static final _tmpVector = Vector2.zero();\n\n  @override\n  void render(Canvas canvas) {\n    canvas.save();\n    final current = _tmpVector\n      ..setFrom(from)\n      ..lerp(to, progress);\n    canvas.translateVector(current);\n    super.render(canvas);\n    canvas.restore();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/particles/paint_particle.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/components/mixins/single_child_particle.dart';\nimport 'package:flame/src/particles/curved_particle.dart';\nimport 'package:flame/src/particles/particle.dart';\n\n/// A particle which renders its child with certain [Paint]\n/// Could be used for applying composite effects.\n/// Be aware that any composite operation is relatively expensive, as involves\n/// copying portions of GPU memory. The less pixels copied, the faster it'll be.\nclass PaintParticle extends CurvedParticle with SingleChildParticle {\n  @override\n  Particle child;\n\n  final Paint paint;\n\n  /// Defines Canvas layer bounds\n  /// for applying this particle composite effect.\n  /// Any child content outside this bounds will be clipped.\n  final Rect bounds;\n\n  PaintParticle({\n    required this.child,\n    required this.paint,\n\n    // Reasonably large rect for most particles\n    this.bounds = const Rect.fromLTRB(-50, -50, 50, 50),\n    super.lifespan,\n  });\n\n  @override\n  void render(Canvas canvas) {\n    canvas.saveLayer(bounds, paint);\n    super.render(canvas);\n    canvas.restore();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/particles/particle.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame/src/particles/accelerated_particle.dart';\nimport 'package:flame/src/particles/composed_particle.dart';\nimport 'package:flame/src/particles/moving_particle.dart';\nimport 'package:flame/src/particles/rotating_particle.dart';\nimport 'package:flame/src/particles/scaled_particle.dart';\nimport 'package:flame/src/particles/scaling_particle.dart';\nimport 'package:flame/src/particles/translated_particle.dart';\nimport 'package:flame/src/timer.dart';\nimport 'package:flutter/animation.dart';\n\n/// A function which returns a [Particle] when called.\ntypedef ParticleGenerator = Particle Function(int);\n\n/// Base class implementing common behavior for all the particles.\n///\n/// Intention is to follow the same \"Extreme Composability\" style as seen across\n/// the whole Flutter framework. Each type of particle implements some\n/// particular behavior which then could be nested and combined to create\n/// the experience you are looking for.\nabstract class Particle {\n  /// Generates a given amount of particles and then combining them into one\n  /// single [ComposedParticle].\n  ///\n  /// Useful for procedural particle generation.\n  static Particle generate({\n    required ParticleGenerator generator,\n    int count = 10,\n    double? lifespan,\n    bool applyLifespanToChildren = true,\n  }) {\n    return ComposedParticle(\n      lifespan: lifespan,\n      applyLifespanToChildren: applyLifespanToChildren,\n      children: List<Particle>.generate(count, generator),\n    );\n  }\n\n  /// Internal timer defining how long this [Particle] will live.\n  ///\n  /// [Particle] will be marked for removal when this timer is over.\n  Timer? _timer;\n\n  /// Stores desired lifespan of the particle in seconds.\n  late double _lifespan;\n\n  /// Will be set to true by [update] when this [Particle] reaches the end of\n  /// its lifespan.\n  bool _shouldBeRemoved = false;\n\n  /// Construct a new [Particle].\n  ///\n  /// The [lifespan] is how long this [Particle] will live in seconds, with\n  /// microsecond precision.\n  Particle({\n    double? lifespan,\n  }) {\n    setLifespan(lifespan ?? 0.5);\n  }\n\n  /// Getter for the current lifespan of this [Particle].\n  double get lifespan => _lifespan;\n\n  /// This method will return true as soon as the particle reaches the end of\n  /// its lifespan.\n  ///\n  /// It will then be ready to be removed by a wrapping container.\n  bool get shouldRemove => _shouldBeRemoved;\n\n  /// Getter which should be used by subclasses to get overall progress.\n  ///\n  /// Also allows to substitute progress with other values, for example adding\n  /// easing as in CurvedParticle.\n  double get progress => _timer?.progress ?? 0.0;\n\n  /// Should render this [Particle] to given [Canvas].\n  ///\n  /// Default behavior is empty, so that it's not required to override this in\n  /// a [Particle] that renders nothing and serve as a behavior container.\n  void render(Canvas canvas) {}\n\n  /// Updates the [_timer] of this [Particle].\n  void update(double dt) {\n    _timer?.update(dt);\n  }\n\n  /// A control method allowing a parent of this [Particle] to pass down it's\n  /// lifespan.\n  ///\n  /// Allows to only specify desired lifespan once, at the very top of the\n  /// [Particle] tree which then will be propagated down using this method.\n  ///\n  /// See `SingleChildParticle` or [ComposedParticle] for details.\n  void setLifespan(double lifespan) {\n    // TODO(wolfenrain): Maybe make it into a setter/getter?\n    _lifespan = lifespan;\n    _timer?.stop();\n    _timer = Timer(lifespan, onTick: () => _shouldBeRemoved = true)..start();\n  }\n\n  /// Wraps this particle with a [TranslatedParticle].\n  ///\n  /// Statically repositioning it for the time of the lifespan.\n  Particle translated(Vector2 offset) {\n    return TranslatedParticle(\n      offset: offset,\n      child: this,\n      lifespan: _lifespan,\n    );\n  }\n\n  /// Wraps this particle with a [MovingParticle].\n  ///\n  /// Allowing it to move from one [Vector2] to another one.\n  Particle moving({\n    required Vector2 to,\n    Vector2? from,\n    Curve curve = Curves.linear,\n  }) {\n    return MovingParticle(\n      from: from ?? Vector2.zero(),\n      to: to,\n      curve: curve,\n      child: this,\n      lifespan: _lifespan,\n    );\n  }\n\n  /// Wraps this particle with a [AcceleratedParticle].\n  ///\n  /// Allowing to specify desired position speed and acceleration and leave\n  /// the basic physics do the rest.\n  Particle accelerated({\n    required Vector2 acceleration,\n    Vector2? position,\n    Vector2? speed,\n  }) {\n    return AcceleratedParticle(\n      position: position ?? Vector2.zero(),\n      speed: speed ?? Vector2.zero(),\n      acceleration: acceleration,\n      child: this,\n      lifespan: _lifespan,\n    );\n  }\n\n  /// Rotates this particle to a fixed angle in radians using the\n  /// [RotatingParticle].\n  Particle rotated(double angle) {\n    return RotatingParticle(\n      child: this,\n      lifespan: _lifespan,\n      from: angle,\n      to: angle,\n    );\n  }\n\n  /// Rotates this particle from a given angle to another one in radians\n  /// using [RotatingParticle].\n  Particle rotating({\n    double from = 0,\n    double to = pi,\n  }) {\n    return RotatingParticle(\n      child: this,\n      lifespan: _lifespan,\n      from: from,\n      to: to,\n    );\n  }\n\n  /// Wraps this particle with a [ScaledParticle].\n  ///\n  /// Allows for changing the size of this particle and/or its children.\n  Particle scaled(double scale) {\n    return ScaledParticle(scale: scale, child: this, lifespan: _lifespan);\n  }\n\n  /// Wraps this particle with a [ScalingParticle].\n  ///\n  /// Allows for changing the size of this particle and/or its children.\n  ScalingParticle scaling({double to = 0, Curve curve = Curves.linear}) {\n    return ScalingParticle(\n      to: to,\n      child: this,\n      lifespan: _lifespan,\n      curve: curve,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/particles/rotating_particle.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/src/components/mixins/single_child_particle.dart';\nimport 'package:flame/src/particles/curved_particle.dart';\nimport 'package:flame/src/particles/particle.dart';\n\n/// A particle which rotates its child over the lifespan\n/// between two given bounds in radians\nclass RotatingParticle extends CurvedParticle with SingleChildParticle {\n  @override\n  Particle child;\n\n  final double from;\n  final double to;\n\n  RotatingParticle({\n    required this.child,\n    this.from = 0,\n    this.to = 2 * pi,\n    super.lifespan,\n  });\n\n  double get angle => lerpDouble(from, to, progress) ?? 0;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.save();\n    canvas.rotate(angle);\n    super.render(canvas);\n    canvas.restore();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/particles/scaled_particle.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/components/mixins/single_child_particle.dart';\nimport 'package:flame/src/particles/particle.dart';\nimport 'package:flame/src/particles/scaling_particle.dart';\n\n/// Statically scales the given child [Particle] by given [scale].\n///\n/// If you're looking to scale the child over time, consider [ScalingParticle].\nclass ScaledParticle extends Particle with SingleChildParticle {\n  @override\n  Particle child;\n\n  final double scale;\n\n  ScaledParticle({\n    required this.child,\n    this.scale = 1.0,\n    super.lifespan,\n  });\n\n  @override\n  void render(Canvas canvas) {\n    canvas.save();\n    canvas.scale(scale);\n    super.render(canvas);\n    canvas.restore();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/particles/scaling_particle.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/components/mixins/single_child_particle.dart';\nimport 'package:flame/src/particles/curved_particle.dart';\nimport 'package:flame/src/particles/particle.dart';\n\n/// A particle which scale its child over the lifespan\n/// between 1 and a provided scale.\nclass ScalingParticle extends CurvedParticle with SingleChildParticle {\n  @override\n  Particle child;\n\n  final double to;\n\n  ScalingParticle({\n    required this.child,\n    this.to = 0,\n    super.lifespan,\n    super.curve,\n  });\n\n  double get scale => lerpDouble(1, to, progress) ?? 0;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.save();\n    canvas.scale(scale);\n    super.render(canvas);\n    canvas.restore();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/particles/sprite_animation_particle.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/anchor.dart';\nimport 'package:flame/src/extensions/vector2.dart';\nimport 'package:flame/src/particles/particle.dart';\nimport 'package:flame/src/sprite_animation.dart';\nimport 'package:flame/src/sprite_animation_ticker.dart';\n\nexport '../sprite_animation.dart';\n\n/// A [Particle] which applies certain [SpriteAnimation].\nclass SpriteAnimationParticle extends Particle {\n  final SpriteAnimation animation;\n  final SpriteAnimationTicker animationTicker;\n  final Vector2? position;\n  final Vector2? size;\n  final Anchor anchor;\n  final Paint? overridePaint;\n  final bool alignAnimationTime;\n\n  SpriteAnimationParticle({\n    required this.animation,\n    this.position,\n    this.size,\n    this.anchor = Anchor.center,\n    this.overridePaint,\n    super.lifespan,\n    this.alignAnimationTime = true,\n  }) : animationTicker = animation.createTicker();\n\n  @override\n  void setLifespan(double lifespan) {\n    super.setLifespan(lifespan);\n\n    if (alignAnimationTime) {\n      animation.stepTime = lifespan / animation.frames.length;\n      animationTicker.reset();\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    animationTicker.getSprite().render(\n      canvas,\n      position: position,\n      size: size,\n      anchor: anchor,\n      overridePaint: overridePaint,\n    );\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    animationTicker.update(dt);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/particles/sprite_particle.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/anchor.dart';\nimport 'package:flame/src/extensions/vector2.dart';\nimport 'package:flame/src/particles/particle.dart';\nimport 'package:flame/src/sprite.dart';\n\nexport '../sprite.dart';\n\n/// A [Particle] which applies certain [Sprite].\nclass SpriteParticle extends Particle {\n  final Sprite sprite;\n  final Vector2? position;\n  final Vector2? size;\n  final Anchor anchor;\n  final Paint? overridePaint;\n\n  SpriteParticle({\n    required this.sprite,\n    this.position,\n    this.size,\n    this.anchor = Anchor.center,\n    this.overridePaint,\n    super.lifespan,\n  });\n\n  @override\n  void render(Canvas canvas) {\n    sprite.render(\n      canvas,\n      position: position,\n      size: size,\n      anchor: anchor,\n      overridePaint: overridePaint,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/particles/translated_particle.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/components/mixins/single_child_particle.dart';\nimport 'package:flame/src/particles/particle.dart';\n\n/// Statically offset given child [Particle] by given [Vector2].\n///\n/// If you're looking to move the child, consider MovingParticle.\nclass TranslatedParticle extends Particle with SingleChildParticle {\n  @override\n  Particle child;\n\n  final Vector2 offset;\n\n  TranslatedParticle({\n    required this.child,\n    required this.offset,\n    super.lifespan,\n  });\n\n  @override\n  void render(Canvas canvas) {\n    canvas.save();\n    canvas.translateVector(offset);\n    super.render(canvas);\n    canvas.restore();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/post_process/post_process.dart",
    "content": "import 'dart:async';\nimport 'dart:ui' as ui show Image;\nimport 'dart:ui' hide Image;\n\nimport 'package:flame/components.dart';\nimport 'package:flame/post_process.dart';\nimport 'package:flutter/material.dart';\nimport 'package:meta/meta.dart';\n\n/// A way to apply effects to a whole component tree.\n///\n/// This is specially useful for applying effects based on [FragmentShader]s.\n///\n/// Post processes are created by sub-classing  and implementing the\n/// [postProcess] method.\n///\n/// Within the [postProcess] method, you can use the [renderSubtree] method\n/// to render the component tree. This method will take care of setting up\n/// the context for the post process, so children can know if they are being\n/// rendered within a post process by using\n/// [PostProcessingContextFinder.findPostProcessFromContext].\n///\n/// The subtree of a post process is the children of the owner, if it is a\n/// [PostProcessComponent], or the [World] if\n/// the owner is a [CameraComponent]. When using a\n/// [PostProcessSequentialGroup], the subtree also includes the preceding\n/// post processes in the group.\n///\n/// Another useful method is [rasterizeSubtree], which will render the component\n/// tree to an image. This is useful for using such image as a texture in\n/// a shader.\n///\n/// Post process can be combined using [PostProcessGroup] or\n/// [PostProcessSequentialGroup] to build a chain of post processes.\n///\n/// Post processes can be used via [CameraComponent.postProcess] or\n/// [PostProcessComponent].\n///\n/// A post process may even render a subtree multiple times if it needs to.\n/// In addition, if children check for\n/// [PostProcessingContextFinder.findPostProcessFromContext],\n/// they may even render differently in each render pass.\n/// Ideal for mask textures.\n///\n/// See also:\n/// - [PostProcessComponent] for a component that applies a post process\n/// - [PostProcessGroup] for a group of post processes that will be applied\n///   in parallel\n/// - [PostProcessSequentialGroup] for a group of post processes that will be\n///   applied in sequence\n/// - [FragmentProgram] for a way to create post processes using fragment\n///   shaders\n/// - [PostProcessingContextFinder] for a way to access the post process\n///   context from within a component during rendering\nabstract class PostProcess {\n  PostProcess({double? pixelRatio})\n    : pixelRatio =\n          pixelRatio ??\n          PlatformDispatcher.instance.views.first.devicePixelRatio;\n\n  /// The pixel ratio of the screen. This is used to scale the image generated\n  /// by  [rasterizeSubtree] to the correct size.\n  ///\n  /// Defaults to [FlutterView.devicePixelRatio].\n  double pixelRatio;\n\n  /// Similarly to components, post processes can be loaded asynchronously.\n  ///\n  /// Use this to load any resources needed for the post process. This is called\n  /// when the post process is added to a [CameraComponent] or a\n  /// [PostProcessComponent].\n  ///\n  /// See also:\n  /// - [Component.onLoad] for more information on how to load components.\n  FutureOr<void> onLoad() {}\n\n  /// This method is called every frame to update the post process.\n  ///\n  /// Use this to update any state that needs to be updated every frame.\n  /// This is called after the [update] method of the [Component] class.\n  ///\n  /// An example of usage is to update the time uniform of a shader.\n  void update(double dt) {}\n\n  void Function(Canvas)? _renderTree;\n  void Function(PostProcess?)? _updateContext;\n  Vector2? _size;\n\n  /// This method is called to render the post process, to be called by the\n  /// \"owner\" of\n  /// the post process, like a [CameraComponent] or a [PostProcessComponent].\n  @internal\n  void render(\n    Canvas canvas,\n    Vector2 size,\n    ValueSetter<Canvas> renderTree,\n    ValueSetter<PostProcess?> updateContext,\n  ) {\n    _renderTree = renderTree;\n    _updateContext = updateContext;\n    _size = size;\n\n    canvas.save();\n    postProcess(size, canvas);\n    canvas.restore();\n\n    _size = null;\n    _renderTree = null;\n    _updateContext = null;\n  }\n\n  /// One of the two methods that subclasses should invoke to render the\n  /// what is considered the \"subtree\" of the post process.\n  ///\n  /// Differently from [renderSubtree], this method will rasterize the\n  /// subtree to an image, which can be used as a texture in a shader. This\n  /// is an expensive operation, so it should be used sparingly.\n  ///\n  /// This method will set the context of the post process, so that\n  /// components can know they are being rendered within a post process.\n  @nonVirtual\n  @protected\n  ui.Image rasterizeSubtree() {\n    final recorder = PictureRecorder();\n    final innerCanvas = Canvas(recorder);\n\n    renderSubtree(innerCanvas);\n\n    final picture = recorder.endRecording();\n    try {\n      return picture.toImageSync(\n        (pixelRatio * _size!.x).ceil(),\n        (pixelRatio * _size!.y).ceil(),\n      );\n    } finally {\n      picture.dispose();\n    }\n  }\n\n  /// One of the two methods that subclasses should invoke to render the\n  /// what is considered the \"subtree\" of the post process.\n  ///\n  /// This method will set the context of the post process, so that\n  /// components can know they are being rendered within a post process.\n  /// See [PostProcessingContextFinder.findPostProcessFromContext].\n  @nonVirtual\n  @protected\n  void renderSubtree(Canvas canvas) {\n    canvas.save();\n    _updateContext!(this);\n    _renderTree!(canvas);\n    _updateContext!(null);\n    canvas.restore();\n  }\n\n  /// There the effects of the post process are applied. This is where you\n  /// should implement the logic of the post process. Including eventual calls\n  /// to [rasterizeSubtree] and [renderSubtree].\n  ///\n  /// If neither is called the post process will not render anything apart from\n  /// what is implemented in this method.\n  void postProcess(Vector2 size, Canvas canvas) {\n    renderSubtree(canvas);\n  }\n}\n\n/// A special type of [PostProcess] that is used to group multiple post\n/// processes together. This is useful for applying multiple post processes\n/// at once.\n///\n/// Beware that all elements in [postProcesses] will be rendered with the same\n/// \"subtree\", sof if more than one item calls [renderSubtree], the subtree\n/// will be rendered multiple times. If multiple post processes need to call\n/// [rasterizeSubtree] or [renderSubtree], consider using\n/// [PostProcessSequentialGroup].\n///\n/// In other words, the subtree of the group post process will be shared in\n/// parallel with all the post processes in the group.\n///\n/// See also:\n/// - [PostProcess] for a single post process and more information on what\n/// is the \"subtree\" of a post process.\n/// - [PostProcessSequentialGroup] for a group of post processes that will be\n/// applied in sequence where each post process will be considered part of the\n/// subtree of the next one.\nclass PostProcessGroup extends PostProcess {\n  PostProcessGroup({\n    required this.postProcesses,\n  });\n\n  final List<PostProcess> postProcesses;\n\n  @override\n  Future<void> onLoad() async {\n    for (final postProcess in postProcesses) {\n      await postProcess.onLoad();\n    }\n  }\n\n  @override\n  void update(double dt) {\n    for (final postProcess in postProcesses) {\n      postProcess.update(dt);\n    }\n  }\n\n  @override\n  void render(\n    Canvas canvas,\n    Vector2 size,\n    ValueSetter<Canvas> renderTree,\n    ValueSetter<PostProcess?> updateContext,\n  ) {\n    for (final postProcess in postProcesses) {\n      postProcess.render(canvas, size, renderTree, updateContext);\n    }\n  }\n}\n\n/// A special type of [PostProcess] that is used to group multiple post\n/// processes together. This is useful for applying multiple post processes\n/// at once, but in a sequential manner.\n///\n/// This means that each post process will be considered part of the subtree\n/// of the next one. This is useful for post processes that need to be\n/// rendered in a specific order, or that need to share the same subtree.\n///\n/// In other words, the subtree of the group post process will be the subtree of\n/// the first post process in the group, and all the other post processes\n/// will be rendered with the same subtree.\n///\n/// See also:\n/// - [PostProcess] for a single post process and more information on what\n/// is the \"subtree\" of a post process.\n/// - [PostProcessGroup] for a group of post processes that will be applied\n/// in parallel where all the post processes will be rendered with the same\n/// subtree.\nclass PostProcessSequentialGroup extends PostProcessGroup {\n  PostProcessSequentialGroup({\n    required super.postProcesses,\n  });\n\n  @override\n  void render(\n    Canvas canvas,\n    Vector2 size,\n    ValueSetter<Canvas> renderTree,\n    ValueSetter<PostProcess?> updateContext,\n  ) {\n    // Build the stack of post processes in reverse order\n    final stack = postProcesses.reversed.toList();\n\n    // Start with the original renderTree\n    void runNext(Canvas c) {\n      if (stack.isEmpty) {\n        renderTree(c);\n        return;\n      }\n\n      final postProcess = stack.removeAt(0);\n      postProcess.render(c, size, runNext, updateContext);\n    }\n\n    canvas.save();\n    runNext(canvas);\n    canvas.restore();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/post_process/post_process_component.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/post_process.dart';\nimport 'package:flame/src/camera/camera_component.dart';\nimport 'package:flame/src/game/notifying_vector2.dart';\nimport 'package:meta/meta.dart';\n\n/// A [PositionComponent] that applies a post-processing effect to its children.\n/// This component is useful for applying effects such as bloom, blur, other\n/// fragment shader effects to a group of components.\n///\n/// As opposed to [CameraComponent.postProcess], this only applies the post\n/// process to the children of this component. This means that if you want to\n/// apply a post process to the whole screen, you should use\n/// [CameraComponent.postProcess] instead.\n///\n/// During the rendering process, children of this component can verify if they\n/// are being rendered within a post process by using\n/// [PostProcessingContextFinder.findPostProcessFromContext].\n///\n/// If a specific [size] is provided the component will be rendered with that\n/// size, otherwise it will calculate the size based on the bounding box of\n/// its children.\n///\n/// See also:\n/// - [PostProcess] for the base class for post processes and more information\n/// about how to create them.\n/// - [PostProcessGroup] for a group of post processes that will be applied\n/// in parallel\n/// - [CameraComponent.postProcess] for a way to apply post processes to the\n/// whole screen.\nclass PostProcessComponent<T extends PostProcess> extends PositionComponent {\n  PostProcessComponent({\n    required this.postProcess,\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.nativeAngle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.key,\n  });\n\n  @override\n  PostProcessComponentRenderContext<T> get renderContext => _renderContext;\n\n  final _renderContext = PostProcessComponentRenderContext<T>(\n    postProcess: null,\n  );\n\n  final T postProcess;\n\n  @override\n  @mustCallSuper\n  Future<void> onLoad() async {\n    await postProcess.onLoad();\n    return super.onLoad();\n  }\n\n  @override\n  @mustCallSuper\n  void update(double dt) {\n    super.update(dt);\n    postProcess.update(dt);\n  }\n\n  @override\n  @mustCallSuper\n  void onChildrenChanged(_, __) {\n    _recalculateBoundingSize();\n  }\n\n  NotifyingVector2? _maybeBoundingSize;\n  NotifyingVector2 get _boundingSizeOfChildren {\n    if (_maybeBoundingSize == null) {\n      _recalculateBoundingSize();\n    }\n    return _maybeBoundingSize!;\n  }\n\n  void _recalculateBoundingSize() {\n    final rectChildren = children.query<PositionComponent>();\n\n    if (rectChildren.isEmpty) {\n      (_maybeBoundingSize ??= NotifyingVector2.zero()).setZero();\n    }\n\n    final boundingBox = rectChildren\n        .map((child) => child.toRect())\n        .reduce((a, b) => a.expandToInclude(b));\n    (_maybeBoundingSize ??= NotifyingVector2.zero()).setValues(\n      boundingBox.width,\n      boundingBox.height,\n    );\n  }\n\n  @override\n  NotifyingVector2 get size {\n    final superSize = super.size;\n    if (superSize.isZero() && hasChildren) {\n      return _boundingSizeOfChildren;\n    }\n    return superSize;\n  }\n\n  @override\n  @mustCallSuper\n  void renderTree(Canvas canvas) {\n    decorator.applyChain(\n      (canvas) {\n        postProcess.render(\n          canvas,\n          size,\n          super.renderTreeWithoutDecorator,\n          (context) {\n            _renderContext.postProcess = postProcess;\n          },\n        );\n      },\n      canvas,\n    );\n  }\n}\n\nclass PostProcessComponentRenderContext<T extends PostProcess>\n    extends ComponentRenderContext {\n  PostProcessComponentRenderContext({\n    required this.postProcess,\n  });\n\n  T? postProcess;\n}\n\nextension PostProcessingContextFinder on Component {\n  T? findPostProcessFromContext<T extends PostProcess>() {\n    final closestContext =\n        findRenderContext<PostProcessComponentRenderContext<T>>();\n    if (closestContext != null) {\n      return closestContext.postProcess;\n    }\n    final contextInCamera =\n        findRenderContext<CameraRenderContext>()?.currentPostProcess;\n    if (contextInCamera is T) {\n      return contextInCamera;\n    }\n\n    return null;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/rendering/decorator.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/rendering/paint_decorator.dart';\nimport 'package:flame/src/rendering/rotate3d_decorator.dart';\nimport 'package:flame/src/rendering/shadow3d_decorator.dart';\nimport 'package:flame/src/rendering/transform2d_decorator.dart';\nimport 'package:meta/meta.dart';\n\n/// [Decorator] is an abstract class that encapsulates a particular visual\n/// effect that should apply to drawing commands wrapped by this class.\n///\n/// The simplest way to apply a [Decorator] to a component is to override its\n/// `renderTree` method like this:\n/// ```dart\n/// @override\n/// void renderTree(Canvas canvas) {\n///   decorator.applyChain(super.renderTree, canvas);\n/// }\n/// ```\n///\n/// Decorators have ability to form a chain, where multiple decorators can be\n/// applied in a sequence. This chain is essentially a unary tree, or a linked\n/// list: each decorator knows only about the next decorator on the chain.\n///\n/// The following implementations are available:\n/// - [PaintDecorator]\n/// - [Rotate3DDecorator]\n/// - [Shadow3DDecorator]\n/// - [Transform2DDecorator]\nclass Decorator {\n  /// The next decorator in the chain, or null if there is none.\n  Decorator? _next;\n\n  /// Applies this and all subsequent decorators if any.\n  ///\n  /// This method is the main method through which the decorator is applied.\n  void applyChain(void Function(Canvas) draw, Canvas canvas) {\n    apply(\n      _next == null\n          ? draw\n          : (nextCanvas) => _next!.applyChain(draw, nextCanvas),\n      canvas,\n    );\n  }\n\n  /// Applies visual effect while [draw]ing on the [canvas].\n  ///\n  /// The default implementation is a no-op; all other non-trivial decorators\n  /// transform the canvas before drawing, or perform some other adjustments.\n  ///\n  /// This method must be implemented by the subclasses, but it is not available\n  /// to external users: use [applyChain] instead.\n  @protected\n  void apply(void Function(Canvas) draw, Canvas canvas) {\n    draw(canvas);\n  }\n\n  //#region Decorator chain functionality\n\n  bool get isLastDecorator => _next == null;\n\n  /// Adds a new decorator onto the chain of decorators\n  void addLast(Decorator? decorator) {\n    if (decorator != null) {\n      if (_next == null) {\n        _next = decorator;\n      } else {\n        _next!.addLast(decorator);\n      }\n    }\n  }\n\n  /// Removes the last decorator from the chain of decorators\n  void removeLast() {\n    if (isLastDecorator) {\n      return;\n    }\n    if (_next!.isLastDecorator) {\n      _next = null;\n    } else {\n      _next!.removeLast();\n    }\n  }\n\n  void replaceLast(Decorator? decorator) {\n    if (decorator == null) {\n      removeLast();\n    } else if (isLastDecorator || _next!.isLastDecorator) {\n      _next = decorator;\n    } else {\n      _next!.replaceLast(decorator);\n    }\n  }\n\n  //#endregion\n}\n"
  },
  {
    "path": "packages/flame/lib/src/rendering/hue_decorator.dart",
    "content": "import 'dart:math' as math;\nimport 'dart:ui';\n\nimport 'package:flame/src/rendering/decorator.dart';\n\n/// Calculates the hue rotation matrix for a given angle in radians.\n///\n/// Uses the standard NTSC luminance weights (R: 0.213, G: 0.715, B: 0.072)\n/// to produce a 4x5 color matrix suitable for [ColorFilter.matrix].\nList<double> hueRotationMatrix(double angle) {\n  final cosT = math.cos(angle);\n  final sinT = math.sin(angle);\n\n  return <double>[\n    0.213 + 0.787 * cosT - 0.213 * sinT,\n    0.715 - 0.715 * cosT - 0.715 * sinT,\n    0.072 - 0.072 * cosT + 0.928 * sinT,\n    0,\n    0,\n    0.213 - 0.213 * cosT + 0.143 * sinT,\n    0.715 + 0.285 * cosT + 0.140 * sinT,\n    0.072 - 0.072 * cosT - 0.283 * sinT,\n    0,\n    0,\n    0.213 - 0.213 * cosT - 0.787 * sinT,\n    0.715 - 0.715 * cosT + 0.715 * sinT,\n    0.072 + 0.928 * cosT + 0.072 * sinT,\n    0,\n    0,\n    0,\n    0,\n    0,\n    1,\n    0,\n  ];\n}\n\n/// [HueDecorator] is a [Decorator] that shifts the hue of the component.\n///\n/// The [hue] value is in radians.\n/// Standard range is from -pi to pi, or 0 to 2*pi.\n///\n/// **Performance Note**: This decorator uses `canvas.saveLayer()` which has\n/// significant overhead compared to direct [Paint] manipulation (like\n/// `HueEffect`). Prefer `HueEffect` for high-density rendering.\nclass HueDecorator extends Decorator {\n  HueDecorator({double hue = 0.0}) : _hue = hue;\n\n  final _paint = Paint();\n  double _hue;\n  bool _isDirty = true;\n\n  /// The hue shift in radians.\n  double get hue => _hue;\n  set hue(double value) {\n    if (_hue != value) {\n      _hue = value;\n      _isDirty = true;\n    }\n  }\n\n  @override\n  void apply(\n    void Function(Canvas) draw,\n    Canvas canvas,\n  ) {\n    if (_hue == 0.0) {\n      draw(canvas);\n      return;\n    }\n\n    if (_isDirty) {\n      _updatePaint();\n      _isDirty = false;\n    }\n\n    canvas.saveLayer(null, _paint);\n    draw(canvas);\n    canvas.restore();\n  }\n\n  void _updatePaint() {\n    _paint.colorFilter = ColorFilter.matrix(hueRotationMatrix(_hue));\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/rendering/mutable_transform.dart",
    "content": "import 'dart:typed_data';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\n\n/// A mutable version of [RSTransform] for custom batch manipulation.\nclass MutableRSTransform implements RSTransform, PositionProvider {\n  final _values = Float32List(4);\n\n  /// This is a cache of `-scos * anchorX + ssin * anchorY`\n  final double _anchorX;\n\n  /// This is a cache of `-ssin * anchorX - scos * anchorY`\n  final double _anchorY;\n\n  final Vector2 _position;\n\n  MutableRSTransform(\n    double scos,\n    double ssin,\n    double tx,\n    double ty,\n    this._anchorX,\n    this._anchorY,\n  ) : _position = Vector2(tx, ty) {\n    _values[0] = scos;\n    _values[1] = ssin;\n    _values[2] = tx + _anchorX;\n    _values[3] = ty + _anchorY;\n  }\n\n  /// The cosine of the rotation multiplied by the scale factor.\n  @override\n  double get scos => _values[0];\n  set scos(double scos) => _values[0] = scos;\n\n  /// The sine of the rotation multiplied by that same scale factor.\n  @override\n  double get ssin => _values[1];\n  set ssin(double ssin) => _values[1] = ssin;\n\n  /// The x coordinate of the translation, minus [scos] multiplied by the\n  /// x-coordinate of the rotation point, plus [ssin] multiplied by the\n  /// y-coordinate of the rotation point.\n  @override\n  double get tx => _values[2];\n  set tx(double tx) => _values[2] = tx;\n\n  /// The y coordinate of the translation, minus [ssin] multiplied by the\n  /// x-coordinate of the rotation point, minus [scos] multiplied by the\n  /// y-coordinate of the rotation point.\n  @override\n  double get ty => _values[3];\n  set ty(double ty) => _values[3] = ty;\n\n  @override\n  set position(Vector2 value) {\n    _values[2] = value.x + _anchorX;\n    _values[3] = value.y + _anchorY;\n    _position.x = value.x;\n    _position.y = value.y;\n  }\n\n  @override\n  Vector2 get position => _position;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/rendering/paint_decorator.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/rendering/decorator.dart';\n\n/// [PaintDecorator] applies a paint filter to a group of drawing operations.\n///\n/// Specifically, the following filters are available:\n/// - [PaintDecorator.blur] adds Gaussian blur to the image, as if your vision\n///   became blurry and out of focus;\n/// - [PaintDecorator.tint] tints the picture with the specified color, as if\n///   looking through a colored glass;\n/// - [PaintDecorator.grayscale] removes all color from the picture, as if it\n///   was a black-and-white photo.\nclass PaintDecorator extends Decorator {\n  PaintDecorator.blur(double amount, [double? amountY]) {\n    addBlur(amount, amountY ?? amount);\n  }\n\n  PaintDecorator.tint(Color color) {\n    _paint.colorFilter = ColorFilter.mode(color, BlendMode.srcATop);\n  }\n\n  PaintDecorator.grayscale({double opacity = 1.0}) {\n    _paint.color = Color.fromARGB((255 * opacity).toInt(), 0, 0, 0);\n    _paint.blendMode = BlendMode.luminosity;\n  }\n\n  final _paint = Paint();\n\n  void addBlur(double amount, [double? amountY]) {\n    _paint.imageFilter = ImageFilter.blur(\n      sigmaX: amount,\n      sigmaY: amountY ?? amount,\n    );\n  }\n\n  @override\n  void apply(void Function(Canvas) draw, Canvas canvas) {\n    canvas.saveLayer(null, _paint);\n    draw(canvas);\n    canvas.restore();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/rendering/rotate3d_decorator.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/src/rendering/decorator.dart';\n\n/// [Rotate3DDecorator] treats the underlying component as if it was a flat\n/// sheet of paper, and applies a 3D rotation to it.\n///\n/// The angles of rotation can be changed dynamically, allowing you to rotate\n/// the content continuously at the desired angular speeds.\nclass Rotate3DDecorator extends Decorator {\n  Rotate3DDecorator({\n    Vector2? center,\n    this.angleX = 0.0,\n    this.angleY = 0.0,\n    this.angleZ = 0.0,\n    this.perspective = 0.001,\n  }) : center = center ?? Vector2.zero();\n\n  /// The center of rotation, in the **parent** coordinate space.\n  Vector2 center;\n\n  /// Angle of rotation around the X axis. This rotation is usually described as\n  /// \"vertical\".\n  double angleX;\n\n  /// Angle of rotation around the Y axis. This rotation is typically described\n  /// as \"horizontal\".\n  double angleY;\n\n  /// Angle of rotation around the Z axis. This is a regular \"2D\" rotation\n  /// because it occurs entirely inside the plane in which the component is\n  /// normally drawn.\n  double angleZ;\n\n  /// The strength of the perspective effect. In other words, how much the\n  /// elements that are \"behind\" the canvas are shrunk, and those in front of\n  /// it are expanded.\n  double perspective;\n\n  /// Returns `true` if the component is currently being rendered from its\n  /// back side, and `false` if it shows the front side.\n  ///\n  /// The \"front\" side is the one displayed at `angleX = angleY = 0`, and the\n  /// \"back\" side is shows if the component is rotated 180º degree around either\n  /// the X or Y axis.\n  bool get isFlipped {\n    final phaseX = (angleX / tau - 0.25) % 1.0;\n    final phaseY = (angleY / tau - 0.25) % 1.0;\n    return (phaseX > 0.5) ^ (phaseY > 0.5);\n  }\n\n  @override\n  void apply(void Function(Canvas) draw, Canvas canvas) {\n    canvas.save();\n    canvas.translate(center.x, center.y);\n    final matrix = Matrix4.identity()\n      ..setEntry(3, 2, perspective)\n      ..rotateX(angleX)\n      ..rotateY(angleY)\n      ..rotateZ(angleZ)\n      ..translateByDouble(-center.x, -center.y, 0.0, 1.0);\n    canvas.transform32(matrix.storage);\n    draw(canvas);\n    canvas.restore();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/rendering/shadow3d_decorator.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame/src/palette.dart';\nimport 'package:flame/src/rendering/decorator.dart';\n// TODO(spydon): Remove this import when flutter version is updated to 3.35.0\n// ignore: unnecessary_import\nimport 'package:vector_math/vector_math.dart';\n\n/// [Shadow3DDecorator] casts a realistic-looking shadow from the component\n/// onto the ground.\n///\n/// This decorator is suitable for games that use an isometric projection.\n///\n/// The shadows are very flexible, allowing for different positions of sun in\n/// the sky, and even supporting airborne objects.\n///\n/// Still, these are not real 3D shadows cast by real 3D objects on a real 3D\n/// terrain, so many limitations apply. For example, the shadow must fall on\n/// the flat ground, having the sun too high in the sky is undesirable as it\n/// would betray the fact that the component is really flat, etc.\nclass Shadow3DDecorator extends Decorator {\n  Shadow3DDecorator({\n    Vector2? base,\n    double? ascent,\n    double? angle,\n    double? xShift,\n    double? yScale,\n    double? blur,\n    double? opacity,\n    Color? baseColor,\n  }) : _base = base?.clone() ?? Vector2.zero(),\n       _ascent = ascent ?? 0,\n       _angle = angle ?? -1.4,\n       _shift = xShift ?? 100.0,\n       _scale = yScale ?? 1.0,\n       _blur = blur ?? 0,\n       _opacity = opacity ?? 0.6,\n       _baseColor = baseColor ?? BasicPalette.black.color;\n\n  /// Coordinates of the point where the component \"touches the ground\". If the\n  /// component is airborne (i.e. [ascent] is non-zero), then this should be the\n  /// coordinate of the point where the component would have touched the ground\n  /// if it landed.\n  ///\n  /// This point is in the parent's coordinate space.\n  Vector2 get base => _base;\n  final Vector2 _base;\n  set base(Vector2 value) {\n    _base.setFrom(value);\n    _transformMatrix = null;\n  }\n\n  /// How high is the component above the ground.\n  double get ascent => _ascent;\n  double _ascent;\n  set ascent(double value) {\n    _ascent = value;\n    _transformMatrix = null;\n  }\n\n  /// The amount of skew the shadow is experiencing. The value of 0 corresponds\n  /// to the shadow being right behind (or in front of) the object. Positive\n  /// shift skews the shadow to the right if it's behind the object, or to the\n  /// left if the shadow is in front of the object. Negative shift skews in the\n  /// opposite direction.\n  ///\n  /// This property should be determined by the meridian position of the sun.\n  double get xShift => _shift;\n  double _shift;\n  set xShift(double value) {\n    _shift = value;\n    _transformMatrix = null;\n  }\n\n  /// The length of the shadow relative to the height of the object. If the sun\n  /// is 45º above the horizon, this scale will be 1. When the sun is higher in\n  /// the sky, the scale factor should be less than 1, and when the sun is\n  /// lower, the scale factor ought to be greater than 1.\n  double get yScale => _scale;\n  double _scale;\n  set yScale(double value) {\n    _scale = value;\n    _transformMatrix = null;\n  }\n\n  /// Visual angle between a vertically standing component and the ground. This\n  /// angle is determined by your isometric projection. Use negative values\n  /// smaller than τ/4 (1.57) in magnitude to create shadows that are behind the\n  /// objects. Use positive angles that are slightly above τ/4 to make shadows\n  /// that are in front of the objects.\n  double get angle => _angle;\n  double _angle;\n  set angle(double value) {\n    _angle = value;\n    _transformMatrix = null;\n  }\n\n  /// The amount of blur to apply to the shadow. The value of 0 produces crisp\n  /// shadows with sharp edges, whereas positive [blur] produces softer-looking\n  /// shadows.\n  ///\n  /// Strictly speaking, the parts of the object that are closer to the ground\n  /// ought to experience less blur than those that are higher up. However, this\n  /// is not supported by this decorator. Still, you can try setting the amount\n  /// of blur proportional to the height of the object, or dependent on its\n  /// ascent above the ground.\n  double get blur => _blur;\n  double _blur;\n  set blur(double value) {\n    _blur = value;\n    _paint = null;\n  }\n\n  /// Shadow's intensity. The value of 1 will create a hard pitch-black shadow,\n  /// which can only happen when there are no ambient sources of light (e.g. in\n  /// a cave). Values close to 0 will make the shadow barely visible, such as\n  /// on a cloudy day.\n  double get opacity => _opacity;\n  double _opacity;\n  set opacity(double value) {\n    _opacity = value;\n    _paint = null;\n  }\n\n  /// Shadow's base color before opacity. This defaults to pitch-black.\n  Color get baseColor => _baseColor;\n  Color _baseColor;\n  set baseColor(Color value) {\n    _baseColor = value;\n    _paint = null;\n  }\n\n  Paint? _paint;\n  Paint _makePaint() {\n    final paint = Paint();\n    final color = baseColor.withValues(alpha: opacity);\n    paint.colorFilter = ColorFilter.mode(color, BlendMode.srcIn);\n    if (_blur > 0) {\n      paint.imageFilter = ImageFilter.blur(sigmaX: blur, sigmaY: blur / _scale);\n    }\n    return paint;\n  }\n\n  Matrix4? _transformMatrix;\n  Matrix4 _makeTransform() {\n    return Matrix4.identity()\n      ..translateByDouble(0.0, 0.0, _scale * _ascent, 1.0)\n      ..setEntry(3, 2, 0.001)\n      ..rotateX(_angle)\n      ..scaleByDouble(1.0, _scale, 1.0, 1.0)\n      ..translateByDouble(\n        -base.x - _shift,\n        -base.y - _scale * _ascent,\n        0.0,\n        1.0,\n      );\n  }\n\n  @override\n  void apply(void Function(Canvas) draw, Canvas canvas) {\n    _transformMatrix ??= _makeTransform();\n    _paint ??= _makePaint();\n\n    canvas.saveLayer(null, _paint!);\n    canvas.translate(base.x + _shift, base.y);\n    canvas.transform32(_transformMatrix!.storage);\n    draw(canvas);\n    canvas.restore();\n    draw(canvas);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/rendering/transform2d_decorator.dart",
    "content": "import 'package:flame/src/components/position_component.dart';\nimport 'package:flame/src/extensions/canvas.dart';\nimport 'package:flame/src/game/transform2d.dart';\nimport 'package:flame/src/rendering/decorator.dart';\n\n/// [Transform2DDecorator] applies a translation/rotation/scale transform to\n/// the canvas.\n///\n/// This decorator is used internally by the [PositionComponent].\nclass Transform2DDecorator extends Decorator {\n  Transform2DDecorator([Transform2D? transform])\n    : transform2d = transform ?? Transform2D();\n\n  final Transform2D transform2d;\n\n  @override\n  void apply(void Function(Canvas) draw, Canvas canvas) {\n    canvas.save();\n    canvas.transform32(transform2d.transformMatrix.storage);\n    draw(canvas);\n    canvas.restore();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/sprite.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/anchor.dart';\nimport 'package:flame/src/cache/images.dart';\nimport 'package:flame/src/flame.dart';\nimport 'package:flame/src/image_composition.dart';\nimport 'package:flame/src/palette.dart';\n\n/// A [Sprite] is a region of an [Image] that can be rendered in the Canvas.\n///\n/// It might represent the entire image or be one of the pieces a spritesheet is\n/// composed of. It holds a reference to the source image from which the region\n/// is extracted, and the [src] rectangle is the portion inside that image that\n/// is to be rendered (not to be confused with the `dest` rect, which is where\n/// in the Canvas the sprite is rendered).\n/// It also has a [paint] field that can be overwritten to apply a tint to this\n/// [Sprite] (default is white, meaning no tint).\nclass Sprite {\n  Paint paint = BasicPalette.white.paint();\n  Image image;\n  Rect src = Rect.zero;\n\n  Sprite(\n    this.image, {\n    Vector2? srcPosition,\n    Vector2? srcSize,\n  }) {\n    this.srcSize = srcSize;\n    this.srcPosition = srcPosition;\n  }\n\n  /// Takes a path of an image, a [srcPosition] and [srcSize] and loads the\n  /// sprite animation.\n  /// When the [images] is omitted, the global [Flame.images] is used.\n  static Future<Sprite> load(\n    String src, {\n    Vector2? srcPosition,\n    Vector2? srcSize,\n    Images? images,\n    String? package,\n  }) async {\n    final imagesCache = images ?? Flame.images;\n    final image = await imagesCache.load(src, package: package);\n    return Sprite(image, srcPosition: srcPosition, srcSize: srcSize);\n  }\n\n  double get _imageWidth => image.width.toDouble();\n\n  double get _imageHeight => image.height.toDouble();\n\n  Vector2 get originalSize => Vector2(_imageWidth, _imageHeight);\n\n  Vector2 get srcSize => Vector2(src.width, src.height);\n\n  set srcSize(Vector2? size) {\n    final actualSize = size ?? image.size;\n    src = srcPosition.toPositionedRect(actualSize);\n  }\n\n  Vector2 get srcPosition => src.topLeft.toVector2();\n\n  set srcPosition(Vector2? position) {\n    src = (position ?? Vector2.zero()).toPositionedRect(srcSize);\n  }\n\n  String _createRasterizeCacheKey() {\n    return '${image.hashCode}-${srcPosition.x}-'\n        '${srcPosition.y}-${srcSize.x}-${srcSize.y}';\n  }\n\n  /// Returns a new [Sprite] where the image in memory is just the region\n  /// defined by the original sprite.\n  ///\n  /// If the [images] argument is passed it will be used to cache the\n  /// rasterized image, otherwise the global [Flame.images] will be used.\n  /// If the [cacheKey] is passed in, that will be the key for the cached image,\n  /// otherwise the hash code of the rasterized image will be used as the key.\n  Future<Sprite> rasterize({String? cacheKey, Images? images}) async {\n    final key = cacheKey ?? _createRasterizeCacheKey();\n\n    final imagesCache = images ?? Flame.images;\n\n    if (imagesCache.containsKey(key)) {\n      final cachedImage = imagesCache.fromCache(key);\n      return Sprite(\n        cachedImage,\n        srcSize: srcSize,\n      );\n    }\n\n    final rasterizedImage = await toImage();\n    imagesCache.add(key, rasterizedImage);\n\n    return Sprite(\n      rasterizedImage,\n      srcSize: srcSize,\n    );\n  }\n\n  /// Same as [render], but takes both the position and the size as a single\n  /// [Rect].\n  ///\n  /// **Note**: only use this if you are already using [Rect]'s to represent\n  /// both the position and dimension of your [Sprite]. If you are using\n  /// [Vector2]s, prefer the other method.\n  void renderRect(\n    Canvas canvas,\n    Rect rect, {\n    Paint? overridePaint,\n  }) {\n    render(\n      canvas,\n      position: rect.topLeft.toVector2(),\n      size: rect.size.toVector2(),\n      overridePaint: overridePaint,\n    );\n  }\n\n  // Used to avoid the creation of new Vector2 objects in render.\n  static final _tmpRenderPosition = Vector2.zero();\n  static final _tmpRenderSize = Vector2.zero();\n  static final _zeroPosition = Vector2.zero();\n\n  /// Renders this sprite onto the [canvas].\n  ///\n  /// * [position]: x,y coordinates where it will be drawn; default to origin.\n  /// * [size]: width/height dimensions; it can be bigger or smaller than the\n  ///   original size -- but it defaults to the original texture size.\n  /// * [anchor]: where in the sprite the x/y coordinates refer to; defaults to\n  ///   topLeft.\n  /// * [overridePaint]: paint to use. You can also change the paint on your\n  ///   Sprite instance. Default is white.\n  void render(\n    Canvas canvas, {\n    Vector2? position,\n    Vector2? size,\n    Anchor anchor = Anchor.topLeft,\n    Paint? overridePaint,\n    double? bleed,\n  }) {\n    if (position != null) {\n      _tmpRenderPosition.setFrom(position);\n    } else {\n      _tmpRenderPosition.setZero();\n    }\n\n    _tmpRenderSize.setFrom(size ?? srcSize);\n\n    _tmpRenderPosition.setValues(\n      _tmpRenderPosition.x - (anchor.x * _tmpRenderSize.x),\n      _tmpRenderPosition.y - (anchor.y * _tmpRenderSize.y),\n    );\n\n    if (bleed != null) {\n      _tmpRenderPosition.x -= bleed;\n      _tmpRenderPosition.y -= bleed;\n      _tmpRenderSize.x += bleed * 2;\n      _tmpRenderSize.y += bleed * 2;\n    }\n\n    final drawRect = _tmpRenderPosition.toPositionedRect(_tmpRenderSize);\n    final drawPaint = overridePaint ?? paint;\n\n    canvas.drawImageRect(image, src, drawRect, drawPaint);\n  }\n\n  /// Return a new [Image] based on the [src] of the Sprite.\n  ///\n  /// **Note:** This is a heavy async operation and should not be called inside\n  /// the game loop. Remember to call dispose on the [Image] object once you\n  /// aren't going to use it anymore.\n  Future<Image> toImage() {\n    final composition = ImageComposition()\n      ..add(image, _zeroPosition, source: src);\n    return composition.compose();\n  }\n\n  /// Return a new [Image] based on the [src] of the Sprite.\n  ///\n  /// A sync version of the [toImage] function. Read [Picture.toImageSync] for a\n  /// detailed description of possible benefits in performance.\n  Image toImageSync() {\n    final composition = ImageComposition()\n      ..add(image, _zeroPosition, source: src);\n    return composition.composeSync();\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/sprite_animation.dart",
    "content": "import 'dart:async';\nimport 'dart:ui';\n\nimport 'package:flame/src/cache/images.dart';\nimport 'package:flame/src/extensions/vector2.dart';\nimport 'package:flame/src/flame.dart';\nimport 'package:flame/src/sprite.dart';\nimport 'package:flame/src/sprite_animation_ticker.dart';\n\nexport 'sprite.dart';\n\nclass SpriteAnimationFrameData {\n  /// Coordinates of the sprite of this Frame\n  final Vector2 srcPosition;\n\n  /// Size of the sprite of this Frame\n  final Vector2 srcSize;\n\n  /// The duration to display it, in seconds.\n  final double stepTime;\n\n  SpriteAnimationFrameData({\n    required this.srcPosition,\n    required this.srcSize,\n    required this.stepTime,\n  });\n}\n\nclass SpriteAnimationData {\n  late List<SpriteAnimationFrameData> frames;\n  final bool loop;\n\n  /// Creates a SpriteAnimationData from the given [frames] and [loop]\n  /// parameters.\n  SpriteAnimationData(this.frames, {this.loop = true});\n\n  /// Takes some parameters and automatically calculate and create the frames\n  /// for the sprite animation data.\n  ///\n  /// [amount] The total amount of frames present on the image.\n  /// [stepTimes] A list of times (in seconds) of each frame, should have a\n  /// length equals to the amount parameter.\n  /// [textureSize] The size of each frame.\n  /// [amountPerRow] An optional parameter to inform how many frames there are\n  /// on which row, useful for sprite sheets where the frames as disposed on\n  /// multiple lines.\n  /// [texturePosition] An optional parameter with the initial coordinate where\n  /// the frames begin on the image, default to (top: 0, left: 0).\n  /// [loop] An optional parameter to inform if this animation loops or has a\n  /// single iteration, defaults to true.\n  SpriteAnimationData.variable({\n    required int amount,\n    required List<double> stepTimes,\n    required Vector2 textureSize,\n    int? amountPerRow,\n    Vector2? texturePosition,\n    this.loop = true,\n  }) : assert(amountPerRow == null || amount >= amountPerRow) {\n    amountPerRow ??= amount;\n    texturePosition ??= Vector2.zero();\n    frames = List<SpriteAnimationFrameData>.generate(amount, (i) {\n      final position = Vector2(\n        texturePosition!.x + (i % amountPerRow!) * textureSize.x,\n        texturePosition.y + (i ~/ amountPerRow) * textureSize.y,\n      );\n      return SpriteAnimationFrameData(\n        stepTime: stepTimes[i],\n        srcPosition: position,\n        srcSize: textureSize,\n      );\n    });\n  }\n\n  /// Specifies the range of the sprite grid.\n  ///\n  /// Make sure your sprites are placed left-to-right and top-to-bottom\n  ///\n  /// [start] is the start frame index.\n  /// [end] is the end frame index.\n  SpriteAnimationData.range({\n    required int start,\n    required int end,\n    required int amount,\n    required List<double> stepTimes,\n    required Vector2 textureSize,\n    int? amountPerRow,\n    Vector2? texturePosition,\n    this.loop = true,\n  }) : assert(amountPerRow == null || amount >= amountPerRow),\n       assert(start <= end && start >= 0 && end <= amount),\n       assert(stepTimes.length >= end - start + 1) {\n    amountPerRow ??= amount;\n    texturePosition ??= Vector2.zero();\n    frames = List<SpriteAnimationFrameData>.generate(end - start + 1, (index) {\n      final i = index + start;\n      final position = Vector2(\n        texturePosition!.x + (i % amountPerRow!) * textureSize.x,\n        texturePosition.y + (i ~/ amountPerRow) * textureSize.y,\n      );\n      return SpriteAnimationFrameData(\n        stepTime: stepTimes[index],\n        srcPosition: position,\n        srcSize: textureSize,\n      );\n    });\n  }\n\n  /// Works just like [SpriteAnimationData.variable] but uses the same\n  /// [stepTime] for all frames.\n  factory SpriteAnimationData.sequenced({\n    required int amount,\n    required double stepTime,\n    required Vector2 textureSize,\n    int? amountPerRow,\n    Vector2? texturePosition,\n    bool loop = true,\n  }) {\n    return SpriteAnimationData.variable(\n      amount: amount,\n      amountPerRow: amountPerRow,\n      texturePosition: texturePosition,\n      textureSize: textureSize,\n      loop: loop,\n      stepTimes: List.filled(amount, stepTime),\n    );\n  }\n}\n\n/// Represents a single sprite animation frame.\nclass SpriteAnimationFrame {\n  /// The [Sprite] to be displayed.\n  Sprite sprite;\n\n  /// The duration to display it, in seconds.\n  double stepTime;\n\n  /// Create based on the parameters.\n  SpriteAnimationFrame(this.sprite, this.stepTime);\n}\n\n/// Represents a sprite animation, that is, a list of sprites that change with\n/// time.\nclass SpriteAnimation {\n  SpriteAnimation(this.frames, {this.loop = true})\n    : assert(frames.isNotEmpty, 'There must be at least one animation frame'),\n      assert(\n        frames.every((frame) => frame.stepTime > 0),\n        'All frames must have positive durations',\n      );\n\n  /// Create animation from a list of [sprites] all having the same transition\n  /// time [stepTime].\n  factory SpriteAnimation.spriteList(\n    List<Sprite> sprites, {\n    required double stepTime,\n    bool loop = true,\n  }) {\n    return SpriteAnimation(\n      sprites.map((sprite) => SpriteAnimationFrame(sprite, stepTime)).toList(),\n      loop: loop,\n    );\n  }\n\n  /// Create animation from a list of [sprites] each having its own duration\n  /// provided in the [stepTimes] list.\n  factory SpriteAnimation.variableSpriteList(\n    List<Sprite> sprites, {\n    required List<double> stepTimes,\n    bool loop = true,\n  }) {\n    assert(\n      stepTimes.length == sprites.length,\n      'Lengths of stepTimes and sprites lists must be equal',\n    );\n    return SpriteAnimation(\n      [\n        for (var i = 0; i < sprites.length; i++)\n          SpriteAnimationFrame(sprites[i], stepTimes[i]),\n      ],\n      loop: loop,\n    );\n  }\n\n  /// Create animation from a single [image] that contains all frames.\n  ///\n  /// The [data] argument provides the description of where the individual\n  /// sprites are located within the main image.\n  factory SpriteAnimation.fromFrameData(\n    Image image,\n    SpriteAnimationData data,\n  ) {\n    return SpriteAnimation(\n      [\n        for (final frameData in data.frames)\n          SpriteAnimationFrame(\n            Sprite(\n              image,\n              srcSize: frameData.srcSize,\n              srcPosition: frameData.srcPosition,\n            ),\n            frameData.stepTime,\n          ),\n      ],\n      loop: data.loop,\n    );\n  }\n\n  /// Automatically creates an Animation Object using animation data provided by\n  /// the json file provided by Aseprite.\n  ///\n  /// [image]: sprite sheet animation image.\n  /// [jsonData]: animation's data in json format.\n  factory SpriteAnimation.fromAsepriteData(\n    Image image,\n    Map<String, dynamic> jsonData,\n  ) {\n    final jsonFrames = jsonData['frames'] as Map<String, dynamic>;\n    return SpriteAnimation(\n      jsonFrames.values.map((dynamic value) {\n        final map = value as Map;\n        final frameData = map['frame'] as Map<String, dynamic>;\n        final x = frameData['x'] as int;\n        final y = frameData['y'] as int;\n        final width = frameData['w'] as int;\n        final height = frameData['h'] as int;\n        final stepTime = (map['duration'] as int) / 1000;\n        final sprite = Sprite(\n          image,\n          srcPosition: Vector2Extension.fromInts(x, y),\n          srcSize: Vector2Extension.fromInts(width, height),\n        );\n        return SpriteAnimationFrame(sprite, stepTime);\n      }).toList(),\n    );\n  }\n\n  /// Takes a path of an image, a [SpriteAnimationData] and loads the sprite\n  /// animation.\n  /// When the [images] is omitted, the global [Flame.images] is used.\n  static Future<SpriteAnimation> load(\n    String src,\n    SpriteAnimationData data, {\n    Images? images,\n    String? package,\n  }) async {\n    final imagesCache = images ?? Flame.images;\n    final image = await imagesCache.load(src, package: package);\n    return SpriteAnimation.fromFrameData(image, data);\n  }\n\n  /// The frames that compose this animation.\n  List<SpriteAnimationFrame> frames = [];\n\n  /// Whether the animation loops after the last sprite of the list, going back\n  /// to the first, or keeps returning the last when done.\n  bool loop = true;\n\n  /// Sets a different step time to each frame.\n  /// The sizes of the arrays must match.\n  set variableStepTimes(List<double> stepTimes) {\n    assert(stepTimes.length == frames.length);\n    for (var i = 0; i < frames.length; i++) {\n      assert(stepTimes[i] > 0, 'All step times must be positive');\n      frames[i].stepTime = stepTimes[i];\n    }\n  }\n\n  /// Sets a fixed step time to all frames.\n  set stepTime(double stepTime) {\n    assert(stepTime > 0, 'Step time must be positive');\n    frames.forEach((frame) => frame.stepTime = stepTime);\n  }\n\n  /// Returns a new Animation equal to this one in definition, but each copy can\n  /// be run independently.\n  SpriteAnimation clone() {\n    return SpriteAnimation(frames.toList(), loop: loop);\n  }\n\n  /// Returns a new Animation based on this animation, but with its frames in\n  /// reversed order\n  SpriteAnimation reversed() {\n    return SpriteAnimation(frames.reversed.toList(), loop: loop);\n  }\n\n  /// Creates and returns a new [SpriteAnimationTicker].\n  SpriteAnimationTicker createTicker() => SpriteAnimationTicker(this);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/sprite_animation_ticker.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:meta/meta.dart';\n\n/// A helper class to make the [spriteAnimation] tick.\nclass SpriteAnimationTicker {\n  SpriteAnimationTicker(this.spriteAnimation);\n\n  // The current sprite animation.\n  final SpriteAnimation spriteAnimation;\n\n  /// Index of the current frame that should be displayed.\n  int currentIndex = 0;\n\n  /// Current clock time (total time) of this animation, in seconds, since last\n  /// frame.\n  ///\n  /// It's ticked by the update method. It's reset every frame change.\n  double clock = 0.0;\n\n  /// Total elapsed time of this animation, in seconds, since start or a reset.\n  double elapsed = 0.0;\n\n  /// Registered method to be triggered when the animation starts.\n  void Function()? onStart;\n\n  /// Registered method to be triggered when the animation frame updates.\n  void Function(int currentIndex)? onFrame;\n\n  /// Registered method to be triggered when the animation complete.\n  void Function()? onComplete;\n\n  @visibleForTesting\n  Completer<void>? completeCompleter;\n\n  /// The current frame that should be displayed.\n  SpriteAnimationFrame get currentFrame => spriteAnimation.frames[currentIndex];\n\n  /// Returns whether the animation is on the first frame.\n  bool get isFirstFrame => currentIndex == 0;\n\n  /// Returns whether the animation is on the last frame.\n  bool get isLastFrame => currentIndex == spriteAnimation.frames.length - 1;\n\n  /// Returns whether the animation has only a single frame (and is, thus, a\n  /// still image).\n  bool get isSingleFrame => spriteAnimation.frames.length == 1;\n\n  bool _paused = false;\n\n  /// Returns current value of paused flag.\n  bool get isPaused => _paused;\n\n  /// Sets the given value of paused flag.\n  set paused(bool value) {\n    _paused = value;\n  }\n\n  /// A future that will complete when the animation completes.\n  ///\n  /// An animation is considered to be completed if it reaches its [isLastFrame]\n  /// and is not looping.\n  Future<void> get completed {\n    if (_done) {\n      return Future.value();\n    }\n\n    completeCompleter ??= Completer<void>();\n\n    return completeCompleter!.future;\n  }\n\n  /// Resets the animation, like it would just have been created.\n  void reset() {\n    clock = 0.0;\n    elapsed = 0.0;\n    currentIndex = 0;\n    _done = false;\n    _started = false;\n    _paused = false;\n\n    // Reset completeCompleter if it's already completed\n    if (completeCompleter?.isCompleted ?? false) {\n      completeCompleter = null;\n    }\n  }\n\n  /// Sets this animation to be on the last frame.\n  void setToLast() {\n    currentIndex = spriteAnimation.frames.length - 1;\n    clock = spriteAnimation.frames[currentIndex].stepTime;\n    elapsed = totalDuration();\n    update(0);\n  }\n\n  /// Computes the total duration of this animation\n  /// (before it's done or repeats).\n  double totalDuration() {\n    return spriteAnimation.frames\n        .map((f) => f.stepTime)\n        .reduce((a, b) => a + b);\n  }\n\n  /// Gets the current [Sprite] that should be shown.\n  ///\n  /// In case it reaches the end:\n  /// If loop is true, it will return the last sprite. Otherwise, it will\n  /// go back to the first.\n  Sprite getSprite() {\n    return currentFrame.sprite;\n  }\n\n  /// If loop is false, returns whether the animation is done (fixed in the\n  /// last Sprite).\n  ///\n  /// Always returns false otherwise.\n  bool _done = false;\n  bool done() => _done;\n\n  /// Local flag to determine if the animation has started to prevent multiple\n  /// calls to [onStart].\n  bool _started = false;\n\n  /// Updates this animation, if not paused, ticking the lifeTime by an amount\n  /// [dt] (in seconds).\n  void update(double dt) {\n    if (_paused) {\n      return;\n    }\n    clock += dt;\n    elapsed += dt;\n    if (_done) {\n      return;\n    }\n    if (!_started) {\n      onStart?.call();\n      onFrame?.call(currentIndex);\n      _started = true;\n    }\n\n    while (clock >= currentFrame.stepTime) {\n      if (isLastFrame) {\n        if (spriteAnimation.loop) {\n          clock -= currentFrame.stepTime;\n          currentIndex = 0;\n          onFrame?.call(currentIndex);\n        } else {\n          _done = true;\n          onComplete?.call();\n          completeCompleter?.complete();\n          return;\n        }\n      } else {\n        clock -= currentFrame.stepTime;\n        currentIndex++;\n        onFrame?.call(currentIndex);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/sprite_batch.dart",
    "content": "import 'dart:collection';\nimport 'dart:math' show pi;\nimport 'dart:ui';\n\nimport 'package:flame/cache.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/game.dart';\nimport 'package:meta/meta.dart';\n\nextension SpriteBatchExtension on Game {\n  /// Utility method to load and cache the image for a [SpriteBatch] based on\n  /// its options.\n  Future<SpriteBatch> loadSpriteBatch(\n    String path, {\n    Color? defaultColor,\n    BlendMode? defaultBlendMode,\n    RSTransform? defaultTransform,\n    Images? imageCache,\n    bool useAtlas = true,\n    String? package,\n  }) {\n    return SpriteBatch.load(\n      path,\n      defaultColor: defaultColor,\n      defaultBlendMode: defaultBlendMode,\n      defaultTransform: defaultTransform,\n      images: imageCache ?? images,\n      useAtlas: useAtlas,\n      package: package,\n    );\n  }\n}\n\n/// A single item in a SpriteBatch.\n///\n/// Holds all the important information of a batch item.\nclass BatchItem {\n  BatchItem({\n    required this.source,\n    required this.transform,\n    Color? color,\n    this.flip = false,\n  }) : color = color ?? const Color(0x00000000),\n       paint = Paint()..color = color ?? const Color(0x00000000),\n       destination = Offset.zero & source.size;\n\n  /// The source rectangle on the [SpriteBatch.atlas].\n  Rect source;\n\n  /// The destination rectangle for the Canvas.\n  ///\n  /// It will be transformed by [matrix].\n  Rect destination;\n\n  /// The transform values for this batch item.\n  RSTransform transform;\n\n  /// The flip value for this batch item.\n  bool flip;\n\n  /// The color of the batch item (used for building the drawAtlas color list).\n  Color color;\n\n  /// Fallback matrix for the web.\n  ///\n  /// Since [Canvas.drawAtlas] is not supported on the web we also\n  /// build a `Matrix4` based on the [transform] and [flip] values.\n  late Matrix4 matrix =\n      Matrix4(\n          transform.scos,\n          transform.ssin,\n          0,\n          0, //\n          -transform.ssin,\n          transform.scos,\n          0,\n          0, //\n          0,\n          0,\n          0,\n          0, //\n          transform.tx,\n          transform.ty,\n          0,\n          1, //\n        )\n        ..translateByDouble(source.width / 2, source.height / 2, 1, 1)\n        ..rotateY(flip ? pi : 0)\n        ..translateByDouble(-source.width / 2, -source.height / 2, 1, 1);\n\n  /// Paint object used for the web.\n  Paint paint;\n}\n\n@internal\nenum FlippedAtlasStatus {\n  /// There is no need to generate a flipped atlas yet.\n  none,\n\n  /// The flipped atlas generation is currently in progress.\n  generating,\n\n  /// The flipped atlas image has been generated.\n  generated\n  ;\n\n  bool get isNone => this == FlippedAtlasStatus.none;\n  bool get isGenerating => this == FlippedAtlasStatus.generating;\n  bool get isGenerated => this == FlippedAtlasStatus.generated;\n}\n\n/// The SpriteBatch API allows for rendering multiple items at once.\n///\n/// This class allows for optimization when you want to draw many parts of an\n/// image onto the canvas. It is more efficient than using multiple calls to\n/// [Canvas.drawImageRect] and provides more functionality by allowing each\n/// [BatchItem] to have their own transform rotation and color.\n///\n/// By collecting all the necessary transforms on a single image and sending\n/// those transforms in a single batch to the GPU, we can render multiple parts\n/// of a single image at once.\n///\n/// **Note**: If you are experiencing problems with ghost lines, the less\n/// performant [Canvas.drawImageRect] can be used instead of [Canvas.drawAtlas].\n/// To activate this mode, pass in `useAtlas = false` to the constructor or\n/// load method that you are using and each [BatchItem] will be rendered using\n/// the [Canvas.drawImageRect] method instead.\nclass SpriteBatch {\n  SpriteBatch(\n    this.atlas, {\n    this.defaultTransform,\n    this.useAtlas = true,\n    this.defaultColor,\n    this.defaultBlendMode,\n    Images? imageCache,\n    String? imageKey,\n  }) : _imageCache = imageCache,\n       _imageKey = imageKey;\n\n  /// Takes a path of an image, and optional arguments for the SpriteBatch.\n  ///\n  /// When the [images] is omitted, the global [Flame.images] is used.\n  static Future<SpriteBatch> load(\n    String path, {\n    RSTransform? defaultTransform,\n    Images? images,\n    Color? defaultColor,\n    BlendMode? defaultBlendMode,\n    bool useAtlas = true,\n    String? package,\n  }) async {\n    final imagesCache = images ?? Flame.images;\n    return SpriteBatch(\n      await imagesCache.load(path, package: package),\n      defaultTransform: defaultTransform ?? RSTransform(1, 0, 0, 0),\n      defaultColor: defaultColor,\n      defaultBlendMode: defaultBlendMode,\n      useAtlas: useAtlas,\n      imageCache: imagesCache,\n      imageKey: path,\n    );\n  }\n\n  FlippedAtlasStatus _flippedAtlasStatus = FlippedAtlasStatus.none;\n\n  final List<BatchItem> _batchItems = <BatchItem>[];\n  final List<Rect> _sources = <Rect>[];\n  final List<RSTransform> _transforms = <RSTransform>[];\n  final List<Color> _colors = <Color>[];\n\n  UnmodifiableListView<Rect> get sources {\n    return UnmodifiableListView<Rect>(_sources);\n  }\n\n  UnmodifiableListView<RSTransform> get transforms {\n    return UnmodifiableListView<RSTransform>(_transforms);\n  }\n\n  UnmodifiableListView<Color> get colors {\n    return UnmodifiableListView<Color>(_colors);\n  }\n\n  /// Handle/index management (free list strategy).\n  final Queue<int> _freeHandles = Queue<int>();\n\n  /// The next handle to allocate if there are no free handles.\n  int _nextHandle = 0;\n\n  /// Map handle -> dense slot index.\n  final Map<int, int> _handleToSlot = <int, int>{};\n\n  /// Reverse map: dense slot -> handle.\n  final List<int> _slotToHandle = <int>[];\n\n  /// The total number of allocated handles.\n  int get allocatedCount => _nextHandle;\n\n  /// The number of free handles.\n  int get freeCount => _freeHandles.length;\n\n  /// The number of used handles.\n  int get usedCount => _handleToSlot.length;\n\n  /// Allocates a new handle.\n  int _allocateHandle() {\n    if (_freeHandles.isNotEmpty) {\n      return _freeHandles.removeFirst();\n    }\n    return _nextHandle++;\n  }\n\n  /// Frees a handle for future reuse.\n  void _freeHandle(int handle) {\n    _freeHandles.addFirst(handle);\n  }\n\n  /// The atlas used by the [SpriteBatch].\n  Image atlas;\n\n  /// The image cache used by the [SpriteBatch] to store image assets.\n  final Images? _imageCache;\n\n  /// When the [_imageCache] isn't specified, the global [Flame.images] is used.\n  Images get imageCache => _imageCache ?? Flame.images;\n\n  /// The root key use by the [SpriteBatch] to store image assets.\n  final String? _imageKey;\n\n  /// When the [_imageKey] isn't specified [imageKey] will return either the key\n  /// for the [atlas] stored in [imageCache] or a key generated from the\n  /// identityHashCode.\n  String get imageKey =>\n      _imageKey ??\n      imageCache.findKeyForImage(atlas) ??\n      'image[${identityHashCode(atlas)}]';\n\n  /// The default color, used as a background color for a [BatchItem] on web.\n  ///\n  /// Note: The drawAtlas color list uses [_defaultColor]\n  /// unless an explicit per-item color is provided.\n  final Color? defaultColor;\n\n  /// The default transform, used when a transform was not supplied for a\n  /// [BatchItem].\n  final RSTransform? defaultTransform;\n\n  /// The default blend mode, used for blending a batch item.\n  final BlendMode? defaultBlendMode;\n\n  /// The width of the [atlas].\n  int get width => atlas.width;\n\n  /// The height of the [atlas].\n  int get height => atlas.height;\n\n  /// The size of the [atlas].\n  Vector2 get size => atlas.size;\n\n  /// Whether to use [Canvas.drawAtlas] or not.\n  final bool useAtlas;\n\n  /// Does this batch contain any operations?\n  bool get isEmpty => _batchItems.isEmpty;\n\n  // Used to not create new Paint objects in [render] and\n  // [generateFlippedAtlas].\n  final _emptyPaint = Paint();\n\n  Future<void> _makeFlippedAtlas() async {\n    _flippedAtlasStatus = FlippedAtlasStatus.generating;\n    final key = '$imageKey#with-flips';\n    atlas = await imageCache.fetchOrGenerate(\n      key,\n      () => _generateFlippedAtlas(atlas),\n    );\n    _flippedAtlasStatus = FlippedAtlasStatus.generated;\n  }\n\n  Future<Image> _generateFlippedAtlas(Image image) {\n    final recorder = PictureRecorder();\n    final canvas = Canvas(recorder);\n    canvas.drawImage(image, Offset.zero, _emptyPaint);\n    canvas.scale(-1, 1);\n    canvas.drawImage(image, Offset(-image.width * 2, 0), _emptyPaint);\n\n    final picture = recorder.endRecording();\n    return picture.toImageSafe(image.width * 2, image.height);\n  }\n\n  /// Resolves the source rectangle for the atlas, taking into account if a\n  /// flipped atlas is being used.\n  Rect _resolveSourceForAtlas(BatchItem batchItem) {\n    if (!batchItem.flip) {\n      return batchItem.source;\n    }\n\n    // The atlas is twice as wide when the flipped atlas is generated.\n    final atlasWidthMultiplier = _flippedAtlasStatus.isGenerated ? 1 : 2;\n    return Rect.fromLTWH(\n      (atlas.width * atlasWidthMultiplier) - batchItem.source.right,\n      batchItem.source.top,\n      batchItem.source.width,\n      batchItem.source.height,\n    );\n  }\n\n  /// Ensures that the given [handle] exists and returns its slot.\n  int _requireSlot(int handle) {\n    final slot = _handleToSlot[handle];\n    if (slot == null) {\n      throw ArgumentError('Index does not exist: $handle');\n    }\n    return slot;\n  }\n\n  /// Replaces the parameters of the batch item at the given [index].\n  /// At least one of the parameters must be different from null.\n  void replace(\n    int index, {\n    Rect? source,\n    Color? color,\n    RSTransform? transform,\n  }) {\n    assert(\n      source != null || color != null || transform != null,\n      'At least one of the parameters must be different from null.',\n    );\n\n    final slot = _requireSlot(index);\n    final currentBatchItem = _batchItems[slot];\n\n    currentBatchItem.source = source ?? currentBatchItem.source;\n    currentBatchItem.transform = transform ?? currentBatchItem.transform;\n    if (color != null) {\n      currentBatchItem.color = color;\n      currentBatchItem.paint.color = color;\n    }\n\n    _sources[slot] = _resolveSourceForAtlas(currentBatchItem);\n    _transforms[slot] = currentBatchItem.transform;\n\n    // If color is not explicitly provided, store transparent.\n    _colors[slot] = color ?? _defaultColor;\n  }\n\n  /// Gets the [BatchItem] at the given [index].\n  BatchItem getBatchItem(int index) {\n    final slot = _requireSlot(index);\n    return _batchItems[slot];\n  }\n\n  /// Add a new batch item using a RSTransform.\n  ///\n  /// The [source] parameter is the source location on the [atlas].\n  ///\n  /// You can position, rotate, scale and flip it on the canvas using the\n  /// [transform] and [flip] parameters.\n  ///\n  /// The [color] parameter allows you to render a color behind the batch item,\n  /// as a background color.\n  ///\n  /// The [add] method may be a simpler way to add a batch item to the batch.\n  /// However, if there is a way to factor out the computations of the sine and\n  /// cosine of the rotation so that they can be reused over multiple calls to\n  /// this constructor, it may be more efficient to directly use this method\n  /// instead.\n  int addTransform({\n    required Rect source,\n    RSTransform? transform,\n    bool flip = false,\n    Color? color,\n  }) {\n    final handle = _allocateHandle();\n\n    final batchItem = BatchItem(\n      source: source,\n      transform: transform ??= defaultTransform ?? RSTransform(1, 0, 0, 0),\n      flip: flip,\n      color: color ?? defaultColor,\n    );\n\n    if (flip && useAtlas && _flippedAtlasStatus.isNone) {\n      _makeFlippedAtlas();\n    }\n\n    final slot = _batchItems.length;\n\n    _handleToSlot[handle] = slot;\n    _slotToHandle.add(handle);\n\n    _batchItems.add(batchItem);\n    _sources.add(_resolveSourceForAtlas(batchItem));\n    _transforms.add(batchItem.transform);\n\n    // If color is not explicitly provided, store transparent.\n    _colors.add(color ?? _defaultColor);\n\n    return handle;\n  }\n\n  /// Add a new batch item.\n  ///\n  /// The [source] parameter is the source location on the [atlas]. You can\n  /// position it on the canvas using the [offset] parameter.\n  ///\n  /// You can transform the sprite from its [offset] using [scale], [rotation],\n  /// [anchor] and [flip].\n  ///\n  /// The [color] parameter allows you to render a color behind the batch item,\n  /// as a background color.\n  ///\n  /// This method creates a new [RSTransform] based on the given transform\n  /// arguments. If many [RSTransform] objects are being created and there is a\n  /// way to factor out the computations of the sine and cosine of the rotation\n  /// (which are computed each time this method is called) and reuse them over\n  /// multiple [RSTransform] objects,\n  /// it may be more efficient to directly use the more direct [addTransform]\n  /// method instead.\n  int add({\n    required Rect source,\n    double scale = 1.0,\n    Vector2? anchor,\n    double rotation = 0,\n    Vector2? offset,\n    bool flip = false,\n    Color? color,\n  }) {\n    anchor ??= Vector2.zero();\n    offset ??= Vector2.zero();\n    RSTransform? transform;\n\n    // If any of the transform arguments is different from the defaults,\n    // then we create one. This is to prevent unnecessary computations\n    // of the sine and cosine of the rotation.\n    if (scale != 1.0 ||\n        anchor != Vector2.zero() ||\n        rotation != 0 ||\n        offset != Vector2.zero()) {\n      transform = RSTransform.fromComponents(\n        scale: scale,\n        anchorX: anchor.x,\n        anchorY: anchor.y,\n        rotation: rotation,\n        translateX: offset.x,\n        translateY: offset.y,\n      );\n    }\n\n    return addTransform(\n      source: source,\n      transform: transform,\n      flip: flip,\n      color: color,\n    );\n  }\n\n  /// Removes the batch item at the given [index].\n  void removeAt(int index) {\n    final slot = _requireSlot(index);\n\n    final lastSlot = _batchItems.length - 1;\n    final removedHandle = _slotToHandle[slot];\n\n    if (slot != lastSlot) {\n      // Move last -> slot.\n      _batchItems[slot] = _batchItems[lastSlot];\n      _sources[slot] = _sources[lastSlot];\n      _transforms[slot] = _transforms[lastSlot];\n      _colors[slot] = _colors[lastSlot];\n\n      final movedHandle = _slotToHandle[lastSlot];\n      _slotToHandle[slot] = movedHandle;\n      _handleToSlot[movedHandle] = slot;\n    }\n\n    _batchItems.removeLast();\n    _sources.removeLast();\n    _transforms.removeLast();\n    _colors.removeLast();\n    _slotToHandle.removeLast();\n\n    _handleToSlot.remove(removedHandle);\n    _freeHandle(removedHandle);\n  }\n\n  /// Clear the SpriteBatch so it can be reused.\n  void clear() {\n    _sources.clear();\n    _transforms.clear();\n    _colors.clear();\n    _batchItems.clear();\n\n    _handleToSlot.clear();\n    _slotToHandle.clear();\n    _freeHandles.clear();\n    _nextHandle = 0;\n  }\n\n  void render(\n    Canvas canvas, {\n    BlendMode? blendMode,\n    Rect? cullRect,\n    Paint? paint,\n  }) {\n    if (isEmpty) {\n      return;\n    }\n\n    final renderPaint = paint ?? _emptyPaint;\n\n    final hasNoColors = _colors.every((c) => c == _defaultColor);\n    final actualBlendMode = blendMode ?? defaultBlendMode;\n\n    if (!hasNoColors && actualBlendMode == null) {\n      throw 'When setting any colors, a blend mode must be provided.';\n    }\n\n    if (useAtlas && !_flippedAtlasStatus.isGenerating) {\n      canvas.drawAtlas(\n        atlas,\n        _transforms,\n        _sources,\n        hasNoColors ? null : _colors,\n        actualBlendMode,\n        cullRect,\n        renderPaint,\n      );\n    } else {\n      for (final batchItem in _batchItems) {\n        renderPaint.blendMode = blendMode ?? renderPaint.blendMode;\n\n        canvas\n          ..save()\n          ..transform32(batchItem.matrix.storage)\n          ..drawRect(batchItem.destination, batchItem.paint)\n          ..drawImageRect(\n            atlas,\n            batchItem.source,\n            batchItem.destination,\n            renderPaint,\n          )\n          ..restore();\n      }\n    }\n  }\n\n  static const _defaultColor = Color(0x00000000);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/sprite_sheet.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/extensions/vector2.dart';\nimport 'package:flame/src/sprite_animation.dart';\n\n/// Utility class to help extract animations and sprites from a sprite sheet\n/// image.\n///\n/// A sprite sheet is a single image in which several regions can be defined as\n/// individual sprites.\n/// For the purposes of this class, all of these regions must be identically\n/// sized rectangles.\n/// You can use the [Sprite] class directly if you want to have varying shapes.\n///\n/// Each sprite in this sheet can be identified either by it's (row, col) pair\n/// or by it's \"id\", which is basically it's sequenced index if the image is put\n/// in a single line. The sprites can be used to compose an animation easily if\n/// all the frames are sequentially on the same row.\n/// Sprites are lazily generated but cached.\nclass SpriteSheet {\n  /// The src image from which each sprite will be generated.\n  final Image image;\n\n  /// The size of each rectangle within the image that define each sprite.\n  ///\n  /// For example, if this sprite sheet is a tile map, this would be the tile\n  /// size. If it's an animation sheet, this would be the frame size.\n  final Vector2 srcSize;\n\n  /// The empty space around the edges of the image.\n  final double margin;\n\n  /// This empty space in between adjacent tiles within the image.\n  final double spacing;\n\n  /// The number of rows in the image based on the image height and the tile\n  /// size.\n  final int rows;\n\n  /// The number of columns in the image based on the image width and the tile\n  /// size.\n  final int columns;\n\n  final Map<int, Sprite> _spriteCache = {};\n\n  /// Creates a sprite sheet given the image and the tile size.\n  SpriteSheet({\n    required this.image,\n    required this.srcSize,\n    this.margin = 0,\n    this.spacing = 0,\n  }) : columns = (image.width - 2 * margin + spacing) ~/ (srcSize.x + spacing),\n       rows = (image.height - 2 * margin + spacing) ~/ (srcSize.y + spacing);\n\n  SpriteSheet.fromColumnsAndRows({\n    required this.image,\n    required this.columns,\n    required this.rows,\n    this.spacing = 0,\n    this.margin = 0,\n  }) : srcSize = Vector2(\n         (image.width - 2 * margin - (columns - 1) * spacing) / columns,\n         (image.height - 2 * margin - (rows - 1) * spacing) / rows,\n       );\n\n  /// Gets the sprite in the position (row, column) on the sprite sheet grid.\n  ///\n  /// This is lazily computed and cached for your convenience.\n  Sprite getSprite(int row, int column) {\n    return getSpriteById(row * columns + column);\n  }\n\n  /// Gets the sprite with id [spriteId] from the grid.\n  ///\n  /// The ids are defined as starting at 0 on the top left and going\n  /// sequentially on each row.\n  /// This is lazily computed and cached for your convenience.\n  Sprite getSpriteById(int spriteId) {\n    return _spriteCache[spriteId] ??= _computeSprite(spriteId);\n  }\n\n  /// Create a [SpriteAnimationFrameData] for the sprite in the position\n  /// (row, column) on the sprite sheet grid.\n  SpriteAnimationFrameData createFrameData(\n    int row,\n    int column, {\n    required double stepTime,\n  }) {\n    return createFrameDataFromId(row * columns + column, stepTime: stepTime);\n  }\n\n  /// Create a [SpriteAnimationFrameData] for the sprite with id [spriteId]\n  /// from the grid.\n  ///\n  /// The ids are defined as starting at 0 on the top left and going\n  /// sequentially on each row.\n  SpriteAnimationFrameData createFrameDataFromId(\n    int spriteId, {\n    required double stepTime,\n  }) {\n    final i = spriteId % columns;\n    final j = spriteId ~/ columns;\n    return SpriteAnimationFrameData(\n      srcPosition: Vector2Extension.fromInts(i, j)..multiply(srcSize),\n      srcSize: srcSize,\n      stepTime: stepTime,\n    );\n  }\n\n  Sprite _computeSprite(int spriteId) {\n    final i = spriteId % columns;\n    final j = spriteId ~/ columns;\n    return Sprite(\n      image,\n      srcPosition: Vector2Extension.fromInts(i, j)\n        ..multiply(srcSize)\n        ..translate(margin + i * spacing, margin + j * spacing),\n      srcSize: srcSize,\n    );\n  }\n\n  List<Sprite> _generateSpriteList({\n    required int row,\n    int from = 0,\n    int? to,\n  }) {\n    to ??= columns;\n\n    return List<int>.generate(\n      to - from,\n      (i) => from + i,\n    ).map((e) => getSprite(row, e)).toList();\n  }\n\n  /// Creates a [SpriteAnimation] from this SpriteSheet, using the sequence\n  /// of sprites on a given row.\n  ///\n  /// [from] and [to] can be specified to create an animation\n  /// from a subset of the columns on the row\n  SpriteAnimation createAnimation({\n    required int row,\n    required double stepTime,\n    bool loop = true,\n    int from = 0,\n    int? to,\n  }) {\n    final spriteList = _generateSpriteList(\n      row: row,\n      to: to,\n      from: from,\n    );\n\n    return SpriteAnimation.spriteList(\n      spriteList,\n      stepTime: stepTime,\n      loop: loop,\n    );\n  }\n\n  /// Creates a [SpriteAnimation] from this SpriteSheet, using the sequence\n  /// of sprites on a given row with different duration for each.\n  ///\n  /// [from] and [to] can be specified to create an animation\n  /// from a subset of the columns on the row\n  SpriteAnimation createAnimationWithVariableStepTimes({\n    required int row,\n    required List<double> stepTimes,\n    bool loop = true,\n    int from = 0,\n    int? to,\n  }) {\n    final spriteList = _generateSpriteList(\n      row: row,\n      to: to,\n      from: from,\n    );\n\n    return SpriteAnimation.variableSpriteList(\n      spriteList,\n      loop: loop,\n      stepTimes: stepTimes,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/common/glyph.dart",
    "content": "import 'package:flame/src/text/common/sprite_font.dart';\nimport 'package:meta/meta.dart';\n\n/// [Glyph] describes a single character/ligature/icon within a [SpriteFont].\n///\n/// A glyph has an associated \"glyph box\", which is the box where the glyph is\n/// logically located. Here \"logically\" means that it includes not only the\n/// character itself, but also some padding around it as necessary for the\n/// character to look nice within the text. For all glyphs in a font, their\n/// boxes will have the same height (which is the font size), the same ascent\n/// and descent, but possibly different widths.\n///\n/// The properties [left], [top], [width] and [height] describe the location of\n/// the glyph box within the source image.\n///\n/// In addition to the logical \"glyph box\", a glyph may also have a \"source\n/// box\", which describes a rectangle within the source image where the glyph's\n/// pixels are actually located. The source box may be larger or smaller than\n/// the glyph box. It will be larger if the glyph has a particularly large\n/// flourish that trespasses upon other characters' space; or smaller if the\n/// characters are packed too tightly in the source image, or if you're trying\n/// to improve rendering performance by not copying empty pixels.\nclass Glyph {\n  Glyph(\n    this.char, {\n    required this.left,\n    required this.top,\n    double? width,\n    double? height,\n    double? srcLeft,\n    double? srcTop,\n    double? srcRight,\n    double? srcBottom,\n  }) : assert((width ?? 0) >= 0, 'The `width` parameter cannot be negative'),\n       assert((height ?? 0) >= 0, 'The `height` parameter cannot be negative'),\n       assert(\n         (srcLeft == null &&\n                 srcTop == null &&\n                 srcRight == null &&\n                 srcBottom == null) ||\n             (srcLeft != null &&\n                 srcTop != null &&\n                 srcRight != null &&\n                 srcBottom != null),\n         'Either all or none of parameters `srcLeft`, `srcTop`, `srcRight` '\n         'and `srcBottom` must be specified',\n       ),\n       width = width ?? -1,\n       height = height ?? -1,\n       srcLeft = srcLeft ?? -1,\n       srcTop = srcTop ?? -1,\n       srcRight = srcRight ?? -1,\n       srcBottom = srcBottom ?? -1;\n\n  final String char;\n  final double left;\n  final double top;\n  double width;\n  double height;\n  double srcLeft;\n  double srcTop;\n  double srcRight;\n  double srcBottom;\n\n  @internal\n  void initialize(double defaultCharWidth, double charHeight) {\n    if (width < 0) {\n      width = defaultCharWidth;\n    }\n    if (height < 0) {\n      height = charHeight;\n    } else {\n      assert(\n        height == charHeight,\n        'The height of all glyphs must be the same and equal to the font size',\n      );\n    }\n    if (srcLeft < 0) {\n      srcLeft = left;\n      srcTop = top;\n      srcRight = left + width;\n      srcBottom = top + height;\n    }\n  }\n\n  @override\n  String toString() {\n    return 'Glyph(char=\"$char\", '\n        'LTWH=[$left, $top, $width, $height], '\n        'srcLTRB=[$srcLeft, $srcTop, $srcRight, $srcBottom])';\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/common/line_metrics.dart",
    "content": "import 'package:flame/extensions.dart';\n\n/// The [LineMetrics] object contains measurements of a text line.\n///\n/// A line of text can be thought of as surrounded by a box (rect) that outlines\n/// the boundaries of the text, plus there is a [baseline] inside the box which\n/// is the line on top of which the text is placed.\n///\n/// The [LineMetrics] box surrounding a piece of text is not necessarily tight:\n/// there's usually some amount of space above and below the text glyphs to\n/// improve legibility of multi-line text.\nclass LineMetrics {\n  LineMetrics({\n    double left = 0,\n    double baseline = 0,\n    double width = 0,\n    double? ascent,\n    double? descent,\n    double? height,\n  }) : _left = left,\n       _baseline = baseline,\n       _width = width,\n       _ascent = ascent ?? (height == null ? 0 : height - (descent ?? 0)),\n       _descent =\n           descent ?? (height == null ? 0 : height - (ascent ?? height)) {\n    _updateSize();\n  }\n\n  /// X-coordinate of the left edge of the box.\n  double get left => _left;\n  double _left;\n\n  /// Y-coordinate of the baseline of the box. When several line fragments are\n  /// placed next to each other, their baselines will match.\n  double get baseline => _baseline;\n  double _baseline;\n\n  /// The total width of the box.\n  double get width => _width;\n  double _width;\n\n  /// The distance from the baseline to the top of the box.\n  double get ascent => _ascent;\n  double _ascent;\n\n  /// The distance from the baseline to the bottom of the box.\n  double get descent => _descent;\n  double _descent;\n\n  double get right => left + width;\n  double get top => baseline - ascent;\n  double get bottom => baseline + descent;\n  double get height => ascent + descent;\n\n  final Vector2 _size = Vector2.zero();\n  Vector2 get size => _size.clone();\n\n  /// Moves the [LineMetrics] box by the specified offset [dx], [dy] leaving its\n  /// width and height unmodified.\n  void translate(double dx, double dy) {\n    _left += dx;\n    _baseline += dy;\n  }\n\n  /// Moves this [LineMetrics] box to the origin, setting [left] and [baseline]\n  /// to 0.\n  void moveToOrigin() {\n    _left = 0;\n    _baseline = 0;\n  }\n\n  /// Sets the position of the left edge of this [LineMetrics] box, leaving the\n  /// [right] edge in place.\n  void setLeftEdge(double x) {\n    _width = right - x;\n    _left = x;\n    _updateSize();\n  }\n\n  /// Appends another [LineMetrics] box that is adjacent to the current and on\n  /// the same baseline. The current object will be modified to encompass the\n  /// [other] box.\n  void append(LineMetrics other) {\n    assert(\n      baseline == other.baseline,\n      'Baselines do not match: $baseline vs ${other.baseline}',\n    );\n    _width = other.right - left;\n    if (_ascent < other.ascent) {\n      _ascent = other.ascent;\n    }\n    if (_descent < other.descent) {\n      _descent = other.descent;\n    }\n    _updateSize();\n  }\n\n  void _updateSize() {\n    _size.setValues(width, height);\n  }\n\n  Rect toRect() => Rect.fromLTWH(left, top, width, height);\n\n  @override\n  String toString() =>\n      'LineMetrics(left: $left, baseline: $baseline, '\n      'width: $width, ascent: $ascent, descent: $descent)';\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/common/sprite_font.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/text/common/glyph.dart';\nimport 'package:meta/meta.dart';\n\n/// [SpriteFont] contains information about a custom font stored in an asset\n/// file.\n///\n/// The [source] parameter is the [Image] where the font's characters are\n/// located. The layout of this image can be arbitrary, however, all characters\n/// for the font must be in the same source image.\n///\n/// The [size] property describes the font size of the sprite font. This font\n/// size must be the same for all characters in the font.\n///\n/// The [ascent] property measures the distance from the top of a character to\n/// its baseline. This property must be equal for all characters in the font.\n///\n/// The main information about the font is in the `glyphs` list of the\n/// constructor. Each [Glyph] in this list describes a single character (or a\n/// ligature) within the source image.\n///\n/// The [SpriteFont] can be either variable-width or monospace. For a monospace\n/// font you can pass the `defaultCharWidth` parameter in the constructor so\n/// that you wouldn't have to specify the width of each glyph.\nclass SpriteFont {\n  SpriteFont({\n    required this.source,\n    required this.size,\n    required this.ascent,\n    required List<Glyph> glyphs,\n    double? defaultCharWidth,\n  }) : _data = <int, _Chain>{} {\n    for (final glyph in glyphs) {\n      var data = _data;\n      for (var i = 0; i < glyph.char.length - 1; i++) {\n        final j = glyph.char.codeUnitAt(i);\n        data = (data[j] ??= _Chain()).followOn ??= <int, _Chain>{};\n      }\n      final j = glyph.char.codeUnitAt(glyph.char.length - 1);\n      final chain = data[j] ??= _Chain();\n      assert(\n        chain.glyph == null,\n        'Duplicate definition for glyph \"${glyph.char}\"',\n      );\n      glyph.initialize(defaultCharWidth ?? size, size);\n      chain.glyph = glyph;\n    }\n  }\n\n  final Image source;\n\n  /// The font size, i.e. the height of all characters in the font.\n  final double size;\n\n  /// The distance from the top of every character to its baseline.\n  final double ascent;\n\n  /// Contains information about the characters of the font. The keys in this\n  /// map are the characters' code points. If a particular \"character\" has a\n  /// length greater than 1, then the key will be the first code point of this\n  /// character, and all subsequent code points will be stored within the\n  /// [_Chain].\n  final Map<int, _Chain> _data;\n\n  /// Splits the provided [text] into a sequence of [Glyph]s.\n  ///\n  /// Any ligatures are consumed greedily: at every position the longest\n  /// possible sequence of characters will be matched. If the text contains a\n  /// character not available in this font, an error will be thrown.\n  @internal\n  Iterable<Glyph> textToGlyphs(String text) sync* {\n    for (var i = 0; i < text.length; i++) {\n      var chain = _data[text.codeUnitAt(i)];\n      var iNext = i;\n      var resolvedGlyph = chain?.glyph;\n      for (var j = i + 1; j < text.length; j++) {\n        if (chain?.followOn == null) {\n          break;\n        }\n        final jCharCode = text.codeUnitAt(j);\n        chain = chain!.followOn![jCharCode];\n        if (chain?.glyph != null) {\n          iNext = j;\n          resolvedGlyph = chain?.glyph;\n        }\n      }\n      if (resolvedGlyph == null) {\n        throw ArgumentError('No glyph data for character \"${text[i]}\"');\n      } else {\n        i = iNext;\n        yield resolvedGlyph;\n      }\n    }\n  }\n}\n\nclass _Chain {\n  Glyph? glyph;\n  Map<int, _Chain>? followOn;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/common/utils.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/text.dart';\nimport 'package:meta/meta.dart';\n\n@internal\ndouble collapseMargin(double margin1, double margin2) {\n  if (margin1 >= 0) {\n    return (margin2 < 0) ? margin1 + margin2 : max(margin1, margin2);\n  } else {\n    return (margin2 < 0) ? min(margin1, margin2) : margin1 + margin2;\n  }\n}\n\n@internal\nTextElement? makeBackground(\n  BackgroundStyle? style,\n  double width,\n  double height,\n) {\n  if (style == null) {\n    return null;\n  }\n  final out = <TextElement>[];\n  final backgroundPaint = style.backgroundPaint;\n  final borderPaint = style.borderPaint;\n  final borders = style.borderWidths;\n  final radius = style.borderRadius;\n\n  if (backgroundPaint != null) {\n    if (radius == 0) {\n      out.add(RectElement(width, height, backgroundPaint));\n    } else {\n      out.add(RRectElement(width, height, radius, backgroundPaint));\n    }\n  }\n  if (borderPaint != null) {\n    if (radius == 0) {\n      out.add(\n        RectElement(\n          width - borders.horizontal / 2,\n          height - borders.vertical / 2,\n          borderPaint,\n        )..translate(borders.left / 2, borders.top / 2),\n      );\n    } else {\n      out.add(\n        RRectElement(\n          width - borders.horizontal / 2,\n          height - borders.vertical / 2,\n          radius,\n          borderPaint,\n        )..translate(borders.left / 2, borders.top / 2),\n      );\n    }\n  }\n  if (out.isEmpty) {\n    return null;\n  }\n  if (out.length == 1) {\n    return out.first;\n  } else {\n    return GroupElement(width: width, height: height, children: out);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/elements/block_element.dart",
    "content": "import 'package:flame/text.dart';\n\n/// [BlockElement] is the base class for [TextElement]s with rectangular shape\n/// and \"block\" placement rules.\n///\n/// Within HTML, this corresponds to elements with `display: block` property,\n/// such as `<div>` or `<blockquote>`.\nabstract class BlockElement extends TextElement {\n  BlockElement(this.width, this.height);\n\n  final double width;\n  final double height;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/elements/group_element.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/text.dart';\n\nclass GroupElement extends BlockElement {\n  GroupElement({\n    required double width,\n    required double height,\n    required this.children,\n  }) : super(width, height);\n\n  final List<TextElement> children;\n\n  @override\n  void translate(double dx, double dy) {\n    for (final child in children) {\n      child.translate(dx, dy);\n    }\n  }\n\n  @override\n  void draw(Canvas canvas) {\n    for (final child in children) {\n      child.draw(canvas);\n    }\n  }\n\n  @override\n  Rect get boundingBox {\n    return children.fold<Rect?>(\n          null,\n          (previousValue, element) {\n            final box = element.boundingBox;\n            return previousValue?.expandToInclude(box) ?? box;\n          },\n        ) ??\n        Rect.zero;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/elements/group_text_element.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/text.dart';\nimport 'package:meta/meta.dart';\n\nclass GroupTextElement extends InlineTextElement {\n  GroupTextElement(List<InlineTextElement> children)\n    : assert(children.isNotEmpty, 'The children list cannot be empty'),\n      _children = children,\n      _metrics = _computeMetrics(children);\n\n  final List<InlineTextElement> _children;\n  final LineMetrics _metrics;\n\n  @override\n  LineMetrics get metrics => _metrics;\n\n  @visibleForTesting\n  List<InlineTextElement> get children => List.unmodifiable(_children);\n\n  @override\n  void draw(Canvas canvas) {\n    for (final child in _children) {\n      child.draw(canvas);\n    }\n  }\n\n  @override\n  void translate(double dx, double dy) {\n    _metrics.translate(dx, dy);\n    for (final child in _children) {\n      child.translate(dx, dy);\n    }\n  }\n\n  static LineMetrics _computeMetrics(List<InlineTextElement> elements) {\n    var width = 0.0;\n    var ascent = 0.0;\n    var descent = 0.0;\n    for (final element in elements) {\n      final metrics = element.metrics;\n      assert(metrics.left == width);\n      assert(metrics.baseline == 0);\n      width += metrics.width;\n      ascent = max(ascent, metrics.ascent);\n      descent = max(descent, metrics.descent);\n    }\n    return LineMetrics(width: width, ascent: ascent, descent: descent);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/elements/inline_text_element.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/anchor.dart';\nimport 'package:flame/text.dart';\n\n/// [InlineTextElement] is the base class that represents a single line of text,\n/// laid out and prepared for rendering.\nabstract class InlineTextElement extends TextElement {\n  /// The dimensions of this line.\n  LineMetrics get metrics;\n\n  void render(\n    Canvas canvas,\n    Vector2 position, {\n    Anchor anchor = Anchor.topLeft,\n  }) {\n    final box = metrics;\n    translate(\n      position.x - box.width * anchor.x,\n      position.y - box.height * anchor.y - box.top,\n    );\n    draw(canvas);\n  }\n\n  @override\n  Rect get boundingBox => metrics.toRect();\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/elements/rect_element.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/text.dart';\n\nclass RectElement extends TextElement {\n  RectElement(double width, double height, this._paint)\n    : _rect = Rect.fromLTWH(0, 0, width, height);\n\n  Rect _rect;\n  final Paint _paint;\n\n  @override\n  void translate(double dx, double dy) {\n    _rect = _rect.translate(dx, dy);\n  }\n\n  @override\n  void draw(Canvas canvas) {\n    canvas.drawRect(_rect, _paint);\n  }\n\n  @override\n  Rect get boundingBox => _rect;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/elements/rrect_element.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/text.dart';\n\nclass RRectElement extends TextElement {\n  RRectElement(\n    double width,\n    double height,\n    double radius,\n    this._paint,\n  ) : _rrect = RRect.fromLTRBR(0, 0, width, height, Radius.circular(radius));\n\n  RRect _rrect;\n  final Paint _paint;\n\n  @override\n  void translate(double dx, double dy) {\n    _rrect = _rrect.shift(Offset(dx, dy));\n  }\n\n  @override\n  void draw(Canvas canvas) {\n    canvas.drawRRect(_rrect, _paint);\n  }\n\n  @override\n  Rect get boundingBox => _rrect.outerRect;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/elements/sprite_font_text_element.dart",
    "content": "import 'dart:typed_data';\nimport 'dart:ui';\n\nimport 'package:flame/text.dart';\n\nclass SpriteFontTextElement extends InlineTextElement {\n  SpriteFontTextElement({\n    required this.source,\n    required this.transforms,\n    required this.rects,\n    required this.paint,\n    required LineMetrics metrics,\n  }) : _box = metrics;\n\n  final Image source;\n  final Float32List transforms;\n  final Float32List rects;\n  final Paint paint;\n  final LineMetrics _box;\n\n  @override\n  LineMetrics get metrics => _box;\n\n  @override\n  void translate(double dx, double dy) {\n    _box.translate(dx, dy);\n    for (var i = 0; i < transforms.length; i += 4) {\n      transforms[i + 2] += dx;\n      transforms[i + 3] += dy;\n    }\n  }\n\n  @override\n  void draw(Canvas canvas) {\n    canvas.drawRawAtlas(source, transforms, rects, null, null, null, paint);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/elements/text_element.dart",
    "content": "import 'dart:ui';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/text.dart';\n\n/// An [TextElement] is a basic rendering block of a rich-text document.\n///\n/// Elements are concrete and \"physical\": they are objects that are ready to be\n/// rendered on a canvas. This property distinguishes them from Nodes (which are\n/// structured pieces of text), and from [FlameTextStyle]s (which are\n/// descriptors for how arbitrary pieces of text ought to be rendered).\n///\n/// Elements are at the final stage of the text rendering pipeline, they are\n/// created during the layout step.\nabstract class TextElement {\n  /// Moves the element by ([dx], [dy]) relative to its current location.\n  void translate(double dx, double dy);\n\n  /// Renders the element on the [canvas], at coordinates determined during the\n  /// layout.\n  ///\n  /// In order to render the element at a different location, consider either\n  /// calling the [translate] method, or applying a translation transform to the\n  /// canvas itself.\n  void draw(Canvas canvas);\n\n  Rect get boundingBox;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/elements/text_painter_text_element.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/text.dart';\nimport 'package:flutter/rendering.dart' as flutter;\nimport 'package:meta/meta.dart';\n\nclass TextPainterTextElement extends InlineTextElement {\n  TextPainterTextElement(this._textPainter)\n    : _box = LineMetrics(\n        ascent: _textPainter.computeDistanceToActualBaseline(\n          flutter.TextBaseline.alphabetic,\n        ),\n        width: _textPainter.width,\n        height: _textPainter.height,\n      );\n\n  final flutter.TextPainter _textPainter;\n  final LineMetrics _box;\n\n  @visibleForTesting\n  flutter.TextPainter get textPainter => _textPainter;\n\n  @override\n  LineMetrics get metrics => _box;\n\n  @override\n  void translate(double dx, double dy) => _box.translate(dx, dy);\n\n  @override\n  void draw(Canvas canvas) {\n    _textPainter.paint(canvas, Offset(_box.left, _box.top));\n  }\n\n  @override\n  String toString() {\n    return 'TextPainterTextElement(text: ${_textPainter.text?.toPlainText()})';\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/nodes/block_node.dart",
    "content": "import 'package:flame/text.dart';\n\n/// [BlockNode] is a base class for all nodes with \"block\" placement rules; it\n/// roughly corresponds to `<div/>` in HTML.\n///\n/// A block node should be able to find its style in the root stylesheet, via\n/// the method [fillStyles], and then based on that style build the\n/// corresponding element in the [format] method. Both of these methods must be\n/// implemented by subclasses.\n///\n/// Implementations include:\n/// * ColumnNode\n/// * TextBlockNode (which itself can be a HeaderNode or ParagraphNode)\nabstract class BlockNode implements TextNode<BlockStyle> {\n  /// The runtime style applied to this node, this will be set by [fillStyles].\n  @override\n  late BlockStyle style;\n\n  BlockElement format(double availableWidth);\n\n  @override\n  void fillStyles(DocumentStyle stylesheet, InlineTextStyle parentTextStyle);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/nodes/bold_text_node.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/text/nodes/inline_text_node.dart';\nimport 'package:flame/text.dart';\n\n/// An [InlineTextNode] representing bold text.\nclass BoldTextNode extends InlineTextNode {\n  BoldTextNode(this.child);\n\n  BoldTextNode.simple(String text) : child = PlainTextNode(text);\n\n  BoldTextNode.group(List<InlineTextNode> children)\n    : child = GroupTextNode(children);\n\n  final InlineTextNode child;\n\n  static final defaultStyle = InlineTextStyle(fontWeight: FontWeight.bold);\n\n  @override\n  void fillStyles(DocumentStyle stylesheet, InlineTextStyle parentTextStyle) {\n    style = FlameTextStyle.merge(parentTextStyle, stylesheet.boldText)!;\n    child.fillStyles(stylesheet, style);\n  }\n\n  @override\n  TextNodeLayoutBuilder get layoutBuilder => child.layoutBuilder;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/nodes/code_text_node.dart",
    "content": "import 'package:flame/src/text/nodes/inline_text_node.dart';\nimport 'package:flame/text.dart';\n\n/// An [InlineTextNode] representing an inline code block string.\nclass CodeTextNode extends InlineTextNode {\n  CodeTextNode(this.child);\n\n  CodeTextNode.simple(String text) : child = PlainTextNode(text);\n\n  CodeTextNode.group(List<InlineTextNode> children)\n    : child = GroupTextNode(children);\n\n  final InlineTextNode child;\n\n  static final defaultStyle = InlineTextStyle(fontFamily: 'monospace');\n\n  @override\n  void fillStyles(DocumentStyle stylesheet, InlineTextStyle parentTextStyle) {\n    style = FlameTextStyle.merge(parentTextStyle, stylesheet.codeText)!;\n    child.fillStyles(stylesheet, style);\n  }\n\n  @override\n  TextNodeLayoutBuilder get layoutBuilder => child.layoutBuilder;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/nodes/column_node.dart",
    "content": "import 'package:flame/src/text/common/utils.dart';\nimport 'package:flame/text.dart';\nimport 'package:meta/meta.dart';\n\n/// [ColumnNode] is a block node containing other block nodes arranged as a\n/// column.\n///\n/// During formatting, produces an element which is as wide as the available\n/// width, and tall enough to fit all the available children elements.\nabstract class ColumnNode extends BlockNode {\n  ColumnNode(this.children);\n\n  final List<BlockNode> children;\n\n  @override\n  GroupElement format(double availableWidth) {\n    final out = <TextElement>[];\n    final blockWidth = availableWidth;\n    final padding = style.padding;\n\n    var verticalOffset = 0.0;\n    var currentMargin = padding.top;\n    for (final node in children) {\n      final nodeMargins = node.style.margin;\n      final marginLeft = collapseMargin(padding.left, nodeMargins.left);\n      final marginRight = collapseMargin(padding.right, nodeMargins.right);\n      final nodeAvailableWidth = blockWidth - marginLeft - marginRight;\n      final nodeElement = node.format(nodeAvailableWidth);\n      out.add(nodeElement);\n\n      verticalOffset += collapseMargin(currentMargin, nodeMargins.top);\n      nodeElement.translate(marginLeft, verticalOffset);\n      verticalOffset += nodeElement.height;\n      currentMargin = nodeMargins.bottom;\n    }\n    // Do not collapse padding if there are no children.\n    final blockHeight = children.isEmpty\n        ? padding.vertical\n        : verticalOffset + collapseMargin(currentMargin, padding.bottom);\n    final background = makeBackground(\n      style.background,\n      blockWidth,\n      blockHeight,\n    );\n    if (background != null) {\n      out.insert(0, background);\n    }\n    return GroupElement(\n      width: blockWidth,\n      height: blockHeight,\n      children: out,\n    );\n  }\n\n  @mustCallSuper\n  @override\n  void fillStyles(DocumentStyle stylesheet, InlineTextStyle parentTextStyle) {\n    for (final node in children) {\n      node.fillStyles(stylesheet, parentTextStyle);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/nodes/custom_text_node.dart",
    "content": "import 'package:flame/src/text/nodes/inline_text_node.dart';\nimport 'package:flame/text.dart';\n\n/// An [InlineTextNode] representing a span of text with a custom style applied.\nclass CustomInlineTextNode extends InlineTextNode {\n  final String styleName;\n\n  CustomInlineTextNode(this.child, {required this.styleName});\n\n  CustomInlineTextNode.simple(String text, {required this.styleName})\n    : child = PlainTextNode(text);\n\n  final InlineTextNode child;\n\n  @override\n  void fillStyles(DocumentStyle stylesheet, InlineTextStyle parentTextStyle) {\n    style =\n        FlameTextStyle.merge(\n          parentTextStyle,\n          stylesheet.getCustomStyle(styleName),\n        ) ??\n        stylesheet.text;\n    child.fillStyles(stylesheet, style);\n  }\n\n  @override\n  TextNodeLayoutBuilder get layoutBuilder => child.layoutBuilder;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/nodes/document_root.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/src/text/common/utils.dart';\nimport 'package:flame/text.dart';\nimport 'package:flutter/painting.dart';\n\nclass DocumentRoot {\n  DocumentRoot(this.children);\n\n  final List<BlockNode> children;\n\n  /// Applies [style] to this document, producing an Element that can be\n  /// rendered on a canvas. Parameters [width] and [height] serve as the\n  /// fallback values if they were not specified in the style itself.\n  /// However, they are ignored if `style.width` and `style.height` are\n  /// provided.\n  GroupElement format(DocumentStyle style, {double? width, double? height}) {\n    assert(\n      style.width != null || width != null,\n      'Width must be either provided explicitly or set in the stylesheet',\n    );\n    final out = <TextElement>[];\n    final border = style.background?.borderWidths ?? EdgeInsets.zero;\n    final padding = style.padding;\n\n    final pageWidth = style.width ?? width!;\n    final contentWidth = pageWidth - padding.horizontal;\n    final horizontalOffset = padding.left;\n    var verticalOffset = border.top;\n    var currentMargin = padding.top;\n    for (final node in children) {\n      node.fillStyles(style, style.text);\n      final blockStyle = node.style;\n      verticalOffset += collapseMargin(currentMargin, blockStyle.margin.top);\n      final nodeElement = node.format(contentWidth);\n      nodeElement.translate(horizontalOffset, verticalOffset);\n      out.add(nodeElement);\n      currentMargin = blockStyle.margin.bottom;\n      verticalOffset += nodeElement.height;\n    }\n    final pageHeight = max(\n      height ?? 0,\n      verticalOffset +\n          collapseMargin(currentMargin, padding.bottom) +\n          border.bottom,\n    );\n    final background = makeBackground(\n      style.background,\n      pageWidth,\n      pageHeight,\n    );\n    if (background != null) {\n      out.insert(0, background);\n    }\n    return GroupElement(width: pageWidth, height: pageHeight, children: out);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/nodes/group_text_node.dart",
    "content": "import 'package:flame/src/text/nodes/inline_text_node.dart';\nimport 'package:flame/text.dart';\n\n/// An [InlineTextNode] to group other [InlineTextNode]s.\nclass GroupTextNode extends InlineTextNode {\n  GroupTextNode(this.children);\n\n  final List<InlineTextNode> children;\n\n  @override\n  void fillStyles(DocumentStyle stylesheet, InlineTextStyle parentTextStyle) {\n    style = parentTextStyle;\n    for (final node in children) {\n      node.fillStyles(stylesheet, style);\n    }\n  }\n\n  @override\n  TextNodeLayoutBuilder get layoutBuilder => _GroupTextLayoutBuilder(this);\n}\n\nclass _GroupTextLayoutBuilder extends TextNodeLayoutBuilder {\n  _GroupTextLayoutBuilder(this.node);\n\n  final GroupTextNode node;\n  int _currentChildIndex = 0;\n  TextNodeLayoutBuilder? _currentChildBuilder;\n\n  @override\n  bool get isDone => _currentChildIndex == node.children.length;\n\n  @override\n  InlineTextElement? layOutNextLine(\n    double availableWidth, {\n    required bool isStartOfLine,\n  }) {\n    assert(!isDone);\n    final out = <InlineTextElement>[];\n    var usedWidth = 0.0;\n    while (true) {\n      if (_currentChildBuilder?.isDone ?? false) {\n        _currentChildBuilder = null;\n        _currentChildIndex += 1;\n        if (_currentChildIndex == node.children.length) {\n          break;\n        }\n      }\n      _currentChildBuilder ??= node.children[_currentChildIndex].layoutBuilder;\n\n      final maybeLine = _currentChildBuilder!.layOutNextLine(\n        availableWidth - usedWidth,\n        isStartOfLine: isStartOfLine && out.isEmpty,\n      );\n      if (maybeLine == null) {\n        break;\n      } else {\n        assert(maybeLine.metrics.left == 0 && maybeLine.metrics.baseline == 0);\n        maybeLine.translate(usedWidth, 0);\n        out.add(maybeLine);\n        usedWidth += maybeLine.metrics.width;\n      }\n    }\n    if (out.isEmpty) {\n      return null;\n    } else {\n      return GroupTextElement(out);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/nodes/header_node.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/text.dart';\nimport 'package:flutter/rendering.dart' show EdgeInsets;\n\nclass HeaderNode extends TextBlockNode {\n  HeaderNode(super.child, {required this.level});\n\n  HeaderNode.simple(String text, {required this.level})\n    : super(PlainTextNode(text));\n\n  final int level;\n\n  static BlockStyle defaultStyleH1 = BlockStyle(\n    text: InlineTextStyle(fontScale: 2.0, fontWeight: FontWeight.bold),\n    margin: const EdgeInsets.fromLTRB(0, 24, 0, 12),\n  );\n  static BlockStyle defaultStyleH2 = BlockStyle(\n    text: InlineTextStyle(fontScale: 1.5, fontWeight: FontWeight.bold),\n    margin: const EdgeInsets.fromLTRB(0, 24, 0, 8),\n  );\n  static BlockStyle defaultStyleH3 = BlockStyle(\n    text: InlineTextStyle(fontScale: 1.25, fontWeight: FontWeight.bold),\n  );\n  static BlockStyle defaultStyleH4 = BlockStyle(\n    text: InlineTextStyle(fontScale: 1.0, fontWeight: FontWeight.bold),\n  );\n  static BlockStyle defaultStyleH5 = BlockStyle(\n    text: InlineTextStyle(fontScale: 0.875, fontWeight: FontWeight.bold),\n  );\n  static BlockStyle defaultStyleH6 = BlockStyle(\n    text: InlineTextStyle(fontScale: 0.85, fontWeight: FontWeight.bold),\n  );\n\n  @override\n  void fillStyles(DocumentStyle stylesheet, InlineTextStyle parentTextStyle) {\n    style = switch (level) {\n      1 => stylesheet.header1,\n      2 => stylesheet.header2,\n      3 => stylesheet.header3,\n      4 => stylesheet.header4,\n      5 => stylesheet.header5,\n      _ => stylesheet.header6,\n    };\n    final textStyle = FlameTextStyle.merge(parentTextStyle, style.text)!;\n    super.fillStyles(stylesheet, textStyle);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/nodes/inline_text_node.dart",
    "content": "import 'package:flame/text.dart';\n\n/// [InlineTextNode] is a base class for all nodes with \"inline\" placement\n/// rules; it roughly corresponds to `<span/>` in HTML.\n///\n/// Implementations include:\n/// * PlainTextNode - just a string of plain text, no special formatting.\n/// * BoldTextNode - bolded string\n/// * ItalicTextNode - italic string\n/// * CodeTextNode - inline code string\n/// * StrikethroughTextNode - strikethrough string\n/// * CustomTextNode - applies arbitrary attributes to a span of text\n/// * GroupTextNode - collection of multiple [InlineTextNode]'s to be joined one\n///                   after the other.\nabstract class InlineTextNode extends TextNode<InlineTextStyle> {\n  @override\n  late InlineTextStyle style;\n\n  @override\n  void fillStyles(DocumentStyle stylesheet, InlineTextStyle parentTextStyle);\n\n  TextNodeLayoutBuilder get layoutBuilder;\n}\n\nabstract class TextNodeLayoutBuilder {\n  InlineTextElement? layOutNextLine(\n    double availableWidth, {\n    required bool isStartOfLine,\n  });\n  bool get isDone;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/nodes/italic_text_node.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/text/nodes/inline_text_node.dart';\nimport 'package:flame/text.dart';\n\n/// An [InlineTextNode] representing italic text.\nclass ItalicTextNode extends InlineTextNode {\n  ItalicTextNode(this.child);\n\n  ItalicTextNode.simple(String text) : child = PlainTextNode(text);\n\n  ItalicTextNode.group(List<InlineTextNode> children)\n    : child = GroupTextNode(children);\n\n  final InlineTextNode child;\n\n  static final defaultStyle = InlineTextStyle(fontStyle: FontStyle.italic);\n\n  @override\n  void fillStyles(DocumentStyle stylesheet, InlineTextStyle parentTextStyle) {\n    style = FlameTextStyle.merge(parentTextStyle, stylesheet.italicText)!;\n    child.fillStyles(stylesheet, style);\n  }\n\n  @override\n  TextNodeLayoutBuilder get layoutBuilder => child.layoutBuilder;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/nodes/paragraph_node.dart",
    "content": "import 'package:flame/text.dart';\nimport 'package:flutter/rendering.dart' show EdgeInsets;\n\nclass ParagraphNode extends TextBlockNode {\n  ParagraphNode(super.child);\n\n  ParagraphNode.simple(String text) : super(PlainTextNode(text));\n\n  ParagraphNode.group(List<InlineTextNode> fragments)\n    : super(GroupTextNode(fragments));\n\n  static const defaultStyle = BlockStyle(\n    margin: EdgeInsets.all(6),\n  );\n\n  @override\n  void fillStyles(DocumentStyle stylesheet, InlineTextStyle parentTextStyle) {\n    style = stylesheet.paragraph;\n    final textStyle = FlameTextStyle.merge(parentTextStyle, style.text)!;\n    super.fillStyles(stylesheet, textStyle);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/nodes/plain_text_node.dart",
    "content": "import 'package:flame/src/text/nodes/inline_text_node.dart';\nimport 'package:flame/text.dart';\n\n/// An [InlineTextNode] representing plain text.\nclass PlainTextNode extends InlineTextNode {\n  PlainTextNode(this.text);\n\n  final String text;\n\n  @override\n  void fillStyles(DocumentStyle stylesheet, InlineTextStyle parentTextStyle) {\n    style = parentTextStyle;\n  }\n\n  @override\n  TextNodeLayoutBuilder get layoutBuilder => _PlainTextLayoutBuilder(this);\n}\n\nclass _PlainTextLayoutBuilder extends TextNodeLayoutBuilder {\n  _PlainTextLayoutBuilder(this.node)\n    : renderer = node.style.asTextRenderer(),\n      words = node.text.split(' ');\n\n  final PlainTextNode node;\n  final TextRenderer renderer;\n  final List<String> words;\n  int index0 = 0;\n  int index1 = 1;\n\n  @override\n  bool get isDone => index1 > words.length;\n\n  @override\n  InlineTextElement? layOutNextLine(\n    double availableWidth, {\n    required bool isStartOfLine,\n  }) {\n    InlineTextElement? tentativeLine;\n    int? tentativeIndex0;\n    while (index1 <= words.length) {\n      final prependSpace = index0 == 0 || isStartOfLine ? '' : ' ';\n      final textPiece = prependSpace + words.sublist(index0, index1).join(' ');\n      final formattedPiece = renderer.format(textPiece);\n      if (formattedPiece.metrics.width > availableWidth) {\n        break;\n      } else {\n        tentativeLine = formattedPiece;\n        tentativeIndex0 = index1;\n        index1 += 1;\n      }\n    }\n    if (tentativeLine != null) {\n      assert(tentativeIndex0 != 0 && tentativeIndex0! > index0);\n      index0 = tentativeIndex0!;\n      return tentativeLine;\n    } else {\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/nodes/strikethrough_text_node.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/text/nodes/inline_text_node.dart';\nimport 'package:flame/text.dart';\n\n/// An [InlineTextNode] representing a text with a strikethrough line.\n///\n/// The exact styling can be controlled by the `strikethroughText` property\n/// on the document style.\nclass StrikethroughTextNode extends InlineTextNode {\n  StrikethroughTextNode(this.child);\n\n  StrikethroughTextNode.simple(String text) : child = PlainTextNode(text);\n\n  StrikethroughTextNode.group(List<InlineTextNode> children)\n    : child = GroupTextNode(children);\n\n  final InlineTextNode child;\n\n  static final defaultStyle = InlineTextStyle(\n    decoration: TextDecoration.lineThrough,\n  );\n\n  @override\n  void fillStyles(DocumentStyle stylesheet, InlineTextStyle parentTextStyle) {\n    style = FlameTextStyle.merge(\n      parentTextStyle,\n      stylesheet.strikethroughText,\n    )!;\n    child.fillStyles(stylesheet, style);\n  }\n\n  @override\n  TextNodeLayoutBuilder get layoutBuilder => child.layoutBuilder;\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/nodes/text_block_node.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/src/text/common/utils.dart';\nimport 'package:flame/text.dart';\nimport 'package:meta/meta.dart';\n\nabstract class TextBlockNode extends BlockNode {\n  TextBlockNode(this.child);\n\n  final InlineTextNode child;\n\n  @mustCallSuper\n  @override\n  void fillStyles(DocumentStyle stylesheet, InlineTextStyle parentTextStyle) {\n    child.fillStyles(stylesheet, parentTextStyle);\n  }\n\n  /// Converts this node into a [BlockElement].\n  ///\n  /// All late variables must be initialized prior to calling this method.\n  @override\n  BlockElement format(double availableWidth) {\n    final layoutBuilder = child.layoutBuilder;\n    final blockWidth = availableWidth;\n    final contentWidth = blockWidth - style.padding.horizontal;\n\n    final lines = <InlineTextElement>[];\n    final horizontalOffset = style.padding.left;\n    var verticalOffset = style.padding.top;\n    final textAlign = style.textAlign ?? TextAlign.left;\n    while (!layoutBuilder.isDone) {\n      final element = layoutBuilder.layOutNextLine(\n        contentWidth,\n        isStartOfLine: true,\n      );\n      if (element == null) {\n        // Not enough horizontal space to lay out. For now we just stop the\n        // layout altogether cutting off the remainder of the content. But is\n        // there a better alternative?\n        break;\n      } else {\n        final metrics = element.metrics;\n        assert(metrics.left == 0 && metrics.baseline == 0);\n\n        final dx =\n            horizontalOffset +\n            (contentWidth - metrics.width) * _relativeOffset(textAlign);\n        final dy = verticalOffset + metrics.ascent;\n        element.translate(dx, dy);\n\n        lines.add(element);\n        verticalOffset += metrics.height;\n      }\n    }\n    verticalOffset += style.padding.bottom;\n    final bg = makeBackground(style.background, blockWidth, verticalOffset);\n    final elements = bg == null ? lines : [bg, ...lines];\n    return GroupElement(\n      width: blockWidth,\n      height: verticalOffset,\n      children: elements,\n    );\n  }\n\n  double _relativeOffset(TextAlign textAlign) {\n    return switch (textAlign) {\n      TextAlign.left => 0,\n      TextAlign.right => 1,\n      TextAlign.center => 0.5,\n      // NOTE: we do not support non-LRT text directions\n      TextAlign.start => 0,\n      TextAlign.end => 1,\n      // Not supported by Flame\n      TextAlign.justify => throw UnimplementedError(\n        'The text rendering pipeline cannot justify text.',\n      ),\n    };\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/nodes/text_node.dart",
    "content": "import 'package:flame/text.dart';\n\nabstract class TextNode<T extends FlameTextStyle> {\n  T get style;\n\n  void fillStyles(DocumentStyle stylesheet, InlineTextStyle parentTextStyle);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/renderers/sprite_font_renderer.dart",
    "content": "import 'dart:typed_data';\nimport 'dart:ui' hide LineMetrics;\n\nimport 'package:flame/text.dart';\n\n/// [SpriteFontRenderer] will render text using a [SpriteFont] font,\n/// creating a [SpriteFontTextElement].\nclass SpriteFontRenderer extends TextRenderer {\n  SpriteFontRenderer.fromFont(\n    this.font, {\n    this.scale = 1.0,\n    this.letterSpacing = 0.0,\n    Color? color,\n  }) : paint = Paint() {\n    if (color != null) {\n      paint.colorFilter = ColorFilter.mode(color, BlendMode.srcIn);\n    }\n  }\n\n  final SpriteFont font;\n  final double scale;\n  final double letterSpacing;\n  final Paint paint;\n\n  @override\n  SpriteFontTextElement format(String text) {\n    var rects = Float32List(text.length * 4);\n    var transforms = Float32List(text.length * 4);\n    var j = 0;\n    var x0 = 0.0;\n    for (final glyph in font.textToGlyphs(text)) {\n      rects[j + 0] = glyph.srcLeft;\n      rects[j + 1] = glyph.srcTop;\n      rects[j + 2] = glyph.srcRight;\n      rects[j + 3] = glyph.srcBottom;\n      transforms[j + 0] = scale;\n      transforms[j + 1] = 0;\n      transforms[j + 2] = x0 + (glyph.srcLeft - glyph.left) * scale;\n      transforms[j + 3] = (glyph.srcTop - glyph.top - font.ascent) * scale;\n      j += 4;\n      x0 += glyph.width * scale + letterSpacing;\n    }\n    if (j < text.length * 4) {\n      rects = rects.sublist(0, j);\n      transforms = transforms.sublist(0, j);\n    }\n    return SpriteFontTextElement(\n      source: font.source,\n      transforms: transforms,\n      rects: rects,\n      paint: paint,\n      metrics: LineMetrics(\n        width: x0 - letterSpacing,\n        height: font.size * scale,\n        ascent: font.ascent * scale,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/renderers/text_paint.dart",
    "content": "import 'package:flame/cache.dart';\nimport 'package:flame/text.dart';\nimport 'package:flutter/rendering.dart';\n\n/// [TextPaint] applies a Flutter [TextStyle] to a string of\n/// text, creating a [TextPainterTextElement].\nclass TextPaint extends TextRenderer {\n  TextPaint({\n    TextStyle? style,\n    this.textDirection = TextDirection.ltr,\n  }) : style = style ?? defaultTextStyle;\n\n  final TextStyle style;\n  final TextDirection textDirection;\n\n  @override\n  TextPainterTextElement format(String text) {\n    final tp = toTextPainter(text);\n    return TextPainterTextElement(tp);\n  }\n\n  final MemoryCache<String, TextPainter> _textPainterCache = MemoryCache();\n\n  static const TextStyle defaultTextStyle = TextStyle(\n    color: Color(0xFFFFFFFF),\n    fontFamily: 'Arial',\n    fontSize: 24,\n  );\n\n  /// Returns a [TextPainter] that allows for text rendering and size\n  /// measuring.\n  ///\n  /// A [TextPainter] has three important properties: paint, width and\n  /// height (or size).\n  ///\n  /// Example usage:\n  ///\n  ///   const config = TextPaint(fontSize: 48.0, fontFamily: 'Arial');\n  ///   final tp = config.toTextPainter('Score: $score');\n  ///   tp.paint(canvas, const Offset(10, 10));\n  ///\n  /// However, you probably want to use the [render] method which already\n  /// takes the anchor into consideration.\n  /// That way, you don't need to perform the math for that yourself.\n  TextPainter toTextPainter(String text) {\n    if (!_textPainterCache.containsKey(text)) {\n      final tp = TextPainter(\n        text: TextSpan(text: text, style: style),\n        textDirection: textDirection,\n      );\n      tp.layout();\n      _textPainterCache.setValue(text, tp);\n    }\n    return _textPainterCache.getValue(text)!;\n  }\n\n  TextPaint copyWith(\n    TextStyle Function(TextStyle) transform, {\n    TextDirection? textDirection,\n  }) {\n    return TextPaint(\n      style: transform(style),\n      textDirection: textDirection ?? this.textDirection,\n    );\n  }\n\n  InlineTextStyle asInlineTextStyle() {\n    return InlineTextStyle(\n      color: style.color,\n      fontFamily: style.fontFamily,\n      fontSize: style.fontSize,\n      fontWeight: style.fontWeight,\n      fontStyle: style.fontStyle,\n      letterSpacing: style.letterSpacing,\n      wordSpacing: style.wordSpacing,\n      height: style.height,\n      leadingDistribution: style.leadingDistribution,\n      shadows: style.shadows,\n      fontFeatures: style.fontFeatures,\n      fontVariations: style.fontVariations,\n      decoration: style.decoration,\n      decorationColor: style.decorationColor,\n      decorationStyle: style.decorationStyle,\n      decorationThickness: style.decorationThickness,\n      background: _extractBackground(style),\n      foreground: style.foreground,\n    );\n  }\n\n  Paint? _extractBackground(TextStyle style) {\n    if (style.background != null) {\n      return style.background;\n    }\n    if (style.backgroundColor != null) {\n      return Paint()..color = style.backgroundColor!;\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/renderers/text_renderer.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/anchor.dart';\nimport 'package:flame/text.dart';\n\n/// [TextRenderer] is an abstract interface for a class that can convert an\n/// arbitrary string of text into a renderable [InlineTextElement].\nabstract class TextRenderer {\n  InlineTextElement format(String text);\n\n  LineMetrics getLineMetrics(String text) {\n    return format(text).metrics;\n  }\n\n  void render(\n    Canvas canvas,\n    String text,\n    Vector2 position, {\n    Anchor anchor = Anchor.topLeft,\n  }) {\n    format(text).render(canvas, position, anchor: anchor);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/renderers/text_renderer_factory.dart",
    "content": "import 'package:flame/text.dart';\n\nclass TextRendererFactory {\n  /// A registry containing default providers for every [TextRenderer] subclass;\n  /// used by [createDefault] to create default parameter values.\n  ///\n  /// If you add a new [TextRenderer] child, you can register it by adding it,\n  /// together with a provider lambda, to this map.\n  static Map<Type, TextRenderer Function()> defaultRegistry = {\n    TextRenderer: TextPaint.new,\n    TextPaint: TextPaint.new,\n  };\n\n  /// Given a generic type [T], creates a default renderer of that type.\n  static T createDefault<T extends TextRenderer>() {\n    final creator = defaultRegistry[T];\n    if (creator != null) {\n      return creator() as T;\n    } else {\n      throw 'Unknown implementation of TextRenderer: $T. Please register it '\n          'under [TextRendererFactory.defaultRegistry].';\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/styles/background_style.dart",
    "content": "import 'package:flame/text.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:meta/meta.dart';\n\n@immutable\nclass BackgroundStyle extends FlameTextStyle {\n  BackgroundStyle({\n    Color? color,\n    Paint? paint,\n    Color? borderColor,\n    double? borderRadius,\n    double? borderWidth,\n  }) : assert(\n         paint == null || color == null,\n         'Parameters `paint` and `color` are exclusive',\n       ),\n       borderWidths = EdgeInsets.all(borderWidth ?? 0),\n       borderRadius = borderRadius ?? 0,\n       backgroundPaint =\n           paint ?? (color != null ? (Paint()..color = color) : null),\n       borderPaint = borderColor != null\n           ? (Paint()\n               ..color = borderColor\n               ..style = PaintingStyle.stroke\n               ..strokeWidth = borderWidth ?? 0)\n           : null;\n\n  final Paint? backgroundPaint;\n  final Paint? borderPaint;\n  final double borderRadius;\n  final EdgeInsets borderWidths;\n\n  @override\n  BackgroundStyle copyWith(BackgroundStyle other) {\n    return BackgroundStyle(\n      paint: other.backgroundPaint ?? backgroundPaint,\n      borderColor: other.borderPaint?.color ?? borderPaint?.color,\n      borderRadius: other.borderRadius,\n      borderWidth: other.borderWidths.top,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/styles/block_style.dart",
    "content": "import 'package:flame/text.dart';\nimport 'package:flutter/painting.dart' hide TextStyle;\nimport 'package:meta/meta.dart';\n\n/// [BlockStyle] is a generic descriptor for a visual appearance of a block-\n/// level element.\n@immutable\nclass BlockStyle extends FlameTextStyle {\n  const BlockStyle({\n    EdgeInsets? margin,\n    EdgeInsets? padding,\n    this.background,\n    this.text,\n    this.textAlign,\n  }) : _margin = margin,\n       _padding = padding;\n\n  final EdgeInsets? _margin;\n  final EdgeInsets? _padding;\n  final BackgroundStyle? background;\n  final InlineTextStyle? text;\n  final TextAlign? textAlign;\n\n  EdgeInsets get margin => _margin ?? EdgeInsets.zero;\n  EdgeInsets get padding => _padding ?? EdgeInsets.zero;\n\n  @override\n  BlockStyle copyWith(BlockStyle other) {\n    return BlockStyle(\n      margin: other._margin ?? _margin,\n      padding: other._padding ?? _padding,\n      background: other.background ?? background,\n      text: FlameTextStyle.merge(text, other.text),\n      textAlign: other.textAlign ?? textAlign,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/styles/document_style.dart",
    "content": "import 'package:flame/src/text/styles/overflow.dart';\nimport 'package:flame/text.dart';\nimport 'package:flutter/painting.dart' show EdgeInsets;\n\n/// [DocumentStyle] is a user-facing description of how to render an entire\n/// body of text; it roughly corresponds to a stylesheet in HTML.\n///\n/// This class represents a top-level style sheet, comprised of properties that\n/// describe the document as a whole (as opposed to lower-level styles that\n/// represent more granular elements such as paragraphs, headers, etc).\n///\n/// All styles that collectively describe how to render text are organized into\n/// a tree, with [DocumentStyle] at the root.\nclass DocumentStyle extends FlameTextStyle {\n  DocumentStyle({\n    this.width,\n    this.height,\n    this.padding = EdgeInsets.zero,\n    this.background,\n    InlineTextStyle? text,\n    InlineTextStyle? boldText,\n    InlineTextStyle? italicText,\n    InlineTextStyle? codeText,\n    InlineTextStyle? strikethroughText,\n    Map<String, InlineTextStyle>? customStyles,\n    BlockStyle? paragraph,\n    BlockStyle? header1,\n    BlockStyle? header2,\n    BlockStyle? header3,\n    BlockStyle? header4,\n    BlockStyle? header5,\n    BlockStyle? header6,\n  }) : _text = FlameTextStyle.merge(DocumentStyle.defaultTextStyle, text),\n       _boldText = FlameTextStyle.merge(BoldTextNode.defaultStyle, boldText),\n       _italicText = FlameTextStyle.merge(\n         ItalicTextNode.defaultStyle,\n         italicText,\n       ),\n       _codeText = FlameTextStyle.merge(CodeTextNode.defaultStyle, codeText),\n       _strikethroughText = FlameTextStyle.merge(\n         StrikethroughTextNode.defaultStyle,\n         strikethroughText,\n       ),\n       _customStyles = customStyles,\n       _paragraph = FlameTextStyle.merge(ParagraphNode.defaultStyle, paragraph),\n       _header1 = FlameTextStyle.merge(HeaderNode.defaultStyleH1, header1),\n       _header2 = FlameTextStyle.merge(HeaderNode.defaultStyleH2, header2),\n       _header3 = FlameTextStyle.merge(HeaderNode.defaultStyleH3, header3),\n       _header4 = FlameTextStyle.merge(HeaderNode.defaultStyleH4, header4),\n       _header5 = FlameTextStyle.merge(HeaderNode.defaultStyleH5, header5),\n       _header6 = FlameTextStyle.merge(HeaderNode.defaultStyleH6, header6);\n\n  final InlineTextStyle? _text;\n  final InlineTextStyle? _boldText;\n  final InlineTextStyle? _italicText;\n  final InlineTextStyle? _codeText;\n  final InlineTextStyle? _strikethroughText;\n  final Map<String, InlineTextStyle>? _customStyles;\n  final BlockStyle? _paragraph;\n  final BlockStyle? _header1;\n  final BlockStyle? _header2;\n  final BlockStyle? _header3;\n  final BlockStyle? _header4;\n  final BlockStyle? _header5;\n  final BlockStyle? _header6;\n\n  /// Outer width of the document page.\n  ///\n  /// This width is the distance between the left edge of the left border, and\n  /// the right edge of the right border. Thus, it corresponds to the\n  /// \"border-box\" box sizing model in HTML.\n  ///\n  /// If this property is `null`, then the page width must be provided when\n  /// formatting a document.\n  final double? width;\n\n  /// Outer height of the document page.\n  ///\n  /// If the [overflow] property is [Overflow.expand], then the height parameter\n  /// is treated as a minimum height, in other cases the height is obeyed\n  /// strictly.\n  ///\n  /// If this property is `null`, then the page height must be provided when\n  /// formatting a document (except for the overflow-expand mode, when the\n  /// value of [height] defaults to 0).\n  final double? height;\n\n  /// Behavior of the document when the amount of content that needs to be laid\n  /// out exceeds the provided [height]. See the [Overflow] enum description for\n  /// more details.\n  Overflow get overflow => Overflow.expand;\n\n  /// The distance from the outer edges of the page to the inner content box of\n  /// the document.\n  ///\n  /// Note that the padding is computed from the outer edge of the page, unlike\n  /// HTML where it is computed from the inner edge of the border box.\n  ///\n  /// If the document's horizontal padding exceeds its width, an exception will\n  /// be thrown.\n  final EdgeInsets padding;\n\n  /// If present, describes what kind of background and borders to draw for the\n  /// document page(s).\n  final BackgroundStyle? background;\n\n  InlineTextStyle get text => _text!;\n  InlineTextStyle get boldText => _boldText!;\n  InlineTextStyle get italicText => _italicText!;\n  InlineTextStyle get codeText => _codeText!;\n  InlineTextStyle get strikethroughText => _strikethroughText!;\n\n  InlineTextStyle? getCustomStyle(String className) {\n    return _customStyles?[className];\n  }\n\n  /// Style for [ParagraphNode]s.\n  BlockStyle get paragraph => _paragraph!;\n\n  /// Styles for [HeaderNode]s, levels 1 to 6.\n  BlockStyle get header1 => _header1!;\n  BlockStyle get header2 => _header2!;\n  BlockStyle get header3 => _header3!;\n  BlockStyle get header4 => _header4!;\n  BlockStyle get header5 => _header5!;\n  BlockStyle get header6 => _header6!;\n\n  static InlineTextStyle defaultTextStyle = InlineTextStyle(fontSize: 16.0);\n\n  @override\n  DocumentStyle copyWith(DocumentStyle other) {\n    return DocumentStyle(\n      width: other.width ?? width,\n      height: other.height ?? height,\n      padding: other.padding,\n      text: FlameTextStyle.merge(_text, other.text),\n      boldText: FlameTextStyle.merge(_boldText, other.boldText),\n      italicText: FlameTextStyle.merge(_italicText, other.italicText),\n      codeText: FlameTextStyle.merge(_codeText, other.codeText),\n      strikethroughText: FlameTextStyle.merge(\n        _strikethroughText,\n        other.strikethroughText,\n      ),\n      background: merge(background, other.background) as BackgroundStyle?,\n      paragraph: merge(paragraph, other.paragraph) as BlockStyle?,\n      header1: merge(header1, other.header1) as BlockStyle?,\n      header2: merge(header2, other.header2) as BlockStyle?,\n      header3: merge(header3, other.header3) as BlockStyle?,\n      header4: merge(header4, other.header4) as BlockStyle?,\n      header5: merge(header5, other.header5) as BlockStyle?,\n      header6: merge(header6, other.header6) as BlockStyle?,\n    );\n  }\n\n  final Map<FlameTextStyle, Map<FlameTextStyle, FlameTextStyle>>\n  _mergedStylesCache = {};\n\n  /// Merges two [FlameTextStyle]s together, preferring the properties of\n  /// [style2] if present, falling back to the properties of [style1].\n  FlameTextStyle? merge(FlameTextStyle? style1, FlameTextStyle? style2) {\n    if (style1 == null) {\n      return style2;\n    } else if (style2 == null) {\n      return style1;\n    } else {\n      return (_mergedStylesCache[style1] ??= {})[style2] ??= style1.copyWith(\n        style2,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/styles/flame_text_style.dart",
    "content": "import 'package:flame/text.dart';\n\n/// A [FlameTextStyle] is a base class for several classes that collectively\n/// describe the desired visual appearance of a \"rich-text\" document.\n///\n/// The style classes mostly are collections of properties that describe how a\n/// potential document should be formatted. However, they have little logic\n/// beyond that. The style classes are then passed to document `Node`s so that\n/// the content of a document can be formatted.\n///\n/// Various [FlameTextStyle] classes are organized into a tree, with\n/// [DocumentStyle] at the root.\n///\n/// The tree of [FlameTextStyle]s is roughly equivalent to a CSS stylesheet.\nabstract class FlameTextStyle {\n  const FlameTextStyle();\n\n  /// Creates a new [FlameTextStyle], preferring the properties of [other]\n  /// if present, falling back to the properties of `this`.\n  FlameTextStyle copyWith(covariant FlameTextStyle other);\n\n  /// Merges two [FlameTextStyle]s together, preferring the properties of\n  /// [style2] if present, falling back to the properties of [style1].\n  static T? merge<T extends FlameTextStyle>(T? style1, T? style2) {\n    if (style1 == null) {\n      return style2;\n    } else if (style2 == null) {\n      return style1;\n    } else {\n      assert(style1.runtimeType == style2.runtimeType);\n      return style1.copyWith(style2) as T;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/styles/inline_text_style.dart",
    "content": "import 'package:flame/text.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:meta/meta.dart';\n\n/// A [FlameTextStyle] used to style an inline text element.\n///\n/// Note: the fields on this class are equivalent to the fields on Flutter's\n/// [TextStyle] class; check its documentation for more details.\n@immutable\nclass InlineTextStyle extends FlameTextStyle {\n  InlineTextStyle({\n    this.color,\n    this.fontFamily,\n    this.fontSize,\n    this.fontScale,\n    this.fontWeight,\n    this.fontStyle,\n    this.letterSpacing,\n    this.wordSpacing,\n    this.height,\n    this.leadingDistribution,\n    this.shadows,\n    this.fontFeatures,\n    this.fontVariations,\n    this.decoration,\n    this.decorationColor,\n    this.decorationStyle,\n    this.decorationThickness,\n    this.background,\n    this.foreground,\n  });\n\n  final Color? color;\n  final String? fontFamily;\n  final double? fontSize;\n  final double? fontScale;\n  final FontWeight? fontWeight;\n  final FontStyle? fontStyle;\n  final double? letterSpacing;\n  final double? wordSpacing;\n  final double? height;\n  final TextLeadingDistribution? leadingDistribution;\n  final List<Shadow>? shadows;\n  final List<FontFeature>? fontFeatures;\n  final List<FontVariation>? fontVariations;\n  final TextDecoration? decoration;\n  final Color? decorationColor;\n  final TextDecorationStyle? decorationStyle;\n  final double? decorationThickness;\n  final Paint? background;\n  final Paint? foreground;\n\n  late final TextRenderer renderer = asTextRenderer();\n\n  @override\n  InlineTextStyle copyWith(InlineTextStyle other) {\n    return InlineTextStyle(\n      color: other.color ?? color,\n      fontFamily: other.fontFamily ?? fontFamily,\n      fontSize: other.fontSize ?? fontSize,\n      fontScale: other.fontScale ?? fontScale,\n      fontWeight: other.fontWeight ?? fontWeight,\n      fontStyle: other.fontStyle ?? fontStyle,\n      letterSpacing: other.letterSpacing ?? letterSpacing,\n      wordSpacing: other.wordSpacing ?? wordSpacing,\n      height: other.height ?? height,\n      leadingDistribution: other.leadingDistribution ?? leadingDistribution,\n      shadows: other.shadows ?? shadows,\n      fontFeatures: other.fontFeatures ?? fontFeatures,\n      fontVariations: other.fontVariations ?? fontVariations,\n      decoration: other.decoration ?? decoration,\n      decorationColor: other.decorationColor ?? decorationColor,\n      decorationStyle: other.decorationStyle ?? decorationStyle,\n      decorationThickness: other.decorationThickness ?? decorationThickness,\n      background: other.background ?? background,\n      foreground: other.foreground ?? foreground,\n    );\n  }\n\n  TextPaint asTextRenderer() {\n    return TextPaint(\n      style: asTextStyle(),\n    );\n  }\n\n  TextStyle asTextStyle() {\n    return TextStyle(\n      color: color,\n      fontFamily: fontFamily,\n      fontSize: fontSize! * (fontScale ?? 1.0),\n      fontWeight: fontWeight,\n      fontStyle: fontStyle,\n      letterSpacing: letterSpacing,\n      wordSpacing: wordSpacing,\n      height: height,\n      leadingDistribution: leadingDistribution,\n      shadows: shadows,\n      fontFeatures: fontFeatures,\n      fontVariations: fontVariations,\n      decoration: decoration,\n      decorationColor: decorationColor,\n      decorationStyle: decorationStyle,\n      decorationThickness: decorationThickness,\n      background: background,\n      foreground: foreground,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/text/styles/overflow.dart",
    "content": "enum Overflow {\n  /// Any content that doesn't fit into the document box will be clipped.\n  hidden,\n\n  /// If there is any content that doesn't fit into the document box, it will\n  /// be removed, and an \"ellipsis\" symbol added at the end to indicate that\n  /// some content was truncated.\n  ellipsis,\n\n  /// The height of the document box will be automatically extended to\n  /// accommodate any content that wouldn't fit otherwise. Under this mode the\n  /// `height` property is treated as \"min-height\".\n  expand,\n\n  /// Any content that doesn't fit into the document box will be moved onto the\n  /// next one or more pages.\n  paginate,\n}\n"
  },
  {
    "path": "packages/flame/lib/src/timer.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\n/// Simple utility class that helps handling time counting and implementing\n/// interval like events.\n///\n/// Timer auto-starts by default.\n///\n/// NOTE: You can change the [limit], but keep in mind that the timer\n/// won't start automatically if the limit is raised and the timer currently\n/// is stopped.\nclass Timer {\n  double limit;\n  VoidCallback? onTick;\n  bool repeat;\n  double _current = 0;\n  bool _running;\n  final int? tickCount;\n  int _currentTick = 0;\n\n  Timer(\n    this.limit, {\n    this.onTick,\n    this.repeat = false,\n    bool autoStart = true,\n    this.tickCount,\n  }) : assert(\n         tickCount == null || tickCount > 0,\n         'tickCount must be null or bigger than 0',\n       ),\n       _running = autoStart;\n\n  /// The current amount of seconds that has passed on this iteration\n  double get current => _current;\n\n  /// If the timer is finished, timers that repeat never finish\n  bool get finished =>\n      (_current >= limit && !repeat) ||\n      (tickCount != null && _currentTick >= tickCount!);\n\n  /// Whether the timer is running or not\n  bool isRunning() => _running;\n\n  /// A value between 0.0 and 1.0 indicating the timer progress\n  double get progress => min(_current / limit, 1.0);\n\n  void update(double dt) {\n    if (_running) {\n      _current += dt;\n      if (_current >= limit) {\n        if (!repeat) {\n          _running = false;\n          _callTicker();\n          return;\n        }\n        // This is used to cover the rare case of _current being more than\n        // two times the value of limit, so that the onTick is called the\n        // correct number of times\n        while (_current >= limit) {\n          _current -= limit;\n          _callTicker();\n        }\n      }\n    }\n  }\n\n  void _callTicker() {\n    onTick?.call();\n    _currentTick += 1;\n    if (tickCount != null && _currentTick >= tickCount!) {\n      stop();\n    }\n  }\n\n  /// Start the timer from 0.\n  void start() {\n    reset();\n    resume();\n  }\n\n  /// Stop and reset the timer.\n  void stop() {\n    reset();\n    pause();\n  }\n\n  /// Reset the timer to 0, but continue running if it currently is running.\n  void reset() {\n    _current = 0;\n  }\n\n  ///  Pause the timer (no-op if it is already paused).\n  void pause() {\n    _running = false;\n  }\n\n  /// Resume a paused timer (no-op if it is already running).\n  void resume() {\n    _running = true;\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/widgets/animation_widget.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/src/anchor.dart';\nimport 'package:flame/src/cache/images.dart';\nimport 'package:flame/src/sprite_animation.dart';\nimport 'package:flame/src/sprite_animation_ticker.dart';\nimport 'package:flame/src/widgets/base_future_builder.dart';\nimport 'package:flame/src/widgets/sprite_painter.dart';\nimport 'package:flutter/material.dart' hide Animation;\n\nexport '../sprite_animation.dart';\n\n/// A [StatelessWidget] that renders a [SpriteAnimation]\nclass SpriteAnimationWidget extends StatefulWidget {\n  /// The positioning [Anchor].\n  final Anchor anchor;\n\n  /// Whether the animation should be playing or not.\n  final bool playing;\n\n  final FutureOr<SpriteAnimation> _animationFuture;\n  final SpriteAnimationTicker? _animationTicker;\n\n  /// A builder function that is called if the loading fails.\n  final WidgetBuilder? errorBuilder;\n\n  /// A builder function that is called while the loading is on the way.\n  final WidgetBuilder? loadingBuilder;\n\n  /// A callback that is called when the animation completes.\n  final VoidCallback? onComplete;\n\n  /// A custom [Paint] to be used when rendering the sprite.\n  /// When omitted the default paint from the [Sprite] class will be used.\n  final Paint? paint;\n\n  const SpriteAnimationWidget({\n    required SpriteAnimation animation,\n    required SpriteAnimationTicker animationTicker,\n    this.playing = true,\n    this.anchor = Anchor.topLeft,\n    this.errorBuilder,\n    this.loadingBuilder,\n    this.onComplete,\n    this.paint,\n    super.key,\n  }) : _animationFuture = animation,\n       _animationTicker = animationTicker;\n\n  /// Loads image from the asset [path] and renders it as a widget.\n  ///\n  /// It will use the [loadingBuilder] while the image from [path] is loading.\n  /// To render without loading, or when you want to have a gapless playback\n  /// when the [path] value changes, consider loading the [SpriteAnimation]\n  /// beforehand and direct pass it to the default constructor.\n  SpriteAnimationWidget.asset({\n    required String path,\n    required SpriteAnimationData data,\n    Images? images,\n    this.playing = true,\n    this.anchor = Anchor.topLeft,\n    this.errorBuilder,\n    this.loadingBuilder,\n    this.onComplete,\n    this.paint,\n    String? package,\n    super.key,\n  }) : _animationFuture = SpriteAnimation.load(\n         path,\n         data,\n         images: images,\n         package: package,\n       ),\n       _animationTicker = null;\n\n  @override\n  State<SpriteAnimationWidget> createState() => _SpriteAnimationWidgetState();\n}\n\nclass _SpriteAnimationWidgetState extends State<SpriteAnimationWidget> {\n  late FutureOr<SpriteAnimation> _animationFuture = widget._animationFuture;\n  late SpriteAnimationTicker? _animationTicker = widget._animationTicker;\n\n  @override\n  void didUpdateWidget(covariant SpriteAnimationWidget oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    _updateAnimation(\n      oldWidget._animationFuture,\n      widget._animationFuture,\n      oldWidget._animationTicker,\n      widget._animationTicker,\n    );\n  }\n\n  Future<void> _updateAnimation(\n    FutureOr<SpriteAnimation> oldFutureValue,\n    FutureOr<SpriteAnimation> newFutureValue,\n    SpriteAnimationTicker? oldTicker,\n    SpriteAnimationTicker? newTicker,\n  ) async {\n    final oldValue = await oldFutureValue;\n    final newValue = await newFutureValue;\n\n    final areFramesDifferent =\n        oldValue != newValue ||\n        oldValue.frames.length != newValue.frames.length ||\n        oldValue.frames.fold(\n          true,\n          (previous, frame) {\n            final newFrame = newValue.frames[oldValue.frames.indexOf(frame)];\n\n            return previous &&\n                (frame.sprite.image == newFrame.sprite.image ||\n                    frame.sprite.src == newFrame.sprite.src);\n          },\n        );\n\n    if (mounted && (areFramesDifferent || oldTicker != newTicker)) {\n      setState(() {\n        _animationFuture = newFutureValue;\n        _animationTicker = newTicker;\n      });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BaseFutureBuilder<SpriteAnimation>(\n      future: _animationFuture,\n      builder: (_, spriteAnimation) {\n        final ticker = _animationTicker ?? spriteAnimation.createTicker();\n        ticker.completed.then((_) => widget.onComplete?.call());\n\n        return InternalSpriteAnimationWidget(\n          animation: spriteAnimation,\n          animationTicker: ticker,\n          anchor: widget.anchor,\n          playing: widget.playing,\n          paint: widget.paint,\n        );\n      },\n      errorBuilder: widget.errorBuilder,\n      loadingBuilder: widget.loadingBuilder,\n    );\n  }\n}\n\n/// A [StatefulWidget] that render a [SpriteAnimation].\n@visibleForTesting\nclass InternalSpriteAnimationWidget extends StatefulWidget {\n  /// The [SpriteAnimation] to be rendered\n  final SpriteAnimation animation;\n\n  /// The [SpriteAnimationTicker] use for updating the [animation].\n  final SpriteAnimationTicker animationTicker;\n\n  /// The positioning [Anchor]\n  final Anchor anchor;\n\n  /// Should the [animation] be playing or not\n  final bool playing;\n\n  final Paint? paint;\n\n  const InternalSpriteAnimationWidget({\n    required this.animation,\n    required this.animationTicker,\n    this.playing = true,\n    this.anchor = Anchor.topLeft,\n    this.paint,\n    super.key,\n  });\n\n  @override\n  State createState() => _InternalSpriteAnimationWidgetState();\n}\n\nclass _InternalSpriteAnimationWidgetState\n    extends State<InternalSpriteAnimationWidget>\n    with SingleTickerProviderStateMixin {\n  AnimationController? _controller;\n  double? _lastUpdated;\n\n  @override\n  void initState() {\n    super.initState();\n    _setupController();\n    if (widget.playing) {\n      _initAnimation();\n    }\n  }\n\n  @override\n  void didUpdateWidget(InternalSpriteAnimationWidget oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    if (oldWidget.animation != widget.animation) {\n      oldWidget.animationTicker.onComplete = null;\n      _setupController();\n      if (widget.playing) {\n        _initAnimation();\n      }\n    }\n\n    if (widget.playing != oldWidget.playing) {\n      if (widget.playing) {\n        _initAnimation();\n      } else {\n        _pauseAnimation();\n      }\n    }\n  }\n\n  void _initAnimation() {\n    widget.animationTicker.reset();\n    _lastUpdated = DateTime.now().microsecondsSinceEpoch.toDouble();\n    _controller?.repeat(\n      // Approximately 60 fps\n      period: const Duration(milliseconds: 16),\n    );\n  }\n\n  void _setupController() {\n    widget.animationTicker.onComplete = _pauseAnimation;\n    _controller ??= AnimationController(vsync: this)\n      ..addListener(_onAnimationValueChanged);\n  }\n\n  void _onAnimationValueChanged() {\n    const microSecond = 1 / 1000000;\n\n    final now = DateTime.now().microsecondsSinceEpoch.toDouble();\n    final lastUpdated = _lastUpdated ??= now;\n    final dt = (now - lastUpdated) * microSecond;\n\n    final frameIndexBeforeTick = widget.animationTicker.currentIndex;\n    widget.animationTicker.update(dt);\n    final frameIndexAfterTick = widget.animationTicker.currentIndex;\n\n    if (mounted && frameIndexBeforeTick != frameIndexAfterTick) {\n      setState(() {});\n    }\n    _lastUpdated = now;\n  }\n\n  void _pauseAnimation() {\n    _controller?.stop();\n  }\n\n  @override\n  void dispose() {\n    _controller?.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext ctx) {\n    return CustomPaint(\n      painter: SpritePainter(\n        widget.animationTicker.getSprite(),\n        widget.anchor,\n        widget.paint,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/widgets/base_future_builder.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/material.dart';\n\nclass BaseFutureBuilder<T> extends StatelessWidget {\n  final FutureOr<T> future;\n  final Widget Function(BuildContext, T) builder;\n  final WidgetBuilder? errorBuilder;\n  final WidgetBuilder? loadingBuilder;\n\n  const BaseFutureBuilder({\n    required this.future,\n    required this.builder,\n    this.loadingBuilder,\n    this.errorBuilder,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    if (future is Future<T>) {\n      return FutureBuilder<T>(\n        future: future as Future<T>,\n        builder: (_, snapshot) {\n          switch (snapshot.connectionState) {\n            case ConnectionState.waiting:\n            case ConnectionState.none:\n            case ConnectionState.active:\n              return loadingBuilder?.call(context) ?? const SizedBox();\n            case ConnectionState.done:\n              if (snapshot.hasError) {\n                return errorBuilder?.call(context) ?? const SizedBox();\n              }\n              final data = snapshot.data;\n              if (data != null) {\n                return builder(context, data);\n              }\n              return loadingBuilder?.call(context) ?? const SizedBox();\n          }\n        },\n      );\n    }\n\n    return builder(context, future as T);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/widgets/components_notifier_builder.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flutter/material.dart';\n\n/// A widget that rebuilds every time the given [notifier] changes.\nclass ComponentsNotifierBuilder<T extends Component> extends StatefulWidget {\n  const ComponentsNotifierBuilder({\n    required this.notifier,\n    required this.builder,\n    super.key,\n  });\n\n  final ComponentsNotifier<T> notifier;\n  final Widget Function(BuildContext, ComponentsNotifier<T>) builder;\n\n  @override\n  State<StatefulWidget> createState() {\n    return _ComponentsNotifierBuilderState<T>();\n  }\n}\n\nclass _ComponentsNotifierBuilderState<T extends Component>\n    extends State<ComponentsNotifierBuilder<T>> {\n  @override\n  void initState() {\n    super.initState();\n\n    widget.notifier.addListener(_listener);\n  }\n\n  @override\n  void dispose() {\n    widget.notifier.removeListener(_listener);\n\n    super.dispose();\n  }\n\n  void _listener() {\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) =>\n      widget.builder(context, widget.notifier);\n}\n"
  },
  {
    "path": "packages/flame/lib/src/widgets/nine_tile_box.dart",
    "content": "import 'dart:async';\nimport 'dart:ui';\n\nimport 'package:flame/cache.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/src/nine_tile_box.dart' as non_widget;\nimport 'package:flame/src/sprite.dart';\nimport 'package:flame/src/widgets/base_future_builder.dart';\nimport 'package:flutter/material.dart' hide Image;\n\nexport '../nine_tile_box.dart';\nexport '../sprite.dart';\n\nclass _Painter extends CustomPainter {\n  final Image image;\n  final double tileSize;\n  final double destTileSize;\n  late final non_widget.NineTileBox _nineTileBox;\n\n  _Painter({\n    required this.image,\n    required this.tileSize,\n    required this.destTileSize,\n  }) : _nineTileBox = non_widget.NineTileBox(\n         Sprite(image),\n         tileSize: tileSize.toInt(),\n         destTileSize: destTileSize.toInt(),\n       );\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    _nineTileBox.drawRect(canvas, Offset.zero & size);\n  }\n\n  @override\n  bool shouldRepaint(_) => false;\n}\n\n/// A [StatelessWidget] that renders NineTileBox\nclass NineTileBoxWidget extends StatefulWidget {\n  final FutureOr<Image> _imageFuture;\n\n  /// The size of the tile on the image\n  final double tileSize;\n\n  /// The size of the tile that will be used to render on the canvas\n  final double destTileSize;\n  final double? width;\n  final double? height;\n\n  final Widget? child;\n\n  final EdgeInsetsGeometry? padding;\n\n  /// A builder function that is called if the loading fails\n  final WidgetBuilder? errorBuilder;\n\n  /// A builder function that is called while the loading is on the way\n  final WidgetBuilder? loadingBuilder;\n\n  const NineTileBoxWidget({\n    required Image image,\n    required this.tileSize,\n    required this.destTileSize,\n    this.width,\n    this.height,\n    this.child,\n    this.padding,\n    super.key,\n  }) : _imageFuture = image,\n       errorBuilder = null,\n       loadingBuilder = null;\n\n  /// Loads image from the asset [path] and renders it as a widget.\n  ///\n  /// It will use the [loadingBuilder] while the image from [path] is loading.\n  /// To render without loading, or when you want to have a gapless playback\n  /// when the [path] value changes, consider loading the image beforehand\n  /// and direct pass it to the default constructor.\n  NineTileBoxWidget.asset({\n    required String path,\n    required this.tileSize,\n    required this.destTileSize,\n    Images? images,\n    this.width,\n    this.height,\n    this.child,\n    this.padding,\n    this.errorBuilder,\n    this.loadingBuilder,\n    super.key,\n  }) : _imageFuture = (images ?? Flame.images).load(path);\n\n  @override\n  State<NineTileBoxWidget> createState() => _NineTileBoxWidgetState();\n}\n\nclass _NineTileBoxWidgetState extends State<NineTileBoxWidget> {\n  late FutureOr<Image> _imageFuture = widget._imageFuture;\n\n  @override\n  void didUpdateWidget(covariant NineTileBoxWidget oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    _updateNineTileBox(widget._imageFuture, oldWidget._imageFuture);\n  }\n\n  Future<void> _updateNineTileBox(\n    FutureOr<Image> imageFuture,\n    FutureOr<Image> oldImageFuture,\n  ) async {\n    final image = await imageFuture;\n    final oldImage = await oldImageFuture;\n\n    if (mounted && image != oldImage) {\n      setState(() {\n        _imageFuture = imageFuture;\n      });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BaseFutureBuilder<Image>(\n      future: _imageFuture,\n      builder: (_, image) {\n        return InternalNineTileBox(\n          image: image,\n          tileSize: widget.tileSize,\n          destTileSize: widget.destTileSize,\n          width: widget.width,\n          height: widget.height,\n          padding: widget.padding,\n          child: widget.child,\n        );\n      },\n      errorBuilder: widget.errorBuilder,\n      loadingBuilder: widget.loadingBuilder,\n    );\n  }\n}\n\n@visibleForTesting\nclass InternalNineTileBox extends StatelessWidget {\n  final Image image;\n  final double tileSize;\n  final double destTileSize;\n  final double? width;\n  final double? height;\n\n  final Widget? child;\n\n  final EdgeInsetsGeometry? padding;\n\n  const InternalNineTileBox({\n    required this.image,\n    required this.tileSize,\n    required this.destTileSize,\n    this.child,\n    this.width,\n    this.height,\n    this.padding,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      width: width,\n      height: height,\n      child: CustomPaint(\n        painter: _Painter(\n          image: image,\n          tileSize: tileSize,\n          destTileSize: destTileSize,\n        ),\n        child: Container(\n          padding: padding,\n          child: child,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/widgets/sprite_button.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/cache.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/src/extensions/vector2.dart';\nimport 'package:flame/src/sprite.dart';\nimport 'package:flame/src/widgets/base_future_builder.dart';\nimport 'package:flutter/widgets.dart';\n\nexport '../sprite.dart';\n\n/// A [StatelessWidget] that uses Sprites to render a pressable button.\nclass SpriteButton extends StatelessWidget {\n  /// Holds the position of the sprite on the image.\n  final Vector2? srcPosition;\n\n  /// Holds the size of the sprite on the image.\n  final Vector2? srcSize;\n\n  /// Holds the position of the pressed sprite on the image.\n  final Vector2? pressedSrcPosition;\n\n  /// Holds the size of the pressed sprite on the image.\n  final Vector2? pressedSrcSize;\n\n  /// Holds the position of the disabled sprite on the image.\n  final Vector2? disabledSrcPosition;\n\n  /// Holds the size of the disabled sprite on the image.\n  final Vector2? disabledSrcSize;\n\n  /// The widget that will be rendered on top of the button.\n  final Widget? label;\n\n  /// The function that will be called when the button is pressed.\n  ///\n  /// If null, the button will not be clickable and render the disabled sprite,\n  /// if provided.\n  final void Function()? onPressed;\n\n  /// The width of the button.\n  final double width;\n\n  /// The height of the button.\n  final double height;\n\n  /// The offset of the button when pressed.\n  final EdgeInsets pressedInsets;\n\n  /// A builder function that is called if the loading fails.\n  final WidgetBuilder? errorBuilder;\n\n  /// A builder function that is called while the loading is on the way.\n  final WidgetBuilder? loadingBuilder;\n\n  final FutureOr<List<Sprite>> _buttonsFuture;\n\n  SpriteButton({\n    required Sprite sprite,\n    required Sprite pressedSprite,\n    required this.onPressed,\n    required this.width,\n    required this.height,\n    this.label,\n    this.srcPosition,\n    this.srcSize,\n    this.pressedSrcPosition,\n    this.pressedSrcSize,\n    Sprite? disabledSprite,\n    this.disabledSrcPosition,\n    this.disabledSrcSize,\n    this.pressedInsets = const EdgeInsets.only(top: 5),\n    this.errorBuilder,\n    this.loadingBuilder,\n    super.key,\n  }) : _buttonsFuture = [\n         sprite,\n         pressedSprite,\n         if (disabledSprite != null) disabledSprite,\n       ];\n\n  SpriteButton.future({\n    required Future<Sprite> sprite,\n    required Future<Sprite> pressedSprite,\n    required this.onPressed,\n    required this.width,\n    required this.height,\n    this.label,\n    this.srcPosition,\n    this.srcSize,\n    this.pressedSrcPosition,\n    this.pressedSrcSize,\n    Future<Sprite>? disabledSprite,\n    this.disabledSrcPosition,\n    this.disabledSrcSize,\n    this.pressedInsets = const EdgeInsets.only(top: 5),\n    this.errorBuilder,\n    this.loadingBuilder,\n    super.key,\n  }) : _buttonsFuture = Future.wait([\n         sprite,\n         pressedSprite,\n         if (disabledSprite != null) disabledSprite,\n       ]);\n\n  /// Loads the images from the asset [path] and [pressedPath] and renders\n  /// it as a widget.\n  ///\n  /// It will use the [loadingBuilder] while the image from [path] is loading.\n  /// To render without loading, or when you want to have a gapless playback\n  /// when the [path] value changes, consider loading the image beforehand\n  /// and direct pass it to the default constructor.\n  SpriteButton.asset({\n    required String path,\n    required String pressedPath,\n    required this.onPressed,\n    required this.width,\n    required this.height,\n    this.label,\n    Images? images,\n    this.srcPosition,\n    this.srcSize,\n    this.pressedSrcPosition,\n    this.pressedSrcSize,\n    String? disabledPath,\n    this.disabledSrcPosition,\n    this.disabledSrcSize,\n    this.pressedInsets = const EdgeInsets.only(top: 5),\n    this.errorBuilder,\n    this.loadingBuilder,\n    super.key,\n  }) : _buttonsFuture =\n           (images ?? Flame.images).containsKey(path) &&\n               (images ?? Flame.images).containsKey(pressedPath) &&\n               (disabledPath == null ||\n                   (images ?? Flame.images).containsKey(disabledPath))\n           ? [\n               Sprite(\n                 (images ?? Flame.images).fromCache(path),\n                 srcPosition: srcPosition,\n                 srcSize: srcSize,\n               ),\n               Sprite(\n                 (images ?? Flame.images).fromCache(pressedPath),\n                 srcPosition: pressedSrcPosition,\n                 srcSize: pressedSrcSize,\n               ),\n               if (disabledPath != null)\n                 Sprite(\n                   (images ?? Flame.images).fromCache(disabledPath),\n                   srcPosition: disabledSrcPosition,\n                   srcSize: disabledSrcSize,\n                 ),\n             ]\n           : Future.wait([\n               Sprite.load(\n                 path,\n                 srcPosition: srcPosition,\n                 srcSize: srcSize,\n                 images: images,\n               ),\n               Sprite.load(\n                 pressedPath,\n                 srcPosition: pressedSrcPosition,\n                 srcSize: pressedSrcSize,\n                 images: images,\n               ),\n               if (disabledPath != null)\n                 Sprite.load(\n                   disabledPath,\n                   srcPosition: disabledSrcPosition,\n                   srcSize: disabledSrcSize,\n                   images: images,\n                 ),\n             ]);\n\n  @override\n  Widget build(BuildContext context) {\n    return BaseFutureBuilder<List<Sprite>>(\n      future: _buttonsFuture,\n      builder: (_, list) {\n        final sprite = list[0];\n        final pressedSprite = list[1];\n\n        return InternalSpriteButton(\n          onPressed: onPressed,\n          label: label,\n          width: width,\n          height: height,\n          sprite: sprite,\n          pressedSprite: pressedSprite,\n          disabledSprite: list.length > 2 ? list[2] : null,\n        );\n      },\n      errorBuilder: errorBuilder,\n      loadingBuilder: loadingBuilder,\n    );\n  }\n}\n\n@visibleForTesting\nclass InternalSpriteButton extends StatefulWidget {\n  final void Function()? onPressed;\n  final Widget? label;\n  final Sprite sprite;\n  final Sprite pressedSprite;\n  final Sprite? disabledSprite;\n  final EdgeInsets pressedInsets;\n  final double width;\n  final double height;\n\n  const InternalSpriteButton({\n    required this.onPressed,\n    required this.sprite,\n    required this.pressedSprite,\n    this.disabledSprite,\n    this.pressedInsets = const EdgeInsets.only(top: 5),\n    this.label,\n    this.width = 200,\n    this.height = 50,\n    super.key,\n  });\n\n  @override\n  State createState() => _ButtonState();\n}\n\nclass _ButtonState extends State<InternalSpriteButton> {\n  bool _pressed = false;\n\n  @override\n  Widget build(_) {\n    final width = widget.width;\n    final height = widget.height;\n    final Sprite sprite;\n    if (widget.onPressed == null) {\n      sprite = widget.disabledSprite ?? widget.sprite;\n    } else if (_pressed) {\n      sprite = widget.pressedSprite;\n    } else {\n      sprite = widget.sprite;\n    }\n\n    return GestureDetector(\n      onTapDown: (_) {\n        if (widget.onPressed == null) {\n          return;\n        }\n        setState(() => _pressed = true);\n      },\n      onTapUp: (_) {\n        if (widget.onPressed == null) {\n          return;\n        }\n        setState(() => _pressed = false);\n        widget.onPressed?.call();\n      },\n      onTapCancel: () {\n        if (widget.onPressed == null) {\n          return;\n        }\n        setState(() => _pressed = false);\n      },\n      child: Container(\n        width: width,\n        height: height,\n        child: CustomPaint(\n          painter: _ButtonPainter(sprite),\n          child: Center(\n            child: Container(\n              padding: _pressed ? widget.pressedInsets : null,\n              child: widget.label,\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _ButtonPainter extends CustomPainter {\n  final Sprite _sprite;\n\n  _ButtonPainter(this._sprite);\n\n  @override\n  bool shouldRepaint(_ButtonPainter old) => old._sprite != _sprite;\n\n  final Vector2 _size = Vector2.zero();\n  @override\n  void paint(Canvas canvas, Size size) {\n    _size.setValues(size.width, size.height);\n    _sprite.render(canvas, size: _size);\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/widgets/sprite_painter.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame/src/anchor.dart';\nimport 'package:flame/src/sprite.dart';\nimport 'package:flutter/widgets.dart';\n\nclass SpritePainter extends CustomPainter {\n  final Sprite _sprite;\n  final Anchor _anchor;\n  final Paint? _paint;\n  final double _angle;\n\n  SpritePainter(this._sprite, this._anchor, this._paint, {double angle = 0})\n    : _angle = angle;\n\n  @override\n  bool shouldRepaint(SpritePainter oldDelegate) {\n    return oldDelegate._sprite != _sprite ||\n        oldDelegate._anchor != _anchor ||\n        oldDelegate._angle != _angle ||\n        oldDelegate._paint != _paint;\n  }\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    final boxSize = size.toVector2();\n    final rate = boxSize.clone()..divide(_sprite.srcSize);\n    final minRate = min(rate.x, rate.y);\n    final paintSize = _sprite.srcSize * minRate;\n    final anchorPosition = _anchor.toVector2();\n    final boxAnchorPosition = boxSize.clone()..multiply(anchorPosition);\n    final spriteAnchorPosition = anchorPosition..multiply(paintSize);\n\n    canvas.translateVector(boxAnchorPosition..sub(spriteAnchorPosition));\n\n    if (_angle == 0) {\n      _sprite.render(canvas, size: paintSize, overridePaint: _paint);\n    } else {\n      canvas.renderRotated(\n        _angle,\n        spriteAnchorPosition,\n        (canvas) => _sprite.render(\n          canvas,\n          size: paintSize,\n          overridePaint: _paint,\n        ),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/src/widgets/sprite_widget.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/cache.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/src/anchor.dart';\nimport 'package:flame/src/widgets/animation_widget.dart';\nimport 'package:flame/src/widgets/base_future_builder.dart';\nimport 'package:flame/src/widgets/sprite_painter.dart';\nimport 'package:flutter/widgets.dart';\n\nexport '../sprite.dart';\n\n/// A [StatelessWidget] which renders a Sprite\n/// To render an animation, use [SpriteAnimationWidget].\nclass SpriteWidget extends StatefulWidget {\n  /// The positioning [Anchor]\n  final Anchor anchor;\n\n  /// The angle to rotate this [Sprite], in rad. (default = 0)\n  final double angle;\n\n  /// A builder function that is called if the loading fails\n  final WidgetBuilder? errorBuilder;\n\n  /// A builder function that is called while the loading is on the way\n  final WidgetBuilder? loadingBuilder;\n\n  /// A custom [Paint] to be used when rendering the sprite.\n  /// When omitted the default paint from the [Sprite] class will be used.\n  final Paint? paint;\n\n  /// If the Sprite should be rasterized or not.\n  final bool rasterize;\n\n  final FutureOr<Sprite> _spriteFuture;\n\n  /// renders the [sprite] as a Widget.\n  ///\n  /// To change the source size and position, see [Sprite.new]\n  const SpriteWidget({\n    required Sprite sprite,\n    this.anchor = Anchor.topLeft,\n    this.angle = 0,\n    this.errorBuilder,\n    this.loadingBuilder,\n    this.paint,\n    this.rasterize = false,\n    super.key,\n  }) : _spriteFuture = sprite;\n\n  /// Load the image from the asset [path] and renders it as a widget.\n  ///\n  /// It will use the [loadingBuilder] while the image from [path] is loading.\n  /// To render without loading, or when you want to have a gapless playback\n  /// when the [path] value changes, consider loading the image beforehand\n  /// and direct pass it to the default constructor.\n  SpriteWidget.asset({\n    required String path,\n    Images? images,\n    this.anchor = Anchor.topLeft,\n    this.angle = 0,\n    Vector2? srcPosition,\n    Vector2? srcSize,\n    this.errorBuilder,\n    this.loadingBuilder,\n    this.paint,\n    this.rasterize = false,\n    String? package,\n    super.key,\n  }) : _spriteFuture = Sprite.load(\n         path,\n         srcSize: srcSize,\n         srcPosition: srcPosition,\n         images: images,\n         package: package,\n       );\n\n  @override\n  State<SpriteWidget> createState() => _SpriteWidgetState();\n}\n\nclass _SpriteWidgetState extends State<SpriteWidget> {\n  late FutureOr<Sprite> _spriteFuture = _initializeFuture();\n\n  FutureOr<Sprite> _initializeFuture() async {\n    if (!widget.rasterize) {\n      return widget._spriteFuture;\n    }\n\n    final sprite = await widget._spriteFuture;\n    return sprite.rasterize();\n  }\n\n  @override\n  void didUpdateWidget(covariant SpriteWidget oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    _updateSprite(\n      widget.rasterize || oldWidget.rasterize,\n      oldWidget._spriteFuture,\n      widget._spriteFuture,\n    );\n  }\n\n  Future<void> _updateSprite(\n    bool rasterize,\n    FutureOr<Sprite> oldFutureValue,\n    FutureOr<Sprite> newFutureValue,\n  ) async {\n    final oldValue = await oldFutureValue;\n    final newValue = await newFutureValue;\n\n    if (rasterize && oldValue.image != newValue.image) {\n      oldValue.image.dispose();\n    }\n\n    if (mounted &&\n        (oldValue.image != newValue.image || oldValue.src != newValue.src)) {\n      setState(() {\n        _spriteFuture = _initializeFuture();\n      });\n    }\n  }\n\n  Future<void> _disposeImage() async {\n    final value = await _spriteFuture;\n    value.image.dispose();\n  }\n\n  @override\n  void dispose() {\n    if (widget.rasterize) {\n      _disposeImage();\n    }\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BaseFutureBuilder<Sprite>(\n      future: _spriteFuture,\n      builder: (_, sprite) {\n        return InternalSpriteWidget(\n          sprite: sprite,\n          anchor: widget.anchor,\n          angle: widget.angle,\n          paint: widget.paint,\n        );\n      },\n      errorBuilder: widget.errorBuilder,\n      loadingBuilder: widget.loadingBuilder,\n    );\n  }\n}\n\n/// A [StatefulWidget] that renders a still [Sprite].\n@visibleForTesting\nclass InternalSpriteWidget extends StatelessWidget {\n  /// The [Sprite] to be rendered\n  final Sprite sprite;\n\n  /// The positioning [Anchor] for the [sprite]\n  final Anchor anchor;\n\n  /// The angle to rotate this [sprite], in rad. (default = 0)\n  final double angle;\n\n  final Paint? paint;\n\n  const InternalSpriteWidget({\n    required this.sprite,\n    this.anchor = Anchor.topLeft,\n    this.angle = 0,\n    this.paint,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return CustomPaint(\n      painter: SpritePainter(sprite, anchor, paint, angle: angle),\n      size: sprite.srcSize.toSize(),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame/lib/text.dart",
    "content": "export 'package:flutter/painting.dart' show TextStyle;\n\nexport 'src/text/common/glyph.dart' show Glyph;\nexport 'src/text/common/line_metrics.dart' show LineMetrics;\nexport 'src/text/common/sprite_font.dart' show SpriteFont;\nexport 'src/text/elements/block_element.dart' show BlockElement;\nexport 'src/text/elements/group_element.dart' show GroupElement;\nexport 'src/text/elements/group_text_element.dart' show GroupTextElement;\nexport 'src/text/elements/inline_text_element.dart' show InlineTextElement;\nexport 'src/text/elements/rect_element.dart' show RectElement;\nexport 'src/text/elements/rrect_element.dart' show RRectElement;\nexport 'src/text/elements/sprite_font_text_element.dart'\n    show SpriteFontTextElement;\nexport 'src/text/elements/text_element.dart' show TextElement;\nexport 'src/text/elements/text_painter_text_element.dart'\n    show TextPainterTextElement;\nexport 'src/text/nodes/block_node.dart' show BlockNode;\nexport 'src/text/nodes/bold_text_node.dart' show BoldTextNode;\nexport 'src/text/nodes/code_text_node.dart' show CodeTextNode;\nexport 'src/text/nodes/column_node.dart' show ColumnNode;\nexport 'src/text/nodes/custom_text_node.dart' show CustomInlineTextNode;\nexport 'src/text/nodes/document_root.dart' show DocumentRoot;\nexport 'src/text/nodes/group_text_node.dart' show GroupTextNode;\nexport 'src/text/nodes/header_node.dart' show HeaderNode;\nexport 'src/text/nodes/inline_text_node.dart' show InlineTextNode;\nexport 'src/text/nodes/italic_text_node.dart' show ItalicTextNode;\nexport 'src/text/nodes/paragraph_node.dart' show ParagraphNode;\nexport 'src/text/nodes/plain_text_node.dart' show PlainTextNode;\nexport 'src/text/nodes/strikethrough_text_node.dart' show StrikethroughTextNode;\nexport 'src/text/nodes/text_block_node.dart' show TextBlockNode;\nexport 'src/text/nodes/text_node.dart' show TextNode;\nexport 'src/text/renderers/sprite_font_renderer.dart' show SpriteFontRenderer;\nexport 'src/text/renderers/text_paint.dart' show TextPaint;\nexport 'src/text/renderers/text_renderer.dart' show TextRenderer;\nexport 'src/text/renderers/text_renderer_factory.dart' show TextRendererFactory;\nexport 'src/text/styles/background_style.dart' show BackgroundStyle;\nexport 'src/text/styles/block_style.dart' show BlockStyle;\nexport 'src/text/styles/document_style.dart' show DocumentStyle;\nexport 'src/text/styles/flame_text_style.dart' show FlameTextStyle;\nexport 'src/text/styles/inline_text_style.dart' show InlineTextStyle;\n"
  },
  {
    "path": "packages/flame/lib/timer.dart",
    "content": "export 'src/timer.dart';\n"
  },
  {
    "path": "packages/flame/lib/widgets.dart",
    "content": "export 'src/anchor.dart';\nexport 'src/widgets/animation_widget.dart';\nexport 'src/widgets/components_notifier_builder.dart';\nexport 'src/widgets/nine_tile_box.dart';\nexport 'src/widgets/sprite_button.dart';\nexport 'src/widgets/sprite_widget.dart';\n"
  },
  {
    "path": "packages/flame/pubspec.yaml",
    "content": "name: flame\nresolution: workspace\ndescription: A minimalist Flutter game engine, provides a nice set of somewhat independent modules you can choose from.\nversion: 1.36.0\nhomepage: https://github.com/flame-engine/flame\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - game-engine\n  - games\n  - animations\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  collection: ^1.18.0\n  flutter:\n    sdk: flutter\n  meta: ^1.12.0\n  ordered_set: ^8.0.0\n  vector_math: ^2.1.4\n\ndev_dependencies:\n  benchmark_harness: ^2.3.1\n  canvas_test: ^0.2.0\n  dartdoc: ^9.0.0\n  flame_lint: ^1.4.3\n  flame_test: ^2.2.3\n  flutter_test:\n    sdk: flutter\n  mocktail: ^1.0.4\n  test: any\n\nscreenshots:\n  - description: The Flame logo.\n    path: screenshots/logo.png\n"
  },
  {
    "path": "packages/flame/test/_resources/chopper.json",
    "content": "{\n  \"frames\": {\n    \"chopper 0.aseprite\": {\n      \"frame\": {\n        \"x\": 0,\n        \"y\": 0,\n        \"w\": 48,\n        \"h\": 48\n      },\n      \"rotated\": false,\n      \"trimmed\": false,\n      \"spriteSourceSize\": {\n        \"x\": 0,\n        \"y\": 0,\n        \"w\": 48,\n        \"h\": 48\n      },\n      \"sourceSize\": {\n        \"w\": 48,\n        \"h\": 48\n      },\n      \"duration\": 100\n    },\n    \"chopper 1.aseprite\": {\n      \"frame\": {\n        \"x\": 48,\n        \"y\": 0,\n        \"w\": 48,\n        \"h\": 48\n      },\n      \"rotated\": false,\n      \"trimmed\": false,\n      \"spriteSourceSize\": {\n        \"x\": 0,\n        \"y\": 0,\n        \"w\": 48,\n        \"h\": 48\n      },\n      \"sourceSize\": {\n        \"w\": 48,\n        \"h\": 48\n      },\n      \"duration\": 100\n    },\n    \"chopper 2.aseprite\": {\n      \"frame\": {\n        \"x\": 96,\n        \"y\": 0,\n        \"w\": 48,\n        \"h\": 48\n      },\n      \"rotated\": false,\n      \"trimmed\": false,\n      \"spriteSourceSize\": {\n        \"x\": 0,\n        \"y\": 0,\n        \"w\": 48,\n        \"h\": 48\n      },\n      \"sourceSize\": {\n        \"w\": 48,\n        \"h\": 48\n      },\n      \"duration\": 100\n    },\n    \"chopper 3.aseprite\": {\n      \"frame\": {\n        \"x\": 144,\n        \"y\": 0,\n        \"w\": 48,\n        \"h\": 48\n      },\n      \"rotated\": false,\n      \"trimmed\": false,\n      \"spriteSourceSize\": {\n        \"x\": 0,\n        \"y\": 0,\n        \"w\": 48,\n        \"h\": 48\n      },\n      \"sourceSize\": {\n        \"w\": 48,\n        \"h\": 48\n      },\n      \"duration\": 100\n    }\n  },\n  \"meta\": {\n    \"app\": \"http://www.aseprite.org/\",\n    \"version\": \"1.3-dev\",\n    \"image\": \"/home/erick/projetos/gamedev/airplane-resource-pack/chopper.png\",\n    \"format\": \"RGBA8888\",\n    \"size\": {\n      \"w\": 192,\n      \"h\": 48\n    },\n    \"scale\": \"1\",\n    \"frameTags\": [\n    ],\n    \"layers\": [\n      {\n        \"name\": \"body\",\n        \"opacity\": 255,\n        \"blendMode\": \"normal\"\n      },\n      {\n        \"name\": \"rotor\",\n        \"opacity\": 255,\n        \"blendMode\": \"normal\"\n      },\n      {\n        \"name\": \"stabilizer\",\n        \"opacity\": 255,\n        \"blendMode\": \"normal\"\n      }\n    ],\n    \"slices\": [\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/_resources/custom_flame_game.dart",
    "content": "import 'package:flame/game.dart';\n\nclass CustomFlameGame extends FlameGame {\n  CustomFlameGame({\n    super.children,\n    Future<void>? Function(FlameGame)? onLoad,\n    void Function(FlameGame)? onMount,\n  }) : _onLoad = onLoad,\n       _onMount = onMount;\n\n  final Future<void>? Function(FlameGame)? _onLoad;\n  final void Function(FlameGame)? _onMount;\n\n  @override\n  Future<void>? onLoad() => _onLoad?.call(this);\n\n  @override\n  void onMount() => _onMount?.call(this);\n}\n"
  },
  {
    "path": "packages/flame/test/_resources/load_image.dart",
    "content": "import 'dart:io';\nimport 'dart:ui';\n\nFuture<Image> loadImage(String fileName) async {\n  final bytes = await File('test/_resources/$fileName').readAsBytes();\n  final codec = await instantiateImageCodec(bytes);\n  final frameInfo = await codec.getNextFrame();\n  return frameInfo.image;\n}\n"
  },
  {
    "path": "packages/flame/test/_resources/test_text_file.txt",
    "content": "This is sample text file for AssetsCache Unit testing."
  },
  {
    "path": "packages/flame/test/anchor_test.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/anchor.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('Anchor', () {\n    test('parses to and from string', () {\n      expect(Anchor.center.toString(), 'center');\n      expect(Anchor.valueOf('topRight'), Anchor.topRight);\n\n      expect(Anchor.values.length, 9);\n      for (final value in Anchor.values) {\n        final thereAndBack = Anchor.valueOf(value.toString());\n        expect(thereAndBack, value);\n      }\n    });\n\n    test('parses custom anchor', () {\n      const anchor = Anchor(0.2, 0.2);\n      expect(anchor.toString(), 'Anchor(0.2, 0.2)');\n      expect(Anchor.valueOf('Anchor(0.2, 0.2)'), anchor);\n    });\n\n    test('fail to parse invalid anchor', () {\n      expect(\n        () => Anchor.valueOf('foobar'),\n        failsAssert(),\n      );\n    });\n\n    test('can convert topLeft anchor to another anchor positions', () {\n      final position = Vector2(3, 1);\n      final size = Vector2(2, 3);\n      final center = Anchor.topLeft.toOtherAnchorPosition(\n        position,\n        Anchor.center,\n        size,\n      );\n      expect(center, position + size / 2);\n    });\n\n    test('can convert center anchor to another anchor positions', () {\n      final position = Vector2(3, 1);\n      final size = Vector2(2, 3);\n      final topLeft = Anchor.center.toOtherAnchorPosition(\n        position,\n        Anchor.topLeft,\n        size,\n      );\n      expect(topLeft, position - size / 2);\n    });\n\n    test('opposite anchor', () {\n      expect(Anchor.topCenter.opposite, Anchor.bottomCenter);\n      expect(Anchor.topLeft.opposite, Anchor.bottomRight);\n      expect(Anchor.center.opposite, Anchor.center);\n      expect(const Anchor(0.2, 0.3).opposite, const Anchor(0.8, 0.7));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/cache/assets_cache_test.dart",
    "content": "import 'package:flame/cache.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nimport '../fixtures/fixture_reader.dart';\n\nclass _MockAssetBundle extends Mock implements AssetBundle {}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('AssetsCache', () {\n    test('readFile', () async {\n      final assetsCache = AssetsCache(prefix: '');\n      final fileName = fixture('test_text_file.txt').path;\n      final file = await assetsCache.readFile(fileName);\n      expect(file, isA<String>());\n\n      expect(\n        () => assetsCache.readBinaryFile(fixture('test_text_file.txt').path),\n        failsAssert('\"$fileName\" was previously loaded as a text file'),\n      );\n    });\n\n    test('readJson', () async {\n      final assetsCache = AssetsCache(prefix: '');\n      final file = await assetsCache.readJson(fixture('chopper.json').path);\n      expect(file, isA<Map<String, dynamic>>());\n    });\n\n    test('readBinaryFile', () async {\n      final assetsCache = AssetsCache(prefix: '');\n      final fileName = fixture('cave_ace.fa').path;\n      final file = await assetsCache.readBinaryFile(fileName);\n      expect(file, isA<Uint8List>());\n\n      expect(\n        () => assetsCache.readFile(fixture('cave_ace.fa').path),\n        failsAssert('\"$fileName\" was previously loaded as a binary file'),\n      );\n    });\n\n    test('clear', () async {\n      final assetsCache = AssetsCache(prefix: '');\n\n      final fileName = fixture('test_text_file.txt').path;\n      final file = await assetsCache.readFile(fileName);\n      expect(file, isA<String>());\n\n      assetsCache.clear(fileName);\n      expect(assetsCache.cacheCount, equals(0));\n    });\n\n    test('clearCache', () async {\n      final assetsCache = AssetsCache(prefix: '');\n\n      final fileName = fixture('test_text_file.txt').path;\n      final file = await assetsCache.readFile(fileName);\n      expect(file, isA<String>());\n\n      assetsCache.clearCache();\n      expect(assetsCache.cacheCount, equals(0));\n    });\n\n    testWithFlameGame(\n      'prefix on assets can not be changed',\n      (game) async {\n        game.assets = AssetsCache();\n        expect(game.assets.prefix, 'assets/');\n      },\n    );\n\n    testWithFlameGame(\n      'Game.assets is same as Flame.assets',\n      (game) async {\n        expect(game.assets, equals(Flame.assets));\n      },\n    );\n\n    test('bundle can be overridden', () async {\n      final bundle = _MockAssetBundle();\n      when(() => bundle.loadString(any())).thenAnswer((_) async => 'Two ducks');\n\n      final cache = AssetsCache(bundle: bundle);\n\n      final result = await cache.readFile('duck_count');\n      expect(result, equals('Two ducks'));\n      verify(() => bundle.loadString('assets/duck_count')).called(1);\n    });\n\n    test('loads from a package', () async {\n      final bundle = _MockAssetBundle();\n      when(\n        () => bundle.loadString(any()),\n      ).thenAnswer((_) async => 'Three ducks');\n\n      final cache = AssetsCache(bundle: bundle);\n\n      final result = await cache.readFile('duck_count', package: 'my_pkg');\n      expect(result, equals('Three ducks'));\n      verify(\n        () => bundle.loadString('packages/my_pkg/assets/duck_count'),\n      ).called(1);\n    });\n\n    group('fromCache', () {\n      test('returns cached string asset', () async {\n        final assetsCache = AssetsCache(prefix: '');\n        final fileName = fixture('test_text_file.txt').path;\n\n        await assetsCache.readFile(fileName);\n\n        final result = assetsCache.fromCache<String>(fileName);\n        expect(\n          result,\n          equals(\n            'This is sample text file for AssetsCache Unit testing.',\n          ),\n        );\n      });\n\n      test('returns cached binary asset', () async {\n        final assetsCache = AssetsCache(prefix: '');\n        final fileName = fixture('cave_ace.fa').path;\n\n        await assetsCache.readBinaryFile(fileName);\n        final result = assetsCache.fromCache<Uint8List>(fileName);\n        expect(result, isA<Uint8List>());\n      });\n\n      test('returns cached json asset', () async {\n        final assetsCache = AssetsCache(prefix: '');\n        final fileName = fixture('chopper.json').path;\n        final file = await assetsCache.readJson(fileName);\n        expect(file, isA<Map<String, dynamic>>());\n\n        await assetsCache.readJson(fileName);\n        final result = assetsCache.fromCache<Map<String, dynamic>>(fileName);\n        expect(result, isA<Map<String, dynamic>>());\n      });\n\n      test('throws assertion when asset not in cache', () {\n        final assetsCache = AssetsCache(prefix: '');\n\n        expect(\n          () => assetsCache.fromCache<String>('nonexistent.txt'),\n          throwsA(isA<AssertionError>()),\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/cache/images_test.dart",
    "content": "import 'dart:convert';\nimport 'dart:ui';\n\nimport 'package:collection/collection.dart';\nimport 'package:flame/cache.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass _MockAssetBundle extends Mock implements AssetBundle {}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n  // A simple 1x1 pixel encoded as base64 - just so that we have something to\n  // load into the Images cache.\n  const pixel =\n      'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJA'\n      'AAAAXNSR0IArs4c6QAAAA1JREFUGFdjWP33/n8ACGUDhwieHSEAAAAASUVORK5CYII=';\n\n  group('Images', () {\n    test('can add a base64 image to the cache', () async {\n      final cache = Images();\n      await cache.addFromBase64Data('img', pixel);\n\n      expect(cache.fromCache('img'), isA<Image>());\n    });\n\n    test('load image', () async {\n      final cache = Images();\n      final image = await cache.fromBase64('img', pixel);\n      expect(image, isA<Image>());\n      expect(cache.fromCache('img'), image);\n    });\n\n    test('access non-existent image', () {\n      final cache = Images();\n      expect(\n        () => cache.fromCache('image'),\n        failsAssert(\n          'Tried to access an image \"image\" that does not exist in the cache. '\n          'Make sure to load() an image before accessing it',\n        ),\n      );\n    });\n\n    test('access non-yet-loaded image', () {\n      final cache = Images();\n      cache.fromBase64('image', pixel); // did not await\n      expect(\n        () => cache.fromCache('image'),\n        failsAssert(\n          'Tried to access an image \"image\" before it was loaded. '\n          'Make sure to await the future from load() before using this method',\n        ),\n      );\n    });\n\n    test('clear', () {\n      final cache = Images();\n      final image = _MockImage();\n      cache.add('test', image);\n      expect(image.disposedCount, 0);\n      cache.clear('test');\n      expect(image.disposedCount, 1);\n    });\n\n    test('clearCache', () {\n      final cache = Images();\n      final images = List.generate(10, (_) => _MockImage());\n      for (var i = 0; i < images.length; i++) {\n        cache.add(i.toString(), images[i]);\n      }\n      expect(images.map((image) => image.disposedCount).sum, 0);\n      cache.clearCache();\n      expect(images.map((image) => image.disposedCount).sum, images.length);\n    });\n\n    test('contains', () {\n      final cache = Images();\n      final images = List.generate(10, (_) => _MockImage());\n      for (var i = 0; i < images.length; i++) {\n        final key = i.toString();\n        cache.add(key, images[i]);\n        expect(cache.containsKey(key), isTrue);\n      }\n      cache.clearCache();\n      for (var i = 0; i < images.length; i++) {\n        expect(cache.containsKey(i.toString()), isFalse);\n      }\n    });\n\n    test('keys', () {\n      final cache = Images();\n      final images = List.generate(10, (_) => _MockImage());\n      for (var i = 0; i < images.length; i++) {\n        cache.add(i.toString(), images[i]);\n      }\n      expect(\n        cache.keys.toSet(),\n        {for (var i = 0; i < images.length; i++) i.toString()},\n      );\n    });\n\n    testWithFlameGame(\n      'prefix on game.images can be changed',\n      (game) async {\n        game.images = Images();\n        expect(game.images.prefix, 'assets/images/');\n        game.images.prefix = 'assets/pictures/';\n        expect(game.images.prefix, 'assets/pictures/');\n        game.images.prefix = '';\n        expect(game.images.prefix, '');\n      },\n    );\n\n    testWithFlameGame(\n      'Game.images is same as Flame.images',\n      (game) async {\n        expect(game.images, equals(Flame.images));\n\n        final img = _MockImage();\n        game.images.add('my image', img);\n        expect(Flame.images.containsKey('my image'), isTrue);\n        expect(Flame.images.keys, hasLength(1));\n\n        game.images = Images();\n        game.images.add('new image', img);\n        expect(Flame.images.containsKey('new image'), isFalse);\n      },\n    );\n\n    test('throws when setting an invalid prefix', () {\n      final images = Images();\n      expect(\n        () => images.prefix = 'foo',\n        failsAssert('Prefix must be empty or end with a \"/\"'),\n      );\n    });\n\n    test('.ready()', () async {\n      final images = Images();\n      images.fromBase64('image1', pixel);\n      images.fromBase64('image2', pixel);\n      expect(() => images.fromCache('image1'), failsAssert());\n      expect(() => images.fromCache('image2'), failsAssert());\n      await images.ready();\n      expect(images.fromCache('image1'), isNotNull);\n      expect(images.fromCache('image2'), isNotNull);\n    });\n\n    test('loads from a package', () async {\n      final bundle = _MockAssetBundle();\n      when(() => bundle.load(any())).thenAnswer(\n        (_) async {\n          final list = base64Decode(pixel.split(',').last);\n          return ByteData.view(list.buffer);\n        },\n      );\n\n      final images = Images(bundle: bundle);\n      await images.load('pixel.png', package: 'my_pkg');\n\n      verify(\n        () => bundle.load('packages/my_pkg/assets/images/pixel.png'),\n      ).called(1);\n    });\n\n    test('can have its bundle overridden', () async {\n      final bundle = _MockAssetBundle();\n      when(() => bundle.load(any())).thenAnswer(\n        (_) async {\n          final list = base64Decode(pixel.split(',').last);\n          return ByteData.view(list.buffer);\n        },\n      );\n\n      final images = Images(bundle: bundle);\n      final image = await images.load('pixel.png');\n\n      expect(image.width, equals(1));\n      expect(image.height, equals(1));\n\n      verify(() => bundle.load('assets/images/pixel.png')).called(1);\n    });\n  });\n}\n\nclass _MockImage extends Mock implements Image {\n  int disposedCount = 0;\n\n  @override\n  void dispose() {\n    disposedCount++;\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/cache/memory_cache_test.dart",
    "content": "import 'package:flame/cache.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('MemoryCache', () {\n    test('basic cache access', () {\n      final cache = MemoryCache<int, String>();\n      cache.setValue(0, 'bla');\n      expect(cache.getValue(0), 'bla');\n    });\n\n    test('containsKey', () {\n      final cache = MemoryCache<int, String>();\n      expect(cache.keys.length, 0);\n      cache.setValue(0, 'bla');\n      expect(cache.containsKey(0), true);\n      expect(cache.containsKey(1), false);\n      expect(cache.keys.length, 1);\n    });\n\n    test('updates key', () {\n      final cache = MemoryCache<int, String>();\n      cache.setValue(0, 'bla');\n      expect(cache.getValue(0), 'bla');\n      cache.setValue(0, 'ble');\n      expect(cache.getValue(0), 'ble');\n    });\n\n    test('cache size', () {\n      final cache = MemoryCache<int, String>(cacheSize: 1);\n      cache.setValue(0, 'bla');\n      cache.setValue(1, 'ble');\n      expect(cache.containsKey(0), false);\n      expect(cache.containsKey(1), true);\n      expect(cache.getValue(0), null);\n      expect(cache.getValue(1), 'ble');\n      expect(cache.size, 1);\n    });\n\n    test('clear', () {\n      final cache = MemoryCache<int, String>();\n      cache.setValue(0, 'bla');\n      expect(cache.containsKey(0), true);\n      expect(cache.size, 1);\n      cache.clear(0);\n      expect(cache.containsKey(0), false);\n      expect(cache.size, 0);\n    });\n\n    test('clearCache', () {\n      final cache = MemoryCache<int, String>();\n      cache.setValue(0, 'bla');\n      cache.setValue(1, 'ble');\n      expect(cache.containsKey(0), true);\n      expect(cache.containsKey(1), true);\n      expect(cache.size, 2);\n      cache.clearCache();\n      expect(cache.containsKey(0), false);\n      expect(cache.containsKey(1), false);\n      expect(cache.size, 0);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/camera/behaviors/bounded_position_behavior_test.dart",
    "content": "import 'package:flame/camera.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('BoundedPositionBehavior', () {\n    testWithFlameGame('target is the parent', (game) async {\n      final bounds = Rectangle.fromLTRB(0, 0, 200, 100);\n      final behavior = BoundedPositionBehavior(bounds: bounds);\n      final component = PositionComponent()\n        ..add(behavior)\n        ..addToParent(game);\n      await game.ready();\n\n      expect(behavior.target, component);\n      expect(behavior.bounds, bounds);\n      expect(behavior.precision, 0.5);\n\n      expect(component.position, Vector2(0, 0));\n      component.position.x -= 1;\n      game.update(0);\n      expect(component.position, Vector2(0, 0));\n      component.position.y += 3;\n      game.update(0);\n      expect(component.position, Vector2(0, 3));\n      component.position = Vector2(-1, 2);\n      game.update(0);\n      expect(component.position, Vector2(0, 2));\n    });\n\n    test('bad precision', () {\n      final shape = Circle(Vector2.zero(), 10);\n      expect(\n        () => BoundedPositionBehavior(bounds: shape, precision: 0),\n        failsAssert('Precision must be positive: 0.0'),\n      );\n    });\n\n    test('update bounds while target is null', () {\n      final circle1 = Circle(Vector2.zero(), 10);\n      final circle2 = Circle(Vector2.all(30), 10);\n      final boundedPositionBehavior = BoundedPositionBehavior(bounds: circle1);\n      expect(() => boundedPositionBehavior.bounds = circle2, returnsNormally);\n    });\n\n    testWithFlameGame('bad parent', (game) async {\n      final shape = Circle(Vector2.zero(), 10);\n      final parent = Component()..addToParent(game);\n      await game.ready();\n      parent.add(BoundedPositionBehavior(bounds: shape));\n      expect(\n        () => game.update(0),\n        failsAssert('Can only apply this behavior to a PositionProvider'),\n      );\n    });\n\n    testWithFlameGame('adjust target position on mount', (game) async {\n      final shape = Circle(Vector2.zero(), 10);\n      final target = PositionComponent(position: Vector2(100, 0));\n      game.add(target);\n      target.add(BoundedPositionBehavior(bounds: shape));\n      await game.ready();\n      expect(target.position, closeToVector(Vector2(10, 0), 0.5));\n    });\n\n    testWithFlameGame('adjust target position on shape change', (game) async {\n      final shape = Circle(Vector2.zero(), 10);\n      final target = PositionComponent(position: Vector2(10, 0));\n      final behavior = BoundedPositionBehavior(bounds: shape, precision: 0.1);\n      game.add(target);\n      target.add(behavior);\n      await game.ready();\n      expect(target.position, Vector2(10, 0));\n\n      behavior.bounds = Circle(Vector2.zero(), 5);\n      expect((behavior.bounds as Circle).radius, 5);\n      expect(target.position, closeToVector(Vector2(5, 0), 0.1));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/camera/behaviors/follow_behavior_test.dart",
    "content": "import 'package:flame/camera.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/src/effects/provider_interfaces.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('FollowBehavior', () {\n    test('basic properties', () {\n      final target = PositionComponent();\n      final owner = PositionComponent();\n      final behavior = FollowBehavior(target: target, owner: owner);\n      expect(behavior.target, target);\n      expect(behavior.owner, owner);\n      expect(behavior.maxSpeed, double.infinity);\n      expect(behavior.horizontalOnly, false);\n      expect(behavior.verticalOnly, false);\n    });\n\n    test('errors', () {\n      expect(\n        () => FollowBehavior(target: PositionComponent(), maxSpeed: 0),\n        failsAssert('maxSpeed must be positive: 0.0'),\n      );\n      expect(\n        () => FollowBehavior(target: PositionComponent(), maxSpeed: -2.45),\n        failsAssert('maxSpeed must be positive: -2.45'),\n      );\n      expect(\n        () => FollowBehavior(\n          target: PositionComponent(),\n          horizontalOnly: true,\n          verticalOnly: true,\n        ),\n        failsAssert(\n          'The behavior cannot be both horizontalOnly and verticalOnly',\n        ),\n      );\n    });\n\n    testWithFlameGame('parent is not position provider', (game) async {\n      final target = PositionComponent()..addToParent(game);\n      final component = Component()..addToParent(game);\n      await game.ready();\n\n      expect(\n        () async {\n          component.add(FollowBehavior(target: target));\n          await game.ready();\n        },\n        failsAssert('Can only apply this behavior to a PositionProvider'),\n      );\n    });\n\n    testWithFlameGame('custom position provider', (game) async {\n      final target = PositionComponent()\n        ..position = Vector2(3, 100)\n        ..addToParent(game);\n      final component = Component()..addToParent(game);\n      await game.ready();\n\n      final followTarget = Vector2(3, 1);\n      component.add(\n        FollowBehavior(\n          target: target,\n          owner: PositionProviderImpl(\n            getValue: () => followTarget,\n            setValue: followTarget.setFrom,\n          ),\n          maxSpeed: 1,\n        ),\n      );\n      await game.ready();\n\n      const dt = 0.11;\n      for (var i = 0; i < 20; i++) {\n        final value = Vector2(3, 1 + i * dt);\n        final tolerance = toleranceVector2Float32(value);\n        expect(followTarget, closeToVector(value, tolerance));\n        game.update(dt);\n      }\n    });\n\n    testWithFlameGame('simple follow', (game) async {\n      final target = PositionComponent()..addToParent(game);\n      final pursuer = PositionComponent()\n        ..add(FollowBehavior(target: target))\n        ..addToParent(game);\n      await game.ready();\n\n      for (var i = 0; i < 10; i++) {\n        target.position = Vector2.random()..scale(1000);\n        // The key here is to make sure you check the tolerance of the pursuer\n        // position before the update, because the error may be higher if e.g.\n        // the pursuer was at a location with a large value.\n        final tolerancePursuer = toleranceVector2Float32(pursuer.position);\n        final toleranceTarget = toleranceVector2Float32(target.position);\n        game.update(0.01);\n        final tolerance = tolerancePursuer + toleranceTarget;\n        expect(\n          pursuer.position,\n          closeToVector(target.position, tolerance),\n        );\n      }\n    });\n\n    testWithFlameGame('follow with max speed', (game) async {\n      const dt = 0.013;\n      const speed = 587.0;\n      final target = PositionComponent()\n        ..position = Vector2(600, 800)\n        ..addToParent(game);\n      final pursuer = PositionComponent()\n        ..add(FollowBehavior(target: target, maxSpeed: speed))\n        ..addToParent(game);\n      await game.ready();\n      var tolerance = toleranceVector2Float32(pursuer.position);\n      for (var i = 0; i < 100; i++) {\n        final distance = speed * i * dt;\n        final value = Vector2(distance * 0.6, distance * 0.8);\n        tolerance += toleranceVector2Float32(value);\n        expect(\n          pursuer.position,\n          closeToVector(value, tolerance),\n        );\n        game.update(dt);\n      }\n    });\n\n    testWithFlameGame('horizontal-only follow', (game) async {\n      final target = PositionComponent(position: Vector2(20, 10));\n      final pursuer = PositionComponent();\n      pursuer.add(\n        FollowBehavior(target: target, horizontalOnly: true, maxSpeed: 1),\n      );\n      game.addAll([target, pursuer]);\n      await game.ready();\n\n      for (var i = 0; i < 10; i++) {\n        expect(pursuer.position.x, i);\n        expect(pursuer.position.y, 0);\n        game.update(1);\n      }\n    });\n\n    testWithFlameGame('vertical-only follow', (game) async {\n      final target = PositionComponent(position: Vector2(20, 100));\n      final pursuer = PositionComponent();\n      pursuer.add(\n        FollowBehavior(target: target, verticalOnly: true, maxSpeed: 1),\n      );\n      game.addAll([target, pursuer]);\n      await game.ready();\n\n      for (var i = 0; i < 10; i++) {\n        expect(pursuer.position.x, 0);\n        expect(pursuer.position.y, i);\n        game.update(1);\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/camera/behaviors/viewport_aware_bounds_behavior_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/camera.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:vector_math/vector_math.dart';\n\nvoid main() {\n  group('ViewportAwareBoundsBehavior', () {\n    testWithFlameGame('setBounds wrt Rectangle', (game) async {\n      final world = World()..addToParent(game);\n      final camera = CameraComponent.withFixedResolution(\n        width: 320,\n        height: 240,\n        world: world,\n      )..addToParent(game);\n      await game.ready();\n      final bounds = Rectangle.fromLTRB(0, 0, 640, 480);\n\n      // With considerViewport = false\n      camera.setBounds(bounds);\n      game.update(0);\n      expect(\n        (_getBounds(camera) as Rectangle?)?.toRect(),\n        Rectangle.fromLTRB(0, 0, 640, 480).toRect(),\n        reason: 'Camera bounds at unexpected location',\n      );\n\n      expect(camera.viewfinder.considerViewport, false);\n\n      // With considerViewport = true\n      camera.setBounds(bounds, considerViewport: true);\n      game.update(0);\n      expect(\n        (_getBounds(camera) as Rectangle?)?.toRect(),\n        Rectangle.fromLTRB(160, 120, 480, 360).toRect(),\n        reason: 'Camera bounds did not consider viewport',\n      );\n\n      expect(camera.viewfinder.considerViewport, true);\n    });\n\n    testWithFlameGame('setBounds wrt RoundedRectangle', (game) async {\n      final world = World()..addToParent(game);\n      final camera = CameraComponent.withFixedResolution(\n        width: 320,\n        height: 240,\n        world: world,\n      )..addToParent(game);\n      await game.ready();\n      final bounds = RoundedRectangle.fromLTRBR(0, 0, 640, 480, 32);\n\n      // With considerViewport = false\n      camera.setBounds(bounds);\n      game.update(0);\n      expect(\n        (_getBounds(camera) as RoundedRectangle?)?.asRRect(),\n        RoundedRectangle.fromLTRBR(0, 0, 640, 480, 32).asRRect(),\n        reason: 'Camera bounds at unexpected location',\n      );\n\n      expect(camera.viewfinder.considerViewport, false);\n\n      // With considerViewport = true\n      camera.setBounds(bounds, considerViewport: true);\n      game.update(0);\n      // Note that floating point drift occurs, so we account for\n      // this error threshold epsilon `E`.\n      const E = 0.03; // +/-3%\n      final camRRect = (_getBounds(camera) as RoundedRectangle?)?.asRRect();\n      final expectedRRect = RoundedRectangle.fromLTRBR(\n        163.2,\n        126.4,\n        476.8,\n        353.6,\n        32,\n      ).asRRect();\n      expect(\n        _epsilonRRectEqualityCheck(camRRect!, expectedRRect, E),\n        true,\n        reason: 'Camera bounds did not consider viewport',\n      );\n\n      expect(camera.viewfinder.considerViewport, true);\n    });\n\n    testWithFlameGame('setBounds wrt Circle', (game) async {\n      final world = World()..addToParent(game);\n      final camera = CameraComponent.withFixedResolution(\n        width: 320,\n        height: 240,\n        world: world,\n      )..addToParent(game);\n      await game.ready();\n      final bounds = Circle(Vector2(320, 240), 320);\n\n      // With considerViewport = false\n      camera.setBounds(bounds);\n      game.update(0);\n      expect(\n        (_getBounds(camera) as Circle?)?.center,\n        Vector2(320, 240),\n        reason: 'Camera bounds at unexpected location (considerViewport=false)',\n      );\n\n      expect(camera.viewfinder.considerViewport, false);\n\n      // With considerViewport = true\n      camera.setBounds(bounds, considerViewport: true);\n      game.update(0);\n      expect(\n        (_getBounds(camera) as Circle?)?.center,\n        Vector2(320, 240),\n        reason: 'Camera bounds at unexpected location (considerViewport=true)',\n      );\n\n      // Check bounds after moving away from the center\n      // while considerViewport = true\n      camera\n        ..setBounds(bounds, considerViewport: true)\n        ..moveBy(Vector2(-320, 0));\n      game.update(0);\n      expect(\n        (_getBounds(camera) as Circle?)?.center,\n        Vector2(320, 240),\n        reason: 'Camera bounds did not consider viewport after move',\n      );\n\n      expect(camera.viewfinder.considerViewport, true);\n    });\n\n    testWithFlameGame('setBounds explicit null Shape request', (game) async {\n      final world = World()..addToParent(game);\n      final camera = CameraComponent.withFixedResolution(\n        width: 320,\n        height: 240,\n        world: world,\n      )..addToParent(game);\n      await game.ready();\n      final bounds = Circle(Vector2(320, 240), 320);\n\n      camera.setBounds(bounds);\n      game.update(0);\n      expect(\n        _getBounds(camera) as Circle?,\n        isNotNull,\n        reason: 'Camera bounds was null but expected a non-null Circle',\n      );\n\n      camera.setBounds(null);\n      game.update(0);\n      expect(\n        _getBounds(camera) as Circle?,\n        isNull,\n        reason:\n            'Camera bounds expected to be null from side-effect of removing it',\n      );\n    });\n  });\n}\n\nShape? _getBounds(CameraComponent camera) =>\n    camera.viewfinder.firstChild<BoundedPositionBehavior>()?.bounds;\n\nbool _epsilonRRectEqualityCheck(RRect a, RRect b, double epsilon) {\n  return (a.left - b.left).abs() <= epsilon &&\n      (a.top - b.top).abs() <= epsilon &&\n      (a.right - b.right).abs() <= epsilon &&\n      (a.bottom - b.bottom).abs() <= epsilon &&\n      (a.tlRadiusX - b.tlRadiusX) == 0 &&\n      (a.tlRadiusY - b.tlRadiusY) == 0 &&\n      (a.trRadiusX - b.trRadiusX) == 0 &&\n      (a.trRadiusY - b.trRadiusY) == 0 &&\n      (a.blRadiusX - b.blRadiusX) == 0 &&\n      (a.blRadiusY - b.blRadiusY) == 0 &&\n      (a.brRadiusX - b.brRadiusX) == 0 &&\n      (a.brRadiusY - b.brRadiusY) == 0;\n}\n"
  },
  {
    "path": "packages/flame/test/camera/camera_component_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/camera.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/post_process.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'camera_test_helpers.dart';\n\nvoid main() {\n  group('CameraComponent', () {\n    testGolden(\n      'CameraComponent.withFixedResolution',\n      (game, tester) async {\n        final world = World()\n          ..addAll([\n            _SolidBackground(const Color(0xFF333333)),\n            RectangleComponent(\n              anchor: Anchor.center,\n              position: Vector2.zero(),\n              size: Vector2(480, 180),\n              paint: Paint()\n                ..style = PaintingStyle.stroke\n                ..strokeWidth = 2\n                ..color = const Color(0xFF00FF33),\n            ),\n            RectangleComponent(\n              anchor: Anchor.center,\n              size: Vector2.all(50),\n              paint: Paint()..color = const Color(0xFFFFEEDD),\n            ),\n          ])\n          ..addToParent(game);\n        CameraComponent.withFixedResolution(\n          world: world,\n          width: 500,\n          height: 200,\n        ).addToParent(game);\n      },\n      goldenFile: '../_goldens/camera_component_test1.png',\n      size: Vector2(200, 150),\n    );\n\n    testWithFlameGame('simple camera follow', (game) async {\n      final world = World()..addToParent(game);\n      final camera = CameraComponent(world: world)..addToParent(game);\n      final player = PositionComponent()..addToParent(world);\n      camera.follow(player);\n      await game.ready();\n\n      expect(camera.viewfinder.children.length, 1);\n      expect(camera.viewfinder.children.first, isA<FollowBehavior>());\n      for (var i = 0; i < 20; i++) {\n        player.position.add(Vector2(i * 5.0, 20.0 - i));\n        game.update(0.01);\n        expect(camera.viewfinder.position, closeToVector(player.position));\n      }\n    });\n\n    testWithFlameGame('camera should be able to retarget follow', (game) async {\n      // Creating new camera as the one included with game is not mounted and\n      // will therefore not be queued.\n      final camera = CameraComponent(world: game.world)..addToParent(game);\n      final player = PositionComponent()..addToParent(game.world);\n      final player2 = PositionComponent()..addToParent(game.world);\n      camera.follow(player);\n      camera.follow(player2);\n      await game.ready();\n\n      expect(camera.viewfinder.children.length, 1);\n      expect(camera.viewfinder.children.first, isA<FollowBehavior>());\n    });\n\n    testWithFlameGame('follow with snap', (game) async {\n      final world = World()..addToParent(game);\n      final player = PositionComponent()\n        ..position = Vector2(100, 100)\n        ..addToParent(world);\n      final camera = CameraComponent(world: world)\n        ..follow(player, maxSpeed: 1, snap: true)\n        ..addToParent(game);\n      await game.ready();\n\n      expect(camera.viewfinder.position, Vector2(100, 100));\n    });\n\n    testWithFlameGame('moveTo', (game) async {\n      final world = World()..addToParent(game);\n      final camera = CameraComponent(world: world)..addToParent(game);\n      await game.ready();\n\n      final point = Vector2(1000, 2000);\n      camera.moveTo(point);\n      game.update(0);\n      expect(camera.viewfinder.position, Vector2(1000, 2000));\n      // updating [point] doesn't affect the camera's target\n      point.x = 0;\n      game.update(1);\n      expect(camera.viewfinder.position, Vector2(1000, 2000));\n    });\n\n    testWithFlameGame('moveTo x 2', (game) async {\n      final world = World()..addToParent(game);\n      final camera = CameraComponent(world: world)..addToParent(game);\n      await game.ready();\n\n      camera.moveTo(Vector2(100, 0), speed: 5);\n      for (var i = 0; i < 10; i++) {\n        expect(camera.viewfinder.position, closeToVector(Vector2(0.5 * i, 0)));\n        game.update(0.1);\n      }\n      camera.moveTo(Vector2(5, 200), speed: 10);\n      for (var i = 0; i < 10; i++) {\n        expect(camera.viewfinder.position, closeToVector(Vector2(5, 1.0 * i)));\n        game.update(0.1);\n      }\n      expect(camera.viewfinder.children.length, 1);\n    });\n\n    testWithFlameGame('moveBy', (game) async {\n      final world = World()..addToParent(game);\n      final camera = CameraComponent(world: world)..addToParent(game);\n      await game.ready();\n\n      final point = Vector2(100, 200);\n      camera.moveBy(point);\n      game.update(1);\n      expect(camera.viewfinder.position, Vector2(100, 200));\n      // updating [point] doesn't affect the offset.\n      point.x = 0;\n      game.update(1);\n      expect(camera.viewfinder.position, Vector2(100, 200));\n    });\n\n    testWithFlameGame('moveBy x 2', (game) async {\n      final world = World()..addToParent(game);\n      final camera = CameraComponent(world: world)..addToParent(game);\n      await game.ready();\n\n      camera.moveBy(Vector2(100, 0), speed: 5);\n      for (var i = 0; i < 10; i++) {\n        expect(camera.viewfinder.position, closeToVector(Vector2(0.5 * i, 0)));\n        game.update(0.1);\n      }\n      camera.moveTo(Vector2(5, 200), speed: 10);\n      for (var i = 0; i < 10; i++) {\n        expect(camera.viewfinder.position, closeToVector(Vector2(5, 1.0 * i)));\n        game.update(0.1);\n      }\n      expect(camera.viewfinder.children.length, 1);\n    });\n\n    testWithFlameGame('setBounds', (game) async {\n      final world = World()..addToParent(game);\n      final camera = CameraComponent(world: world)..addToParent(game);\n      await game.ready();\n\n      camera.setBounds(Rectangle.fromLTRB(0, 0, 400, 50));\n      camera.viewfinder.position = Vector2(10, 10);\n      game.update(0);\n      expect(camera.viewfinder.position, Vector2(10, 10));\n      camera.viewfinder.position = Vector2(-10, 10);\n      game.update(0);\n      expect(camera.viewfinder.position, closeToVector(Vector2(0, 10), 0.5));\n\n      camera.moveTo(Vector2(-20, 0), speed: 10);\n      for (var i = 0; i < 20; i++) {\n        expect(\n          camera.viewfinder.position,\n          closeToVector(Vector2(0, 10 - i * 0.45), 0.5),\n        );\n        game.update(0.1);\n      }\n\n      expect(\n        camera.viewfinder.firstChild<BoundedPositionBehavior>(),\n        isNotNull,\n      );\n      expect(\n        camera.viewfinder.firstChild<BoundedPositionBehavior>()!.bounds,\n        isA<Rectangle>(),\n      );\n      camera.setBounds(Circle(Vector2.zero(), 100));\n      expect(\n        camera.viewfinder.firstChild<BoundedPositionBehavior>()!.bounds,\n        isA<Circle>(),\n      );\n      camera.setBounds(null);\n      game.update(0);\n      expect(\n        camera.viewfinder.firstChild<BoundedPositionBehavior>(),\n        isNull,\n      );\n    });\n\n    testWithFlameGame('componentsAtPoint', (game) async {\n      game.camera.viewport = FixedSizeViewport(600, 400)\n        ..anchor = Anchor.center\n        ..position = Vector2(400, 300)\n        ..priority = -1;\n      game.camera.viewfinder.position = Vector2(100, 50);\n      final component = PositionComponent(\n        size: Vector2(300, 100),\n        position: Vector2(50, 30),\n      );\n      final world = game.world;\n      final camera = game.camera;\n      world.add(component);\n      await game.ready();\n\n      final nested = <Vector2>[];\n      final it = game.componentsAtPoint(Vector2(400, 300), nested).iterator;\n      expect(it.moveNext(), true);\n      expect(it.current, camera.viewport);\n      expect(nested, [Vector2(400, 300), Vector2(300, 200)]);\n      expect(it.moveNext(), true);\n      expect(it.current, component);\n      expect(nested, [Vector2(400, 300), Vector2(100, 50), Vector2(50, 20)]);\n      expect(it.moveNext(), true);\n      expect(it.current, world);\n      expect(nested, [Vector2(400, 300), Vector2(100, 50)]);\n      expect(it.moveNext(), true);\n      expect(it.current, game);\n      expect(nested, [Vector2(400, 300)]);\n      expect(it.moveNext(), false);\n\n      // Check that `componentsAtPoint` is usable with non-top-level components\n      // also.\n      expect(\n        world.componentsAtPoint(Vector2(100, 100)).toList(),\n        [component, world],\n      );\n      expect(\n        component.componentsAtPoint(Vector2(100, 50)).toList(),\n        [component],\n      );\n    });\n\n    testWithFlameGame('visibleWorldRect', (game) async {\n      await game.ready();\n      final camera = game.camera;\n      game.onGameResize(Vector2(60, 40));\n\n      // By default, the viewfinder's position is (0,0), and its anchor is in\n      // the center of the viewport.\n      expect(camera.visibleWorldRect, const Rect.fromLTRB(-30, -20, 30, 20));\n\n      camera.viewfinder.position = Vector2(100, 200);\n      expect(camera.visibleWorldRect, const Rect.fromLTRB(70, 180, 130, 220));\n\n      camera.viewfinder.zoom = 2;\n      camera.viewfinder.position = Vector2(20, 30);\n      expect(camera.visibleWorldRect, const Rect.fromLTRB(5, 20, 35, 40));\n\n      camera.viewport.size = Vector2(100, 60);\n      expect(camera.visibleWorldRect, const Rect.fromLTRB(-5, 15, 45, 45));\n\n      camera.viewfinder.position = Vector2.zero();\n      expect(camera.visibleWorldRect, const Rect.fromLTRB(-25, -15, 25, 15));\n\n      // Rotation angle: cos(a) = 0.6, sin(a) = 0.8\n      // Each point (x, y) becomes (x*cos(a) - y*sin(a), x*sin(a) + y*cos(a)),\n      // and each of the 4 corners turns into\n      //   (25, 15) -> (3, 29)\n      //   (25, -15) -> (27, 11)\n      //   (-25, -15) -> (-3, -29)\n      //   (-25, 15) -> (-27, -11)\n      // which means the culling rect is (-27, -29, 27, 29)\n      camera.viewfinder.angle = acos(0.6);\n      expect(camera.visibleWorldRect, const Rect.fromLTRB(-27, -29, 27, 29));\n    });\n\n    testWithFlameGame('visibleWorldRect with FixedSizeViewport', (game) async {\n      final world = World();\n      final camera = CameraComponent(\n        world: world,\n        viewport: FixedSizeViewport(60, 40),\n      );\n      game.addAll([world, camera]);\n      await game.ready();\n\n      // By default, the viewfinder's position is (0,0), and its anchor is in\n      // the center of the viewport.\n      expect(camera.visibleWorldRect, const Rect.fromLTRB(-30, -20, 30, 20));\n\n      camera.viewfinder.position = Vector2(100, 200);\n      expect(camera.visibleWorldRect, const Rect.fromLTRB(70, 180, 130, 220));\n\n      camera.viewfinder.zoom = 2;\n      camera.viewfinder.position = Vector2(20, 30);\n      expect(camera.visibleWorldRect, const Rect.fromLTRB(5, 20, 35, 40));\n\n      camera.viewport.size = Vector2(100, 60);\n      expect(camera.visibleWorldRect, const Rect.fromLTRB(-5, 15, 45, 45));\n\n      camera.viewfinder.position = Vector2.zero();\n      expect(camera.visibleWorldRect, const Rect.fromLTRB(-25, -15, 25, 15));\n\n      // Rotation angle: cos(a) = 0.6, sin(a) = 0.8\n      // Each point (x, y) becomes (x*cos(a) - y*sin(a), x*sin(a) + y*cos(a)),\n      // and each of the 4 corners turns into\n      //   (25, 15) -> (3, 29)\n      //   (25, -15) -> (27, 11)\n      //   (-25, -15) -> (-3, -29)\n      //   (-25, 15) -> (-27, -11)\n      // which means the culling rect is (-27, -29, 27, 29)\n      camera.viewfinder.angle = acos(0.6);\n      expect(camera.visibleWorldRect, const Rect.fromLTRB(-27, -29, 27, 29));\n    });\n\n    testWithFlameGame('visibleWorldRect accessed too early', (game) async {\n      final world = World();\n      final camera = CameraComponent(\n        world: world,\n        viewport: FixedSizeViewport(60, 40),\n      );\n\n      expect(\n        () => camera.visibleWorldRect,\n        failsAssert(),\n      );\n    });\n\n    testWithFlameGame('component is in view for the camera', (game) async {\n      final component = PositionComponent(\n        size: Vector2(10, 10),\n        position: Vector2(0, 0),\n      );\n      final world = World(children: [component]);\n      final camera = CameraComponent(\n        world: world,\n        viewport: FixedSizeViewport(60, 40),\n      );\n      game.addAll([world, camera]);\n      await game.ready();\n\n      expect(camera.canSee(component), isTrue);\n    });\n\n    testWithFlameGame('component is out of view for the camera', (game) async {\n      final component = PositionComponent(\n        size: Vector2(10, 10),\n        position: Vector2(100, 100),\n      );\n      final world = World(children: [component]);\n      final camera = CameraComponent(\n        world: world,\n        viewport: FixedSizeViewport(60, 40),\n      );\n      game.addAll([world, camera]);\n      await game.ready();\n\n      expect(camera.canSee(component), isFalse);\n    });\n\n    testGolden(\n      'Correct order of rendering',\n      (game, tester) async {\n        final world = World();\n        final camera = CameraComponent(world: world);\n        game.addAll([world, camera]);\n        camera.viewfinder.position = Vector2.all(4);\n        camera.backdrop.add(\n          CrossHair(\n            size: Vector2.all(28),\n            position: camera.viewport.size / 2 + Vector2.all(4),\n            color: Colors.teal,\n          ),\n        );\n        camera.viewfinder.add(\n          CrossHair(\n            size: Vector2.all(20),\n            position: Vector2(-2, 4),\n            color: Colors.white,\n          ),\n        );\n        world.add(\n          CrossHair(size: Vector2.all(14), color: Colors.green),\n        );\n        camera.viewport.add(\n          CrossHair(\n            size: Vector2.all(8),\n            position: camera.viewport.size / 2 + Vector2(4, -4),\n            color: Colors.red,\n          ),\n        );\n      },\n      goldenFile: '../_goldens/camera_component_order_test.png',\n      size: Vector2(50, 50),\n    );\n  });\n\n  testGolden(\n    'Correct scale of rendering',\n    (game, tester) async {\n      final world = World();\n      final resolution = Vector2(40, 60);\n      final camera = CameraComponent.withFixedResolution(\n        world: world,\n        width: resolution.x,\n        height: resolution.y,\n      );\n      game.addAll([world, camera]);\n      camera.viewfinder.position = Vector2.all(4);\n      camera.backdrop.add(\n        CrossHair(\n          size: Vector2.all(28),\n          position: resolution / 2 + Vector2.all(4),\n          color: Colors.teal,\n        ),\n      );\n      camera.viewfinder.add(\n        CrossHair(\n          size: Vector2.all(20),\n          position: Vector2(0, 2),\n          color: Colors.white,\n        ),\n      );\n      world.add(\n        CrossHair(size: Vector2.all(14), color: Colors.green),\n      );\n      camera.viewport.add(\n        CrossHair(\n          size: Vector2.all(8),\n          position: resolution / 2 + Vector2(2, -2),\n          color: Colors.red,\n        ),\n      );\n    },\n    goldenFile: '../_goldens/camera_component_fixed_resolution_order_test.png',\n    size: Vector2(50, 50),\n  );\n\n  testGolden(\n    'Correct scale of rendering after zoom',\n    (game, tester) async {\n      final world = World();\n      final resolution = Vector2(40, 60);\n      final camera = CameraComponent.withFixedResolution(\n        world: world,\n        width: resolution.x,\n        height: resolution.y,\n      );\n      game.addAll([world, camera]);\n      camera.viewfinder.position = Vector2.all(4);\n      camera.viewfinder.zoom = 1.5;\n      camera.backdrop.add(\n        CrossHair(\n          size: Vector2.all(28),\n          position: resolution / 2 + Vector2.all(4),\n          color: Colors.teal,\n        ),\n      );\n      camera.viewfinder.add(\n        CrossHair(\n          size: Vector2.all(20),\n          position: Vector2(-2, 4),\n          color: Colors.white,\n        ),\n      );\n      world.add(\n        CrossHair(size: Vector2.all(14), color: Colors.green),\n      );\n      camera.viewport.add(\n        CrossHair(\n          size: Vector2.all(8),\n          position: resolution / 2 + Vector2(4, -4),\n          color: Colors.red,\n        ),\n      );\n    },\n    goldenFile:\n        '../_goldens/camera_component_fixed_resolution_order_zoom_test.png',\n    size: Vector2(50, 50),\n  );\n\n  group('CameraComponent.canSee', () {\n    testWithFlameGame('null world', (game) async {\n      final player = PositionComponent();\n      final world = World(children: [player]);\n      final camera = CameraComponent();\n\n      await game.addAll([camera, world]);\n      await game.ready();\n      expect(camera.canSee(player), false);\n\n      camera.world = world;\n      expect(camera.canSee(player), true);\n    });\n\n    testWithFlameGame('unmounted world', (game) async {\n      final player = PositionComponent();\n      final world = World(children: [player]);\n      final camera = CameraComponent(world: world);\n\n      await game.addAll([camera]);\n      await game.ready();\n      expect(camera.canSee(player), false);\n\n      await game.add(world);\n      await game.ready();\n      expect(camera.canSee(player), true);\n    });\n\n    testWithFlameGame('unmounted component', (game) async {\n      final player = PositionComponent();\n      final world = World();\n      final camera = CameraComponent(world: world);\n\n      await game.addAll([camera, world]);\n      await game.ready();\n      expect(camera.canSee(player), false);\n\n      await world.add(player);\n      await game.ready();\n      expect(camera.canSee(player), true);\n    });\n\n    testWithFlameGame('component from another world', (game) async {\n      final player = PositionComponent();\n      final world1 = World(children: [player]);\n      final world2 = World();\n      final camera = CameraComponent(world: world2);\n\n      await game.addAll([camera, world1, world2]);\n      await game.ready();\n\n      // can see when player world is not known.\n      expect(camera.canSee(player), true);\n\n      // can't see when the player world is known.\n      expect(camera.canSee(player, componentWorld: world1), false);\n    });\n\n    testWithFlameGame('coordinate transformations', (game) async {\n      game.onGameResize(Vector2.all(1000.0));\n\n      final size = Vector2.all(100.0);\n      final world = World();\n      final camera = CameraComponent.withFixedResolution(\n        width: size.x,\n        height: size.y,\n        world: world,\n      );\n\n      await game.addAll([camera, world]);\n      await game.ready();\n\n      camera.moveBy(size / 2);\n      game.update(0);\n\n      expect(camera.globalToLocal(Vector2.zero()), Vector2.zero());\n      expect(camera.globalToLocal(Vector2.all(100.0)), Vector2.all(10.0));\n      expect(camera.globalToLocal(Vector2.all(500.0)), Vector2.all(50.0));\n      expect(camera.globalToLocal(Vector2.all(1000.0)), Vector2.all(100.0));\n\n      expect(camera.localToGlobal(Vector2.zero()), Vector2.zero());\n      expect(camera.localToGlobal(Vector2.all(10.0)), Vector2.all(100.0));\n      expect(camera.localToGlobal(Vector2.all(50.0)), Vector2.all(500.0));\n      expect(camera.localToGlobal(Vector2.all(100.0)), Vector2.all(1000.0));\n    });\n\n    testWithFlameGame('postProcess can be changed', (game) async {\n      game.camera.postProcess = _PostProcessChecker();\n      await game.ready();\n      expect(\n        () => game.camera.postProcess = _PostProcessChecker(),\n        returnsNormally,\n      );\n    });\n\n    testWithFlameGame('postProcess onLoad is called', (game) async {\n      game.camera.postProcess = _PostProcessChecker();\n      await game.ready();\n      expect(\n        (game.camera.postProcess! as _PostProcessChecker).isLoaded,\n        isTrue,\n      );\n    });\n\n    testWithFlameGame('multiple postProcess add and remove ', (game) async {\n      final postProcessA = _PostProcessChecker();\n      final postProcessB = _PostProcessChecker();\n      final postProcessC = _PostProcessChecker();\n      game.camera.postProcess = postProcessA;\n      game.camera.postProcess = postProcessB;\n      game.camera.postProcess = postProcessC;\n      await game.ready();\n      expect(game.camera.postProcess, postProcessC);\n\n      game.camera.postProcess = postProcessA;\n      game.camera.postProcess = postProcessB;\n      game.camera.postProcess = postProcessA;\n      await game.ready();\n      expect(game.camera.postProcess, postProcessA);\n\n      game.camera.postProcess = postProcessB;\n      game.camera.postProcess = null;\n      await game.ready();\n      expect(game.camera.postProcess, isNull);\n    });\n  });\n}\n\nclass _SolidBackground extends Component with HasPaint {\n  _SolidBackground(this.color);\n  final Color color;\n  @override\n  void render(Canvas canvas) => canvas.drawColor(color, BlendMode.src);\n}\n\nclass _PostProcessChecker extends PostProcess {\n  bool isLoaded = false;\n\n  @override\n  void onLoad() {\n    isLoaded = true;\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/camera/camera_test_helpers.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\n\nclass CrossHair extends PositionComponent {\n  CrossHair({super.size, super.position, this.color = const Color(0xFFFF0000)})\n    : super(anchor: Anchor.center);\n\n  final Color color;\n  Paint get _paint => Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 2.0\n    ..color = color;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawLine(Offset(size.x / 2, 0), Offset(size.x / 2, size.y), _paint);\n    canvas.drawLine(Offset(0, size.y / 2), Offset(size.x, size.y / 2), _paint);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/camera/viewfinder_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:canvas_test/canvas_test.dart';\nimport 'package:flame/camera.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Viewfinder', () {\n    testWithFlameGame(\n      'default camera centers the view in canvas',\n      (game) async {\n        final world = World()..addToParent(game);\n        final camera = CameraComponent(world: world)..addToParent(game);\n        world.add(_Rect());\n        await game.ready();\n        expect(game.canvasSize, closeToVector(Vector2(800, 600)));\n        expect(camera.viewfinder.position, closeToVector(Vector2(0, 0)));\n        expect(camera.viewfinder.zoom, 1);\n\n        // By default, the camera uses anchor=center, and places the world's\n        // point (0,0) there. This means the camera applies default translation\n        // of (400,300).\n        final canvas = MockCanvas();\n        game.render(canvas);\n        expect(\n          canvas,\n          MockCanvas()\n            ..translate(400, 300)\n            ..drawRect(const Rect.fromLTWH(0, 0, 80, 60))\n            ..translate(0, 0),\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'default camera centers on a given world point',\n      (game) async {\n        final world = World()..addToParent(game);\n        final camera = CameraComponent(world: world)\n          ..viewfinder.position = Vector2(100, -50)\n          ..addToParent(game);\n        world.add(_Rect());\n        await game.ready();\n        expect(camera.viewfinder.position, closeToVector(Vector2(100, -50)));\n\n        final canvas = MockCanvas();\n        game.render(canvas);\n        expect(\n          canvas,\n          MockCanvas()\n            ..translate(300, 350) // (800,600)/2 - (100,-50)\n            ..drawRect(const Rect.fromLTWH(0, 0, 80, 60))\n            ..translate(0, 0),\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'default camera apply zoom > 1',\n      (game) async {\n        final world = World()..addToParent(game);\n        final camera = CameraComponent(world: world)\n          ..viewfinder.zoom = 2\n          ..addToParent(game);\n        world.add(_Rect()..position = Vector2(20, 10));\n        await game.ready();\n        expect(camera.viewfinder.zoom, 2);\n\n        final canvas = MockCanvas();\n        game.render(canvas);\n        expect(\n          canvas,\n          MockCanvas()\n            ..translate(400, 300) // viewfinder translate\n            ..scale(2)\n            ..translate(20, 10) // rectangle translate\n            ..drawRect(const Rect.fromLTWH(0, 0, 80, 60))\n            ..translate(0, 0),\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'default camera with zoom > 1 and non-zero position',\n      (game) async {\n        final world = World()..addToParent(game);\n        final camera = CameraComponent(world: world)\n          ..viewfinder.zoom = 5\n          ..viewfinder.position = Vector2(60, 40)\n          ..addToParent(game);\n        world.add(_Rect()..position = Vector2(20, 10));\n        await game.ready();\n        expect(camera.viewfinder.zoom, 5);\n\n        final canvas = MockCanvas();\n        game.render(canvas);\n        expect(\n          canvas,\n          MockCanvas()\n            ..translate(400, 300) // canvas center\n            ..scale(5)\n            ..translate(-60, -40) // viewfinder translate\n            ..translate(20, 10) // rectangle translate\n            ..drawRect(const Rect.fromLTWH(0, 0, 80, 60))\n            ..translate(0, 0),\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'camera move effect',\n      (game) async {\n        final world = World()..addToParent(game);\n        final camera = CameraComponent(world: world)..addToParent(game);\n        camera.viewfinder.add(\n          MoveEffect.by(Vector2(5, 13), EffectController(duration: 1)),\n        );\n        camera.viewport.add(\n          MoveEffect.by(Vector2(40, -77), EffectController(duration: 1)),\n        );\n        await game.ready();\n\n        for (var t = 0.0; t < 1.0; t += 0.1) {\n          final value1 = Vector2(5 * t, 13 * t);\n          expect(\n            camera.viewfinder.position,\n            closeToVector(value1, toleranceVector2Float32(value1)),\n          );\n          final value2 = Vector2(40 * t, -77 * t);\n          expect(\n            camera.viewport.position,\n            closeToVector(value2, toleranceVector2Float32(value2)),\n          );\n          game.update(0.1);\n        }\n      },\n    );\n\n    testWithFlameGame(\n      'default camera with non-central anchor and zoom',\n      (game) async {\n        final world = World()\n          ..add(_Rect()..position = Vector2(20, 10))\n          ..addToParent(game);\n        CameraComponent(world: world)\n          ..viewfinder.zoom = 2\n          ..viewfinder.anchor = const Anchor(0.1, 0.7)\n          ..addToParent(game);\n        await game.ready();\n\n        final canvas = MockCanvas();\n        game.render(canvas);\n        expect(\n          canvas,\n          MockCanvas()\n            ..translate(800 * 0.1, 600 * 0.7) // viewfinder logical center\n            ..scale(2) // zoom\n            ..translate(20, 10) // rectangle translate\n            ..drawRect(const Rect.fromLTWH(0, 0, 80, 60))\n            ..translate(0, 0),\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'camera zoom effect',\n      (game) async {\n        final world = World()..addToParent(game);\n        final camera = CameraComponent(world: world)..addToParent(game);\n        camera.viewfinder.add(\n          ScaleEffect.to(Vector2.all(2), EffectController(duration: 1)),\n        );\n        await game.ready();\n\n        for (var t = 0.0; t < 1.0; t += 0.1) {\n          final value = 1 + t;\n          expect(\n            camera.viewfinder.zoom,\n            closeTo(value, toleranceFloat32(value)),\n          );\n          game.update(0.1);\n        }\n      },\n    );\n\n    testWithFlameGame(\n      'camera rotate effect',\n      (game) async {\n        final world = World()..addToParent(game);\n        final camera = CameraComponent(world: world)..addToParent(game);\n        camera.viewfinder.add(\n          RotateEffect.by(1, EffectController(duration: 1)),\n        );\n        await game.ready();\n\n        for (var t = 0.0; t < 1.0; t += 0.1) {\n          expect(camera.viewfinder.angle, closeTo(t, 1e-15));\n          game.update(0.1);\n        }\n      },\n    );\n\n    testWithFlameGame('can change visibleGameSize directly', (game) async {\n      final world = World()..addToParent(game);\n      final cameraComponent = CameraComponent(world: world)..addToParent(game);\n      expect(\n        () => cameraComponent.viewfinder.visibleGameSize = Vector2(100, 100),\n        returnsNormally,\n      );\n    });\n\n    testWithFlameGame(\n      'can change visibleGameSize directly with FixedAspectRatioViewport',\n      (game) async {\n        final world = World()..addToParent(game);\n        final cameraComponent = CameraComponent(\n          world: world,\n          viewport: FixedAspectRatioViewport(aspectRatio: 0.2),\n        )..addToParent(game);\n        expect(\n          () => cameraComponent.viewfinder.visibleGameSize = Vector2(100, 100),\n          returnsNormally,\n        );\n      },\n    );\n  });\n}\n\n/// Simple rectangle with default size 80x60.\nclass _Rect extends PositionComponent {\n  _Rect() : super(size: Vector2(80, 60));\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(size.toRect(), Paint());\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/camera/viewports/circular_viewport_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/camera.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../camera_test_helpers.dart';\n\nvoid main() {\n  group('CircularViewport', () {\n    // This should produce a white ellipse on a black background. The ellipse\n    // should be centered within the image, and has no anti-aliasing.\n    testGolden(\n      'elliptical viewport',\n      (game, tester) async {\n        final world = _MyWorld();\n        final camera = CameraComponent(\n          world: world,\n          viewport: CircularViewport.ellipse(80, 20)\n            ..position = Vector2(20, 30),\n        );\n        game.addAll([world, camera]);\n      },\n      goldenFile: '../../_goldens/circular_viewport_test1.png',\n      size: Vector2(200, 100),\n    );\n\n    // Same as before, but the position is set with an anchor.\n    testGolden(\n      'elliptical viewport with anchor',\n      (game, tester) async {\n        final world = _MyWorld();\n        final camera = CameraComponent(\n          world: world,\n          viewport: CircularViewport.ellipse(80, 20)\n            ..anchor = Anchor.center\n            ..position = Vector2(100, 50),\n        );\n        game.addAll([world, camera]);\n      },\n      goldenFile: '../../_goldens/circular_viewport_test1.png',\n      size: Vector2(200, 100),\n    );\n\n    // Renders magenta border around the viewport's edge behind the world.\n    testGolden(\n      'circular viewport with debug mode',\n      (game, tester) async {\n        final world = _MyWorld();\n        final camera = CameraComponent(\n          world: world,\n          viewport: CircularViewport(20)\n            ..position = Vector2(5, 5)\n            ..debugMode = true,\n        );\n        game.addAll([world, camera]);\n      },\n      goldenFile: '../../_goldens/circular_viewport_test2.png',\n      size: Vector2(50, 50),\n    );\n\n    // Modifies the viewport after it was initially initialized, and ensures\n    // that the new clip mask matches what we'd expect.\n    testGolden(\n      'circular viewport after resize',\n      (game, tester) async {\n        final world = _MyWorld();\n        final camera = CameraComponent(\n          world: world,\n          viewport: CircularViewport.ellipse(5, 10)..debugMode = true,\n        );\n        game.addAll([world, camera]);\n        await game.ready();\n        camera.viewport.position = Vector2(5, 5);\n        camera.viewport.size = Vector2(40, 40);\n      },\n      goldenFile: '../../_goldens/circular_viewport_test3.png',\n      size: Vector2(50, 50),\n    );\n\n    // The cross-hair should render in the center of the viewport.\n    testGolden(\n      'circular viewport with a HUD element',\n      (game, tester) async {\n        final world = _MyWorld();\n        final viewport = CircularViewport(20)..position = Vector2(5, 5);\n        final camera = CameraComponent(world: world, viewport: viewport);\n        viewport.add(\n          CrossHair(size: Vector2.all(16), position: viewport.size / 2),\n        );\n        game.addAll([world, camera]);\n      },\n      goldenFile: '../../_goldens/circular_viewport_test4.png',\n      size: Vector2(50, 50),\n    );\n\n    // Renders magenta border around the viewfinder's edge behind the world.\n    // Should not be visible.\n    testGolden(\n      'circular viewport with debug mode',\n      (game, tester) async {\n        final world = _MyWorld();\n        final camera = CameraComponent(\n          world: world,\n          viewport: CircularViewport(20)..position = Vector2(5, 5),\n          viewfinder: Viewfinder()..debugMode = true,\n        );\n        game.addAll([world, camera]);\n      },\n      goldenFile: '../../_goldens/circular_viewport_test5.png',\n      size: Vector2(50, 50),\n    );\n\n    testWithGame(\n      'hit testing',\n      () => FlameGame(\n        camera: CameraComponent(\n          viewport: CircularViewport.ellipse(80, 20)\n            ..position = Vector2(20, 30),\n          world: _MyWorld(),\n        ),\n      ),\n      (game) async {\n        await game.ready();\n        final viewport = game.camera.viewport;\n\n        bool hit(double x, double y) {\n          final components = game.componentsAtPoint(Vector2(x, y)).toList();\n          return components.first == viewport && components[1] == game.world;\n        }\n\n        expect(hit(10, 20), false);\n        expect(hit(20, 30), false);\n        expect(hit(25, 35), false);\n        expect(hit(100, 35), true);\n        expect(hit(100, 50), true);\n        expect(hit(180, 50), true);\n        expect(hit(180, 49), false);\n        expect(hit(180, 51), false);\n\n        final nestedPoints = <Vector2>[];\n        final center = Vector2(100, 50);\n        for (final component in game.componentsAtPoint(\n          center,\n          nestedPoints,\n        )) {\n          if (component == viewport) {\n            continue;\n          }\n          expect(component, game.world);\n          expect(nestedPoints.last, Vector2.zero());\n          break;\n        }\n      },\n    );\n\n    test('set wrong size', () {\n      expect(\n        () => CircularViewport(-1.0),\n        failsAssert(\"Viewport's size cannot be negative: [-2.0,-2.0]\"),\n      );\n      expect(\n        () => CircularViewport(0)..size = Vector2(-3.0, 4.0),\n        failsAssert(\"Viewport's size cannot be negative: [-3.0,4.0]\"),\n      );\n    });\n  });\n}\n\nclass _MyWorld extends World {\n  @override\n  void render(Canvas canvas) {\n    canvas.drawColor(const Color(0xFFFFFFFF), BlendMode.src);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/camera/viewports/fixed_aspect_ratio_viewport_test.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/camera.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:vector_math/vector_math.dart';\n\nvoid main() {\n  group('FixedAspectRatioViewport', () {\n    testGolden(\n      'viewport with tall screen',\n      (game, tester) async {\n        final world = _MyWorld();\n        final camera = CameraComponent(\n          world: world,\n          viewport: FixedAspectRatioViewport(aspectRatio: 1.5),\n        );\n        game.addAll([world, camera]);\n      },\n      goldenFile: '../../_goldens/fixed_aspect_ratio_viewport_test1.png',\n      size: Vector2(100, 200),\n    );\n\n    testGolden(\n      'viewport with wide screen',\n      (game, tester) async {\n        final world = _MyWorld();\n        final camera = CameraComponent(\n          world: world,\n          viewport: FixedAspectRatioViewport(aspectRatio: 1.5),\n        );\n        game.addAll([world, camera]);\n      },\n      goldenFile: '../../_goldens/fixed_aspect_ratio_viewport_test2.png',\n      size: Vector2(200, 100),\n    );\n\n    testWithFlameGame('auto-resizing', (game) async {\n      final viewport = FixedAspectRatioViewport(aspectRatio: 2.0);\n      final camera = CameraComponent(\n        world: World()..addToParent(game),\n        viewport: viewport,\n      );\n      game.add(camera);\n      await game.ready();\n\n      final random = Random();\n      for (var i = 0; i < 20; i++) {\n        // keep it as a float32 value\n        final width = prevFloat32(random.nextDouble() * 1000.0 + 10.0);\n        final height = prevFloat32(random.nextDouble() * 1000.0 + 10.0);\n        game.onGameResize(Vector2(width, height));\n        expect(viewport.size.x == width || viewport.size.y == height, true);\n        expect(\n          viewport.size.x / viewport.size.y,\n          closeTo(2.0, toleranceFloat32(2.0)),\n        );\n        if (viewport.size.x == width) {\n          expect(viewport.position.x, 0);\n          expect(\n            viewport.position.y * 2 + viewport.size.y,\n            closeTo(height, toleranceFloat32(height)),\n          );\n        }\n        if (viewport.size.y == height) {\n          expect(viewport.position.y, 0);\n          expect(\n            viewport.position.x * 2 + viewport.size.x,\n            closeTo(width, toleranceFloat32(width)),\n          );\n        }\n      }\n    });\n\n    testWithFlameGame('hit testing', (game) async {\n      final world = World();\n      final viewport = FixedAspectRatioViewport(aspectRatio: 1);\n      final camera = CameraComponent(\n        world: world,\n        viewport: viewport,\n      );\n      game.addAll([world, camera]);\n      game.onGameResize(Vector2(100, 200));\n      await game.ready();\n\n      bool hit(double x, double y) {\n        final components = game.componentsAtPoint(Vector2(x, y)).toList();\n        return components.isNotEmpty &&\n            components.first == viewport &&\n            components[1] == world;\n      }\n\n      for (final x in [0.0, 5.0, 50.0, 100.0]) {\n        expect(hit(x, 1), false);\n        expect(hit(x, 45), false);\n        expect(hit(x, 50), true);\n        expect(hit(x, 100), true);\n        expect(hit(x, 150), true);\n        expect(hit(x, 155), false);\n        expect(hit(x, 199), false);\n      }\n    });\n  });\n}\n\nclass _MyWorld extends World {\n  @override\n  void render(Canvas canvas) {\n    canvas.drawColor(const Color(0xFF777777), BlendMode.src);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/camera/viewports/fixed_resolution_viewport_test.dart",
    "content": "import 'package:flame/camera.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/src/components/position_component.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('FixedResolutionViewport.virtualSize', () {\n    testWithFlameGame('remains the same after resize', (game) async {\n      final fixedResolution = Vector2(800, 600);\n      final viewport = FixedResolutionViewport(resolution: fixedResolution);\n      game.camera.viewport = viewport;\n\n      await game.ready();\n\n      final resize = Vector2(400, 300);\n      game.onGameResize(resize);\n\n      expect(viewport.size, resize);\n      expect(viewport.virtualSize, fixedResolution);\n    });\n\n    testWithFlameGame(\n      'children components receive virtualSize in onParentResize',\n      (game) async {\n        final fixedResolution = Vector2(800, 600);\n        final viewport = FixedResolutionViewport(resolution: fixedResolution);\n        game.camera.viewport = viewport;\n\n        final child = _OnParentResizeTesterComponent();\n        await child.addToParent(viewport);\n\n        await game.ready();\n\n        expect(child._parentSize, fixedResolution);\n\n        game.onGameResize(Vector2(400, 300));\n        expect(child._parentSize, fixedResolution);\n      },\n    );\n  });\n}\n\nclass _OnParentResizeTesterComponent extends PositionComponent {\n  _OnParentResizeTesterComponent();\n\n  Vector2? _parentSize;\n\n  @override\n  void onParentResize(Vector2 size) {\n    _parentSize = size;\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/camera/viewports/fixed_size_viewport_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/camera.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('FixedSizeViewport', () {\n    testWithFlameGame('camera with FixedSizeViewport', (game) async {\n      final camera = CameraComponent(\n        world: World(),\n        viewport: FixedSizeViewport(300, 100),\n      );\n      game.addAll([camera.world!, camera]);\n      await game.ready();\n\n      expect(camera.viewport, isA<FixedSizeViewport>());\n      expect(camera.viewport.size, Vector2(300, 100));\n      expect(camera.viewport.position, Vector2(0, 0));\n\n      game.onGameResize(Vector2(200, 200));\n      expect(camera.viewport.size, Vector2(300, 100));\n      expect(camera.viewport.position, Vector2(0, 0));\n    });\n\n    testWithFlameGame('hit-testing', (game) async {\n      final camera = CameraComponent(\n        world: World(),\n        viewport: FixedSizeViewport(400, 100),\n      );\n      game.addAll([camera.world!, camera]);\n      await game.ready();\n\n      final viewport = camera.viewport;\n      expect(viewport, isA<FixedSizeViewport>());\n      expect(viewport.containsLocalPoint(Vector2(0, 0)), true);\n      expect(viewport.containsLocalPoint(Vector2(-1, -1)), false);\n      expect(viewport.containsLocalPoint(Vector2(400, 100)), true);\n      expect(viewport.containsLocalPoint(Vector2(150, 50)), true);\n      expect(viewport.containsLocalPoint(Vector2(-150, 50)), false);\n      expect(viewport.containsLocalPoint(Vector2(150, -50)), false);\n      expect(viewport.containsLocalPoint(Vector2(100, 20)), true);\n      expect(viewport.containsLocalPoint(Vector2(1000, -1000)), false);\n      expect(viewport.containsLocalPoint(Vector2(300, 100)), true);\n    });\n\n    testGolden(\n      'Clipping behavior',\n      (game, tester) async {\n        final world = World();\n        final camera =\n            CameraComponent(\n                world: world,\n                viewport: FixedSizeViewport(500, 200),\n              )\n              ..viewport.position = Vector2(400, 300)\n              ..viewport.anchor = Anchor.center\n              ..viewfinder.position = Vector2.zero();\n        world.add(\n          CircleComponent(\n            position: Vector2.zero(),\n            radius: 200,\n            anchor: Anchor.center,\n            paint: Paint()..color = const Color(0x4400ffff),\n          ),\n        );\n        world.addAll([\n          for (var i = -6; i <= 6; i++)\n            CircleComponent(\n              position: Vector2(i * 60.0 - i * i.abs() * 3, 0),\n              radius: 25 - i.abs() * 3,\n              anchor: Anchor.center,\n            ),\n        ]);\n        game.addAll([world, camera]);\n      },\n      goldenFile: '../../_goldens/fixed_size_viewport_test_1.png',\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/camera/viewports/max_viewport_test.dart",
    "content": "import 'package:flame/camera.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('MaxViewport', () {\n    testWithFlameGame('camera with MaxViewport', (game) async {\n      expect(game.size, Vector2(800, 600));\n      final world = World()..addToParent(game);\n      final camera = CameraComponent(world: world)..addToParent(game);\n      await game.ready();\n\n      expect(camera.viewport, isA<MaxViewport>());\n      expect(camera.viewport.size, Vector2(800, 600));\n      expect(camera.viewport.position, Vector2(0, 0));\n\n      game.onGameResize(Vector2(500, 200));\n      expect(camera.viewport.size, Vector2(500, 200));\n      expect(camera.viewport.position, Vector2(0, 0));\n    });\n\n    testWithFlameGame('hit-testing', (game) async {\n      final world = World()..addToParent(game);\n      final camera = CameraComponent(world: world)..addToParent(game);\n      await game.ready();\n\n      final viewport = camera.viewport;\n      expect(viewport, isA<MaxViewport>());\n      expect(viewport.containsLocalPoint(Vector2(0, 0)), true);\n      expect(viewport.containsLocalPoint(Vector2(100, 200)), true);\n      expect(viewport.containsLocalPoint(Vector2(-1, -1)), true);\n      expect(viewport.containsLocalPoint(Vector2(1000, -1000)), true);\n    });\n\n    testWithFlameGame('check that onViewportResize is called', (game) async {\n      final world = World();\n      final camera = CameraComponent(world: world, viewport: _MyMaxViewport());\n      game.addAll([world, camera]);\n      await game.ready();\n\n      final viewport = camera.viewport;\n      expect(viewport, isA<_MyMaxViewport>());\n      expect((viewport as _MyMaxViewport).onViewportResizeCalled, 2);\n      game.onGameResize(Vector2(200, 200));\n      expect(viewport.onViewportResizeCalled, 3);\n    });\n\n    testWithGame(\n      'check that onViewportResize is called with game CameraComponent',\n      () {\n        return FlameGame(\n          camera: CameraComponent(viewport: _MyMaxViewport()),\n        );\n      },\n      (game) async {\n        final viewport = game.camera.viewport;\n        expect(viewport, isA<_MyMaxViewport>());\n        expect((viewport as _MyMaxViewport).onViewportResizeCalled, 3);\n        game.onGameResize(Vector2(200, 200));\n        expect(viewport.onViewportResizeCalled, 5);\n      },\n    );\n  });\n}\n\nclass _MyMaxViewport extends MaxViewport {\n  int onViewportResizeCalled = 0;\n\n  @override\n  void onViewportResize() {\n    onViewportResizeCalled++;\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/camera/world_test.dart",
    "content": "import 'package:flame/camera.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('World', () {\n    testWithFlameGame(\n      'by default it has a negative max 32bit int priority',\n      (game) async {\n        game.removeAll(game.children);\n        final world = World()..addToParent(game);\n        final camera = CameraComponent(world: world)..addToParent(game);\n        await game.ready();\n\n        expect(world.priority, equals(-0x7fffffff));\n        expect(game.children, equals([world, camera]));\n      },\n    );\n\n    testWithFlameGame(\n      'with a custom priority putting it in front of the camera in the tree',\n      (game) async {\n        game.removeAll(game.children);\n        final world = World(priority: 4);\n        final camera = CameraComponent(world: world)..priority = 0;\n        game.addAll([world, camera]);\n        await game.ready();\n\n        expect(world.priority, equals(4));\n        expect(game.children, equals([camera, world]));\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/collisions/collision_callback_benchmark_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nimport 'collision_test_helpers.dart';\n\nclass _TestBlock extends PositionComponent with CollisionCallbacks {\n  final Vector2 velocity;\n  static int collisionCounter = 0;\n\n  _TestBlock(Vector2 position, Vector2 size, this.velocity)\n    : super(\n        position: position,\n        size: size,\n      ) {\n    add(CircleHitbox());\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n    collisionCounter++;\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    if (position.length2 > 100000) {\n      velocity.negate();\n    }\n    position.add(velocity * dt);\n  }\n}\n\nvoid main() {\n  group('Benchmark collision detection', () {\n    runCollisionTestRegistry({\n      'collidable callbacks are called': (collisionSystem) async {\n        final game = collisionSystem as FlameGame;\n        final rng = Random(0);\n        final blocks = List.generate(\n          100,\n          (_) => _TestBlock(\n            Vector2.random(rng) * game.size.x,\n            Vector2.random(rng) * 100,\n            Vector2(\n              rng.nextInt(100) * (rng.nextBool() ? 1 : -1),\n              rng.nextInt(100) * (rng.nextBool() ? 1 : -1),\n            ),\n          ),\n        );\n        await game.ensureAddAll(blocks);\n        final startTime = DateTime.now();\n        const ticks = 1000;\n        for (var i = 0; i < ticks; i++) {\n          game.update(1 / 60);\n        }\n        final totalTime =\n            DateTime.now().millisecondsSinceEpoch -\n            startTime.millisecondsSinceEpoch;\n        // ignore:avoid_print\n        print(\n          '$totalTime ms\\n'\n          '${1000 / (totalTime / ticks)} runs per second\\n'\n          '${_TestBlock.collisionCounter}',\n        );\n      },\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/collisions/collision_callback_test.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nimport 'collision_test_helpers.dart';\n\nvoid main() {\n  group('Collision callbacks', () {\n    runCollisionTestRegistry({\n      'collidable callbacks are called': (game) async {\n        final blockA = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n        );\n        final blockB = TestBlock(\n          Vector2.all(1),\n          Vector2.all(10),\n        );\n        await game.ensureAddAll([blockA, blockB]);\n        game.update(0);\n        expect(blockA.collidingWith(blockB), isTrue);\n        expect(blockB.collidingWith(blockA), isTrue);\n        expect(blockA.activeCollisions.length, 1);\n        expect(blockB.activeCollisions.length, 1);\n        expect(blockA.startCounter, 1);\n        expect(blockB.startCounter, 1);\n        expect(blockA.onCollisionCounter, 1);\n        expect(blockB.onCollisionCounter, 1);\n        expect(blockA.endCounter, 0);\n        expect(blockB.endCounter, 0);\n        game.update(0);\n        expect(blockA.activeCollisions.length, 1);\n        expect(blockB.activeCollisions.length, 1);\n        expect(blockA.startCounter, 1);\n        expect(blockB.startCounter, 1);\n        expect(blockA.onCollisionCounter, 2);\n        expect(blockB.onCollisionCounter, 2);\n        blockB.position = Vector2.all(21);\n        expect(blockA.endCounter, 0);\n        expect(blockB.endCounter, 0);\n        game.update(0);\n        expect(blockA.collidingWith(blockB), isFalse);\n        expect(blockB.collidingWith(blockA), isFalse);\n        expect(blockA.activeCollisions.length, 0);\n        expect(blockB.activeCollisions.length, 0);\n        game.update(0);\n        expect(blockA.endCounter, 1);\n        expect(blockB.endCounter, 1);\n      },\n      'collidable callbacks are called when removing a Collidable':\n          (game) async {\n            final blockA = TestBlock(\n              Vector2.zero(),\n              Vector2.all(10),\n            );\n            final blockB = TestBlock(\n              Vector2.all(1),\n              Vector2.all(10),\n            );\n            await game.ensureAddAll([blockA, blockB]);\n            game.update(0);\n            expect(blockA.collidingWith(blockB), true);\n            expect(blockB.collidingWith(blockA), true);\n            expect(blockA.activeCollisions.length, 1);\n            expect(blockB.activeCollisions.length, 1);\n            game.remove(blockA);\n            game.update(0);\n            expect(blockA.endCounter, 1);\n            expect(blockB.endCounter, 1);\n            expect(blockA.collidingWith(blockB), false);\n            expect(blockB.collidingWith(blockA), false);\n            expect(blockA.activeCollisions.length, 0);\n            expect(blockB.activeCollisions.length, 0);\n          },\n      'hitbox callbacks are called': (game) async {\n        final blockA = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n        );\n        final blockB = TestBlock(\n          Vector2.all(1),\n          Vector2.all(10),\n        );\n        final hitboxA = blockA.hitbox;\n        final hitboxB = blockB.hitbox;\n        await game.ensureAddAll([blockA, blockB]);\n        game.update(0);\n        expect(hitboxA.collidingWith(hitboxB), true);\n        expect(hitboxB.collidingWith(hitboxA), true);\n        expect(hitboxA.activeCollisions.length, 1);\n        expect(hitboxB.activeCollisions.length, 1);\n        expect(hitboxA.startCounter, 1);\n        expect(hitboxB.startCounter, 1);\n        expect(hitboxA.onCollisionCounter, 1);\n        expect(hitboxB.onCollisionCounter, 1);\n        game.update(0);\n        expect(hitboxA.startCounter, 1);\n        expect(hitboxB.startCounter, 1);\n        expect(hitboxA.onCollisionCounter, 2);\n        expect(hitboxB.onCollisionCounter, 2);\n        blockB.position = Vector2.all(21);\n        expect(hitboxA.endCounter, 0);\n        expect(hitboxB.endCounter, 0);\n        game.update(0);\n        expect(hitboxA.collidingWith(hitboxB), false);\n        expect(hitboxB.collidingWith(hitboxA), false);\n        expect(hitboxA.activeCollisions.length, 0);\n        expect(hitboxB.activeCollisions.length, 0);\n        expect(hitboxA.endCounter, 1);\n        expect(hitboxB.endCounter, 1);\n      },\n      'hitboxes are in any descendants': (game) async {\n        // The hitboxParent of the `testHitbox` will be the `player`.\n        final player = TestBlock(\n          Vector2.all(0),\n          Vector2.all(10),\n          addTestHitbox: false,\n          children: [\n            Component(\n              children: [\n                Component(children: [TestHitbox()]),\n              ],\n            ),\n          ],\n        );\n        final block = TestBlock(Vector2.all(100), Vector2.all(10));\n\n        await game.ensureAddAll([player, block]);\n\n        expect(block.startCounter, 0);\n        expect(block.onCollisionCounter, 0);\n        expect(block.endCounter, 0);\n\n        // player <== collides with  ==> block\n        block.position = Vector2.all(5);\n        game.update(0);\n\n        expect(block.startCounter, 1);\n        expect(block.onCollisionCounter, 1);\n        expect(block.endCounter, 0);\n      },\n      'hitboxParent is PositionComponent but not CollisionCallbacks':\n          (game) async {\n            final player = PositionComponent(\n              position: Vector2.all(0),\n              size: Vector2.all(10),\n              children: [\n                Component(\n                  children: [\n                    Component(children: [TestHitbox()]),\n                  ],\n                ),\n              ],\n            );\n            final block = TestBlock(Vector2.all(100), Vector2.all(10));\n\n            await game.ensureAddAll([player, block]);\n\n            block.position = Vector2.all(5);\n            game.update(0);\n\n            expect(block.startCounter, 1);\n            expect(block.onCollisionCounter, 1);\n            expect(block.endCounter, 0);\n          },\n    });\n  });\n\n  runCollisionTestRegistry({\n    'hitbox callbacks are called when Collidable is removed': (game) async {\n      final blockA = TestBlock(\n        Vector2.zero(),\n        Vector2.all(10),\n      );\n      final blockB = TestBlock(\n        Vector2.all(1),\n        Vector2.all(10),\n      );\n      final hitboxA = blockA.hitbox;\n      final hitboxB = blockB.hitbox;\n      await game.ensureAddAll([blockA, blockB]);\n      game.update(0);\n      expect(hitboxA.collidingWith(hitboxB), true);\n      expect(hitboxB.collidingWith(hitboxA), true);\n      expect(hitboxA.activeCollisions.length, 1);\n      expect(hitboxB.activeCollisions.length, 1);\n      game.remove(blockA);\n      game.update(0);\n      expect(hitboxA.collidingWith(hitboxB), false);\n      expect(hitboxB.collidingWith(hitboxA), false);\n      expect(hitboxA.activeCollisions.length, 0);\n      expect(hitboxB.activeCollisions.length, 0);\n      expect(hitboxA.endCounter, 1);\n      expect(hitboxB.endCounter, 1);\n    },\n    'hitbox end callbacks are called when hitbox is moved away fast':\n        (game) async {\n          final blockA = TestBlock(\n            Vector2.zero(),\n            Vector2.all(10),\n          );\n          final blockB = TestBlock(\n            Vector2.all(1),\n            Vector2.all(10),\n          );\n          final hitboxA = blockA.hitbox;\n          final hitboxB = blockB.hitbox;\n          await game.ensureAddAll([blockA, blockB]);\n          game.update(0);\n          expect(hitboxA.collidingWith(hitboxB), true);\n          expect(hitboxB.collidingWith(hitboxA), true);\n          expect(hitboxA.activeCollisions.length, 1);\n          expect(hitboxB.activeCollisions.length, 1);\n          blockB.position = Vector2.all(1000);\n          game.update(0);\n          expect(hitboxA.collidingWith(hitboxB), false);\n          expect(hitboxB.collidingWith(hitboxA), false);\n          expect(hitboxA.activeCollisions.length, 0);\n          expect(hitboxB.activeCollisions.length, 0);\n          expect(hitboxA.endCounter, 1);\n          expect(hitboxB.endCounter, 1);\n        },\n    'onCollisionEnd is only called when there previously was a collision':\n        (game) async {\n          final blockA = TestBlock(\n            Vector2.zero(),\n            Vector2.all(10),\n          );\n          final blockB = TestBlock(\n            Vector2(5, 11),\n            Vector2.all(10),\n          );\n          await game.ensureAddAll([blockA, blockB]);\n          game.update(0);\n          blockB.position.x += 2 * blockA.size.x;\n          game.update(0);\n          expect(blockA.startCounter, 0);\n          expect(blockB.startCounter, 0);\n          expect(blockA.onCollisionCounter, 0);\n          expect(blockB.onCollisionCounter, 0);\n          expect(blockA.endCounter, 0);\n          expect(blockB.endCounter, 0);\n        },\n    'callbacks are only called once for hitboxes on each other': (game) async {\n      final blockA = TestBlock(\n        Vector2.zero(),\n        Vector2.all(10),\n      );\n      final blockB = TestBlock(\n        Vector2.all(100),\n        Vector2.all(10),\n      );\n      await game.ensureAddAll([blockA, blockB]);\n\n      game.update(0);\n      expect(blockA.startCounter, 0);\n      expect(blockB.startCounter, 0);\n      expect(blockA.onCollisionCounter, 0);\n      expect(blockB.onCollisionCounter, 0);\n      expect(blockA.endCounter, 0);\n      expect(blockB.endCounter, 0);\n\n      blockB.position = Vector2.all(5);\n      game.update(0);\n      expect(blockA.startCounter, 1);\n      expect(blockB.startCounter, 1);\n      expect(blockA.onCollisionCounter, 1);\n      expect(blockB.onCollisionCounter, 1);\n      expect(blockA.endCounter, 0);\n      expect(blockB.endCounter, 0);\n\n      blockB.position = blockA.position;\n      game.update(0);\n      expect(blockA.startCounter, 1);\n      expect(blockB.startCounter, 1);\n      expect(blockA.onCollisionCounter, 2);\n      expect(blockB.onCollisionCounter, 2);\n      expect(blockA.endCounter, 0);\n      expect(blockB.endCounter, 0);\n\n      game.update(0);\n      expect(blockA.startCounter, 1);\n      expect(blockB.startCounter, 1);\n      expect(blockA.onCollisionCounter, 3);\n      expect(blockB.onCollisionCounter, 3);\n      expect(blockA.endCounter, 0);\n      expect(blockB.endCounter, 0);\n\n      blockB.position.y += 20;\n      game.update(0);\n      expect(blockA.startCounter, 1);\n      expect(blockB.startCounter, 1);\n      expect(blockA.onCollisionCounter, 3);\n      expect(blockB.onCollisionCounter, 3);\n      expect(blockA.endCounter, 1);\n      expect(blockB.endCounter, 1);\n    },\n    'callbacks return the parent of the composite hitbox on collisions':\n        (game) async {\n          final size = Vector2.all(10);\n          final hitboxA = TestHitbox();\n          final compositeA = CompositeTestHitbox(\n            size: size,\n            children: [hitboxA],\n          );\n          final blockA = TestBlock(\n            Vector2.zero(),\n            size,\n            children: [compositeA],\n            addTestHitbox: false,\n          );\n          final hitboxB = TestHitbox();\n          final compositeB = CompositeTestHitbox(\n            size: size,\n            children: [hitboxB],\n          );\n          final blockB = TestBlock(\n            Vector2.all(15),\n            size,\n            children: [compositeB],\n            addTestHitbox: false,\n          );\n          await game.ensureAddAll([blockA, blockB]);\n\n          game.update(0);\n          expect(blockA.startCounter, 0);\n          expect(blockB.startCounter, 0);\n          expect(blockA.onCollisionCounter, 0);\n          expect(blockB.onCollisionCounter, 0);\n          expect(blockA.endCounter, 0);\n          expect(blockB.endCounter, 0);\n\n          blockB.position = Vector2.all(5);\n          game.update(0);\n          expect(blockA.startCounter, 1);\n          expect(blockB.startCounter, 1);\n          expect(blockA.onCollisionCounter, 1);\n          expect(blockB.onCollisionCounter, 1);\n          expect(blockA.endCounter, 0);\n          expect(blockB.endCounter, 0);\n          expect(blockA.collidingWith(blockB), isTrue);\n          expect(blockB.collidingWith(blockA), isTrue);\n\n          blockB.position.y += 20;\n          game.update(0);\n          expect(blockA.startCounter, 1);\n          expect(blockB.startCounter, 1);\n          expect(blockA.onCollisionCounter, 1);\n          expect(blockB.onCollisionCounter, 1);\n          expect(blockA.endCounter, 1);\n          expect(blockB.endCounter, 1);\n          expect(blockA.collidingWith(blockB), isFalse);\n          expect(blockB.collidingWith(blockA), isFalse);\n        },\n    'end and start callbacks are only called once for hitboxes sharing a side':\n        (game) async {\n          final blockA = TestBlock(\n            Vector2.all(10),\n            Vector2.all(10),\n          );\n          final blockB = TestBlock(\n            Vector2.all(21),\n            Vector2.all(12),\n          );\n          await game.ensureAddAll([blockA, blockB]);\n\n          game.update(0);\n          expect(blockA.startCounter, 0);\n          expect(blockB.startCounter, 0);\n          expect(blockA.onCollisionCounter, 0);\n          expect(blockB.onCollisionCounter, 0);\n          expect(blockA.endCounter, 0);\n          expect(blockB.endCounter, 0);\n\n          blockB.position = Vector2(10, 14);\n          game.update(0);\n          expect(blockA.startCounter, 1);\n          expect(blockB.startCounter, 1);\n          expect(blockA.onCollisionCounter, 1);\n          expect(blockB.onCollisionCounter, 1);\n          expect(blockA.endCounter, 0);\n          expect(blockB.endCounter, 0);\n\n          game.update(0);\n          expect(blockA.startCounter, 1);\n          expect(blockB.startCounter, 1);\n          expect(blockA.onCollisionCounter, 2);\n          expect(blockB.onCollisionCounter, 2);\n          expect(blockA.endCounter, 0);\n          expect(blockB.endCounter, 0);\n\n          game.update(0);\n          expect(blockA.startCounter, 1);\n          expect(blockB.startCounter, 1);\n          expect(blockA.onCollisionCounter, 3);\n          expect(blockB.onCollisionCounter, 3);\n          expect(blockA.endCounter, 0);\n          expect(blockB.endCounter, 0);\n\n          blockB.position =\n              blockA.positionOfAnchor(Anchor.topRight) + Vector2(1, 0);\n          game.update(0);\n          expect(blockA.startCounter, 1);\n          expect(blockB.startCounter, 1);\n          expect(blockA.onCollisionCounter, 3);\n          expect(blockB.onCollisionCounter, 3);\n          expect(blockA.endCounter, 1);\n          expect(blockB.endCounter, 1);\n        },\n    'component collision callbacks are not called with hitbox '\n        'triggersParentCollision option': (game) async {\n      final utilityHitboxA = TestHitbox('hitboxA')\n        ..triggersParentCollision = false;\n      final blockA = TestBlock(\n        Vector2.all(10),\n        Vector2.all(10),\n      );\n      blockA.add(utilityHitboxA);\n\n      final utilityHitboxB = TestHitbox('hitboxB')\n        ..triggersParentCollision = false;\n      final blockB = TestBlock(\n        Vector2.all(15),\n        Vector2.all(10),\n      );\n      blockB.add(utilityHitboxB);\n      await game.ensureAddAll([blockA, blockB]);\n\n      game.update(0);\n      expect(blockA.startCounter, 1);\n      expect(blockB.startCounter, 1);\n      expect(blockA.onCollisionCounter, 1);\n      expect(blockB.onCollisionCounter, 1);\n      expect(blockA.endCounter, 0);\n      expect(blockB.endCounter, 0);\n      expect(utilityHitboxA.startCounter, 2);\n      expect(utilityHitboxB.startCounter, 2);\n      expect(utilityHitboxA.onCollisionCounter, 2);\n      expect(utilityHitboxB.onCollisionCounter, 2);\n      expect(utilityHitboxA.endCounter, 0);\n      expect(utilityHitboxB.endCounter, 0);\n\n      blockB.position = Vector2(30, 30);\n      game.update(0);\n      expect(blockA.startCounter, 1);\n      expect(blockB.startCounter, 1);\n      expect(blockA.onCollisionCounter, 1);\n      expect(blockB.onCollisionCounter, 1);\n      expect(blockA.endCounter, 1);\n      expect(blockB.endCounter, 1);\n      expect(utilityHitboxA.startCounter, 2);\n      expect(utilityHitboxB.startCounter, 2);\n      expect(utilityHitboxA.onCollisionCounter, 2);\n      expect(utilityHitboxB.onCollisionCounter, 2);\n      expect(utilityHitboxA.endCounter, 2);\n      expect(utilityHitboxB.endCounter, 2);\n    },\n\n    // Reproduced #1478\n    'collision callbacks with many hitboxes added': (game) async {\n      const side = 10.0;\n      final player = TestBlock(Vector2.zero(), Vector2.all(side))\n        ..anchor = Anchor.center;\n      await game.ensureAdd(player);\n\n      final blocks = <PositionComponent>[];\n      // Change 0 or 100 and there will be different blocks breaking\n      const amount = 100;\n      for (var id = 0; id < amount; id++) {\n        final pos = 2 * id * side;\n        final component = PositionComponent(\n          position: Vector2.all(pos),\n          size: Vector2.all(side),\n          anchor: Anchor.center,\n        )..add(RectangleHitbox());\n        blocks.add(component);\n      }\n      await game.ensureAddAll(blocks);\n\n      for (var i = 0; i < blocks.length; i++) {\n        player.position = Vector2.all(-10000);\n        game.update(0);\n        final centerOfABlock = blocks[i].position;\n        player.startCounter = 0;\n        player.onCollisionCounter = 0;\n        player.endCounter = 0;\n\n        player.position = Vector2(\n          centerOfABlock.x,\n          centerOfABlock.y + 100,\n        ); // no sides are overlaps\n        game.update(0);\n        expect(player.startCounter, 0);\n        expect(player.onCollisionCounter, 0);\n        expect(player.endCounter, 0);\n\n        player.position = Vector2(\n          centerOfABlock.x,\n          centerOfABlock.y + 10,\n        ); // two sides are overlaps\n\n        game.update(0);\n        expect(player.startCounter, 1);\n        expect(player.onCollisionCounter, 1);\n        expect(player.endCounter, 0);\n      }\n\n      game.update(0);\n      expect(player.startCounter, 1);\n      expect(player.onCollisionCounter, 2);\n      expect(player.endCounter, 0);\n\n      game.update(0);\n      expect(player.startCounter, 1);\n      expect(player.onCollisionCounter, 3);\n      expect(player.endCounter, 0);\n    },\n    // Reproduced #1478\n    'collision callbacks with changed game size': (collisionSystem) async {\n      final game = collisionSystem as FlameGame;\n      final block = TestBlock(Vector2.all(20), Vector2.all(10))\n        ..anchor = Anchor.center;\n      final screenHitbox = ScreenHitbox();\n      game.world.addAll([block, screenHitbox]);\n      await game.ready();\n      game.world.update(0);\n\n      game.update(0);\n      expect(block.startCounter, 0);\n      expect(block.onCollisionCounter, 0);\n      expect(block.endCounter, 0);\n      block.position.x = game.size.x / 2;\n\n      game.update(0);\n      expect(block.startCounter, 1);\n      expect(block.onCollisionCounter, 1);\n      expect(block.endCounter, 0);\n      game.onGameResize(game.size * 2);\n\n      game.update(0);\n      expect(block.startCounter, 1);\n      expect(block.onCollisionCounter, 1);\n      expect(block.endCounter, 1);\n      block.position.y = game.size.y / 2;\n\n      game.update(0);\n      expect(block.startCounter, 2);\n      expect(block.onCollisionCounter, 2);\n      expect(block.endCounter, 1);\n    },\n    'collision callbacks for solid hitbox': (game) async {\n      final innerBlock = TestBlock(Vector2.zero(), Vector2.all(2))\n        ..anchor = Anchor.center\n        ..hitbox.isSolid = true;\n      final outerBlock = TestBlock(Vector2.zero(), Vector2.all(4))\n        ..anchor = Anchor.center\n        ..hitbox.isSolid = true;\n      await game.ensureAddAll([innerBlock, outerBlock]);\n\n      game.update(0);\n      expect(innerBlock.startCounter, 1);\n      expect(innerBlock.onCollisionCounter, 1);\n      expect(innerBlock.endCounter, 0);\n\n      innerBlock.position = Vector2.all(5);\n      game.update(0);\n\n      expect(outerBlock.startCounter, 1);\n      expect(outerBlock.onCollisionCounter, 1);\n      expect(outerBlock.endCounter, 1);\n    },\n    'collision callbacks for nested World': (outerCollisionSystem) async {\n      final game = outerCollisionSystem as FlameGame;\n      final world1 = CollisionDetectionWorld();\n      final world2 = CollisionDetectionWorld();\n      final camera1 = CameraComponent(world: world1);\n      final camera2 = CameraComponent(world: world2);\n      await game.ensureAddAll([world1, world2, camera1, camera2]);\n      final block1 = TestBlock(Vector2(1, 1), Vector2.all(2))\n        ..anchor = Anchor.center;\n      final block2 = TestBlock(Vector2(1, -1), Vector2.all(2))\n        ..anchor = Anchor.center;\n      final block3 = TestBlock(Vector2(-1, 1), Vector2.all(2))\n        ..anchor = Anchor.center;\n      final block4 = TestBlock(Vector2(-1, -1), Vector2.all(2))\n        ..anchor = Anchor.center;\n      await world1.ensureAddAll([block1, block2]);\n      await world2.ensureAddAll([block3, block4]);\n\n      game.update(0);\n      for (final block in [block1, block2, block3, block4]) {\n        expect(block.startCounter, 1);\n        expect(block.onCollisionCounter, 1);\n        expect(block.endCounter, 0);\n      }\n      expect(block1.collidedWithExactly([block2]), isTrue);\n      expect(block2.collidedWithExactly([block1]), isTrue);\n      expect(block3.collidedWithExactly([block4]), isTrue);\n      expect(block4.collidedWithExactly([block3]), isTrue);\n\n      for (final block in [block1, block2, block3, block4]) {\n        block.position.scale(3);\n      }\n\n      game.update(0);\n      for (final block in [block1, block2, block3, block4]) {\n        expect(block.startCounter, 1);\n        expect(block.onCollisionCounter, 1);\n        expect(block.endCounter, 1);\n      }\n    },\n  });\n\n  runCollisionTestRegistry({\n    'parent isColliding is consistent when hitbox is replaced': (game) async {\n      // Regression test for https://github.com/flame-engine/flame/issues/3417\n      // When a hitbox is removed and a new one added to the same parent,\n      // the parent's isColliding should correctly reflect the new collision.\n      final obstacle = TestBlock(\n        Vector2.all(5),\n        Vector2.all(10),\n        type: CollisionType.passive,\n      );\n      final player = TestBlock(\n        Vector2.zero(),\n        Vector2.all(10),\n        addTestHitbox: false,\n      );\n      await game.ensureAddAll([obstacle, player]);\n\n      // Add first hitbox that collides with obstacle.\n      final hitboxA = TestHitbox();\n      player.add(hitboxA);\n      game.update(0);\n\n      expect(player.isColliding, isTrue);\n      expect(player.startCounter, 1);\n\n      // Remove old hitbox and add new one that also collides.\n      player.remove(hitboxA);\n      final hitboxB = TestHitbox();\n      player.add(hitboxB);\n      game.update(0);\n\n      // The parent should still report as colliding since the new hitbox\n      // collides with the obstacle.\n      expect(player.isColliding, isTrue);\n      expect(player.startCounter, 2);\n      expect(player.endCounter, 1);\n    },\n  });\n\n  group('ComponentTypeCheck(only supported in the QuadTree)', () {\n    testQuadTreeCollisionDetectionGame(\n      'makes the correct Component type start to collide',\n      (game) async {\n        final water = Water(\n          position: Vector2.all(0),\n          size: Vector2.all(10),\n          children: [\n            Component(\n              children: [\n                Component(children: [TestHitbox()]),\n              ],\n            ),\n          ],\n        );\n        final brick = Brick(\n          position: Vector2.all(50),\n          size: Vector2.all(10),\n          children: [\n            Component(\n              children: [\n                Component(children: [TestHitbox()]),\n              ],\n            ),\n          ],\n        );\n\n        final block = TestBlock(\n          Vector2.all(100),\n          Vector2.all(10),\n          onComponentTypeCheck: (other) {\n            if (other is Water) {\n              return false;\n            }\n            return true;\n          },\n        );\n\n        await game.ensureAddAll([water, brick, block]);\n\n        // block <== collides with  ==> water\n        // But as [TestBlock.onComponentTypeCheck] returns false with Water,\n        // they do not actually start to collide.\n        block.position = Vector2.all(5);\n        game.update(0);\n\n        expect(block.startCounter, 0);\n        expect(block.onCollisionCounter, 0);\n        expect(block.endCounter, 0);\n\n        // block <== collides with  ==> brick\n        block.position = Vector2.all(55);\n        game.update(0);\n\n        expect(block.startCounter, 1);\n        expect(block.onCollisionCounter, 1);\n        expect(block.endCounter, 0);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/collisions/collision_detection_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart' as geometry;\nimport 'package:flame/geometry.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nimport 'collision_test_helpers.dart';\n\nvoid main() {\n  group('LineSegment.isPointOnSegment', () {\n    test('can catch simple point', () {\n      final segment = LineSegment(\n        Vector2.all(0),\n        Vector2.all(1),\n      );\n      final point = Vector2.all(0.5);\n      expect(\n        segment.containsPoint(point),\n        true,\n        reason: 'Point should be on segment',\n      );\n    });\n\n    test('should not catch point outside of segment, but on line', () {\n      final segment = LineSegment(\n        Vector2.all(0),\n        Vector2.all(1),\n      );\n      final point = Vector2.all(3);\n      expect(\n        segment.containsPoint(point),\n        false,\n        reason: 'Point should not be on segment',\n      );\n    });\n\n    test('should not catch point outside of segment', () {\n      final segment = LineSegment(\n        Vector2.all(0),\n        Vector2.all(1),\n      );\n      final point = Vector2(3, 2);\n      expect(\n        segment.containsPoint(point),\n        false,\n        reason: 'Point should not be on segment',\n      );\n    });\n\n    test('point on end of segment', () {\n      final segment = LineSegment(\n        Vector2.all(0),\n        Vector2.all(1),\n      );\n      final point = Vector2.all(1);\n      expect(\n        segment.containsPoint(point),\n        true,\n        reason: 'Point should be on segment',\n      );\n    });\n\n    test('point on beginning of segment', () {\n      final segment = LineSegment(\n        Vector2.all(0),\n        Vector2.all(1),\n      );\n      final point = Vector2.all(0);\n      expect(\n        segment.containsPoint(point),\n        true,\n        reason: 'Point should be on segment',\n      );\n    });\n  });\n\n  group('LineSegment.intersections', () {\n    test('simple intersection', () {\n      final segmentA = LineSegment(Vector2.all(0), Vector2.all(1));\n      final segmentB = LineSegment(Vector2(0, 1), Vector2(1, 0));\n      final intersection = segmentA.intersections(segmentB);\n      expect(\n        intersection.isNotEmpty,\n        true,\n        reason: 'Should have intersection at (0.5, 0.5)',\n      );\n      expect(intersection.first == Vector2.all(0.5), true);\n    });\n\n    test('no intersection', () {\n      final segmentA = LineSegment(Vector2.all(0), Vector2.all(1));\n      final segmentB = LineSegment(Vector2(0, 1), Vector2(1, 2));\n      final intersection = segmentA.intersections(segmentB);\n      expect(\n        intersection.isEmpty,\n        true,\n        reason: 'Should not have any intersection',\n      );\n    });\n\n    test('same line segments', () {\n      final segmentA = LineSegment(Vector2.all(0), Vector2.all(1));\n      final segmentB = LineSegment(Vector2.all(0), Vector2.all(1));\n      final intersection = segmentA.intersections(segmentB);\n      expect(\n        intersection.isNotEmpty,\n        true,\n        reason: 'Should have intersection at (0.5, 0.5)',\n      );\n      expect(intersection.first == Vector2.all(0.5), true);\n    });\n\n    test('overlapping line segments', () {\n      final segmentA = LineSegment(Vector2.all(0), Vector2.all(1));\n      final segmentB = LineSegment(Vector2.all(0.5), Vector2.all(1.5));\n      final intersection = segmentA.intersections(segmentB);\n      expect(\n        intersection.isNotEmpty,\n        true,\n        reason: 'Should intersect at (0.75, 0.75)',\n      );\n      expect(intersection.first == Vector2.all(0.75), true);\n    });\n\n    test('one pixel overlap in different angles', () {\n      final segmentA = LineSegment(Vector2.all(0), Vector2.all(1));\n      final segmentB = LineSegment(Vector2.all(0), Vector2(1, -1));\n      final intersection = segmentA.intersections(segmentB);\n      expect(\n        intersection.isNotEmpty,\n        true,\n        reason: 'Should have intersection at (0, 0)',\n      );\n      expect(intersection.first == Vector2.all(0), true);\n    });\n\n    test('one pixel parallel overlap in same angle', () {\n      final segmentA = LineSegment(Vector2.all(0), Vector2.all(1));\n      final segmentB = LineSegment(Vector2.all(1), Vector2.all(2));\n      final intersection = segmentA.intersections(segmentB);\n      expect(\n        intersection.isNotEmpty,\n        true,\n        reason: 'Should have intersection at (1, 1)',\n      );\n      expect(intersection.first == Vector2.all(1), true);\n    });\n  });\n\n  group('Line.intersections', () {\n    test('simple line intersection', () {\n      const line1 = Line(1, -1, 0);\n      const line2 = Line(1, 1, 0);\n      final intersection = line1.intersections(line2);\n      expect(intersection.isNotEmpty, true, reason: 'Should have intersection');\n      expect(intersection.first == Vector2.all(0), true);\n    });\n\n    test('lines with c value', () {\n      const line1 = Line(1, 1, 1);\n      const line2 = Line(1, -1, 1);\n      final intersection = line1.intersections(line2);\n      expect(intersection.isNotEmpty, true, reason: 'Should have intersection');\n      expect(intersection.first == Vector2(1, 0), true);\n    });\n\n    test('does not catch parallel lines', () {\n      const line1 = Line(1, 1, -3);\n      const line2 = Line(1, 1, 6);\n      final intersection = line1.intersections(line2);\n      expect(\n        intersection.isEmpty,\n        true,\n        reason: 'Should not have intersection',\n      );\n    });\n\n    test('does not catch same line', () {\n      const line1 = Line(1, 1, 1);\n      const line2 = Line(1, 1, 1);\n      final intersection = line1.intersections(line2);\n      expect(\n        intersection.isEmpty,\n        true,\n        reason: 'Should not have intersection',\n      );\n    });\n  });\n\n  group('LinearEquation.fromPoints', () {\n    test('simple line from points', () {\n      final line = Line.fromPoints(Vector2.zero(), Vector2.all(1));\n      expect(line.a == 1.0, true, reason: 'a value is not correct');\n      expect(line.b == -1.0, true, reason: 'b value is not correct');\n      expect(line.c == 0.0, true, reason: 'c value is not correct');\n    });\n\n    test('line not going through origin', () {\n      final line = Line.fromPoints(Vector2(-2, 0), Vector2(0, 2));\n      expect(line.a == 2.0, true, reason: 'a value is not correct');\n      expect(line.b == -2.0, true, reason: 'b value is not correct');\n      expect(line.c == -4.0, true, reason: 'c value is not correct');\n    });\n\n    test('straight vertical line', () {\n      final line = Line.fromPoints(Vector2.all(1), Vector2(1, -1));\n      expect(line.a == -2.0, true, reason: 'a value is not correct');\n      expect(line.b == 0.0, true, reason: 'b value is not correct');\n      expect(line.c == -2.0, true, reason: 'c value is not correct');\n    });\n\n    test('straight horizontal line', () {\n      final line = Line.fromPoints(Vector2.all(1), Vector2(2, 1));\n      expect(line.a == 0.0, true, reason: 'a value is not correct');\n      expect(line.b == -1.0, true, reason: 'b value is not correct');\n      expect(line.c == -1.0, true, reason: 'c value is not correct');\n    });\n  });\n\n  group('LineSegment.pointsAt', () {\n    test('simple pointing', () {\n      final segment = LineSegment(Vector2.zero(), Vector2.all(1));\n      const line = Line(1, 1, 3);\n      final isPointingAt = segment.pointsAt(line);\n      expect(isPointingAt, true, reason: 'Line should be pointed at');\n    });\n\n    test('is not pointed at when crossed', () {\n      final segment = LineSegment(Vector2.zero(), Vector2.all(3));\n      const line = Line(1, 1, 3);\n      final isPointingAt = segment.pointsAt(line);\n      expect(isPointingAt, false, reason: 'Line should not be pointed at');\n    });\n\n    test('is not pointed at when parallel', () {\n      final segment = LineSegment(Vector2.zero(), Vector2(1, -1));\n      const line = Line(1, 1, 3);\n      final isPointingAt = segment.pointsAt(line);\n      expect(isPointingAt, false, reason: 'Line should not be pointed at');\n    });\n\n    test('horizontal line can be pointed at', () {\n      final segment = LineSegment(Vector2.zero(), Vector2.all(1));\n      const line = Line(0, 1, 2);\n      final isPointingAt = segment.pointsAt(line);\n      expect(isPointingAt, true, reason: 'Line should be pointed at');\n    });\n\n    test('vertical line can be pointed at', () {\n      final segment = LineSegment(Vector2.zero(), Vector2.all(1));\n      const line = Line(1, 0, 2);\n      final isPointingAt = segment.pointsAt(line);\n      expect(isPointingAt, true, reason: 'Line should be pointed at');\n    });\n  });\n\n  group('Polygon intersections tests', () {\n    test('simple polygon collision', () {\n      final polygonA = PolygonComponent(\n        [\n          Vector2(2, 2),\n          Vector2(3, 1),\n          Vector2(2, 0),\n          Vector2(1, 1),\n        ],\n      );\n      final polygonB = PolygonComponent(\n        [\n          Vector2(1, 2),\n          Vector2(2, 1),\n          Vector2(1, 0),\n          Vector2(0, 1),\n        ],\n      );\n      final intersections = geometry.intersections(polygonA, polygonB);\n      expect(\n        intersections.contains(Vector2(1.5, 0.5)),\n        true,\n        reason: 'Missed one intersection',\n      );\n      expect(\n        intersections.contains(Vector2(1.5, 1.5)),\n        true,\n        reason: 'Missed one intersection',\n      );\n      expect(\n        intersections.length == 2,\n        true,\n        reason: 'Wrong number of intersections',\n      );\n    });\n\n    test('collision on shared line segment', () {\n      final polygonA = PolygonComponent([\n        Vector2(1, 1),\n        Vector2(1, 2),\n        Vector2(2, 2),\n        Vector2(2, 1),\n      ]);\n      final polygonB = PolygonComponent([\n        Vector2(2, 1),\n        Vector2(2, 2),\n        Vector2(3, 2),\n        Vector2(3, 1),\n      ]);\n      final intersections = geometry.intersections(polygonA, polygonB);\n      expect(\n        intersections.containsAll([\n          Vector2(2.0, 2.0),\n          Vector2(2.0, 1.5),\n          Vector2(2.0, 1.0),\n        ]),\n        true,\n        reason: 'Does not have all the correct intersection points',\n      );\n      expect(\n        intersections.length == 3,\n        true,\n        reason: 'Wrong number of intersections',\n      );\n    });\n\n    test('one point collision', () {\n      final polygonA = PolygonComponent([\n        Vector2(1, 1),\n        Vector2(1, 2),\n        Vector2(2, 2),\n        Vector2(2, 1),\n      ]);\n      final polygonB = PolygonComponent([\n        Vector2(2, 2),\n        Vector2(2, 3),\n        Vector2(3, 3),\n        Vector2(3, 2),\n      ]);\n      final intersections = geometry.intersections(polygonA, polygonB);\n      expect(\n        intersections.contains(Vector2(2.0, 2.0)),\n        true,\n        reason: 'Does not have all the correct intersection points',\n      );\n      expect(\n        intersections.length == 1,\n        true,\n        reason: 'Wrong number of intersections',\n      );\n    });\n\n    test('collision while no corners are inside the other body', () {\n      final polygonA = PolygonComponent.relative(\n        [\n          Vector2(1, 1),\n          Vector2(1, -1),\n          Vector2(-1, -1),\n          Vector2(-1, 1),\n        ],\n        position: Vector2.zero(),\n        parentSize: Vector2(2, 4),\n      );\n      final polygonB = PolygonComponent.relative(\n        [\n          Vector2(1, 1),\n          Vector2(1, -1),\n          Vector2(-1, -1),\n          Vector2(-1, 1),\n        ],\n        position: Vector2.zero(),\n        parentSize: Vector2(4, 2),\n      );\n      final intersections = geometry.intersections(polygonA, polygonB);\n      expect(\n        intersections.containsAll([\n          Vector2(2, 0),\n          Vector2(2, 2),\n          Vector2(1, 0),\n          Vector2(0, 0),\n          Vector2(0, 1),\n          Vector2(0, 2),\n        ]),\n        true,\n        reason: 'Does not have all the correct intersection points',\n      );\n      expect(\n        intersections.length == 6,\n        true,\n        reason: 'Wrong number of intersections',\n      );\n    });\n\n    test('collision with advanced hitboxes in different quadrants', () {\n      final polygonA = PolygonComponent([\n        Vector2(0, 0),\n        Vector2(-1, 1),\n        Vector2(0, 3),\n        Vector2(2, 2),\n        Vector2(1.5, 0.5),\n      ]);\n      final polygonB = PolygonComponent([\n        Vector2(-2, -2),\n        Vector2(-3, 0),\n        Vector2(-2, 3),\n        Vector2(1, 2),\n        Vector2(2, 1),\n      ]);\n      final intersections = geometry.intersections(polygonA, polygonB);\n      intersections.containsAll([\n        Vector2(-0.2857142857142857, 2.4285714285714284),\n        Vector2(1.7500000000000002, 1.2500000000000002),\n        Vector2(1.5555555555555556, 0.6666666666666667),\n        Vector2(1.1999999999999997, 0.39999999999999997),\n      ]);\n      expect(\n        intersections.length == 4,\n        true,\n        reason: 'Wrong number of intersections',\n      );\n    });\n\n    test('Rectangle should intersect with tilted polygon', () {\n      final polygonA = RectangleComponent.square(\n        size: 5,\n        position: Vector2(-25, 0),\n        anchor: Anchor.center,\n      );\n      const bottomDisplacement = 1 / 18;\n      final polygonB = PolygonComponent([\n        Vector2(-25, -25),\n        Vector2(55, -25),\n        Vector2(55 + bottomDisplacement, 25),\n        Vector2(-25 + bottomDisplacement, 25),\n      ]);\n      final intersections = geometry.intersections(polygonA, polygonB);\n      expect(intersections, isNotEmpty);\n    });\n  });\n\n  group('Rectangle intersections tests', () {\n    test('simple intersection', () {\n      final rectangleA = RectangleComponent(\n        position: Vector2(4, 0),\n        size: Vector2.all(4),\n      );\n      final rectangleB = RectangleComponent(\n        position: Vector2.zero(),\n        size: Vector2.all(4),\n      );\n      final intersections = geometry.intersections(rectangleA, rectangleB);\n      expect(\n        intersections.containsAll([\n          Vector2(4, 0),\n          Vector2(4, 2),\n          Vector2(4, 4),\n        ]),\n        true,\n        reason: 'Missed intersections',\n      );\n      expect(\n        intersections.length == 3,\n        true,\n        reason: 'Wrong number of intersections',\n      );\n    });\n\n    test('flipped rectangle intersection', () {\n      final rectangleA = RectangleComponent(\n        position: Vector2.zero(),\n        size: Vector2.all(4),\n      );\n      final rectangleB = RectangleComponent(\n        position: Vector2.all(2),\n        size: Vector2.all(4),\n      );\n      final intersections = geometry.intersections(rectangleA, rectangleB);\n      expect(\n        intersections,\n        containsAll([\n          Vector2(2, 4),\n          Vector2(4, 2),\n        ]),\n        reason: 'Missed intersections',\n      );\n      expect(\n        intersections,\n        hasLength(2),\n        reason: 'Wrong number of intersections',\n      );\n\n      rectangleB.flipVerticallyAroundCenter();\n      final flippedIntersections = geometry.intersections(\n        rectangleA,\n        rectangleB,\n      );\n\n      expect(\n        flippedIntersections,\n        containsAll([\n          Vector2(2, 4),\n          Vector2(4, 2),\n        ]),\n        reason: 'Missed intersections',\n      );\n      expect(\n        flippedIntersections,\n        hasLength(2),\n        reason: 'Wrong number of intersections',\n      );\n\n      rectangleB.flipHorizontallyAroundCenter();\n      final doubleFlippedIntersections = geometry.intersections(\n        rectangleA,\n        rectangleB,\n      );\n\n      expect(\n        doubleFlippedIntersections,\n        containsAll([\n          Vector2(2, 4),\n          Vector2(4, 2),\n        ]),\n        reason: 'Missed intersections',\n      );\n      expect(\n        doubleFlippedIntersections,\n        hasLength(2),\n        reason: 'Wrong number of intersections',\n      );\n    });\n\n    test('vertically flipped and angled rectangle intersections', () {\n      final rectangleA = RectangleComponent(\n        position: Vector2.all(4),\n        size: Vector2.all(4),\n        angle: tau / 4,\n        anchor: Anchor.center,\n      );\n      final rectangleB = RectangleComponent(\n        position: Vector2.all(4),\n        size: Vector2.all(4),\n      );\n      final intersections = geometry.intersections(rectangleA, rectangleB);\n      expect(\n        intersections,\n        containsAll([\n          Vector2(4, 6),\n          Vector2(6, 4),\n        ]),\n        reason: 'Missed intersections',\n      );\n      expect(\n        intersections,\n        hasLength(2),\n        reason: 'Wrong number of intersections',\n      );\n\n      rectangleB.flipVerticallyAroundCenter();\n      final flippedIntersections = geometry.intersections(\n        rectangleA,\n        rectangleB,\n      );\n\n      expect(\n        flippedIntersections,\n        containsAll([\n          Vector2(4, 6),\n          Vector2(6, 4),\n        ]),\n        reason: 'Missed intersections',\n      );\n      expect(\n        flippedIntersections,\n        hasLength(2),\n        reason: 'Wrong number of intersections',\n      );\n\n      rectangleB.flipHorizontallyAroundCenter();\n      final doubleFlippedIntersections = geometry.intersections(\n        rectangleA,\n        rectangleB,\n      );\n\n      expect(\n        doubleFlippedIntersections,\n        containsAll([\n          Vector2(4, 6),\n          Vector2(6, 4),\n        ]),\n        reason: 'Missed intersections',\n      );\n      expect(\n        doubleFlippedIntersections,\n        hasLength(2),\n        reason: 'Wrong number of intersections',\n      );\n    });\n\n    testWithFlameGame(\n      'horizontally flipped and angled rectangle intersections with parents',\n      (game) async {\n        final rectangleA = RectangleHitbox();\n        final rectangleB = RectangleHitbox();\n        final parentA = RectangleComponent(\n          position: Vector2(1, 0),\n          size: Vector2.all(4),\n          children: [rectangleA],\n        )..flipHorizontallyAroundCenter();\n        final parentB = RectangleComponent(\n          position: Vector2(2, 1),\n          size: Vector2.all(4),\n          children: [rectangleB],\n        );\n        await game.ensureAddAll([parentA, parentB]);\n\n        final intersections = geometry.intersections(rectangleA, rectangleB);\n        expect(\n          intersections,\n          containsAll([\n            Vector2(5, 1),\n            Vector2(2, 4),\n          ]),\n          reason: 'Missed intersections',\n        );\n        expect(\n          intersections,\n          hasLength(2),\n          reason: 'Wrong number of intersections',\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'flipped and angled rectangle intersections with parents',\n      (game) async {\n        final rectangleA = RectangleHitbox();\n        final rectangleB = RectangleHitbox();\n        final parentA = RectangleComponent(\n          position: Vector2(1, 0),\n          size: Vector2.all(4),\n          children: [rectangleA],\n        )..flipVerticallyAroundCenter();\n        final parentB = RectangleComponent(\n          position: Vector2(2, 1),\n          size: Vector2.all(4),\n          children: [rectangleB],\n        );\n        await game.ensureAddAll([parentA, parentB]);\n\n        final intersections = geometry.intersections(rectangleA, rectangleB);\n        expect(\n          intersections,\n          containsAll([\n            Vector2(5, 1),\n            Vector2(2, 4),\n          ]),\n          reason: 'Missed intersections',\n        );\n        expect(\n          intersections,\n          hasLength(2),\n          reason: 'Wrong number of intersections',\n        );\n      },\n    );\n  });\n\n  group('Circle intersections tests', () {\n    test('simple collision', () {\n      final circleA = CircleComponent(radius: 2.0, position: Vector2(4, 0));\n      final circleB = CircleComponent(radius: 2.0, position: Vector2.zero());\n      final intersections = geometry.intersections(circleA, circleB);\n      expect(\n        intersections.contains(Vector2(4, 2)),\n        true,\n        reason: 'Missed one intersection',\n      );\n      expect(\n        intersections.length == 1,\n        true,\n        reason: 'Wrong number of intersections',\n      );\n    });\n\n    test('two point collision', () {\n      final circleA = CircleComponent(radius: 2.0, position: Vector2(2, -2));\n      final circleB = CircleComponent(radius: 2.0, position: Vector2(0, -2));\n      final intersections = geometry.intersections(circleA, circleB).toList();\n      expect(\n        intersections.contains(Vector2(3.0, -1.7320508075688772)),\n        true,\n        reason: 'Missed one intersection',\n      );\n      expect(\n        intersections.contains(Vector2(3.0, 1.7320508075688772)),\n        true,\n        reason: 'Missed one intersection',\n      );\n      expect(\n        intersections.length == 2,\n        true,\n        reason: 'Wrong number of intersections',\n      );\n    });\n\n    test('same size and position', () {\n      final circleA = CircleComponent(radius: 4.0, position: Vector2.all(3));\n      final circleB = CircleComponent(radius: 4.0, position: Vector2.all(3));\n      final intersections = geometry.intersections(circleA, circleB);\n      expect(\n        intersections.containsAll([\n          Vector2(11, 7),\n          Vector2(7, 3),\n          Vector2(3, 7),\n          Vector2(7, 11),\n        ]),\n        true,\n        reason: 'Missed intersections',\n      );\n      expect(\n        intersections.length == 4,\n        true,\n        reason: 'Wrong number of intersections',\n      );\n    });\n\n    test('not overlapping', () {\n      final circleA = CircleComponent(\n        radius: 2.0,\n        position: Vector2.all(-1),\n        anchor: Anchor.center,\n      );\n      final circleB = CircleComponent(\n        radius: 2.0,\n        position: Vector2.all(1.83),\n        anchor: Anchor.center,\n      );\n      final intersections = geometry.intersections(circleA, circleB);\n      expect(\n        intersections.isEmpty,\n        true,\n        reason: 'Should not have any intersections',\n      );\n    });\n\n    test('in third quadrant', () {\n      final circleA = CircleComponent(\n        radius: 1.0,\n        position: Vector2.all(-1),\n        anchor: Anchor.center,\n      );\n      final circleB = CircleComponent(\n        radius: 1.0,\n        position: Vector2.all(-2),\n        anchor: Anchor.center,\n      );\n      final intersections = geometry.intersections(circleA, circleB).toList();\n      expect(\n        intersections.any((v) => v.distanceTo(Vector2(-1, -2)) < 0.000001),\n        true,\n      );\n      expect(\n        intersections.any((v) => v.distanceTo(Vector2(-2, -1)) < 0.000001),\n        true,\n      );\n      expect(\n        intersections.length == 2,\n        true,\n        reason: 'Wrong number of intersections',\n      );\n    });\n\n    test('in different quadrants', () {\n      final circleA = CircleComponent(\n        radius: 2.0,\n        position: Vector2.all(-1),\n        anchor: Anchor.center,\n      );\n      final circleB = CircleComponent(\n        radius: 2.0,\n        position: Vector2.all(1),\n        anchor: Anchor.center,\n      );\n      final intersections = geometry.intersections(circleA, circleB).toList();\n      expect(\n        intersections.any((v) => v.distanceTo(Vector2(1, -1)) < 0.000001),\n        true,\n      );\n      expect(\n        intersections.any((v) => v.distanceTo(Vector2(-1, 1)) < 0.000001),\n        true,\n      );\n      expect(\n        intersections.length == 2,\n        true,\n        reason: 'Wrong number of intersections',\n      );\n    });\n  });\n\n  group('Circle-Polygon intersections tests', () {\n    test('simple circle-polygon intersection', () {\n      final circle = CircleComponent(\n        radius: 1.0,\n        position: Vector2.zero(),\n        anchor: Anchor.center,\n      );\n      final polygon = PolygonComponent(\n        [\n          Vector2(1, 2),\n          Vector2(2, 1),\n          Vector2(1, 0),\n          Vector2(0, 1),\n        ],\n        anchor: Anchor.center,\n      );\n      final intersections = geometry.intersections(circle, polygon);\n      expect(\n        intersections.containsAll([Vector2(0, 1), Vector2(1, 0)]),\n        true,\n        reason: 'Missed intersections',\n      );\n      expect(intersections.length, 2, reason: 'Wrong number of intersections');\n    });\n\n    test('single point circle-polygon intersection', () {\n      final circle = CircleComponent(\n        radius: 1.0,\n        position: Vector2(-1, 1),\n        anchor: Anchor.center,\n      );\n      final polygon = PolygonComponent([\n        Vector2(1, 2),\n        Vector2(2, 1),\n        Vector2(1, 0),\n        Vector2(0, 1),\n      ]);\n      final intersections = geometry.intersections(circle, polygon);\n      expect(\n        intersections.contains(Vector2(0, 1)),\n        true,\n        reason: 'Missed intersections',\n      );\n      expect(\n        intersections.length == 1,\n        true,\n        reason: 'Wrong number of intersections',\n      );\n    });\n\n    test('four point circle-polygon intersection', () {\n      final circle = CircleComponent(\n        radius: 1.0,\n        position: Vector2.all(1),\n        anchor: Anchor.center,\n      );\n      final polygon = PolygonComponent([\n        Vector2(1, 2),\n        Vector2(2, 1),\n        Vector2(1, 0),\n        Vector2(0, 1),\n      ]);\n      final intersections = geometry.intersections(circle, polygon);\n      expect(\n        intersections.containsAll([\n          Vector2(1, 2),\n          Vector2(2, 1),\n          Vector2(1, 0),\n          Vector2(0, 1),\n        ]),\n        true,\n        reason: 'Missed intersections',\n      );\n      expect(\n        intersections.length == 4,\n        true,\n        reason: 'Wrong number of intersections',\n      );\n    });\n\n    test('polygon within circle, no intersections', () {\n      final circle = CircleComponent(\n        radius: 1.1,\n        position: Vector2.all(1),\n        anchor: Anchor.center,\n      );\n      final polygon = PolygonComponent([\n        Vector2(1, 2),\n        Vector2(2, 1),\n        Vector2(1, 0),\n        Vector2(0, 1),\n      ]);\n      final intersections = geometry.intersections(circle, polygon);\n      expect(\n        intersections.isEmpty,\n        true,\n        reason: 'Should not be any intersections',\n      );\n    });\n  });\n\n  group('Solid intersections', () {\n    test('solid circle enclosed by solid circle', () {\n      final outerCircle = CircleComponent(\n        radius: 4.0,\n        anchor: Anchor.center,\n      )..isSolid = true;\n      final innerCircle = CircleComponent(\n        radius: 2.0,\n        anchor: Anchor.center,\n      )..isSolid = true;\n      final intersections = geometry.intersections(outerCircle, innerCircle);\n      expect(\n        intersections,\n        {innerCircle.position},\n        reason: \"Should return the enclosed circle's position\",\n      );\n    });\n\n    test('solid circle enclosed by hollow circle', () {\n      final outerCircle = CircleComponent(\n        radius: 4.0,\n        anchor: Anchor.center,\n      );\n      final innerCircle = CircleComponent(\n        radius: 2.0,\n        anchor: Anchor.center,\n      )..isSolid = true;\n      final intersections = geometry.intersections(outerCircle, innerCircle);\n      expect(\n        intersections.isEmpty,\n        isTrue,\n        reason: 'Should not contain any intersection',\n      );\n    });\n\n    test('hollow circle enclosed by solid circle', () {\n      final outerCircle = CircleComponent(\n        radius: 4.0,\n        anchor: Anchor.center,\n      )..isSolid = true;\n      final innerCircle = CircleComponent(\n        radius: 2.0,\n        anchor: Anchor.center,\n      );\n      final intersections = geometry.intersections(outerCircle, innerCircle);\n      expect(\n        intersections,\n        {innerCircle.position},\n        reason: \"Should return the enclosed circle's position\",\n      );\n    });\n\n    test('solid rectangle enclosed by solid rectangle', () {\n      final outerRectangle = RectangleComponent(\n        size: Vector2.all(4),\n        anchor: Anchor.center,\n      )..isSolid = true;\n      final innerRectangle = RectangleComponent(\n        size: Vector2.all(2),\n        anchor: Anchor.center,\n      )..isSolid = true;\n      final intersections = geometry.intersections(\n        outerRectangle,\n        innerRectangle,\n      );\n      expect(\n        intersections,\n        {innerRectangle.position},\n        reason: \"Should return the enclosed rectangle's position\",\n      );\n    });\n\n    test('solid rectangle enclosed by hollow rectangle', () {\n      final outerRectangle = RectangleComponent(\n        size: Vector2.all(4),\n        anchor: Anchor.center,\n      );\n      final innerRectangle = RectangleComponent(\n        size: Vector2.all(2),\n        anchor: Anchor.center,\n      )..isSolid = true;\n      final intersections = geometry.intersections(\n        outerRectangle,\n        innerRectangle,\n      );\n      expect(\n        intersections.isEmpty,\n        isTrue,\n        reason: 'Should not contain any intersection',\n      );\n    });\n\n    test('hollow rectangle enclosed by solid rectangle', () {\n      final outerRectangle = RectangleComponent(\n        size: Vector2.all(4),\n        anchor: Anchor.center,\n      )..isSolid = true;\n      final innerRectangle = RectangleComponent(\n        size: Vector2.all(2),\n        anchor: Anchor.center,\n      );\n      final intersections = geometry.intersections(\n        outerRectangle,\n        innerRectangle,\n      );\n      expect(\n        intersections,\n        {innerRectangle.position},\n        reason: \"Should return the enclosed rectangle's position\",\n      );\n    });\n\n    test('solid rectangle with intersections by other solid rectangle', () {\n      final rectangleA = RectangleComponent(\n        position: Vector2.all(1),\n        size: Vector2.all(2),\n      )..isSolid = true;\n      final rectangleB = RectangleComponent(\n        size: Vector2.all(2),\n      )..isSolid = true;\n      final intersections = geometry.intersections(rectangleA, rectangleB);\n      expect(\n        intersections,\n        {Vector2(1, 2), Vector2(2, 1)},\n        reason: \"Should return the enclosed rectangle's position\",\n      );\n    });\n\n    testCollisionDetectionGame('circles as children', (game) async {\n      final world = game.world;\n      final outerCircle = CircleHitbox(\n        radius: 4.0,\n        anchor: Anchor.center,\n      )..isSolid = true;\n      final outerContainer = PositionComponent(\n        position: Vector2.all(100),\n        children: [outerCircle],\n      );\n      final innerCircle = CircleHitbox(\n        radius: 2.0,\n        anchor: Anchor.center,\n      )..isSolid = true;\n      final innerContainer = PositionComponent(\n        position: Vector2.all(100),\n        children: [innerCircle],\n      );\n      await world.ensureAddAll([outerContainer, innerContainer]);\n      final intersections = game.collisionDetection.intersections(\n        innerCircle,\n        outerCircle,\n      );\n      expect(\n        intersections,\n        {innerCircle.absolutePosition},\n        reason: \"Should return the enclosed circle's position\",\n      );\n    });\n\n    test('solid circle enclosed by solid polygon', () {\n      final outerPolygon = PolygonComponent(\n        [\n          Vector2(5, 1),\n          Vector2(1, 1),\n          Vector2(1, 5),\n          Vector2(6, 5),\n        ],\n      )..isSolid = true;\n      final innerCircle = CircleComponent(\n        position: Vector2.all(3),\n        radius: 1.5,\n        anchor: Anchor.center,\n      )..isSolid = true;\n      final intersections = geometry.intersections(outerPolygon, innerCircle);\n      expect(\n        intersections,\n        {innerCircle.position},\n        reason: \"Should return the enclosed circle's position\",\n      );\n    });\n\n    test('solid circle enclosed by hollow polygon', () {\n      final outerPolygon = PolygonComponent(\n        [\n          Vector2(5, 1),\n          Vector2(1, 1),\n          Vector2(1, 5),\n          Vector2(6, 5),\n        ],\n      );\n      final innerCircle = CircleComponent(\n        position: Vector2.all(3),\n        radius: 1.5,\n        anchor: Anchor.center,\n      )..isSolid = true;\n      final intersections = geometry.intersections(outerPolygon, innerCircle);\n      expect(\n        intersections.isEmpty,\n        isTrue,\n        reason: 'Should not contain any intersection',\n      );\n    });\n\n    test('hollow circle enclosed by solid polygon', () {\n      final outerPolygon = PolygonComponent(\n        [\n          Vector2(5, 1),\n          Vector2(1, 1),\n          Vector2(1, 5),\n          Vector2(6, 5),\n        ],\n      )..isSolid = true;\n      final innerCircle = CircleComponent(\n        position: Vector2.all(3),\n        radius: 1.5,\n        anchor: Anchor.center,\n      );\n      final intersections = geometry.intersections(outerPolygon, innerCircle);\n      expect(\n        intersections,\n        {innerCircle.position},\n        reason: \"Should return the enclosed circle's position\",\n      );\n    });\n\n    test('solid polygon enclosed by solid circle', () {\n      final outerCircle = CircleComponent(\n        position: Vector2.all(3),\n        radius: 3,\n        anchor: Anchor.center,\n      )..isSolid = true;\n      final innerPolygon = PolygonComponent(\n        [\n          Vector2(4, 2),\n          Vector2(2, 2),\n          Vector2(2, 4),\n          Vector2(4.5, 4.5),\n        ],\n        anchor: Anchor.center,\n      )..isSolid = true;\n      final intersections = geometry.intersections(outerCircle, innerPolygon);\n      expect(\n        intersections,\n        {innerPolygon.position},\n        reason: \"Should return the enclosed polygon's position\",\n      );\n    });\n\n    test('solid polygon enclosed by hollow circle', () {\n      final outerCircle = CircleComponent(\n        position: Vector2.all(3),\n        radius: 3,\n        anchor: Anchor.center,\n      );\n      final innerPolygon = PolygonComponent(\n        [\n          Vector2(4, 2),\n          Vector2(2, 2),\n          Vector2(2, 4),\n          Vector2(4.5, 4.5),\n        ],\n        anchor: Anchor.center,\n      )..isSolid = true;\n      final intersections = geometry.intersections(outerCircle, innerPolygon);\n      expect(\n        intersections.isEmpty,\n        isTrue,\n        reason: 'Should not contain any intersection',\n      );\n    });\n\n    test('hollow polygon enclosed by solid circle', () {\n      final outerCircle = CircleComponent(\n        position: Vector2.all(3),\n        radius: 3,\n        anchor: Anchor.center,\n      )..isSolid = true;\n      final innerPolygon = PolygonComponent(\n        [\n          Vector2(4, 2),\n          Vector2(2, 2),\n          Vector2(2, 4),\n          Vector2(4.5, 4.5),\n        ],\n        anchor: Anchor.center,\n      );\n      final intersections = geometry.intersections(outerCircle, innerPolygon);\n      expect(\n        intersections,\n        {innerPolygon.position},\n        reason: \"Should return the enclosed polygon's position\",\n      );\n    });\n\n    testWithGame<_CollisionDetectionGame>(\n      'circle enclosed by solid polygon defined in clockwise (wrong) order',\n      _CollisionDetectionGame.new,\n      (game) async {\n        final world = game.world;\n        final polygonSize = Vector2.all(3);\n        final innerCircle = CircleHitbox();\n        final outerPolygon = PolygonHitbox.relative(\n          [\n            Vector2(0.5, 1.0),\n            Vector2(-0.5, 1.0),\n            Vector2(-1.0, 0.5),\n            Vector2(-1.0, -0.5),\n            Vector2(-0.5, -1.0),\n            Vector2(0.5, -1.0),\n            Vector2(1.0, -0.5),\n            Vector2(1.0, 0.5),\n          ],\n          parentSize: polygonSize,\n        )..isSolid = true;\n        await world.ensureAddAll([\n          PositionComponent(\n            position: Vector2.all(3),\n            size: Vector2.all(1),\n            anchor: Anchor.center,\n            children: [innerCircle],\n          ),\n          PositionComponent(\n            position: Vector2.all(3),\n            size: polygonSize,\n            anchor: Anchor.center,\n            children: [outerPolygon],\n          ),\n        ]);\n        final intersections = game.collisionDetection.intersections(\n          innerCircle,\n          outerPolygon,\n        );\n        expect(\n          intersections.isNotEmpty,\n          isTrue,\n          reason: \"Should return the enclosed circle's position\",\n        );\n      },\n    );\n  });\n\n  group('Raycasting', () {\n    runCollisionTestRegistry({\n      'one hitbox': (collisionSystem) async {\n        final game = collisionSystem as FlameGame;\n        final world = game.world;\n        await world.ensureAdd(\n          PositionComponent(\n            children: [RectangleHitbox()],\n            position: Vector2(100, 0),\n            size: Vector2.all(100),\n            anchor: Anchor.center,\n          ),\n        );\n        await game.ready();\n\n        final ray = Ray2(\n          origin: Vector2.zero(),\n          direction: Vector2(1, 0),\n        );\n        final result = collisionSystem.collisionDetection.raycast(ray);\n        expect(result?.hitbox?.parent, world.children.first);\n        expect(result?.reflectionRay?.origin, closeToVector(Vector2(50, 0)));\n        expect(result?.reflectionRay?.direction, closeToVector(Vector2(-1, 0)));\n      },\n      'multiple hitboxes after each other': (collisionSystem) async {\n        final game = collisionSystem as FlameGame;\n        final world = game.world;\n        await world.ensureAddAll([\n          for (var i = 0.0; i < 10; i++)\n            PositionComponent(\n              position: Vector2.all(100 + i * 10),\n              size: Vector2.all(20 - i),\n              anchor: Anchor.center,\n            )..add(RectangleHitbox()),\n        ]);\n        await game.ready();\n        final ray = Ray2(\n          origin: Vector2.zero(),\n          direction: Vector2.all(1)..normalize(),\n        );\n        final result = collisionSystem.collisionDetection.raycast(ray);\n        expect(result?.hitbox?.parent, world.children.first);\n        expect(result?.reflectionRay?.origin, closeToVector(Vector2.all(90)));\n        expect(\n          result?.reflectionRay?.direction,\n          closeToVector(Vector2(-1, 1)..normalize()),\n        );\n      },\n      'multiple hitboxes after each other with one ignored':\n          (collisionSystem) async {\n            final game = collisionSystem as FlameGame;\n            final world = game.world;\n            await world.ensureAddAll([\n              for (var i = 0.0; i < 10; i++)\n                PositionComponent(\n                  position: Vector2.all(100 + i * 10),\n                  size: Vector2.all(20 - i),\n                  anchor: Anchor.center,\n                )..add(RectangleHitbox()),\n            ]);\n            await game.ready();\n            final ray = Ray2(\n              origin: Vector2.zero(),\n              direction: Vector2.all(1)..normalize(),\n            );\n            final result = collisionSystem.collisionDetection.raycast(\n              ray,\n              ignoreHitboxes: [\n                world.children.first.children.first as ShapeHitbox,\n              ],\n            );\n            expect(result?.hitbox?.parent, game.world.children.toList()[1]);\n            expect(\n              result?.reflectionRay?.origin,\n              closeToVector(Vector2.all(100.5)),\n            );\n            expect(\n              result?.reflectionRay?.direction,\n              closeToVector(Vector2(-1, 1)..normalize()),\n            );\n          },\n      'multiple hitboxes after each other with filter':\n          (collisionSystem) async {\n            final game = collisionSystem as FlameGame;\n            final world = game.world;\n            await world.ensureAddAll([\n              for (var i = 0.0; i < 10; i++)\n                PositionComponent(\n                  position: Vector2.all(100 + i * 10),\n                  size: Vector2.all(20 - i),\n                  anchor: Anchor.center,\n                )..add(RectangleHitbox()),\n            ]);\n            await game.ready();\n            final ray = Ray2(\n              origin: Vector2.zero(),\n              direction: Vector2.all(1)..normalize(),\n            );\n            final result = collisionSystem.collisionDetection.raycast(\n              ray,\n              hitboxFilter: (hitbox) => hitbox.parent != world.children.first,\n            );\n            expect(result?.hitbox?.parent, game.world.children.toList()[1]);\n            expect(\n              result?.reflectionRay?.origin,\n              closeToVector(Vector2.all(100.5)),\n            );\n            expect(\n              result?.reflectionRay?.direction,\n              closeToVector(Vector2(-1, 1)..normalize()),\n            );\n          },\n      'ray with origin on hitbox corner': (collisionSystem) async {\n        final game = collisionSystem as FlameGame;\n        final world = game.world;\n        await world.ensureAddAll([\n          PositionComponent(\n            position: Vector2.all(10),\n            size: Vector2.all(10),\n          )..add(RectangleHitbox()),\n        ]);\n        await game.ready();\n        final ray = Ray2(\n          origin: Vector2.all(10),\n          direction: Vector2.all(1)..normalize(),\n        );\n        final result = collisionSystem.collisionDetection.raycast(ray);\n        expect(result?.hitbox?.parent, world.children.first);\n        expect(result?.reflectionRay?.origin, closeToVector(Vector2(20, 20)));\n        expect(\n          result?.reflectionRay?.direction,\n          closeToVector(Vector2(1, -1)..normalize()),\n        );\n      },\n      'raycast with maxDistance': (collisionSystem) async {\n        final game = collisionSystem as FlameGame;\n        final world = game.world;\n        await world.ensureAddAll([\n          PositionComponent(\n            position: Vector2.all(20),\n            size: Vector2.all(40),\n            children: [RectangleHitbox()],\n          ),\n        ]);\n        await game.ready();\n        final ray = Ray2(\n          origin: Vector2.all(10),\n          direction: Vector2.all(1)..normalize(),\n        );\n\n        final result = RaycastResult<ShapeHitbox>();\n\n        // No hit cast\n        collisionSystem.collisionDetection.raycast(\n          ray,\n          maxDistance: Vector2.all(9).length,\n          out: result,\n        );\n        expect(result.hitbox?.parent, isNull);\n\n        // Extended cast\n        collisionSystem.collisionDetection.raycast(\n          ray,\n          maxDistance: Vector2.all(nextFloat32(10)).length,\n          out: result,\n        );\n        expect(result.hitbox?.parent, world.children.first);\n      },\n    });\n\n    group('Rectangle hitboxes', () {\n      runCollisionTestRegistry({\n        'ray from within RectangleHitbox': (collisionSystem) async {\n          final game = collisionSystem as FlameGame;\n          final world = game.world;\n          await world.ensureAddAll([\n            PositionComponent(\n              position: Vector2.all(0),\n              size: Vector2.all(10),\n            )..add(RectangleHitbox()),\n          ]);\n          await game.ready();\n          final ray = Ray2(\n            origin: Vector2.all(5),\n            direction: Vector2.all(1)..normalize(),\n          );\n          final result = collisionSystem.collisionDetection.raycast(ray);\n          expect(result?.hitbox?.parent, world.children.first);\n          expect(result?.normal, closeToVector(Vector2(0, -1)));\n          expect(result?.reflectionRay?.origin, closeToVector(Vector2(10, 10)));\n          expect(\n            result?.reflectionRay?.direction,\n            closeToVector(Vector2(1, -1)..normalize()),\n          );\n        },\n        'ray from the left of RectangleHitbox': (collisionSystem) async {\n          final game = collisionSystem as FlameGame;\n          final world = game.world;\n          await world.ensureAddAll([\n            PositionComponent(\n              position: Vector2.zero(),\n              size: Vector2.all(10),\n            )..add(RectangleHitbox()),\n          ]);\n          await game.ready();\n          final ray = Ray2(origin: Vector2(-5, 5), direction: Vector2(1, 0));\n          final result = collisionSystem.collisionDetection.raycast(ray);\n          expect(result?.hitbox?.parent, world.children.first);\n          expect(result?.reflectionRay?.origin, closeToVector(Vector2(0, 5)));\n          expect(\n            result?.reflectionRay?.direction,\n            closeToVector(Vector2(-1, 0)),\n          );\n        },\n        'ray from the top of RectangleHitbox': (collisionSystem) async {\n          final game = collisionSystem as FlameGame;\n          final world = game.world;\n          await world.ensureAddAll([\n            PositionComponent(\n              position: Vector2.zero(),\n              size: Vector2.all(10),\n            )..add(RectangleHitbox()),\n          ]);\n          await game.ready();\n          final ray = Ray2(origin: Vector2(5, -5), direction: Vector2(0, 1));\n          final result = collisionSystem.collisionDetection.raycast(ray);\n          expect(result?.hitbox?.parent, world.children.first);\n          expect(result?.reflectionRay?.origin, closeToVector(Vector2(5, 0)));\n          expect(\n            result?.reflectionRay?.direction,\n            closeToVector(Vector2(0, -1)),\n          );\n        },\n        'ray from the right of RectangleHitbox': (collisionSystem) async {\n          final game = collisionSystem as FlameGame;\n          final world = game.world;\n          await world.ensureAddAll([\n            PositionComponent(\n              position: Vector2.zero(),\n              size: Vector2.all(10),\n            )..add(RectangleHitbox()),\n          ]);\n          await game.ready();\n          final ray = Ray2(origin: Vector2(15, 5), direction: Vector2(-1, 0));\n          final result = collisionSystem.collisionDetection.raycast(ray);\n          expect(result?.hitbox?.parent, world.children.first);\n          expect(result?.reflectionRay?.origin, closeToVector(Vector2(10, 5)));\n          expect(\n            result?.reflectionRay?.direction,\n            closeToVector(Vector2(1, 0)),\n          );\n        },\n        'ray from the bottom of RectangleHitbox': (collisionSystem) async {\n          final game = collisionSystem as FlameGame;\n          final world = game.world;\n          await world.ensureAddAll([\n            PositionComponent(\n              position: Vector2.zero(),\n              size: Vector2.all(10),\n            )..add(RectangleHitbox()),\n          ]);\n          await game.ready();\n          final ray = Ray2(origin: Vector2(5, 15), direction: Vector2(0, -1));\n          final result = collisionSystem.collisionDetection.raycast(ray);\n          expect(result?.hitbox?.parent, world.children.first);\n          expect(result?.reflectionRay?.origin, closeToVector(Vector2(5, 10)));\n          expect(\n            result?.reflectionRay?.direction,\n            closeToVector(Vector2(0, 1)),\n          );\n        },\n      });\n    });\n\n    group('Circle hitboxes', () {\n      runCollisionTestRegistry({\n        'ray from top to bottom within CircleHitbox': (collisionSystem) async {\n          final game = collisionSystem as FlameGame;\n          final world = game.world;\n          await world.ensureAddAll([\n            PositionComponent(\n              position: Vector2.zero(),\n              size: Vector2.all(10),\n            )..add(CircleHitbox()),\n          ]);\n          await game.ready();\n          final ray = Ray2(origin: Vector2(5, 4), direction: Vector2(0, 1));\n          final result = collisionSystem.collisionDetection.raycast(ray);\n          expect(result?.hitbox?.parent, world.children.first);\n          expect(result?.normal, closeToVector(Vector2(0, -1)));\n          expect(result?.reflectionRay?.origin, closeToVector(Vector2(5, 10)));\n          expect(\n            result?.reflectionRay?.direction,\n            closeToVector(Vector2(0, -1)),\n          );\n        },\n        'ray from bottom-right to top-left within CircleHitbox':\n            (collisionSystem) async {\n              final game = collisionSystem as FlameGame;\n              final world = game.world;\n              await world.ensureAddAll([\n                PositionComponent(\n                  position: Vector2.zero(),\n                  size: Vector2.all(10),\n                )..add(CircleHitbox()),\n              ]);\n              await game.ready();\n              final ray = Ray2(\n                origin: Vector2.all(6),\n                direction: Vector2.all(-1)..normalize(),\n              );\n              final result = collisionSystem.collisionDetection.raycast(ray);\n              expect(result?.hitbox?.parent, world.children.first);\n              expect(\n                result?.normal,\n                closeToVector(Vector2.all(0.707106781186547)),\n              );\n              expect(\n                result?.intersectionPoint,\n                closeToVector(Vector2.all(1.4644660940672631)),\n              );\n              expect(\n                result?.reflectionRay?.direction,\n                closeToVector(Vector2.all(1)..normalize(), 10e-8),\n              );\n            },\n        'ray from bottom within CircleHitbox going down':\n            (collisionSystem) async {\n              final game = collisionSystem as FlameGame;\n              final world = game.world;\n              await world.ensureAddAll([\n                PositionComponent(\n                  position: Vector2.zero(),\n                  size: Vector2.all(10),\n                )..add(CircleHitbox()),\n              ]);\n              await game.ready();\n              final direction = Vector2(0, 1);\n              final ray = Ray2(origin: Vector2(5, 6), direction: direction);\n              final result = collisionSystem.collisionDetection.raycast(ray);\n              expect(result?.hitbox?.parent, world.children.first);\n              expect(result?.normal, closeToVector(Vector2(0, -1)));\n              expect(\n                result?.intersectionPoint,\n                closeToVector(Vector2(5, 10)),\n              );\n              expect(\n                result?.reflectionRay?.direction,\n                closeToVector(direction.inverted()),\n              );\n            },\n        'ray from the left of CircleHitbox': (collisionSystem) async {\n          final game = collisionSystem as FlameGame;\n          final world = game.world;\n          await world.ensureAddAll([\n            PositionComponent(\n              position: Vector2.zero(),\n              size: Vector2.all(10),\n            )..add(CircleHitbox()),\n          ]);\n          await game.ready();\n          final ray = Ray2(origin: Vector2(-5, 5), direction: Vector2(1, 0));\n          final result = collisionSystem.collisionDetection.raycast(ray);\n          expect(result?.hitbox?.parent, world.children.first);\n          expect(result?.reflectionRay?.origin, closeToVector(Vector2(0, 5)));\n          expect(\n            result?.reflectionRay?.direction,\n            closeToVector(Vector2(-1, 0)),\n          );\n        },\n        'ray from the top of CircleHitbox': (collisionSystem) async {\n          final game = collisionSystem as FlameGame;\n          final world = game.world;\n          await world.ensureAddAll([\n            PositionComponent(\n              position: Vector2.zero(),\n              size: Vector2.all(10),\n            )..add(CircleHitbox()),\n          ]);\n          await game.ready();\n          final ray = Ray2(origin: Vector2(5, -5), direction: Vector2(0, 1));\n          final result = collisionSystem.collisionDetection.raycast(ray);\n          expect(result?.hitbox?.parent, world.children.first);\n          expect(result?.reflectionRay?.origin, closeToVector(Vector2(5, 0)));\n          expect(\n            result?.reflectionRay?.direction,\n            closeToVector(Vector2(0, -1)),\n          );\n        },\n        'ray from the right of CircleHitbox': (collisionSystem) async {\n          final game = collisionSystem as FlameGame;\n          final world = game.world;\n          await world.ensureAddAll([\n            PositionComponent(\n              position: Vector2.zero(),\n              size: Vector2.all(10),\n            )..add(CircleHitbox()),\n          ]);\n          await game.ready();\n          final ray = Ray2(origin: Vector2(15, 5), direction: Vector2(-1, 0));\n          final result = collisionSystem.collisionDetection.raycast(ray);\n          expect(result?.hitbox?.parent, world.children.first);\n          expect(result?.reflectionRay?.origin, closeToVector(Vector2(10, 5)));\n          expect(\n            result?.reflectionRay?.direction,\n            closeToVector(Vector2(1, 0)),\n          );\n        },\n        'ray from the bottom of CircleHitbox': (collisionSystem) async {\n          final game = collisionSystem as FlameGame;\n          final world = game.world;\n          await world.ensureAddAll([\n            PositionComponent(\n              position: Vector2.zero(),\n              size: Vector2.all(10),\n            )..add(CircleHitbox()),\n          ]);\n          await game.ready();\n          final ray = Ray2(origin: Vector2(5, 15), direction: Vector2(0, -1));\n          final result = collisionSystem.collisionDetection.raycast(ray);\n          expect(result?.hitbox?.parent, world.children.first);\n          expect(result?.reflectionRay?.origin, closeToVector(Vector2(5, 10)));\n          expect(\n            result?.reflectionRay?.direction,\n            closeToVector(Vector2(0, 1)),\n          );\n        },\n        'ray from the center of CircleHitbox': (collisionSystem) async {\n          final game = collisionSystem as FlameGame;\n          final world = game.world;\n          final positionComponent = PositionComponent(\n            position: Vector2.zero(),\n            size: Vector2.all(10),\n          )..add(CircleHitbox());\n          await world.ensureAdd(positionComponent);\n\n          await game.ready();\n          final ray = Ray2(\n            origin: positionComponent.absoluteCenter,\n            direction: Vector2(0, -1),\n          );\n          final result = collisionSystem.collisionDetection.raycast(ray);\n          expect(result?.hitbox?.parent, positionComponent);\n          expect(result?.reflectionRay?.origin, closeToVector(Vector2(5, 0)));\n          expect(\n            result?.reflectionRay?.direction,\n            closeToVector(Vector2(0, 1)),\n          );\n        },\n        'ray from slightly outside of the CircleHitbox should not be counted '\n            'as inside': (collisionSystem) async {\n          final game = collisionSystem as FlameGame;\n          final world = game.world;\n          final positionComponent = PositionComponent(\n            position: Vector2.zero(),\n            anchor: Anchor.center,\n            size: Vector2.all(120),\n          )..add(CircleHitbox());\n          await world.ensureAdd(positionComponent);\n          await game.ready();\n          final ray = Ray2(\n            origin: Vector2(-38.06044293218409, -48.5986651724067),\n            direction: Vector2(0.927474693393028, -0.3738859359691247),\n          );\n          final result = collisionSystem.collisionDetection.raycast(ray);\n          expect(result?.hitbox?.parent, positionComponent);\n          expect(result?.isInsideHitbox, isFalse);\n        },\n      });\n    });\n\n    group('raycastAll', () {\n      runCollisionTestRegistry({\n        'All directions and all hits': (collisionSystem) async {\n          final game = collisionSystem as FlameGame;\n          final world = game.world;\n          await world.ensureAddAll([\n            PositionComponent(\n              position: Vector2(10, 0),\n              size: Vector2.all(10),\n            )..add(RectangleHitbox()),\n            PositionComponent(\n              position: Vector2(20, 10),\n              size: Vector2.all(10),\n            )..add(RectangleHitbox()),\n            PositionComponent(\n              position: Vector2(10, 20),\n              size: Vector2.all(10),\n            )..add(RectangleHitbox()),\n            PositionComponent(\n              position: Vector2(0, 10),\n              size: Vector2.all(10),\n            )..add(RectangleHitbox()),\n          ]);\n          await game.ready();\n          final origin = Vector2.all(15);\n          final results = collisionSystem.collisionDetection.raycastAll(\n            origin,\n            numberOfRays: 4,\n          );\n          expect(results.every((r) => r.isActive), isTrue);\n          expect(results.length, 4);\n        },\n        'raycastAll with maxDistance': (collisionSystem) async {\n          final game = collisionSystem as FlameGame;\n          final world = game.world;\n          await world.ensureAddAll([\n            PositionComponent(\n              position: Vector2(10, 0),\n              size: Vector2.all(10),\n            )..add(RectangleHitbox()),\n            PositionComponent(\n              position: Vector2(20, 10),\n              size: Vector2.all(10),\n            )..add(RectangleHitbox()),\n            PositionComponent(\n              position: Vector2(10, 20),\n              size: Vector2.all(10),\n            )..add(RectangleHitbox()),\n            PositionComponent(\n              position: Vector2(0, 10),\n              size: Vector2.all(10),\n            )..add(RectangleHitbox()),\n          ]);\n          await game.ready();\n          final origin = Vector2.all(15);\n\n          // No hit\n          final results1 = collisionSystem.collisionDetection.raycastAll(\n            origin,\n            maxDistance: 4,\n            numberOfRays: 4,\n          );\n          expect(results1.length, isZero);\n\n          // Hit all four\n          final results2 = collisionSystem.collisionDetection.raycastAll(\n            origin,\n            maxDistance: 5,\n            numberOfRays: 4,\n          );\n          expect(results2.length, 4);\n        },\n      });\n    });\n\n    runCollisionTestRegistry({\n      'All directions and all hits': (collisionSystem) async {\n        final game = collisionSystem as FlameGame;\n        final world = game.world;\n        await world.ensureAddAll([\n          PositionComponent(\n            position: Vector2(10, 0),\n            size: Vector2.all(10),\n          )..add(RectangleHitbox()),\n          PositionComponent(\n            position: Vector2(20, 10),\n            size: Vector2.all(10),\n          )..add(RectangleHitbox()),\n          PositionComponent(\n            position: Vector2(10, 20),\n            size: Vector2.all(10),\n          )..add(RectangleHitbox()),\n          PositionComponent(\n            position: Vector2(0, 10),\n            size: Vector2.all(10),\n          )..add(RectangleHitbox()),\n        ]);\n        await game.ready();\n        final origin = Vector2.all(15);\n        final ignoreHitbox = world.children.first.children.first as ShapeHitbox;\n        final results = collisionSystem.collisionDetection.raycastAll(\n          origin,\n          numberOfRays: 4,\n          ignoreHitboxes: [ignoreHitbox],\n        );\n        expect(results.any((r) => r.hitbox == ignoreHitbox), isFalse);\n        expect(results.every((r) => r.isActive), isTrue);\n        expect(results.length, 3);\n      },\n    });\n  });\n\n  group('Raytracing', () {\n    runCollisionTestRegistry({\n      'on single circle': (collisionSystem) async {\n        final game = collisionSystem as FlameGame;\n        final world = game.world;\n        final circle = CircleComponent(\n          radius: 10.0,\n          position: Vector2.all(20),\n          anchor: Anchor.center,\n        )..add(CircleHitbox());\n        await world.ensureAdd(circle);\n        final ray = Ray2(\n          origin: Vector2(0, 10),\n          direction: Vector2.all(1.0)..normalize(),\n        );\n        final results = collisionSystem.collisionDetection.raytrace(ray);\n        expect(results.length, 1);\n        expect(results.first.isActive, isTrue);\n        expect(results.first.isInsideHitbox, isFalse);\n        expect(results.first.intersectionPoint, Vector2(10, 20));\n        final reflectionRay = results.first.reflectionRay;\n        expect(reflectionRay?.origin, Vector2(10, 20));\n        expect(reflectionRay?.direction, Vector2(-1, 1)..normalize());\n        expect(results.first.normal, Vector2(-1, 0));\n      },\n      'on single rectangle': (game) async {\n        final world = (game as FlameGame).world;\n        final rectangle = RectangleComponent(\n          position: Vector2.all(20),\n          size: Vector2.all(20),\n          anchor: Anchor.center,\n        )..add(RectangleHitbox());\n        await world.ensureAdd(rectangle);\n        final ray = Ray2(\n          origin: Vector2(0, 10),\n          direction: Vector2.all(1.0)..normalize(),\n        );\n        final results = game.collisionDetection.raytrace(ray);\n        expect(results.length, 1);\n        expect(results.first.isActive, isTrue);\n        expect(results.first.isInsideHitbox, isFalse);\n        expect(results.first.intersectionPoint, Vector2(10, 20));\n        final reflectionRay = results.first.reflectionRay;\n        expect(reflectionRay?.origin, Vector2(10, 20));\n        expect(reflectionRay?.direction, Vector2(-1, 1)..normalize());\n        expect(results.first.normal, Vector2(-1, 0));\n      },\n      'on single rectangle with ray with negative X': (game) async {\n        final world = (game as FlameGame).world;\n        final rectangle = RectangleComponent(\n          position: Vector2(-20, 40),\n          size: Vector2.all(20),\n          anchor: Anchor.center,\n        )..add(RectangleHitbox());\n        await world.ensureAdd(rectangle);\n        final ray = Ray2(\n          origin: Vector2(10, 20),\n          direction: Vector2(-1, 1)..normalize(),\n        );\n        final results = game.collisionDetection.raytrace(ray);\n        expect(results.length, 1);\n        expect(results.first.isActive, isTrue);\n        expect(results.first.isInsideHitbox, isFalse);\n        expect(results.first.intersectionPoint, Vector2(-10, 40));\n        final reflectionRay = results.first.reflectionRay;\n        expect(reflectionRay?.origin, Vector2(-10, 40));\n        expect(reflectionRay?.direction, Vector2(1, 1)..normalize());\n        expect(results.first.normal, Vector2(1, 0));\n      },\n      'on two circles': (game) async {\n        final world = (game as FlameGame).world;\n        final circle1 = CircleComponent(\n          position: Vector2.all(20),\n          radius: 10,\n          anchor: Anchor.center,\n        )..add(CircleHitbox());\n        final circle2 = CircleComponent(\n          position: Vector2(-20, 40),\n          radius: 10,\n          anchor: Anchor.center,\n        )..add(CircleHitbox());\n        await world.ensureAddAll([circle1, circle2]);\n        final ray = Ray2(\n          origin: Vector2(0, 10),\n          direction: Vector2.all(1.0)..normalize(),\n        );\n        final results = game.collisionDetection.raytrace(ray).toList();\n        expect(results.length, 2);\n        expect(results.every((e) => e.isActive), isTrue);\n        expect(results.every((e) => e.isInsideHitbox), isFalse);\n        // First box\n        expect(results[0].intersectionPoint, Vector2(10, 20));\n        expect(results[0].normal, Vector2(-1, 0));\n        final reflectionRay1 = results[0].reflectionRay;\n        expect(reflectionRay1?.origin, Vector2(10, 20));\n        expect(reflectionRay1?.direction, Vector2(-1, 1)..normalize());\n        final results2 = game.collisionDetection.raytrace(reflectionRay1!);\n        expect(results2.length, 1);\n        // Second box\n        expect(results[1].intersectionPoint, Vector2(-10, 40));\n        expect(results[1].normal, Vector2(1, 0));\n        final reflectionRay2 = results[1].reflectionRay;\n        expect(reflectionRay2?.origin, Vector2(-10, 40));\n        expect(reflectionRay2?.direction, Vector2(1, 1)..normalize());\n      },\n      'on two rectangles': (game) async {\n        final world = (game as FlameGame).world;\n        final rectangle1 = RectangleComponent(\n          position: Vector2.all(20),\n          size: Vector2.all(20),\n          anchor: Anchor.center,\n        )..add(RectangleHitbox());\n        final rectangle2 = RectangleComponent(\n          position: Vector2(-20, 40),\n          size: Vector2.all(20),\n          anchor: Anchor.center,\n        )..add(RectangleHitbox());\n        await world.ensureAddAll([rectangle1, rectangle2]);\n        final ray = Ray2(\n          origin: Vector2(0, 10),\n          direction: Vector2.all(1.0)..normalize(),\n        );\n        final results = game.collisionDetection.raytrace(ray).toList();\n        expect(results.length, 2);\n        expect(results.every((e) => e.isActive), isTrue);\n        expect(results.every((e) => e.isInsideHitbox), isFalse);\n        // First box\n        expect(results[0].intersectionPoint, Vector2(10, 20));\n        expect(results[0].normal, Vector2(-1, 0));\n        final reflectionRay1 = results[0].reflectionRay;\n        expect(reflectionRay1?.origin, Vector2(10, 20));\n        expect(reflectionRay1?.direction, Vector2(-1, 1)..normalize());\n        final results2 = game.collisionDetection\n            .raytrace(reflectionRay1!)\n            .toList();\n        expect(results2.length, 1);\n        // Second box\n        expect(results[1].intersectionPoint, Vector2(-10, 40));\n        expect(results[1].normal, Vector2(1, 0));\n        final reflectionRay2 = results[1].reflectionRay;\n        expect(reflectionRay2?.origin, Vector2(-10, 40));\n        expect(reflectionRay2?.direction, Vector2(1, 1)..normalize());\n      },\n      'on two rectangles with one ignored': (game) async {\n        final world = (game as FlameGame).world;\n        final rectangle1 = RectangleComponent(\n          position: Vector2.all(20),\n          size: Vector2.all(20),\n          anchor: Anchor.center,\n        )..add(RectangleHitbox());\n        final rectangle2 = RectangleComponent(\n          position: Vector2(-20, 40),\n          size: Vector2.all(20),\n          anchor: Anchor.center,\n        )..add(RectangleHitbox());\n        await world.ensureAddAll([rectangle1, rectangle2]);\n        final ray = Ray2(\n          origin: Vector2(0, 10),\n          direction: Vector2.all(1.0)..normalize(),\n        );\n        final ignoreHitbox =\n            world.children.toList()[1].children.first as ShapeHitbox;\n        final results = game.collisionDetection\n            .raytrace(ray, ignoreHitboxes: [ignoreHitbox])\n            .toList();\n        expect(results.length, 1);\n        expect(results.every((e) => e.isActive), isTrue);\n        expect(results.every((e) => e.isInsideHitbox), isFalse);\n        // First box\n        expect(results[0].intersectionPoint, Vector2(10, 20));\n        expect(results[0].normal, Vector2(-1, 0));\n        final reflectionRay1 = results[0].reflectionRay;\n        expect(reflectionRay1?.origin, Vector2(10, 20));\n        expect(reflectionRay1?.direction, Vector2(-1, 1)..normalize());\n        final results2 = game.collisionDetection\n            .raytrace(reflectionRay1!)\n            .toList();\n        expect(results2.length, 1);\n      },\n      'on a rectangle within another': (game) async {\n        final world = (game as FlameGame).world;\n        final rectangle1 = RectangleComponent(\n          position: Vector2.all(20),\n          size: Vector2.all(20),\n        )..add(RectangleHitbox());\n        final rectangle2 = RectangleComponent(\n          size: Vector2.all(200),\n        )..add(RectangleHitbox());\n        await world.ensureAddAll([rectangle1, rectangle2]);\n        final ray = Ray2(\n          origin: Vector2(20, 10),\n          direction: Vector2.all(1.0)..normalize(),\n        );\n        final results = game.collisionDetection.raytrace(ray).toList();\n        expect(results.length, 10);\n        expect(results.every((e) => e.isActive), isTrue);\n        expect(results[0].isInsideHitbox, isFalse);\n        expect(results[1].isInsideHitbox, isTrue);\n        // First box\n        expect(results[0].intersectionPoint, Vector2(30, 20));\n        expect(results[0].normal, Vector2(0, -1));\n        final reflectionRay1 = results[0].reflectionRay;\n        expect(reflectionRay1?.origin, Vector2(30, 20));\n        expect(reflectionRay1?.direction, Vector2(1, -1)..normalize());\n        final results2 = game.collisionDetection\n            .raytrace(reflectionRay1!)\n            .toList();\n        expect(results2.length, 10);\n        // Second box\n        expect(results[1].intersectionPoint, Vector2(50, 0));\n        expect(results[1].normal, Vector2(0, 1));\n        final reflectionRay2 = results[1].reflectionRay;\n        expect(reflectionRay2?.origin, Vector2(50, 0));\n        expect(reflectionRay2?.direction, Vector2(1, 1)..normalize());\n      },\n      'make sure that ray does not escape circle hitbox': (game) async {\n        final world = (game as FlameGame).world;\n        final circle = CircleComponent(\n          position: Vector2(0, 0),\n          radius: 5,\n          anchor: Anchor.center,\n        )..add(CircleHitbox());\n        await world.ensureAdd(circle);\n        final ray = Ray2(\n          origin: Vector2(0, 0),\n          direction: Vector2(1.0, 0),\n        );\n        final results = game.collisionDetection.raytrace(ray);\n        expect(results.length, 10);\n        expect(results.first.isActive, isTrue);\n        expect(results.first.isInsideHitbox, isTrue);\n        expect(\n          results.first.intersectionPoint,\n          Vector2(5, 0),\n        );\n        final reflectionRay = results.first.reflectionRay;\n        expect(reflectionRay?.origin, Vector2(5, 0));\n        expect(reflectionRay?.direction, Vector2(-1, 0)..normalize());\n        expect(results.first.normal, Vector2(-1, 0));\n      },\n\n      // Regression test for a crash in CircleHitbox.rayIntersection.\n      //\n      // After each bounce, raytrace() reuses the same RaycastResult object\n      // from the `out` list. The reflected direction from one bounce becomes\n      // the incident direction for the next. Each call to reflect() introduces\n      // a tiny floating-point rounding error, so after many bounces the\n      // direction vector's length drifts slightly away from 1.0.\n      //\n      // Ray2 requires directions to have length² within 1e-6 of 1.0. Once the\n      // drift exceeds that threshold, the next call to Ray2.setWith (which\n      // stores the reflected direction into the next ray) fires an assertion:\n      //   'direction must be normalized'\n      //\n      // Concrete values from a live failure:\n      //   incidentDir.length2   = 1.0000009335  (just under the threshold)\n      //   reflectionDir.length2 = 1.0000010253  (just over — assertion fires)\n      //\n      // To reproduce this deterministically in a test, we inject a direction\n      // that is already above the threshold by writing directly to the\n      // direction vector (bypassing the Ray2 setter, which would reject it).\n      // reflect() then preserves the drift and passes it to setWith, which\n      // triggers the assertion.\n      //\n      // The fix is to call normalize() on the reflected direction before\n      // passing it to Ray2, so the drift is corrected on every bounce.\n      'CircleHitbox raycast does not throw when incident direction has drift':\n          (game) async {\n            final world = (game as FlameGame).world;\n            // Circle at (0, 20) r=10, anchor center — a ray from the\n            // origin pointing up will hit it at (0, 10).\n            final circle = CircleComponent(\n              position: Vector2(0, 20),\n              radius: 10,\n              anchor: Anchor.center,\n            )..add(CircleHitbox());\n            await world.ensureAdd(circle);\n\n            // Start with a valid unit direction so Ray2 accepts it.\n            final ray = Ray2(\n              origin: Vector2.zero(),\n              direction: Vector2(0, 1),\n            );\n            // Inject a direction whose length² exceeds the 1e-6 threshold\n            // by writing to the direction vector directly, bypassing the\n            // Ray2 setter (which would reject it). This simulates the drift\n            // that builds up across many bounces in a real raytrace session.\n            // length² = (sqrt(1+2e-6))² = 1+2e-6, just above 1+1e-6.\n            ray.direction.setValues(0.0, sqrt(1.0 + 2e-6));\n\n            // Without the fix, reflect() passes the drifted direction to\n            // Ray2.setWith, which fires the assertion. With the fix,\n            // normalize() corrects the length before it reaches Ray2.\n            expect(\n              () => game.collisionDetection.raycast(ray),\n              returnsNormally,\n              reason:\n                  'raycast on a CircleHitbox must not throw when the ray '\n                  'direction has accumulated normalization drift',\n            );\n          },\n\n      // Same regression test for RectangleHitbox. RectangleHitbox uses the\n      // PolygonRayIntersection mixin which has the same reflect() pattern and\n      // therefore the same bug.\n      'RectangleHitbox raycast does not throw when '\n          'incident direction has drift': (game) async {\n        final world = (game as FlameGame).world;\n        // Rectangle positioned so a ray from the origin pointing\n        // right will hit it.\n        final rect = RectangleComponent(\n          position: Vector2(20, -5),\n          size: Vector2(10, 10),\n        )..add(RectangleHitbox());\n        await world.ensureAdd(rect);\n\n        final ray = Ray2(\n          origin: Vector2.zero(),\n          direction: Vector2(1, 0),\n        );\n        // Same drift injection as the CircleHitbox test above.\n        ray.direction.setValues(sqrt(1.0 + 2e-6), 0.0);\n\n        expect(\n          () => game.collisionDetection.raycast(ray),\n          returnsNormally,\n          reason:\n              'raycast on a RectangleHitbox must not throw when the ray '\n              'direction has accumulated normalization drift',\n        );\n      },\n    });\n  });\n\n  group('collisionsCompletedNotifier', () {\n    runCollisionTestRegistry({\n      'collisionsCompletedNotifier calls listeners': (game) async {\n        var calledTimes = 0;\n        final listeners = List.generate(\n          10,\n          (_) =>\n              () => calledTimes++,\n        );\n        for (final listener in listeners) {\n          game.collisionDetection.collisionsCompletedNotifier.addListener(\n            listener,\n          );\n        }\n        game.update(0);\n        expect(calledTimes, listeners.length);\n      },\n    });\n  });\n\n  group('Scaled CircleHitbox', () {\n    group('Circle-Circle intersections with scale', () {\n      test('scaled circles collide when unscaled would not', () {\n        final circleA = CircleComponent(\n          radius: 1.0,\n          position: Vector2.zero(),\n          anchor: Anchor.center,\n          scale: Vector2.all(3),\n        );\n        final circleB = CircleComponent(\n          radius: 1.0,\n          position: Vector2(4, 0),\n          anchor: Anchor.center,\n        );\n        // Without scale: distance=4, radiusA+radiusB=2 -> no collision.\n        // With scale: scaledRadiusA=3, so 3+1=4 -> should just touch.\n        final intersections = geometry.intersections(circleA, circleB);\n        expect(\n          intersections.isNotEmpty,\n          isTrue,\n          reason: 'Scaled circle should intersect with the other circle',\n        );\n      });\n\n      test('unscaled circles that just miss do not collide', () {\n        final circleA = CircleComponent(\n          radius: 1.0,\n          position: Vector2.zero(),\n          anchor: Anchor.center,\n        );\n        final circleB = CircleComponent(\n          radius: 1.0,\n          position: Vector2(4, 0),\n          anchor: Anchor.center,\n        );\n        final intersections = geometry.intersections(circleA, circleB);\n        expect(\n          intersections.isEmpty,\n          isTrue,\n          reason: 'Unscaled circles should not collide at this distance',\n        );\n      });\n\n      test('both circles scaled', () {\n        final circleA = CircleComponent(\n          radius: 1.0,\n          position: Vector2.zero(),\n          anchor: Anchor.center,\n          scale: Vector2.all(2),\n        );\n        final circleB = CircleComponent(\n          radius: 1.0,\n          position: Vector2(3, 0),\n          anchor: Anchor.center,\n          scale: Vector2.all(2),\n        );\n        // scaledRadiusA=2, scaledRadiusB=2, distance=3 -> 2+2=4 > 3 -> collide\n        final intersections = geometry.intersections(circleA, circleB);\n        expect(\n          intersections.isNotEmpty,\n          isTrue,\n          reason: 'Both scaled circles should collide',\n        );\n      });\n    });\n\n    group('Circle-Polygon intersections with scale', () {\n      test('scaled circle intersects polygon when unscaled would not', () {\n        final circle = CircleComponent(\n          radius: 1.0,\n          position: Vector2.zero(),\n          anchor: Anchor.center,\n          scale: Vector2.all(3),\n        );\n        final polygon = PolygonComponent(\n          [\n            Vector2(2, -1),\n            Vector2(3, -1),\n            Vector2(3, 1),\n            Vector2(2, 1),\n          ],\n          anchor: Anchor.center,\n        );\n        // Without scale: radius=1, polygon starts at x=2 -> no intersection.\n        // With scale: scaledRadius=3 -> circle extends to x=3 -> intersects.\n        final intersections = geometry.intersections(circle, polygon);\n        expect(\n          intersections.isNotEmpty,\n          isTrue,\n          reason:\n              'Scaled circle should intersect polygon that unscaled would miss',\n        );\n      });\n    });\n\n    group('Raycasting with scaled CircleHitbox', () {\n      runCollisionTestRegistry({\n        'ray hits scaled CircleHitbox': (collisionSystem) async {\n          final game = collisionSystem as FlameGame;\n          final world = game.world;\n          await world.ensureAddAll([\n            PositionComponent(\n              position: Vector2.zero(),\n              size: Vector2.all(10),\n              scale: Vector2.all(2),\n            )..add(CircleHitbox()),\n          ]);\n          await game.ready();\n          // Scaled radius is 10 (5 * 2), center at (10, 10).\n          // A ray from (25, 10) going left should hit the scaled circle.\n          final ray = Ray2(\n            origin: Vector2(25, 10),\n            direction: Vector2(-1, 0),\n          );\n          final result = collisionSystem.collisionDetection.raycast(ray);\n          expect(\n            result?.hitbox?.parent,\n            world.children.first,\n            reason: 'Ray should hit the scaled CircleHitbox',\n          );\n        },\n        'ray misses unscaled CircleHitbox at same distance':\n            (collisionSystem) async {\n              final game = collisionSystem as FlameGame;\n              final world = game.world;\n              await world.ensureAddAll([\n                PositionComponent(\n                  position: Vector2.zero(),\n                  size: Vector2.all(10),\n                )..add(CircleHitbox()),\n              ]);\n              await game.ready();\n              // Unscaled radius is 5, center at (5, 5).\n              // A ray at y=5 from x=25 going left would hit at x=10, but\n              // let's shoot from outside the unscaled range at a tangent.\n              final ray = Ray2(\n                origin: Vector2(25, 12),\n                direction: Vector2(-1, 0),\n              );\n              final result = collisionSystem.collisionDetection.raycast(ray);\n              expect(\n                result,\n                isNull,\n                reason:\n                    'Ray should miss the unscaled CircleHitbox at this '\n                    'position',\n              );\n            },\n        'ray from within scaled CircleHitbox': (collisionSystem) async {\n          final game = collisionSystem as FlameGame;\n          final world = game.world;\n          await world.ensureAddAll([\n            PositionComponent(\n              position: Vector2.zero(),\n              size: Vector2.all(10),\n              scale: Vector2.all(3),\n            )..add(CircleHitbox()),\n          ]);\n          await game.ready();\n          // Scaled radius is 15 (5 * 3), center at (15, 15).\n          // A point at (20, 15) is inside the scaled circle (dist=5 < 15).\n          final ray = Ray2(\n            origin: Vector2(20, 15),\n            direction: Vector2(1, 0),\n          );\n          final result = collisionSystem.collisionDetection.raycast(ray);\n          expect(\n            result?.hitbox?.parent,\n            world.children.first,\n            reason: 'Ray from within scaled CircleHitbox should hit',\n          );\n          expect(\n            result?.isInsideHitbox,\n            isTrue,\n            reason: 'Ray origin should be detected as inside the hitbox',\n          );\n        },\n      });\n    });\n\n    group('Collision callbacks with scaled CircleHitbox', () {\n      runCollisionTestRegistry({\n        'scaled circle hitboxes trigger collision callbacks': (game) async {\n          final componentA = PositionComponent(\n            position: Vector2.zero(),\n            size: Vector2.all(10),\n            scale: Vector2.all(2),\n          );\n          final componentB = PositionComponent(\n            position: Vector2(15, 0),\n            size: Vector2.all(10),\n          );\n          final hitboxA = CircleHitbox();\n          final hitboxB = CircleHitbox();\n          await componentA.add(hitboxA);\n          await componentB.add(hitboxB);\n          await game.ensureAddAll([componentA, componentB]);\n          // componentA: center=(10,10), scaledRadius=10\n          // componentB: center=(20,5), scaledRadius=5\n          // distance = sqrt(100+25)=~11.18, sum of radii=15 -> should collide\n          game.update(0);\n          expect(\n            hitboxA.isColliding,\n            isTrue,\n            reason: 'Scaled CircleHitbox A should be colliding',\n          );\n          expect(\n            hitboxB.isColliding,\n            isTrue,\n            reason: 'CircleHitbox B should be colliding with scaled A',\n          );\n        },\n        'unscaled circle hitboxes do not collide at same distance':\n            (game) async {\n              final componentA = PositionComponent(\n                position: Vector2.zero(),\n                size: Vector2.all(10),\n              );\n              final componentB = PositionComponent(\n                position: Vector2(15, 0),\n                size: Vector2.all(10),\n              );\n              final hitboxA = CircleHitbox();\n              final hitboxB = CircleHitbox();\n              await componentA.add(hitboxA);\n              await componentB.add(hitboxB);\n              await game.ensureAddAll([componentA, componentB]);\n              // componentA: center=(5,5), radius=5\n              // componentB: center=(20,5), radius=5\n              // distance = 15, sum of radii=10 -> should NOT collide\n              game.update(0);\n              expect(\n                hitboxA.isColliding,\n                isFalse,\n                reason: 'Unscaled CircleHitbox A should not be colliding',\n              );\n            },\n      });\n    });\n\n    group('Hitboxes with parent non-uniform scale and rotation', () {\n      runCollisionTestRegistry({\n        'parent non-uniform scale (2,1) + child rotation pi/2': (game) async {\n          // Parent scaled (2,1) with child rotated 90 degrees.\n          // The correct transform produces global vertices at roughly\n          // (0,0), (-20,0), (-20,10), (0,10).\n          final parent = PositionComponent(\n            position: Vector2.zero(),\n            size: Vector2.all(10),\n            scale: Vector2(2, 1),\n          );\n          final child = PositionComponent(\n            position: Vector2.zero(),\n            size: Vector2.all(10),\n            angle: pi / 2,\n          );\n          final hitboxA = RectangleHitbox();\n          await child.add(hitboxA);\n          await parent.add(child);\n\n          // Block from (-2,3) to (2,7) straddles the right edge at x=0.\n          final blockComp = PositionComponent(\n            position: Vector2(-2, 3),\n            size: Vector2.all(4),\n          );\n          final hitboxB = RectangleHitbox();\n          await blockComp.add(hitboxB);\n\n          await game.ensureAddAll([parent, blockComp]);\n          game.update(0);\n          expect(\n            hitboxA.isColliding,\n            isTrue,\n            reason:\n                'Hitbox with parent non-uniform scale and child rotation '\n                'should collide with correctly positioned block',\n          );\n        },\n        'parent flip (-1,1) + child rotation + polygon hitbox': (game) async {\n          // Parent flipped on x-axis, child rotated 45 degrees.\n          // Produces a diamond at roughly (30,0),(23,7),(30,14),(37,7).\n          final parent = PositionComponent(\n            position: Vector2(30, 0),\n            size: Vector2.all(10),\n            scale: Vector2(-1, 1),\n          );\n          final child = PositionComponent(\n            position: Vector2.zero(),\n            size: Vector2.all(10),\n            angle: pi / 4,\n          );\n          final hitbox = PolygonHitbox([\n            Vector2(0, 0),\n            Vector2(10, 0),\n            Vector2(10, 10),\n            Vector2(0, 10),\n          ]);\n          await child.add(hitbox);\n          await parent.add(child);\n\n          // Block from (21,5) to (25,9) straddles the left edge.\n          final blockComp = PositionComponent(\n            position: Vector2(21, 5),\n            size: Vector2.all(4),\n          );\n          final hitboxB = RectangleHitbox();\n          await blockComp.add(hitboxB);\n\n          await game.ensureAddAll([parent, blockComp]);\n          game.update(0);\n          expect(\n            hitbox.isColliding,\n            isTrue,\n            reason:\n                'Flipped parent + rotated child polygon hitbox should '\n                'collide correctly',\n          );\n        },\n        'nested 3-level hierarchy: grandparent scale + parent rotation + '\n            'child hitbox': (game) async {\n          // Grandparent scaled (2,1), parent rotated 90 degrees at (5,5).\n          // Produces global vertices at (-10,5),(-10,15),(10,15),(10,5).\n          final grandparent = PositionComponent(\n            position: Vector2.zero(),\n            size: Vector2.all(20),\n            scale: Vector2(2, 1),\n          );\n          final midParent = PositionComponent(\n            position: Vector2(5, 5),\n            size: Vector2.all(10),\n            angle: pi / 2,\n          );\n          final child = PositionComponent(\n            position: Vector2.zero(),\n            size: Vector2.all(10),\n          );\n          final hitbox = RectangleHitbox();\n          await child.add(hitbox);\n          await midParent.add(child);\n          await grandparent.add(midParent);\n\n          // Block from (-12,8) to (-8,12) straddles the left edge at x=-10.\n          final blockComp = PositionComponent(\n            position: Vector2(-12, 8),\n            size: Vector2.all(4),\n          );\n          final hitboxB = RectangleHitbox();\n          await blockComp.add(hitboxB);\n\n          await game.ensureAddAll([grandparent, blockComp]);\n          game.update(0);\n          expect(\n            hitbox.isColliding,\n            isTrue,\n            reason:\n                '3-level nested hierarchy with scale + rotation should '\n                'produce correct collision detection',\n          );\n        },\n      });\n    });\n  });\n}\n\nclass _CollisionDetectionGame extends FlameGame with HasCollisionDetection {}\n"
  },
  {
    "path": "packages/flame/test/collisions/collision_passthrough_test.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nimport 'collision_test_helpers.dart';\n\nclass _Passthrough extends TestBlock with CollisionPassthrough {\n  _Passthrough() : super(Vector2.zero(), Vector2.all(10));\n}\n\nvoid main() {\n  group('CollisionPassthrough', () {\n    runCollisionTestRegistry({\n      'Passing collisions to parent': (game) async {\n        final passthrough = _Passthrough();\n        final hitboxParent = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n          addTestHitbox: false,\n        )..add(passthrough);\n        final collisionBlock = TestBlock(Vector2.all(1), Vector2.all(10));\n        game.add(collisionBlock);\n        game.add(hitboxParent);\n        game.update(0);\n        expect(hitboxParent.isColliding, isTrue);\n        expect(passthrough.isColliding, isTrue);\n        expect(passthrough.startCounter, 1);\n        expect(passthrough.onCollisionCounter, 1);\n        expect(passthrough.endCounter, 0);\n        expect(hitboxParent.startCounter, 1);\n        expect(hitboxParent.onCollisionCounter, 1);\n        expect(hitboxParent.endCounter, 0);\n        collisionBlock.position = Vector2.all(12);\n        game.update(0);\n        expect(hitboxParent.isColliding, isFalse);\n        expect(passthrough.isColliding, isFalse);\n        expect(passthrough.startCounter, 1);\n        expect(passthrough.endCounter, 1);\n        expect(passthrough.onCollisionCounter, 1);\n        expect(hitboxParent.startCounter, 1);\n        expect(hitboxParent.endCounter, 1);\n        expect(hitboxParent.onCollisionCounter, 1);\n      },\n    });\n\n    testWithFlameGame('Null passthrough', (game) async {\n      final hitbox = CompositeHitbox(children: [RectangleHitbox()]);\n      final component = PositionComponent(children: [hitbox]);\n      final testBlock = TestBlock(Vector2.zero(), Vector2.all(10));\n\n      await game.addAll([component, testBlock]);\n      await game.ready();\n\n      expect(hitbox.passthroughParent, isNull);\n\n      hitbox.removeFromParent();\n      testBlock.add(hitbox);\n      await game.ready();\n\n      expect(hitbox.passthroughParent, testBlock);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/collisions/collision_test_helpers.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/image_composition.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:meta/meta.dart';\n\nclass HasCollidablesGame extends FlameGame with HasCollisionDetection {}\n\nclass HasQuadTreeCollidablesGame extends FlameGame\n    with HasQuadTreeCollisionDetection {}\n\nclass CollisionDetectionWorld extends World with HasCollisionDetection {}\n\n@isTest\nvoid testCollisionDetectionGame(\n  String testName,\n  Future<void> Function(HasCollidablesGame) testBody,\n) {\n  return testWithGame(testName, HasCollidablesGame.new, testBody);\n}\n\n@isTest\nvoid testQuadTreeCollisionDetectionGame(\n  String testName,\n  Future<void> Function(HasCollisionDetection) testBody,\n) {\n  return testWithGame(\n    testName,\n    () {\n      final game = HasQuadTreeCollidablesGame();\n      game.initializeCollisionDetection(\n        mapDimensions: const Rect.fromLTWH(0, 0, 1000, 1000),\n      );\n      return game;\n    },\n    testBody,\n  );\n}\n\nFuture<void> runCollisionTestRegistry(\n  Map<String, Future<void> Function(HasCollisionDetection)> testRegistry,\n) async {\n  for (final entry in testRegistry.entries) {\n    final name = entry.key;\n    final testFunction = entry.value;\n    testCollisionDetectionGame('[Sweep] $name', testFunction);\n    testQuadTreeCollisionDetectionGame('[QuadTree] $name', testFunction);\n  }\n}\n\nclass TestHitbox extends RectangleHitbox {\n  int startCounter = 0;\n  int onCollisionCounter = 0;\n  int endCounter = 0;\n  String? name;\n\n  TestHitbox([this.name]) {\n    onCollisionCallback = (_, __) {\n      onCollisionCounter++;\n    };\n    onCollisionStartCallback = (_, __) {\n      startCounter++;\n    };\n    onCollisionEndCallback = (_) {\n      endCounter++;\n    };\n  }\n\n  @override\n  String toString() {\n    return name == null\n        ? '_TestHitbox[${identityHashCode(this)}]'\n        : '_TestHitbox[$name]';\n  }\n}\n\nclass CompositeTestHitbox extends CompositeHitbox {\n  int startCounter = 0;\n  int onCollisionCounter = 0;\n  int endCounter = 0;\n\n  CompositeTestHitbox({super.size, super.children}) {\n    onCollisionCallback = (_, __) {\n      onCollisionCounter++;\n    };\n    onCollisionStartCallback = (_, __) {\n      startCounter++;\n    };\n    onCollisionEndCallback = (_) {\n      endCounter++;\n    };\n  }\n}\n\nclass TestBlock extends PositionComponent with CollisionCallbacks {\n  String? name;\n  final hitbox = TestHitbox();\n  int startCounter = 0;\n  int onCollisionCounter = 0;\n  int endCounter = 0;\n\n  final bool Function(PositionComponent other)? _onComponentTypeCheck;\n\n  TestBlock(\n    Vector2 position,\n    Vector2 size, {\n    CollisionType type = CollisionType.active,\n    bool addTestHitbox = true,\n    super.children,\n    this.name,\n    bool Function(PositionComponent other)? onComponentTypeCheck,\n  }) : _onComponentTypeCheck = onComponentTypeCheck,\n       super(\n         position: position,\n         size: size,\n       ) {\n    children.register<ShapeHitbox>();\n    if (addTestHitbox) {\n      add(hitbox..collisionType = type);\n    }\n  }\n\n  @override\n  bool collidingWith(PositionComponent other) {\n    return activeCollisions.contains(other);\n  }\n\n  bool collidedWithExactly(List<PositionComponent> collidables) {\n    final otherCollidables = collidables.toSet()..remove(this);\n    return activeCollisions.containsAll(otherCollidables) &&\n        otherCollidables.containsAll(activeCollisions);\n  }\n\n  @override\n  String toString() {\n    return name == null\n        ? '_TestBlock[${identityHashCode(this)}]'\n        : '_TestBlock[$name]';\n  }\n\n  Set<Vector2> intersections(TestBlock other) {\n    final result = <Vector2>{};\n    for (final hitboxA in children.query<ShapeHitbox>()) {\n      for (final hitboxB in other.children.query<ShapeHitbox>()) {\n        result.addAll(hitboxA.intersections(hitboxB));\n      }\n    }\n    return result;\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    super.onCollisionStart(intersectionPoints, other);\n    startCounter++;\n  }\n\n  @override\n  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {\n    super.onCollision(intersectionPoints, other);\n    onCollisionCounter++;\n  }\n\n  @override\n  void onCollisionEnd(PositionComponent other) {\n    super.onCollisionEnd(other);\n    endCounter++;\n  }\n\n  @override\n  bool onComponentTypeCheck(PositionComponent other) {\n    return (_onComponentTypeCheck?.call(other) ?? true) &&\n        super.onComponentTypeCheck(other);\n  }\n}\n\nclass Water extends PositionComponent {\n  Water({super.position, super.size, super.children});\n}\n\nclass Brick extends PositionComponent {\n  Brick({super.position, super.size, super.children});\n}\n"
  },
  {
    "path": "packages/flame/test/collisions/collision_type_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nimport 'collision_test_helpers.dart';\n\nvoid main() {\n  group('CollisionType', () {\n    runCollisionTestRegistry({\n      'actives do collide': (game) async {\n        final blockA = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n        );\n        final blockB = TestBlock(\n          Vector2.all(1),\n          Vector2.all(10),\n        );\n        await game.ensureAddAll([blockA, blockB]);\n        game.update(0);\n        expect(blockA.collidingWith(blockB), true);\n        expect(blockB.collidingWith(blockA), true);\n        expect(blockA.activeCollisions.length, 1);\n        expect(blockB.activeCollisions.length, 1);\n      },\n      'intersections are returned': (game) async {\n        final blockA = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n        );\n        final blockB = TestBlock(\n          Vector2.all(1),\n          Vector2.all(10),\n        );\n        await game.ensureAddAll([blockA, blockB]);\n        game.update(0);\n\n        final points = blockA.intersections(blockB);\n        expect(points.length, 2);\n        expect(points, contains(Vector2(1, 10)));\n        expect(points, contains(Vector2(10, 1)));\n      },\n      'passives do not collide': (game) async {\n        final blockA = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n          type: CollisionType.passive,\n        );\n        final blockB = TestBlock(\n          Vector2.all(1),\n          Vector2.all(10),\n          type: CollisionType.passive,\n        );\n        await game.ensureAddAll([blockA, blockB]);\n        game.update(0);\n        expect(blockA.activeCollisions.isEmpty, true);\n        expect(blockB.activeCollisions.isEmpty, true);\n      },\n      'inactives do not collide': (game) async {\n        final blockA = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n          type: CollisionType.inactive,\n        );\n        final blockB = TestBlock(\n          Vector2.all(1),\n          Vector2.all(10),\n          type: CollisionType.inactive,\n        );\n        await game.ensureAddAll([blockA, blockB]);\n        game.update(0);\n        expect(blockA.activeCollisions.isEmpty, true);\n        expect(blockB.activeCollisions.isEmpty, true);\n      },\n      'active collides with static': (game) async {\n        final blockA = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n        );\n        final blockB = TestBlock(\n          Vector2.all(1),\n          Vector2.all(10),\n          type: CollisionType.passive,\n        );\n        await game.ensureAddAll([blockA, blockB]);\n        game.update(0);\n        expect(blockA.collidingWith(blockB), true);\n        expect(blockB.collidingWith(blockA), true);\n        expect(blockA.activeCollisions.length, 1);\n        expect(blockB.activeCollisions.length, 1);\n      },\n      'passive collides with active': (game) async {\n        final blockA = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n          type: CollisionType.passive,\n        );\n        final blockB = TestBlock(\n          Vector2.all(1),\n          Vector2.all(10),\n        );\n        await game.ensureAddAll([blockA, blockB]);\n        game.update(0);\n        expect(blockA.collidingWith(blockB), true);\n        expect(blockB.collidingWith(blockA), true);\n        expect(blockA.activeCollisions.length, 1);\n        expect(blockB.activeCollisions.length, 1);\n      },\n      'passive does not collide with inactive': (game) async {\n        final blockA = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n          type: CollisionType.passive,\n        );\n        final blockB = TestBlock(\n          Vector2.all(1),\n          Vector2.all(10),\n          type: CollisionType.inactive,\n        );\n        await game.ensureAddAll([blockA, blockB]);\n        game.update(0);\n        expect(blockA.activeCollisions.length, 0);\n        expect(blockB.activeCollisions.length, 0);\n      },\n      'inactive does not collide with static': (game) async {\n        final blockA = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n          type: CollisionType.inactive,\n        );\n        final blockB = TestBlock(\n          Vector2.all(1),\n          Vector2.all(10),\n          type: CollisionType.passive,\n        );\n        await game.ensureAddAll([blockA, blockB]);\n        game.update(0);\n        expect(blockA.activeCollisions.length, 0);\n        expect(blockB.activeCollisions.length, 0);\n      },\n      'active does not collide with inactive': (game) async {\n        final blockA = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n        );\n        final blockB = TestBlock(\n          Vector2.all(1),\n          Vector2.all(10),\n          type: CollisionType.inactive,\n        );\n        await game.ensureAddAll([blockA, blockB]);\n        game.update(0);\n        expect(blockA.activeCollisions.length, 0);\n        expect(blockB.activeCollisions.length, 0);\n      },\n      'inactive does not collide with active': (game) async {\n        final blockA = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n          type: CollisionType.inactive,\n        );\n        final blockB = TestBlock(\n          Vector2.all(1),\n          Vector2.all(10),\n        );\n        await game.ensureAddAll([blockA, blockB]);\n        game.update(0);\n        expect(blockA.activeCollisions.length, 0);\n        expect(blockB.activeCollisions.length, 0);\n      },\n      'correct collisions with many involved collidables': (game) async {\n        final rng = Random(0);\n        List<TestBlock> generateBlocks(CollisionType type) {\n          return List.generate(\n            100,\n            (_) => TestBlock(\n              Vector2.random(rng) - Vector2.random(rng),\n              Vector2.all(prevFloat32(10)),\n              type: type,\n            ),\n          );\n        }\n\n        final actives = generateBlocks(CollisionType.active);\n        final passives = generateBlocks(CollisionType.passive);\n        final inactives = generateBlocks(CollisionType.inactive);\n        await game.ensureAddAll((actives + passives + inactives)..shuffle(rng));\n        game.update(0);\n        expect(\n          actives.every((c) => c.collidedWithExactly(actives + passives)),\n          isTrue,\n        );\n        expect(passives.every((c) => c.collidedWithExactly(actives)), isTrue);\n        expect(inactives.every((c) => c.activeCollisions.isEmpty), isTrue);\n      },\n      'detects collision after scale': (game) async {\n        final blockA = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n        );\n        final blockB = TestBlock(\n          Vector2.all(11),\n          Vector2.all(10),\n        );\n        expect(blockA.collidingWith(blockB), isFalse);\n        await game.ensureAddAll([blockA, blockB]);\n        expect(blockA.collidingWith(blockB), isFalse);\n        game.update(0);\n        expect(blockA.collidingWith(blockB), isFalse);\n        expect(blockB.collidingWith(blockA), isFalse);\n        expect(blockA.activeCollisions.length, 0);\n        expect(blockB.activeCollisions.length, 0);\n        blockA.scale = Vector2.all(2.0);\n        game.update(0);\n        expect(blockA.collidingWith(blockB), isTrue);\n        expect(blockB.collidingWith(blockA), isTrue);\n        expect(blockA.activeCollisions.length, 1);\n        expect(blockB.activeCollisions.length, 1);\n      },\n      'detects collision after flip': (game) async {\n        final blockA = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n        );\n        final blockB = TestBlock(\n          Vector2(11, 0),\n          Vector2.all(10),\n        );\n        expect(blockA.collidingWith(blockB), isFalse);\n        await game.ensureAddAll([blockA, blockB]);\n        expect(blockA.collidingWith(blockB), isFalse);\n        game.update(0);\n        expect(blockA.collidingWith(blockB), isFalse);\n        expect(blockB.collidingWith(blockA), isFalse);\n        expect(blockA.activeCollisions.length, 0);\n        expect(blockB.activeCollisions.length, 0);\n        blockB.flipHorizontally();\n        game.update(0);\n        expect(blockA.collidingWith(blockB), isTrue);\n        expect(blockB.collidingWith(blockA), isTrue);\n        expect(blockA.activeCollisions.length, 1);\n        expect(blockB.activeCollisions.length, 1);\n      },\n      'testPoint detects point after flip': (game) async {\n        final blockA = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n        );\n        await game.ensureAdd(blockA);\n        game.update(0);\n        expect(blockA.containsPoint(Vector2(-1, 1)), false);\n        blockA.flipHorizontally();\n        game.update(0);\n        expect(blockA.containsPoint(Vector2(-1, 1)), true);\n      },\n      'testPoint detects point after scale': (game) async {\n        final blockA = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n        );\n        await game.ensureAdd(blockA);\n        game.update(0);\n        expect(blockA.containsPoint(Vector2.all(11)), false);\n        blockA.scale = Vector2.all(2.0);\n        game.update(0);\n        expect(blockA.containsPoint(Vector2.all(11)), true);\n      },\n      'detects collision on child components': (game) async {\n        final blockA = TestBlock(\n          Vector2.zero(),\n          Vector2.all(10),\n          name: 'A',\n        );\n        final innerBlockA = TestBlock(\n          blockA.size / 4,\n          blockA.size / 2,\n          name: 'iA',\n        );\n        blockA.add(innerBlockA);\n\n        final blockB = TestBlock(\n          Vector2.all(5),\n          Vector2.all(10),\n          name: 'B',\n        );\n        final innerBlockB = TestBlock(\n          blockA.size / 4,\n          blockA.size / 2,\n          name: 'iB',\n        );\n        blockB.add(innerBlockB);\n\n        await game.ensureAddAll([blockA, blockB]);\n        game.update(0);\n        expect(blockA.activeCollisions, {blockB, innerBlockB});\n        expect(blockB.activeCollisions, {blockA, innerBlockA});\n        expect(innerBlockA.activeCollisions, {blockB, innerBlockB});\n        expect(innerBlockB.activeCollisions, {blockA, innerBlockA});\n      },\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/collisions/screen_hibox_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:test/test.dart';\n\nimport 'collision_test_helpers.dart';\n\nvoid main() {\n  group('ScreenHitbox', () {\n    runCollisionTestRegistry({\n      'collides': (hasCollisionDetection) async {\n        final game = hasCollisionDetection as FlameGame;\n        final visibleRect = game.camera.visibleWorldRect;\n        final testBlock = TestBlock(\n          visibleRect.topLeft.toVector2(),\n          Vector2.all(10),\n        )..anchor = Anchor.center;\n        final screenHitbox = ScreenHitbox();\n        await game.world.addAll([screenHitbox, testBlock]);\n        await game.ready();\n        game.update(0);\n\n        expect(testBlock.startCounter, 1);\n        expect(testBlock.onCollisionCounter, 1);\n        expect(testBlock.endCounter, 0);\n\n        testBlock.position = Vector2.zero();\n        game.update(0);\n\n        expect(testBlock.startCounter, 1);\n        expect(testBlock.onCollisionCounter, 1);\n        expect(testBlock.endCounter, 1);\n      },\n    });\n\n    runCollisionTestRegistry({\n      'collides when zoom is not 1.0': (hasCollisionDetection) async {\n        final game = hasCollisionDetection as FlameGame;\n        game.camera.viewfinder.zoom = 2.5;\n        final visibleRect = game.camera.visibleWorldRect;\n        final testBlock = TestBlock(\n          visibleRect.topLeft.toVector2(),\n          Vector2.all(10),\n        )..anchor = Anchor.center;\n        final screenHitbox = ScreenHitbox();\n        await game.world.addAll([screenHitbox, testBlock]);\n        await game.ready();\n        game.update(0);\n\n        expect(testBlock.startCounter, 1);\n        expect(testBlock.onCollisionCounter, 1);\n        expect(testBlock.endCounter, 0);\n\n        testBlock.position = Vector2.zero();\n        game.update(0);\n\n        expect(testBlock.startCounter, 1);\n        expect(testBlock.onCollisionCounter, 1);\n        expect(testBlock.endCounter, 1);\n      },\n    });\n\n    runCollisionTestRegistry({\n      'collides when game size changes': (hasCollisionDetection) async {\n        final game = hasCollisionDetection as FlameGame;\n        final visibleRect = game.camera.visibleWorldRect;\n        final testBlock = TestBlock(\n          visibleRect.topLeft.toVector2() / 2,\n          Vector2.all(10),\n        )..anchor = Anchor.center;\n        final screenHitbox = ScreenHitbox();\n        await game.world.addAll([screenHitbox, testBlock]);\n        await game.ready();\n        game.update(0);\n\n        expect(testBlock.startCounter, 0);\n        expect(testBlock.onCollisionCounter, 0);\n        expect(testBlock.endCounter, 0);\n\n        testBlock.position = visibleRect.topLeft.toVector2() / 2;\n        game.onGameResize(game.size / 2);\n        game.update(0);\n\n        expect(testBlock.startCounter, 1);\n        expect(testBlock.onCollisionCounter, 1);\n        expect(testBlock.endCounter, 0);\n      },\n    });\n\n    runCollisionTestRegistry({\n      'collides when angle is not 0.0': (hasCollisionDetection) async {\n        final game = hasCollisionDetection as FlameGame;\n        final visibleRectBeforeRotation = game.camera.visibleWorldRect;\n        game.camera.viewfinder.angle = tau / 8;\n        final visibleRect = game.camera.visibleWorldRect;\n        final testBlock = TestBlock(\n          visibleRect.topLeft.toVector2(),\n          Vector2.all(10),\n        )..anchor = Anchor.center;\n        final screenHitbox = ScreenHitbox();\n        await game.world.addAll([screenHitbox, testBlock]);\n        await game.ready();\n        game.update(0);\n\n        expect(testBlock.startCounter, 0);\n        expect(testBlock.onCollisionCounter, 0);\n        expect(testBlock.endCounter, 0);\n\n        testBlock.position = visibleRectBeforeRotation.topLeft.toVector2();\n        game.update(0);\n\n        expect(testBlock.startCounter, 1);\n        expect(testBlock.onCollisionCounter, 1);\n        expect(testBlock.endCounter, 0);\n      },\n    });\n\n    runCollisionTestRegistry({\n      'collides with FixedResolutionViewport': (hasCollisionDetection) async {\n        final game = hasCollisionDetection as FlameGame;\n        game.camera = CameraComponent.withFixedResolution(\n          width: 100,\n          height: 100,\n        );\n        final testBlock = TestBlock(\n          Vector2.all(-50),\n          Vector2.all(2),\n        )..anchor = Anchor.center;\n        final screenHitbox = ScreenHitbox();\n        await game.world.addAll([screenHitbox, testBlock]);\n        await game.ready();\n        game.update(0);\n\n        expect(testBlock.startCounter, 1);\n        expect(testBlock.onCollisionCounter, 1);\n        expect(testBlock.endCounter, 0);\n\n        testBlock.position = Vector2.all(50);\n        game.update(0);\n\n        expect(testBlock.startCounter, 1);\n        expect(testBlock.onCollisionCounter, 2);\n        expect(testBlock.endCounter, 0);\n\n        testBlock.position = Vector2.all(0);\n        game.update(0);\n\n        expect(testBlock.startCounter, 1);\n        expect(testBlock.onCollisionCounter, 2);\n        expect(testBlock.endCounter, 1);\n      },\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/advanced_button_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/src/events/flame_game_mixins/multi_tap_dispatcher.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('AdvancedButtonComponent', () {\n    testGolden(\n      'label renders correctly',\n      (game, tester) async {\n        await game.add(\n          AdvancedButtonComponent(\n            defaultSkin: RectangleComponent(size: Vector2(40, 20)),\n            defaultLabel: RectangleComponent(\n              size: Vector2(10, 5),\n              paint: Paint()..color = const Color(0xFFFF0000),\n            ),\n          ),\n        );\n      },\n      size: Vector2(50, 30),\n      goldenFile: '../_goldens/advanced_button_component.png',\n    );\n\n    testWithFlameGame('correctly registers taps', (game) async {\n      var pressedTimes = 0;\n      var releasedTimes = 0;\n      final initialGameSize = Vector2.all(200);\n      final componentSize = Vector2.all(10);\n      final buttonPosition = Vector2.all(100);\n      late final AdvancedButtonComponent button;\n      game.onGameResize(initialGameSize);\n      await game.ensureAdd(\n        button = AdvancedButtonComponent(\n          defaultSkin: RectangleComponent(size: componentSize),\n          onPressed: () => pressedTimes++,\n          onReleased: () => releasedTimes++,\n          position: buttonPosition,\n          size: componentSize,\n        ),\n      );\n\n      expect(pressedTimes, 0);\n      expect(releasedTimes, 0);\n      final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n      tapDispatcher.handleTapDown(1, TapDownDetails());\n      expect(pressedTimes, 0);\n      expect(releasedTimes, 0);\n\n      tapDispatcher.handleTapUp(\n        1,\n        createTapUpDetails(\n          globalPosition: button.positionOfAnchor(Anchor.center).toOffset(),\n        ),\n      );\n      expect(pressedTimes, 0);\n      expect(releasedTimes, 0);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: buttonPosition.toOffset()),\n      );\n      expect(pressedTimes, 1);\n      expect(releasedTimes, 0);\n\n      tapDispatcher.handleTapUp(\n        1,\n        createTapUpDetails(globalPosition: buttonPosition.toOffset()),\n      );\n      expect(pressedTimes, 1);\n      expect(releasedTimes, 1);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: buttonPosition.toOffset()),\n      );\n      tapDispatcher.handleTapCancel(1);\n      expect(pressedTimes, 2);\n      expect(releasedTimes, 1);\n    });\n\n    testWithFlameGame('correctly registers taps onGameResize', (game) async {\n      var pressedTimes = 0;\n      var releasedTimes = 0;\n      final initialGameSize = Vector2.all(100);\n      final componentSize = Vector2.all(10);\n      final buttonPosition = Vector2.all(100);\n      late final AdvancedButtonComponent button;\n      game.onGameResize(initialGameSize);\n      await game.ensureAdd(\n        button = AdvancedButtonComponent(\n          defaultSkin: RectangleComponent(size: componentSize),\n          onPressed: () => pressedTimes++,\n          onReleased: () => releasedTimes++,\n          position: buttonPosition,\n          size: componentSize,\n        ),\n      );\n      final previousPosition = button\n          .positionOfAnchor(Anchor.center)\n          .toOffset();\n      game.onGameResize(initialGameSize * 2);\n      final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: previousPosition),\n      );\n      expect(pressedTimes, 1);\n      expect(releasedTimes, 0);\n\n      tapDispatcher.handleTapUp(\n        1,\n        createTapUpDetails(globalPosition: previousPosition),\n      );\n      expect(pressedTimes, 1);\n      expect(releasedTimes, 1);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: previousPosition),\n      );\n      tapDispatcher.handleTapCancel(1);\n      expect(pressedTimes, 2);\n      expect(releasedTimes, 1);\n    });\n\n    testWithFlameGame('correctly work isDisabled', (game) async {\n      var pressedTimes = 0;\n      var releasedTimes = 0;\n      final initialGameSize = Vector2.all(100);\n      final componentSize = Vector2.all(10);\n      final buttonPosition = Vector2.all(100);\n      late final AdvancedButtonComponent button;\n      game.onGameResize(initialGameSize);\n      await game.ensureAdd(\n        button = AdvancedButtonComponent(\n          defaultSkin: RectangleComponent(size: componentSize),\n          onPressed: () => pressedTimes++,\n          onReleased: () => releasedTimes++,\n          position: buttonPosition,\n          size: componentSize,\n        ),\n      );\n      button.isDisabled = true;\n      final previousPosition = button\n          .positionOfAnchor(Anchor.center)\n          .toOffset();\n      game.onGameResize(initialGameSize * 2);\n      final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: previousPosition),\n      );\n      expect(pressedTimes, 0);\n      expect(releasedTimes, 0);\n\n      tapDispatcher.handleTapUp(\n        1,\n        createTapUpDetails(globalPosition: previousPosition),\n      );\n      expect(pressedTimes, 0);\n      expect(releasedTimes, 0);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: previousPosition),\n      );\n      tapDispatcher.handleTapCancel(1);\n      expect(pressedTimes, 0);\n      expect(releasedTimes, 0);\n    });\n\n    testWidgets(\n      '[#1723] can be pressed while the engine is paused',\n      (tester) async {\n        final game = FlameGame();\n        game.add(\n          AdvancedButtonComponent(\n            defaultSkin: CircleComponent(radius: 40),\n            downSkin: CircleComponent(radius: 40),\n            position: Vector2(400, 300),\n            anchor: Anchor.center,\n            onPressed: () {\n              game.pauseEngine();\n              game.overlays.add('pause-menu');\n            },\n          ),\n        );\n        await tester.pumpWidget(\n          GameWidget(\n            game: game,\n            overlayBuilderMap: {\n              'pause-menu': (context, _) {\n                return _SimpleStatelessWidget(\n                  build: (context) {\n                    return Center(\n                      child: OutlinedButton(\n                        onPressed: () {\n                          game.overlays.remove('pause-menu');\n                          game.resumeEngine();\n                        },\n                        child: const Text('Resume'),\n                      ),\n                    );\n                  },\n                );\n              },\n            },\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n\n        await tester.tapAt(const Offset(400, 300));\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.paused, true);\n\n        await tester.tapAt(const Offset(400, 300));\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.paused, false);\n      },\n    );\n  });\n}\n\nclass _SimpleStatelessWidget extends StatelessWidget {\n  const _SimpleStatelessWidget({\n    required Widget Function(BuildContext) build,\n  }) : _build = build;\n\n  final Widget Function(BuildContext) _build;\n\n  @override\n  Widget build(BuildContext context) => _build(context);\n}\n"
  },
  {
    "path": "packages/flame/test/components/button_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/src/events/flame_game_mixins/multi_tap_dispatcher.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('ButtonComponent', () {\n    testWithFlameGame('correctly registers taps', (game) async {\n      var pressedTimes = 0;\n      var releasedTimes = 0;\n      var cancelledTimes = 0;\n      final initialGameSize = Vector2.all(200);\n      final componentSize = Vector2.all(10);\n      final buttonPosition = Vector2.all(100);\n      late final ButtonComponent button;\n      game.onGameResize(initialGameSize);\n      await game.ensureAdd(\n        button = ButtonComponent(\n          button: RectangleComponent(size: componentSize),\n          onPressed: () => pressedTimes++,\n          onReleased: () => releasedTimes++,\n          onCancelled: () => cancelledTimes++,\n          position: buttonPosition,\n          size: componentSize,\n        ),\n      );\n\n      expect(pressedTimes, 0);\n      expect(releasedTimes, 0);\n      expect(cancelledTimes, 0);\n      final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n      tapDispatcher.handleTapDown(1, TapDownDetails());\n      expect(pressedTimes, 0);\n      expect(releasedTimes, 0);\n      expect(cancelledTimes, 0);\n\n      tapDispatcher.handleTapUp(\n        1,\n        createTapUpDetails(\n          globalPosition: button.positionOfAnchor(Anchor.center).toOffset(),\n        ),\n      );\n      expect(pressedTimes, 0);\n      expect(releasedTimes, 0);\n      expect(cancelledTimes, 0);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: buttonPosition.toOffset()),\n      );\n      expect(pressedTimes, 1);\n      expect(releasedTimes, 0);\n      expect(cancelledTimes, 0);\n\n      tapDispatcher.handleTapUp(\n        1,\n        createTapUpDetails(globalPosition: buttonPosition.toOffset()),\n      );\n      expect(pressedTimes, 1);\n      expect(releasedTimes, 1);\n      expect(cancelledTimes, 0);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: buttonPosition.toOffset()),\n      );\n      tapDispatcher.handleTapCancel(1);\n      expect(pressedTimes, 2);\n      expect(releasedTimes, 1);\n      expect(cancelledTimes, 1);\n    });\n\n    testWithFlameGame('correctly registers taps onGameResize', (game) async {\n      var pressedTimes = 0;\n      var releasedTimes = 0;\n      var cancelledTimes = 0;\n      final initialGameSize = Vector2.all(100);\n      final componentSize = Vector2.all(10);\n      final buttonPosition = Vector2.all(100);\n      late final ButtonComponent button;\n      game.onGameResize(initialGameSize);\n      await game.ensureAdd(\n        button = ButtonComponent(\n          button: RectangleComponent(size: componentSize),\n          onPressed: () => pressedTimes++,\n          onReleased: () => releasedTimes++,\n          onCancelled: () => cancelledTimes++,\n          position: buttonPosition,\n          size: componentSize,\n        ),\n      );\n      final previousPosition = button\n          .positionOfAnchor(Anchor.center)\n          .toOffset();\n      game.onGameResize(initialGameSize * 2);\n      final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: previousPosition),\n      );\n      expect(pressedTimes, 1);\n      expect(releasedTimes, 0);\n      expect(cancelledTimes, 0);\n\n      tapDispatcher.handleTapUp(\n        1,\n        createTapUpDetails(globalPosition: previousPosition),\n      );\n      expect(pressedTimes, 1);\n      expect(releasedTimes, 1);\n      expect(cancelledTimes, 0);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: previousPosition),\n      );\n      tapDispatcher.handleTapCancel(1);\n      expect(pressedTimes, 2);\n      expect(releasedTimes, 1);\n      expect(cancelledTimes, 1);\n    });\n\n    testWidgets(\n      '[#1723] can be pressed while the engine is paused',\n      (tester) async {\n        final game = FlameGame();\n        game.add(\n          ButtonComponent(\n            button: CircleComponent(radius: 40),\n            position: Vector2(400, 300),\n            anchor: Anchor.center,\n            onPressed: () {\n              game.pauseEngine();\n              game.overlays.add('pause-menu');\n            },\n          ),\n        );\n        await tester.pumpWidget(\n          GameWidget(\n            game: game,\n            overlayBuilderMap: {\n              'pause-menu': (context, _) {\n                return _SimpleStatelessWidget(\n                  build: (context) {\n                    return Center(\n                      child: OutlinedButton(\n                        onPressed: () {\n                          game.overlays.remove('pause-menu');\n                          game.resumeEngine();\n                        },\n                        child: const Text('Resume'),\n                      ),\n                    );\n                  },\n                );\n              },\n            },\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n\n        await tester.tapAt(const Offset(400, 300));\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.paused, true);\n\n        await tester.tapAt(const Offset(400, 300));\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.paused, false);\n      },\n    );\n  });\n}\n\nclass _SimpleStatelessWidget extends StatelessWidget {\n  const _SimpleStatelessWidget({\n    required Widget Function(BuildContext) build,\n  }) : _build = build;\n\n  final Widget Function(BuildContext) _build;\n\n  @override\n  Widget build(BuildContext context) => _build(context);\n}\n"
  },
  {
    "path": "packages/flame/test/components/clip_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _Rectangle extends RectangleComponent {\n  _Rectangle()\n    : super(\n        size: Vector2(200, 200),\n        anchor: Anchor.center,\n        paint: Paint()..color = Colors.blue,\n      );\n}\n\nvoid main() {\n  group('ClipComponent', () {\n    group('RectangleClipComponent', () {\n      testGolden(\n        'renders correctly',\n        (game, tester) async {\n          await game.add(\n            ClipComponent.rectangle(\n              size: Vector2(100, 100),\n              children: [_Rectangle()],\n            ),\n          );\n        },\n        goldenFile: '../_goldens/clip_component_rect.png',\n      );\n    });\n\n    group('CircleClipComponent', () {\n      testGolden(\n        'renders correctly',\n        (game, tester) async {\n          await game.add(\n            ClipComponent.circle(\n              size: Vector2(100, 100),\n              children: [_Rectangle()],\n            ),\n          );\n        },\n        goldenFile: '../_goldens/clip_component_circle.png',\n      );\n    });\n\n    group('PolygonClipComponent', () {\n      testGolden(\n        'renders correctly',\n        (game, tester) async {\n          await game.add(\n            ClipComponent.polygon(\n              points: [\n                Vector2(1, 0),\n                Vector2(1, 1),\n                Vector2(0, 1),\n                Vector2(1, 0),\n              ],\n              size: Vector2(100, 100),\n              children: [_Rectangle()],\n            ),\n          );\n        },\n        goldenFile: '../_goldens/clip_component_polygon.png',\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/component_pool_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _TestComponent extends PositionComponent {\n  int mountCount = 0;\n\n  @override\n  void onMount() {\n    super.onMount();\n    mountCount++;\n    position.setZero();\n    size.setZero();\n  }\n}\n\nvoid main() {\n  group('ComponentPool', () {\n    group('initialization', () {\n      test('pre-populates pool with initialSize components', () {\n        final pool = ComponentPool<_TestComponent>(\n          factory: _TestComponent.new,\n          initialSize: 5,\n        );\n\n        expect(pool.availableCount, 5);\n\n        final components = <_TestComponent>{};\n        for (var i = 0; i < 5; i++) {\n          components.add(pool.acquire());\n        }\n\n        // All 5 should be unique instances from the pool\n        expect(components.length, 5);\n        expect(pool.availableCount, 0);\n      });\n\n      test('clamps initial size to max size', () {\n        final pool = ComponentPool<_TestComponent>(\n          factory: _TestComponent.new,\n          initialSize: 20,\n          maxSize: 10,\n        );\n\n        expect(pool.availableCount, 10);\n      });\n    });\n\n    group('acquire', () {\n      test('creates distinct instances via factory when pool is empty', () {\n        final pool = ComponentPool<_TestComponent>(\n          factory: _TestComponent.new,\n        );\n\n        expect(pool.availableCount, 0);\n\n        final component1 = pool.acquire();\n        final component2 = pool.acquire();\n\n        expect(identical(component1, component2), false);\n        expect(pool.availableCount, 0);\n      });\n\n      testWithFlameGame(\n        'returns same instance after it is removed from parent',\n        (game) async {\n          final pool = ComponentPool<_TestComponent>(\n            factory: _TestComponent.new,\n            maxSize: 5,\n          );\n\n          final original = pool.acquire();\n          original.position.setValues(10, 20);\n          await game.add(original);\n          await game.ready();\n\n          original.removeFromParent();\n          game.update(0);\n          await game.ready();\n\n          final reacquired = pool.acquire();\n\n          expect(identical(original, reacquired), true);\n          expect(pool.availableCount, 0);\n        },\n      );\n    });\n\n    group('automatic release', () {\n      testWithFlameGame(\n        'returns component to pool when removed from parent',\n        (game) async {\n          final pool = ComponentPool<_TestComponent>(\n            factory: _TestComponent.new,\n            maxSize: 5,\n          );\n\n          final component = pool.acquire();\n          await game.add(component);\n          await game.ready();\n\n          expect(component.isMounted, true);\n          expect(pool.availableCount, 0);\n\n          component.removeFromParent();\n          game.update(0);\n          await game.ready();\n\n          expect(component.isMounted, false);\n          expect(pool.availableCount, 1);\n        },\n      );\n\n      testWithFlameGame(\n        'does not prematurely release recycled component before mounting',\n        (game) async {\n          final pool = ComponentPool<_TestComponent>(\n            factory: _TestComponent.new,\n            maxSize: 5,\n          );\n\n          // First cycle: acquire → mount → remove\n          final comp = pool.acquire();\n          await game.add(comp);\n          await game.ready();\n          comp.removeFromParent();\n          game.update(0);\n          await game.ready();\n\n          expect(pool.availableCount, 1);\n\n          // Re-acquire the recycled component. At this point its `removed`\n          // future is already completed (Future.value()). Without the\n          // mounted.then guard, _release would fire as a microtask and\n          // return it to the pool immediately.\n          final recycled = pool.acquire();\n          expect(identical(comp, recycled), true);\n          expect(pool.availableCount, 0);\n\n          // Mount it again\n          await game.add(recycled);\n          await game.ready();\n\n          // Still in use (mounted, not yet removed)\n          expect(pool.availableCount, 0);\n\n          // Remove it — now it should return to the pool\n          recycled.removeFromParent();\n          game.update(0);\n          await game.ready();\n\n          expect(pool.availableCount, 1);\n        },\n      );\n\n      testWithFlameGame(\n        'supports multiple acquire-remove cycles on same component',\n        (game) async {\n          final pool = ComponentPool<_TestComponent>(\n            factory: _TestComponent.new,\n            maxSize: 1,\n          );\n\n          for (var i = 0; i < 3; i++) {\n            final component = pool.acquire();\n            component.position.setValues(100, 200);\n            await game.add(component);\n            await game.ready();\n\n            expect(component.isMounted, true);\n\n            component.removeFromParent();\n            game.update(0);\n            await game.ready();\n\n            expect(pool.availableCount, 1);\n          }\n\n          // All 3 cycles should have reused the same instance\n          final finalComponent = pool.acquire();\n          expect(finalComponent.mountCount, 3);\n        },\n      );\n\n      testWithFlameGame(\n        'discards components when pool is at max size',\n        (game) async {\n          final pool = ComponentPool<_TestComponent>(\n            factory: _TestComponent.new,\n            maxSize: 2,\n          );\n\n          final component1 = pool.acquire();\n          final component2 = pool.acquire();\n          final component3 = pool.acquire();\n\n          await game.add(component1);\n          await game.add(component2);\n          await game.add(component3);\n          await game.ready();\n\n          component1.removeFromParent();\n          component2.removeFromParent();\n          component3.removeFromParent();\n          game.update(0);\n          await game.ready();\n\n          // Only 2 of 3 should be kept\n          expect(pool.availableCount, 2);\n\n          // After exhausting the pool, a new instance is created\n          pool.acquire();\n          pool.acquire();\n          final fresh = pool.acquire();\n\n          expect(\n            [\n              component1,\n              component2,\n              component3,\n            ].every((c) => !identical(c, fresh)),\n            true,\n          );\n        },\n      );\n\n      testWithFlameGame(\n        'returns components in LIFO order',\n        (game) async {\n          final pool = ComponentPool<_TestComponent>(\n            factory: _TestComponent.new,\n            maxSize: 10,\n          );\n\n          final comp1 = pool.acquire();\n          final comp2 = pool.acquire();\n          final comp3 = pool.acquire();\n\n          await game.add(comp1);\n          await game.add(comp2);\n          await game.add(comp3);\n          await game.ready();\n\n          comp1.removeFromParent();\n          comp2.removeFromParent();\n          comp3.removeFromParent();\n          game.update(0);\n          await game.ready();\n\n          expect(identical(pool.acquire(), comp3), true);\n          expect(identical(pool.acquire(), comp2), true);\n          expect(identical(pool.acquire(), comp1), true);\n        },\n      );\n    });\n\n    group('clear', () {\n      test('removes all available components from pool', () {\n        final pool = ComponentPool<_TestComponent>(\n          factory: _TestComponent.new,\n          initialSize: 5,\n          maxSize: 10,\n        );\n\n        expect(pool.availableCount, 5);\n        pool.clear();\n        expect(pool.availableCount, 0);\n      });\n\n      testWithFlameGame(\n        'does not affect mounted components',\n        (game) async {\n          final pool = ComponentPool<_TestComponent>(\n            factory: _TestComponent.new,\n            maxSize: 5,\n          );\n\n          final component = pool.acquire();\n          await game.add(component);\n          await game.ready();\n\n          pool.clear();\n\n          expect(component.isMounted, true);\n        },\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/component_render_context_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:canvas_test/canvas_test.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Component Render Context', () {\n    testWithFlameGame('simple parent and child', (game) async {\n      final child = _ChildReadsContext();\n      final parent = _ParentWithContext(\n        startingValue: 42,\n        children: [\n          child,\n        ],\n      );\n      game.add(parent);\n\n      await game.ready();\n      game.render(MockCanvas());\n\n      expect(child.myContext, 42);\n    });\n\n    testWithFlameGame('complex parent and child', (game) async {\n      final child1 = _ChildReadsContext();\n      final child2 = _ChildReadsContext();\n      final child3 = _ChildReadsContext();\n\n      final parent = Component(\n        children: [\n          child1,\n          _ParentWithContext(\n            startingValue: 1,\n            children: [\n              child2,\n              _ParentWithContext(\n                startingValue: 2,\n                children: [\n                  child3,\n                ],\n              ),\n            ],\n          ),\n        ],\n      );\n      game.add(parent);\n\n      await game.ready();\n      game.render(MockCanvas());\n\n      expect(child1.myContext, null);\n      expect(child2.myContext, 1);\n      expect(child3.myContext, 2);\n    });\n\n    testWithFlameGame('mutating context', (game) async {\n      final child = _ChildReadsContext();\n\n      final parent = _ParentWithContext(\n        startingValue: 10,\n        children: [\n          child,\n        ],\n      );\n      game.add(parent);\n\n      await game.ready();\n      final canvas = MockCanvas();\n\n      game.render(canvas);\n      expect(child.myContext, 10);\n\n      parent._myContext.value = 20;\n      game.render(canvas);\n      expect(child.myContext, 20);\n    });\n  });\n}\n\nclass _IntContext extends ComponentRenderContext {\n  int value;\n\n  _IntContext(this.value);\n}\n\nclass _ParentWithContext extends Component {\n  final int startingValue;\n  late final _IntContext _myContext = _IntContext(startingValue);\n\n  _ParentWithContext({\n    required this.startingValue,\n    super.children,\n  });\n\n  @override\n  _IntContext get renderContext => _myContext;\n}\n\nclass _ChildReadsContext extends Component {\n  int? myContext;\n\n  @override\n  void render(Canvas canvas) {\n    final context = findRenderContext<_IntContext>();\n    myContext = context?.value;\n\n    super.render(canvas);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/components/component_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/src/components/core/component_tree_root.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ordered_set/mapping_ordered_set.dart';\nimport 'package:ordered_set/ordered_set.dart';\n\nimport '../custom_component.dart';\n\nvoid main() {\n  group('Component', () {\n    group('Lifecycle', () {\n      testWithFlameGame('correct order', (game) async {\n        final component = _LifecycleComponent();\n        await game.world.add(component);\n        await game.ready();\n\n        expect(\n          component.events,\n          ['onLoad', 'onGameResize [800.0,600.0]', 'onMount'],\n        );\n      });\n\n      testWithFlameGame('component mounted completes', (game) async {\n        final component = _LifecycleComponent();\n        final mounted = component.mounted;\n        await game.world.add(component);\n        await game.ready();\n\n        await expectLater(mounted, completes);\n        await expectLater(component.mounted, completes);\n      });\n\n      testWithFlameGame(\n        'component.removed completes if obtained before the game was ready',\n        (game) async {\n          final component = _LifecycleComponent();\n          final removed = component.removed;\n          await game.world.add(component);\n          await game.ready();\n\n          game.world.remove(component);\n          game.update(0);\n\n          await expectLater(removed, completes);\n        },\n      );\n\n      testWithFlameGame(\n        'component removed completes when set after game is ready',\n        (game) async {\n          final component = _LifecycleComponent();\n          await game.world.add(component);\n          await game.ready();\n          final removed = component.removed;\n\n          game.world.remove(component);\n          game.update(0);\n          await expectLater(removed, completes);\n        },\n      );\n\n      testWithFlameGame(\n        'component removed completes after changing parent',\n        (game) async {\n          final parent = _LifecycleComponent('parent')..addToParent(game);\n          final child = _LifecycleComponent('child')..addToParent(parent);\n          await game.ready();\n          final removed = child.removed;\n\n          child.parent = game;\n          game.update(0);\n          await expectLater(removed, completes);\n\n          final removedFromParent = child.removed;\n          child.removeFromParent();\n          game.update(0);\n          await expectLater(removedFromParent, completes);\n        },\n      );\n\n      testWithFlameGame('remove parent of child that has removed set', (\n        game,\n      ) async {\n        final parent = _LifecycleComponent('parent')..addToParent(game);\n        final child = _LifecycleComponent('child')..addToParent(parent);\n        await game.ready();\n        final removed = child.removed;\n\n        parent.removeFromParent();\n        game.update(0);\n        await expectLater(removed, completes);\n        expect(child.isRemoved, true);\n      });\n\n      testWithFlameGame(\n        'component mounted completes when changing parent',\n        (game) async {\n          final parent = _LifecycleComponent('parent');\n          final child = _LifecycleComponent('child');\n          parent.add(child);\n          game.world.add(parent);\n\n          var mounted = child.mounted;\n          await game.ready();\n\n          await expectLater(mounted, completes);\n\n          child.parent = game;\n          mounted = child.mounted;\n          game.update(0);\n          await game.ready();\n\n          await expectLater(mounted, completes);\n        },\n      );\n\n      testWithFlameGame(\n        'component mounted completes when changing parent from a null parent',\n        (game) async {\n          final parent = _LifecycleComponent('parent');\n          final child = _LifecycleComponent('child');\n          game.world.add(parent);\n\n          final mounted = child.mounted;\n          await game.ready();\n\n          child.parent = parent;\n          game.update(0);\n          await game.ready();\n\n          expect(child.parent, parent);\n          expect(child.isMounted, true);\n          await expectLater(mounted, completes);\n        },\n      );\n\n      testWithFlameGame(\n        'Component.mounted completes after the component is mounted',\n        (game) async {\n          final child = _LifecycleComponent();\n          var mountedFutureCompleted = false;\n          final future = child.mounted.then((_) {\n            expect(child.isMounted, true);\n            expect(child.events, contains('onMount'));\n            mountedFutureCompleted = true;\n          });\n          game.world.add(child);\n          await game.ready();\n          expect(child.isMounted, true);\n          await future;\n          expect(mountedFutureCompleted, true);\n        },\n      );\n\n      testWithFlameGame('component loaded completes', (game) async {\n        final component = _LifecycleComponent();\n        await game.world.add(component);\n        final loaded = component.loaded;\n\n        await game.ready();\n\n        await expectLater(loaded, completes);\n        await expectLater(component.loaded, completes);\n      });\n\n      testWithFlameGame(\n        'loaded completes even if accessed before the component added to game',\n        (game) async {\n          final component = Component();\n          final loadedFuture = component.loaded;\n          game.world.add(component);\n          await game.ready();\n          expectLater(loadedFuture, completes);\n        },\n      );\n\n      testWithFlameGame('correct lifecycle on parent change', (game) async {\n        final parent = _LifecycleComponent('parent');\n        final child = _LifecycleComponent('child');\n        parent.add(child);\n        game.world.add(parent);\n        await game.ready();\n        child.parent = game;\n        game.update(0);\n        await game.ready();\n\n        expect(\n          parent.events,\n          ['onLoad', 'onGameResize [800.0,600.0]', 'onMount'],\n        );\n        // onLoad should only be called the first time that the component is\n        // loaded.\n        expect(\n          child.events,\n          [\n            'onLoad',\n            'onGameResize [800.0,600.0]',\n            'onMount',\n            'onRemove',\n            'onGameResize [800.0,600.0]',\n            'onMount',\n          ],\n        );\n      });\n\n      testWithFlameGame(\n        'removed children are not re-added when parent is moved',\n        (game) async {\n          final grandParent1 = Component();\n          final grandParent2 = Component();\n          final parent = Component();\n          final child = Component();\n          parent.add(child);\n          await game.world.ensureAdd(grandParent1);\n          await game.world.ensureAdd(grandParent2);\n          await grandParent1.ensureAdd(parent);\n\n          expect(parent.children.length, 1);\n          expect(child.parent, parent);\n\n          // In the same tick: move parent and remove child\n          parent.parent = grandParent2;\n          parent.remove(child);\n          game.update(0);\n          await game.ready();\n\n          expect(parent.children.length, 0);\n          expect(child.parent, isNull);\n          expect(child.isMounted, false);\n        },\n      );\n\n      testWithFlameGame(\n        'removed children are not re-added when parent is moved '\n        '(remove before move)',\n        (game) async {\n          final grandParent1 = Component();\n          final grandParent2 = Component();\n          final parent = Component();\n          final child = Component();\n          parent.add(child);\n          await game.world.ensureAdd(grandParent1);\n          await game.world.ensureAdd(grandParent2);\n          await grandParent1.ensureAdd(parent);\n\n          expect(parent.children.length, 1);\n          expect(child.parent, parent);\n\n          // In the same tick: remove child then move parent\n          parent.remove(child);\n          parent.parent = grandParent2;\n          game.update(0);\n          await game.ready();\n\n          expect(parent.children.length, 0);\n          expect(child.parent, isNull);\n          expect(child.isMounted, false);\n        },\n      );\n\n      testWithFlameGame(\n        'only removed children are skipped when parent is moved',\n        (game) async {\n          final grandParent1 = Component();\n          final grandParent2 = Component();\n          final parent = Component();\n          final child1 = Component();\n          final child2 = Component();\n          parent.add(child1);\n          parent.add(child2);\n          await game.world.ensureAdd(grandParent1);\n          await game.world.ensureAdd(grandParent2);\n          await grandParent1.ensureAdd(parent);\n\n          expect(parent.children.length, 2);\n\n          // Move parent and remove only child1\n          parent.parent = grandParent2;\n          parent.remove(child1);\n          game.update(0);\n          await game.ready();\n\n          expect(parent.children.length, 1);\n          expect(child1.parent, isNull);\n          expect(child1.isMounted, false);\n          expect(child2.parent, parent);\n          expect(child2.isMounted, true);\n        },\n      );\n\n      testWithFlameGame(\n        'components added in correct order even with different load times',\n        (game) async {\n          final a = _SlowComponent('A', 0.1);\n          final b = _SlowComponent('B', 0.02);\n          final c = _SlowComponent('C', 0.05);\n          final d = _SlowComponent('D', 0);\n          game.world.add(a);\n          game.world.add(b);\n          game.world.add(c);\n          game.world.add(d);\n          await game.ready();\n          expect(game.world.children.toList(), equals([a, b, c, d]));\n        },\n      );\n\n      testWidgets('Multi-widget game', (WidgetTester tester) {\n        return tester.runAsync(() async {\n          final game1 = FlameGame();\n          final game2 = FlameGame();\n          // Device size is set to 800x600\n          await tester.pumpWidget(\n            Row(\n              textDirection: TextDirection.ltr,\n              children: [\n                SizedBox(width: 295, child: GameWidget(game: game1)),\n                SizedBox(width: 505, child: GameWidget(game: game2)),\n              ],\n            ),\n          );\n          final component1 = _LifecycleComponent('A')..addToParent(game1);\n          final component2 = _LifecycleComponent('B')..addToParent(game2);\n          await game1.ready();\n          await game2.ready();\n          expect(\n            component1.events,\n            ['onLoad', 'onGameResize [295.0,600.0]', 'onMount'],\n          );\n          expect(\n            component2.events,\n            ['onLoad', 'onGameResize [505.0,600.0]', 'onMount'],\n          );\n        });\n      });\n\n      testWithFlameGame(\n        'Remove and re-add component with children',\n        (game) async {\n          final parent = _LifecycleComponent('parent');\n          final child = _LifecycleComponent('child')..addToParent(parent);\n          await game.world.add(parent);\n          await game.ready();\n\n          expect(parent.isMounted, true);\n          expect(child.isMounted, true);\n          expect(parent.parent, game.world);\n          expect(parent.parent?.parent, game);\n          expect(child.parent, parent);\n\n          parent.removeFromParent();\n          await game.ready();\n\n          expect(parent.isMounted, false);\n          expect(child.isMounted, false);\n          expect(parent.parent, isNull);\n          expect(child.parent, isNotNull);\n\n          await game.world.add(parent);\n          await game.ready();\n\n          expect(parent.isMounted, true);\n          expect(child.isMounted, true);\n          expect(parent.parent, game.world);\n          expect(parent.parent?.parent, game);\n          expect(child.parent, parent);\n        },\n      );\n\n      testWithFlameGame(\n        'Parent removal should not lead to null parent of descendants',\n        (game) async {\n          final parent = _LifecycleComponent('parent');\n          final child = _LifecycleComponent('child')..addToParent(parent);\n          final grandChild = _LifecycleComponent('grandchild')\n            ..addToParent(child);\n          await game.world.add(parent);\n          await game.ready();\n\n          expect(parent.isMounted, true);\n          expect(child.isMounted, true);\n          expect(grandChild.isMounted, true);\n          expect(parent.parent, game.world);\n          expect(parent.parent?.parent, game);\n          expect(child.parent, parent);\n          expect(grandChild.parent, child);\n\n          parent.removeFromParent();\n          await game.ready();\n\n          expect(parent.isMounted, false);\n          expect(child.isMounted, false);\n          expect(grandChild.isMounted, false);\n          expect(parent.parent, isNull);\n          expect(child.parent, isNotNull);\n          expect(grandChild.parent, isNotNull);\n        },\n      );\n\n      _myDetachableGame(open: false).testGameWidget(\n        'Confirm child component only loads once with game widget change',\n        verify: (game, tester) async {\n          final child = _LifecycleComponent();\n          expect(game.onAttachCalled, false);\n          expect(game.isLoaded, false);\n          expect(game.isMounted, false);\n\n          await tester.tap(find.text('Toggle'));\n          // First will be the build of the wrapper\n          await tester.pump();\n          // Second will be the build of the game widget itself\n          await tester.pump();\n\n          expect(game.onAttachCalled, true);\n          expect(game.onDetachCalled, false);\n          expect(game.isAttached, true);\n          expect(game.isLoaded, true);\n          expect(game.isMounted, true);\n          expect(child.isLoaded, false);\n          expect(child.isMounted, false);\n          await game.world.add(child);\n          expect(child.isLoaded, true);\n          await tester.pump();\n          expect(child.isMounted, true);\n          expect(game.world.children.length, 1);\n\n          await tester.tap(find.text('Toggle'));\n          await tester.pump();\n\n          expect(game.onDetachCalled, true);\n          expect(game.world.children.length, 1);\n          game.resetValues();\n          expect(game.isAttached, false);\n          expect(game.isMounted, true);\n          expect(child.isMounted, true);\n\n          await tester.tap(find.text('Toggle'));\n          await tester.pump();\n          await tester.pump();\n\n          expect(game.onAttachCalled, true);\n          expect(game.isAttached, true);\n          expect(game.isMounted, true);\n          expect(child.isMounted, true);\n        },\n      );\n\n      group('lifecycleEventsProcessed', () {\n        testWithFlameGame('waits for unprocessed events', (game) async {\n          await game.ready();\n          final component = _LifecycleComponent();\n          await game.world.add(component);\n          expect(game.hasLifecycleEvents, isTrue);\n\n          Future.delayed(Duration.zero).then((_) => game.update(0));\n          await game.lifecycleEventsProcessed;\n          expect(game.hasLifecycleEvents, isFalse);\n        });\n\n        testWithFlameGame(\"doesn't block when there are no events\", (\n          game,\n        ) async {\n          await game.ready();\n          expect(game.hasLifecycleEvents, isFalse);\n          await game.lifecycleEventsProcessed;\n          expect(game.hasLifecycleEvents, isFalse);\n        });\n\n        testWithFlameGame('guarantees addition even with heavy onLoad', (\n          game,\n        ) async {\n          await game.ready();\n          final component = _SlowComponent('heavy', 0.1);\n          final child = _SlowComponent('child', 0.1);\n          await component.add(child);\n          await game.world.add(component);\n          expect(game.world.children, isNot(contains(component)));\n\n          game.lifecycleEventsProcessed.then(\n            expectAsync1((_) {\n              expect(game.world.children, contains(component));\n              expect(component.children, contains(child));\n            }),\n          );\n\n          await game.ready();\n        });\n\n        testWithFlameGame('completes even with dequeued event', (game) async {\n          final parent1 = Component();\n          final parent2 = Component();\n          game.addAll([parent1, parent2]);\n          await game.ready();\n          final component = _SlowComponent('heavy', 0.1);\n          final child = _SlowComponent('child', 0.1);\n          await component.add(child);\n          await parent1.add(component);\n\n          expect(game.lifecycleEventsProcessed, completes);\n\n          await Future.delayed(Duration.zero).then((_) => game.update(0));\n          assert(\n            game.hasLifecycleEvents,\n            'One update should not have been enough '\n            'to add the heavy component',\n          );\n\n          // Trigger dequeue.\n          component.parent = parent2;\n\n          await game.ready();\n        });\n      });\n\n      testWithFlameGame('Can wait for lifecycleEventsProcessed', (game) async {\n        await game.ready();\n        final component = Component();\n        await game.world.add(component);\n        expect(game.hasLifecycleEvents, isTrue);\n\n        Future.delayed(Duration.zero).then((_) => game.update(0));\n        await game.lifecycleEventsProcessed;\n        expect(game.hasLifecycleEvents, isFalse);\n      });\n    });\n\n    group('onGameResize', () {\n      testWithFlameGame('game calls onGameResize during add', (game) async {\n        final a = _GameResizeComponent('a');\n        await game.ensureAdd(a);\n        expect(a.gameSize, Vector2(800, 600));\n      });\n\n      testWithFlameGame('game calls resize after added', (game) async {\n        final a = _GameResizeComponent('a');\n        await game.ensureAdd(a);\n        game.onGameResize(Vector2(100, 100));\n        expect(a.gameSize, Vector2(100, 100));\n      });\n\n      testWithFlameGame(\n        \"game calls doesn't change component size\",\n        (game) async {\n          final a = _GameResizeComponent('a');\n          await game.ensureAdd(a);\n          game.onGameResize(Vector2.all(100));\n          expect(a.size, isNot(Vector2.all(100)));\n        },\n      );\n    });\n\n    group('Adding components', () {\n      testWithFlameGame(\n        'child is not added until the component is loaded',\n        (game) async {\n          final child = Component();\n          final parent = Component();\n          await parent.add(child);\n\n          expect(child.isLoaded, false);\n          expect(child.isMounted, false);\n\n          await game.ensureAdd(parent);\n\n          expect(child.isLoaded, true);\n          expect(child.isMounted, true);\n          expect(parent.contains(child), true);\n        },\n      );\n\n      testWithFlameGame('children in the constructor', (game) async {\n        game.world.add(\n          Component(\n            children: [_ComponentA(), _ComponentB()],\n          ),\n        );\n        await game.ready();\n\n        expect(game.world.children.length, 1);\n        expect(game.world.children.first.children.length, 2);\n        expect(\n          game.world.children.first.children.elementAt(0),\n          isA<_ComponentA>(),\n        );\n        expect(\n          game.world.children.first.children.elementAt(1),\n          isA<_ComponentB>(),\n        );\n      });\n\n      testWithFlameGame('add multiple children with addAll', (game) async {\n        final children = List.generate(10, (_) => _AsyncLoadingChild());\n        final parent = Component(children: children);\n        await game.ensureAdd(parent);\n        expect(parent.children.length, children.length);\n      });\n\n      testWithFlameGame(\n        'removing a component and re-adding it to the same parent in the '\n        'same tick',\n        (game) async {\n          final child = Component();\n          final parent = Component(children: [child]);\n          await game.ensureAdd(parent);\n          child.removeFromParent();\n          parent.add(child);\n          game.update(0);\n          expect(child.parent, parent);\n          expect(parent.children, [child]);\n          expect(child.isMounted, isTrue);\n          expect(child.isRemoving, isFalse);\n        },\n      );\n\n      testWithFlameGame(\n        'removing a component and re-adding it to the same parent in the '\n        'same tick with setter',\n        (game) async {\n          final child = Component();\n          final parent = Component(children: [child]);\n          await game.ensureAdd(parent);\n          child.removeFromParent();\n          child.parent = parent;\n          game.update(0);\n          expect(child.parent, parent);\n          expect(parent.children, [child]);\n          expect(child.isMounted, isTrue);\n          expect(child.isRemoving, isFalse);\n        },\n      );\n\n      testWithFlameGame(\n        'removing a component and adding it to another parent in the same tick',\n        (game) async {\n          final child = Component();\n          final parent = Component(children: [child]);\n          final otherParent = Component();\n          await game.ensureAddAll([parent, otherParent]);\n          child.removeFromParent();\n          otherParent.add(child);\n          game.update(0);\n          expect(child.parent, otherParent);\n          expect(parent.children, []);\n          expect(otherParent.children, [child]);\n          expect(child.isMounted, isTrue);\n        },\n      );\n\n      testWithFlameGame(\n        'move a component from a mounted parent to an unmounted one',\n        (game) async {\n          final child = Component();\n          final mountedParent = Component(children: [child]);\n          final unmountedParent = Component();\n          await game.ensureAdd(mountedParent);\n          unmountedParent.add(child);\n          game.update(0);\n          expect(child.parent, unmountedParent);\n          expect(mountedParent.children, []);\n          expect(unmountedParent.children, [child]);\n          expect(child.isMounted, isFalse);\n          await game.ensureAdd(unmountedParent);\n          expect(child.isMounted, isTrue);\n        },\n      );\n\n      testWithFlameGame(\n        'swapping between multiple parents in the same tick',\n        (game) async {\n          final child = Component();\n          final parents = [\n            Component(children: [child]),\n            Component(),\n            Component(),\n          ];\n          await game.ensureAddAll(parents);\n          child.parent = parents[1];\n          child.parent = parents[2];\n          game.update(0);\n          expect(child.parent, parents[2]);\n          expect(parents[0].children, []);\n          expect(parents[1].children, []);\n          expect(parents[2].children, [child]);\n          expect(child.isMounted, isTrue);\n        },\n      );\n\n      testWithFlameGame('children in constructor and onLoad', (game) async {\n        final component = _TwoChildrenComponent(\n          children: [_ComponentA(), _ComponentB()],\n        );\n        game.world.add(component);\n        await game.ready();\n\n        expect(game.world.children.length, 1);\n        expect(game.world.children.first, component);\n        expect(component.children.length, 4);\n        expect(component.children.elementAt(0), isA<_ComponentA>());\n        expect(component.children.elementAt(1), isA<_ComponentB>());\n        expect(component.children.elementAt(2), component.child1);\n        expect(component.children.elementAt(3), component.child2);\n      });\n\n      testWithGame<_PrepareGame>(\n        'adding children to a parent that is not yet added to a game should '\n        'not run double onMount',\n        _PrepareGame.new,\n        (game) async {\n          await game.ready();\n          final parent = game.prepareParent;\n          expect(parent.onMountRuns, 1);\n          expect(parent.children.isNotEmpty, true);\n          expect((parent.children.first as _OnPrepareComponent).onMountRuns, 1);\n        },\n      );\n\n      testWithFlameGame(\n        'game resize while components are being added',\n        (game) async {\n          final component = _ComponentWithSizeHistory();\n          game.world.add(component);\n          expect(component.history, isEmpty);\n          expect(component.isLoading, false);\n          expect(component.isLoaded, true);\n          expect(component.isMounted, false);\n          game.onGameResize(Vector2(500, 300));\n          game.onGameResize(Vector2(300, 500));\n          expect(\n            component.history,\n            equals([Vector2(500, 300), Vector2(300, 500)]),\n          );\n          await game.ready();\n          expect(component.history.length, 3);\n          expect(component.history.last, equals(Vector2(300, 500)));\n        },\n      );\n\n      testWithFlameGame(\n        'when child is async loading, the child is added to the component '\n        'only after loading',\n        (game) async {\n          final child = _AsyncLoadingChild();\n          final wrapper = Component();\n          await game.ensureAdd(wrapper);\n\n          final future = wrapper.add(child);\n          expect(wrapper.contains(child), false);\n          await future;\n          expect(wrapper.contains(child), false);\n          await game.ready();\n          expect(wrapper.contains(child), true);\n        },\n      );\n\n      testWithFlameGame('when parent is in removing state', (game) async {\n        final parent = Component();\n        final child = Component();\n\n        await game.add(parent);\n        await game.ready();\n\n        // Remove the parent and add the child in the same tick.\n        parent.removeFromParent();\n        await parent.add(child);\n\n        // Timeout is added because processLifecycleEvents of ComponentTreeRoot\n        // gets blocked in such cases.\n\n        // Expect the ready future to complete\n        await expectLater(\n          game.ready().timeout(const Duration(seconds: 2)),\n          completes,\n        );\n        expect(game.hasLifecycleEvents, isFalse);\n\n        // Adding the parent again should eventually mount the child as well.\n        await game.add(parent);\n        await game.ready();\n        expect(child.isMounted, true);\n      });\n\n      testWithFlameGame(\n        \"can remove component's children before adding the parent\",\n        (game) async {\n          final c = _ComponentWithChildrenRemoveAll();\n          game.add(c);\n\n          await game.ready();\n        },\n      );\n    });\n\n    group('Removing components', () {\n      testWithFlameGame('removing child from a component', (game) async {\n        final child = Component();\n        final parent = Component();\n        await game.ensureAdd(parent);\n        expect(parent.isMounted, true);\n\n        await parent.add(child);\n        game.update(0); // children are only added on the next tick\n        expect(parent.contains(child), true);\n\n        parent.remove(child);\n        game.update(0); // children are only removed on the next tick\n        expect(parent.contains(child), false);\n      });\n\n      testWithFlameGame(\n        'removeFromParent()',\n        (game) async {\n          final component = Component()..addToParent(game);\n          await game.ready();\n\n          expect(component.isMounted, true);\n          component.removeFromParent();\n          await game.ready();\n\n          expect(component.isMounted, false);\n          expect(component.isLoaded, true);\n          expect(game.world.children.length, 0);\n        },\n      );\n\n      testWithFlameGame(\n        'remove and re-add should not double trigger onRemove',\n        (game) async {\n          final component = _LifecycleComponent();\n          await game.ensureAdd(component);\n\n          component.removeFromParent();\n          game.update(0);\n          expect(component.countEvents('onRemove'), 1);\n          expect(component.isMounted, false);\n\n          game.world.add(component);\n          await game.ready();\n          expect(component.countEvents('onRemove'), 1);\n          expect(game.world.children.length, 1);\n        },\n      );\n\n      testWithFlameGame(\n        'try to remove a component before it was ever added',\n        (game) async {\n          expect(\n            () => game.world.remove(Component()),\n            failsAssert(\n              \"Trying to remove a component that doesn't belong to any parent\",\n            ),\n          );\n        },\n      );\n\n      testWithFlameGame(\n        'try to remove component from a wrong parent',\n        (game) async {\n          final badParent = Component()..addToParent(game);\n          final child = Component()..addToParent(game);\n          await game.ready();\n          expect(\n            () => badParent.remove(child),\n            failsAssert(\n              'Trying to remove a component that belongs to a different '\n              \"parent: this = Instance of 'Component', component's parent = \"\n              \"Instance of 'FlameGame<World>'\",\n            ),\n          );\n        },\n      );\n\n      testWithFlameGame(\n        'remove an uninitialized component',\n        (game) async {\n          final parent = Component();\n          final child = Component()..addToParent(parent);\n          expect(child.isLoading, false);\n          expect(child.isLoaded, false);\n          expect(child.isMounted, false);\n          child.removeFromParent();\n\n          game.world.add(parent);\n          await game.ready();\n\n          expect(child.isLoading, false);\n          expect(child.isLoaded, false);\n          expect(child.isMounted, false);\n          expect(parent.isMounted, true);\n          expect(parent.children.length, 0);\n          expect(child.parent, isNull);\n        },\n      );\n\n      testWithFlameGame(\n        'remove and re-add uninitialized component with non-trivial onLoad',\n        (game) async {\n          final parent = Component();\n          final component = _SlowLoadingComponent();\n          parent.add(component);\n          // Since [parent] is detached, the [component] cannot start loading\n          expect(component.isLoading, false);\n          parent.remove(component);\n          expect(component.isLoading, false);\n          expect(component.onLoadCalledCount, 0);\n\n          final newParent = Component()..addToParent(game);\n          newParent.add(component);\n          expect(component.isLoading, true);\n          expect(component.onLoadCalledCount, 1);\n          await game.ready();\n          expect(component.isMounted, true);\n          expect(newParent.children.length, 1);\n          expect(newParent.children.first, component);\n        },\n      );\n\n      testWithFlameGame(\n        'remove component immediately after adding',\n        (game) async {\n          final component = _LifecycleComponent();\n          game.world.add(component);\n          expect(component.isLoading, true);\n          expect(component.isLoaded, false);\n          game.world.remove(component);\n          await game.ready();\n\n          expect(game.world.children.length, 0);\n          expect(component.isLoaded, true);\n          expect(component.isMounted, false);\n          // onRemove shouldn't be called because there was never an onMount\n          expect(component.events, ['onLoad']);\n        },\n      );\n\n      testWithFlameGame(\n        'remove slow-loading component immediately after adding',\n        (game) async {\n          final component = _SlowLoadingComponent();\n          game.world.add(component);\n          expect(component.isLoading, true);\n          expect(component.isLoaded, false);\n          game.world.remove(component);\n          await game.ready();\n\n          expect(game.world.children.length, 0);\n          expect(component.isMounted, false);\n        },\n      );\n\n      testWithFlameGame(\n        'component removes itself from onLoad',\n        (game) async {\n          final component = _SelfRemovingOnLoadComponent();\n          game.world.add(component);\n          await game.ready();\n\n          expect(game.world.children.length, 0);\n          expect(component.isLoaded, true);\n          expect(component.isMounted, false);\n        },\n      );\n\n      testWithFlameGame(\n        'component removes itself from onMount',\n        (game) async {\n          final component = _SelfRemovingOnMountComponent();\n          game.world.add(component);\n          await game.ready();\n\n          expect(game.world.children.length, 0);\n          expect(component.isLoaded, true);\n          expect(component.isMounted, false);\n        },\n      );\n\n      testWithFlameGame(\n        'Quickly removed component can be re-added',\n        (game) async {\n          final component = _LifecycleComponent();\n          game.world.add(component);\n          game.world.remove(component);\n          await game.ready();\n          component.events.add('--');\n\n          expect(game.world.children.length, 0);\n          game.world.add(component);\n          await game.ready();\n\n          expect(game.world.children.length, 1);\n          expect(component.isMounted, true);\n          expect(component.isLoaded, true);\n          expect(\n            component.events,\n            [\n              'onLoad',\n              '--',\n              'onGameResize [800.0,600.0]',\n              'onMount',\n            ],\n          );\n        },\n      );\n\n      testWithFlameGame('remove a tree of components', (game) async {\n        final component1 = Component();\n        final component2 = Component();\n        final component3 = Component();\n        component1.addAll([component2, component3]);\n        game.world.add(component1);\n        await game.ready();\n\n        expect(component1.isMounted, true);\n        expect(component2.isMounted, true);\n        expect(component3.isMounted, true);\n        component1.removeFromParent();\n        component1.remove(component2);\n        await game.ready();\n\n        expect(component1.isMounted, false);\n        expect(component2.isMounted, false);\n        expect(component3.isMounted, false);\n\n        game.world.add(component1);\n        game.world.add(component2);\n        await game.ready();\n\n        expect(component1.isMounted, true);\n        expect(component2.isMounted, true);\n        expect(component3.isMounted, true);\n        expect(game.world.children.length, 2);\n        expect(component1.children.length, 1);\n\n        game.descendants().forEach((component) {\n          expect(component.isMounted, true);\n          expect(component.parent!.children.contains(component), true);\n        });\n      });\n\n      testWithFlameGame(\n        'remove component from a paused game',\n        (game) async {\n          game.pauseEngine();\n\n          final component = Component();\n          await game.world.add(component);\n          game.world.remove(component);\n\n          game.resumeEngine();\n          game.update(0);\n        },\n      );\n\n      testWithFlameGame(\n        'removeWhere removes the correct components',\n        (game) async {\n          final components = List.generate(\n            10,\n            _IdentifiableComponent.new,\n          );\n          game.world.addAll(components);\n          await game.ready();\n          expect(game.world.children.length, 10);\n          game.world.removeWhere(\n            (c) => (c as _IdentifiableComponent).id.isEven,\n          );\n          game.update(0);\n          expect(game.world.children.length, 5);\n          expect(\n            game.world.children.every(\n              (c) => (c as _IdentifiableComponent).id.isOdd,\n            ),\n            true,\n          );\n        },\n      );\n\n      testWithFlameGame(\n        'removeWhere works before all components are mounted',\n        (game) async {\n          game.world.add(_RemoveWhereComponent());\n          expect(\n            () async {\n              await game.ready();\n            },\n            returnsNormally,\n          );\n        },\n      );\n\n      testWithFlameGame(\n        'removeAll(children) in onRemove detaches children',\n        (game) async {\n          final parent = _RemoveAllChildrenComponent();\n          final child = _LifecycleComponent('child')..addToParent(parent);\n          await game.world.add(parent);\n          await game.ready();\n          parent.removeFromParent();\n          game.update(0);\n          expect(parent.isMounted, false);\n          expect(child.isMounted, false);\n          expect(child.parent, isNull);\n          expect(parent.children.length, 0);\n          expect(parent.parent, isNull);\n        },\n      );\n\n      testWithFlameGame(\n        'children retain parent when ancestor is removed without explicit '\n        'child removal',\n        (game) async {\n          final parent = Component();\n          final child = _LifecycleComponent('child')..addToParent(parent);\n          await game.world.add(parent);\n          await game.ready();\n          parent.removeFromParent();\n          game.update(0);\n          expect(parent.isMounted, false);\n          expect(child.isMounted, false);\n          expect(child.parent, parent);\n          expect(parent.parent, isNull);\n        },\n      );\n    });\n\n    group('Moving components', () {\n      testWithFlameGame('moving to unrelated component', (game) async {\n        final parentA = Component()..addToParent(game);\n        final parentB = Component()..addToParent(game);\n        final child = Component()..addToParent(parentA);\n        await game.ready();\n\n        expect(child.isMounted, true);\n        expect(child.parent, parentA);\n\n        child.parent = parentB;\n        await game.ready();\n        expect(child.isMounted, true);\n        expect(child.parent, parentB);\n        expect(parentA.hasChildren, false);\n        expect(parentB.hasChildren, true);\n      });\n\n      testWithFlameGame('moving to sibling', (game) async {\n        final componentA = Component()..addToParent(game.world);\n        final componentB = Component()..addToParent(game.world);\n        await game.ready();\n        expect(game.world.children.toList(), [componentA, componentB]);\n        expect(componentA.hasChildren, false);\n        expect(componentB.hasChildren, false);\n\n        componentA.parent = componentB;\n        await game.ready();\n        expect(game.world.children.toList(), [componentB]);\n        expect(componentB.children.toList(), [componentA]);\n        expect(componentA.parent, componentB);\n      });\n\n      testWithFlameGame('moving to parent', (game) async {\n        final parent = Component()..addToParent(game.world);\n        final child = Component()..addToParent(parent);\n        await game.ready();\n        expect(game.world.children.toList(), [parent]);\n        expect(parent.children.toList(), [child]);\n\n        child.parent = game.world;\n        await game.ready();\n        expect(game.world.children.toList(), [parent, child]);\n        expect(parent.children.toList(), isEmpty);\n      });\n    });\n\n    group('Rebalancing components', () {\n      testWithFlameGame(\n        'rebalance is correctly queued',\n        (game) async {\n          final c = Component();\n          await game.world.ensureAdd(c);\n\n          c.priority = 10;\n          expect(c.priority, 10);\n          expect(\n            game.queue.any(\n              (e) =>\n                  e.child == c &&\n                  e.parent == game.world &&\n                  e.kind == LifecycleEventKind.rebalance,\n            ),\n            isTrue,\n          );\n\n          await game.ready();\n          expect(c.priority, 10);\n          expect(game.queue.isEmpty, isTrue);\n        },\n      );\n\n      testWithFlameGame(\n        'the order of children is not changed until after rebalance',\n        (game) async {\n          final c1 = Component(priority: 2);\n          final c2 = Component(priority: 1);\n          await game.world.ensureAddAll([c1, c2]);\n\n          c1.priority = 0;\n          expect(c1.priority, 0);\n          expect(c2.priority, 1);\n          expect(\n            game.world.children.toList(),\n            [c2, c1],\n          );\n\n          game.update(0);\n          expect(c1.priority, 0);\n          expect(c2.priority, 1);\n          expect(\n            game.world.children.toList(),\n            [c1, c2],\n          );\n        },\n      );\n\n      testWithFlameGame(\n        'async rebalance, add and remove have no race condition',\n        (game) async {\n          final r = Random(69420);\n          for (var i = 0; i < 10; i++) {\n            game.world.add(Component());\n          }\n\n          Future<void> add() async {\n            final white = Component();\n            game.world.add(white);\n            await Future.delayed(\n              const Duration(milliseconds: 300),\n              white.removeFromParent,\n            );\n          }\n\n          Future<void> rebalance() async {\n            game.world.children.forEach((it) {\n              it.priority = r.nextInt(1_000);\n            });\n          }\n\n          var completed = 0;\n          var total = 0;\n          void waitFor(int milliseconds, Future<void> Function() fn) {\n            total++;\n            Future.delayed(\n              Duration(milliseconds: milliseconds),\n              fn,\n            ).whenComplete(() => completed++);\n          }\n\n          Future<void> start() async {\n            for (var i = 0; i < 100; i++) {\n              waitFor(17 * i, rebalance);\n              waitFor(31 * i, add);\n            }\n          }\n\n          waitFor(0, start);\n\n          while (completed < total || game.hasLifecycleEvents) {\n            game.update(0);\n            await Future.delayed(const Duration(milliseconds: 1));\n          }\n\n          await game.ready();\n          expect(game.world.children.length, 10);\n        },\n      );\n    });\n\n    group('descendants()', () {\n      testWithFlameGame(\n        'descendants in a deep component tree',\n        (game) async {\n          expect(game.world.descendants().length, 0);\n          final component = Component()..add(Component()..add(Component()));\n          game.world.add(component);\n          expect(game.hasLifecycleEvents, true);\n          expect(game.world.descendants().length, 0);\n          await game.ready();\n\n          expect(game.world.descendants().length, 3);\n\n          final descendantsWithSelf = game.world.descendants(includeSelf: true);\n          expect(descendantsWithSelf.length, 4);\n          for (final component in descendantsWithSelf) {\n            expect(component.findGame() != null, true);\n          }\n        },\n      );\n\n      testWithFlameGame(\n        'length should not change when hasPendingLifecycleEvents is true after '\n        'adding',\n        (game) async {\n          final component = Component()..add(Component()..add(Component()));\n          await game.world.add(component);\n          await game.ready();\n          expect(game.hasLifecycleEvents, false);\n\n          game.world.add(Component());\n\n          expect(game.hasLifecycleEvents, true);\n          expect(game.world.descendants().length, 3);\n          // Remember that CameraComponent, Viewport, Viewfinder, Backdrop and\n          // World are added by default.\n          expect(game.descendants().length, 8);\n        },\n      );\n\n      testWithFlameGame(\n        'order must adhere to the \"depth-first search\" algorithm',\n        (game) async {\n          final world = game.world;\n          final componentA = Component()..addToParent(world);\n          final componentB = Component()..addToParent(world);\n          final componentC = Component()..addToParent(world);\n          final componentD = Component()..addToParent(componentB);\n          final componentE = Component()..addToParent(componentB);\n          final componentF = Component()..addToParent(componentE);\n          await game.ready();\n\n          final expectedOrder = [\n            componentA,\n            componentB,\n            componentD,\n            componentE,\n            componentF,\n            componentC,\n          ];\n          expect(\n            world.descendants().toList(),\n            expectedOrder,\n          );\n          expect(\n            world.descendants(includeSelf: true).toList(),\n            [world, ...expectedOrder],\n          );\n          expect(\n            world.descendants(reversed: true).toList(),\n            expectedOrder.reversed.toList(),\n          );\n          expect(\n            world.descendants(reversed: true, includeSelf: true).toList(),\n            [...expectedOrder.reversed, world],\n          );\n        },\n      );\n\n      testWithFlameGame('descendants() iterator is lazy', (game) async {\n        final componentA = _Visitor()..addToParent(game);\n        final componentB = _Visitor()..addToParent(game);\n        final componentC = _Visitor()..addToParent(componentB);\n        final componentD = _Visitor()..addToParent(componentB);\n        final componentE = _Visitor()..addToParent(game);\n        await game.ready();\n\n        game.descendants().whereType<_Visitor>().firstWhere((component) {\n          component.visited = true;\n          return component == componentC;\n        });\n        expect(componentA.visited, true);\n        expect(componentB.visited, true);\n        expect(componentC.visited, true);\n        expect(componentD.visited, false);\n        expect(componentE.visited, false);\n      });\n\n      testWithFlameGame(\n        'firstChild returns the first child on the matching type',\n        (game) async {\n          final firstA = _ComponentA();\n          final firstB = _ComponentB();\n\n          await game.ensureAdd(firstA);\n          await game.ensureAdd(_ComponentA());\n          await game.ensureAdd(firstB);\n          await game.ensureAdd(_ComponentB());\n\n          final childA = game.firstChild<_ComponentA>();\n          expect(childA, isNotNull);\n          expect(childA, equals(firstA));\n\n          final childB = game.firstChild<_ComponentB>();\n          expect(childB, isNotNull);\n          expect(childB, equals(firstB));\n\n          final nonExistentChild = game.firstChild<SpriteComponent>();\n          expect(nonExistentChild, isNull);\n        },\n      );\n\n      testWithFlameGame(\n        'lastChild returns the last child on the matching type',\n        (game) async {\n          final lastA = _ComponentA();\n          final lastB = _ComponentB();\n\n          await game.ensureAdd(_ComponentA());\n          await game.ensureAdd(lastA);\n          await game.ensureAdd(_ComponentB());\n          await game.ensureAdd(lastB);\n\n          final childA = game.lastChild<_ComponentA>();\n          expect(childA, isNotNull);\n          expect(childA, equals(lastA));\n\n          final childB = game.lastChild<_ComponentB>();\n          expect(childB, isNotNull);\n          expect(childB, equals(lastB));\n\n          final nonExistentChild = game.lastChild<SpriteComponent>();\n          expect(nonExistentChild, isNull);\n        },\n      );\n    });\n\n    group('onChildrenChanged()', () {\n      testWithFlameGame(\n        'after adding a child the method onChildrenChanged should be called',\n        (game) async {\n          final child = Component();\n          final parent = _OnChildrenChangedComponent();\n          await game.ensureAdd(parent);\n          expect(parent.onChangedChildrenRuns, 0);\n          await parent.ensureAdd(child);\n          expect(parent.onChangedChildrenRuns, 1);\n          expect(parent.lastChangeType, ChildrenChangeType.added);\n        },\n      );\n\n      testWithFlameGame(\n        'after adding several children using addAll the method '\n        'onChildrenChanged should be called list.length times',\n        (game) async {\n          final list = [Component(), Component()];\n          final parent = _OnChildrenChangedComponent();\n          await game.ensureAdd(parent);\n          expect(parent.onChangedChildrenRuns, 0);\n          await parent.ensureAddAll(list);\n          expect(parent.onChangedChildrenRuns, 2);\n          expect(parent.lastChangeType, ChildrenChangeType.added);\n        },\n      );\n\n      testWithFlameGame(\n        'changing the parent should call onChildrenChanged on both parents',\n        (game) async {\n          final child = Component();\n          final parent1 = _OnChildrenChangedComponent();\n          final parent2 = _OnChildrenChangedComponent();\n          await game.ensureAdd(parent1);\n          await game.ensureAdd(parent2);\n          await parent1.ensureAdd(child);\n          child.parent = parent2;\n          await game.ready();\n          expect(parent1.onChangedChildrenRuns, 2);\n          expect(parent1.lastChangeType, ChildrenChangeType.removed);\n          expect(parent2.onChangedChildrenRuns, 1);\n          expect(parent2.lastChangeType, ChildrenChangeType.added);\n        },\n      );\n\n      testWithFlameGame(\n        'after removing a child the method onChildrenChanged should be called',\n        (game) async {\n          final child = Component();\n          final parent = _OnChildrenChangedComponent();\n          await game.ensureAdd(parent);\n          await parent.ensureAdd(child);\n          parent.remove(child);\n          await game.ready();\n          expect(parent.onChangedChildrenRuns, 2);\n          expect(parent.lastChangeType, ChildrenChangeType.removed);\n        },\n      );\n\n      testWithFlameGame(\n        'after removing a list of components the method onChildrenChanged '\n        'should be called list.length times',\n        (game) async {\n          final list = [Component(), Component()];\n          final parent = _OnChildrenChangedComponent();\n          await game.ensureAdd(parent);\n          await parent.ensureAddAll(list);\n          parent.removeAll(list);\n          await game.ready();\n          expect(parent.onChangedChildrenRuns, 4);\n          expect(parent.lastChangeType, ChildrenChangeType.removed);\n        },\n      );\n    });\n\n    group('componentsAtPoint()', () {\n      testWithFlameGame('nested components', (game) async {\n        final world = game.world;\n        final componentA = PositionComponent()\n          ..size = Vector2(200, 150)\n          ..scale = Vector2.all(2)\n          ..position = Vector2(350, 50)\n          ..addToParent(world);\n        final componentB = CircleComponent(radius: 10)\n          ..position = Vector2(150, 75)\n          ..anchor = Anchor.center\n          ..addToParent(componentA);\n        await game.ready();\n\n        void matchComponentsAtPoint(Vector2 point, List<_Pair> expected) {\n          final nested = <Vector2>[];\n          var i = 0;\n          for (final component in world.componentsAtPoint(point, nested)) {\n            expect(i, lessThan(expected.length));\n            expect(component, expected[i].component);\n            expect(nested, expected[i].points);\n            i++;\n          }\n          expect(i, expected.length);\n        }\n\n        matchComponentsAtPoint(Vector2(0, 0), [\n          _Pair(world, [Vector2(0, 0)]),\n        ]);\n        matchComponentsAtPoint(Vector2(400, 100), [\n          _Pair(componentA, [Vector2(400, 100), Vector2(25, 25)]),\n          _Pair(world, [Vector2(400, 100)]),\n        ]);\n        matchComponentsAtPoint(Vector2(650, 200), [\n          _Pair(\n            componentB,\n            [Vector2(650, 200), Vector2(150, 75), Vector2(10, 10)],\n          ),\n          _Pair(componentA, [Vector2(650, 200), Vector2(150, 75)]),\n          _Pair(world, [Vector2(650, 200)]),\n        ]);\n        matchComponentsAtPoint(Vector2(664, 214), [\n          _Pair(\n            componentB,\n            [Vector2(664, 214), Vector2(157, 82), Vector2(17, 17)],\n          ),\n          _Pair(componentA, [Vector2(664, 214), Vector2(157, 82)]),\n          _Pair(world, [Vector2(664, 214)]),\n        ]);\n        matchComponentsAtPoint(Vector2(664, 216), [\n          _Pair(componentA, [Vector2(664, 216), Vector2(157, 83)]),\n          _Pair(world, [Vector2(664, 216)]),\n        ]);\n      });\n    });\n\n    group('findRootGame()', () {\n      testWithFlameGame('finds root game in nested game structure', (\n        game,\n      ) async {\n        final component = Component();\n        await game.ensureAdd(\n          FlameGame(\n            children: [\n              Component(children: [component]),\n            ],\n          ),\n        );\n        expect(component.findRootGame(), game);\n      });\n\n      testWithFlameGame('finds root game in non-nested game structure', (\n        game,\n      ) async {\n        final component = Component();\n        await game.ensureAdd(component);\n        expect(component.findRootGame(), game);\n      });\n    });\n\n    group('miscellaneous', () {\n      testWithFlameGame('childrenFactory', (game) async {\n        final component0 = Component();\n        expect(component0.children.strictMode, false);\n\n        Component.childrenFactory = () => OrderedSet.mapping<num, Component>(\n          (e) => e.priority,\n          // ignore: avoid_redundant_argument_values\n          strictMode: true,\n        );\n        final component1 = Component();\n        final component2 = Component();\n        component1.add(component2);\n        component2.add(Component());\n        expect(component1.children, isInstanceOf<MappingOrderedSet>());\n        expect(component1.children.strictMode, isTrue);\n        expect(component2.children, isInstanceOf<MappingOrderedSet>());\n        expect(component2.children.strictMode, isTrue);\n      });\n\n      testWithFlameGame('initially same debugMode as parent', (game) async {\n        final child = Component();\n        final parent = Component();\n        parent.debugMode = true;\n\n        parent.add(child);\n        game.world.add(parent);\n        await game.ready();\n\n        expect(child.debugMode, true);\n        parent.debugMode = false;\n        expect(child.debugMode, true);\n      });\n\n      testWithFlameGame('debugMode propagates to descendants', (game) async {\n        final child = Component();\n        final parent = Component();\n        final grandParent = Component();\n        parent.add(child);\n        grandParent.add(parent);\n        grandParent.debugMode = true;\n\n        game.world.add(grandParent);\n        await game.ready();\n\n        expect(child.debugMode, true);\n        expect(parent.debugMode, true);\n        expect(grandParent.debugMode, true);\n      });\n\n      testWithFlameGame(\n        'propagateToChildren visits children in the correct order',\n        (game) async {\n          final component1 = _IntComponent()..addToParent(game.world);\n          final component2 = _IntComponent()..addToParent(game.world);\n          final component3 = _IntComponent()..addToParent(component2);\n          final component4 = _IntComponent()..addToParent(component2);\n          await game.ready();\n\n          var order = 0;\n          game.world.propagateToChildren(\n            (component) {\n              order += 1;\n              if (component is _IntComponent) {\n                expect(component.value, 0);\n                component.value = order;\n              } else {\n                expect(component, equals(game.world));\n                expect(order, 5);\n              }\n              return true;\n            },\n            includeSelf: true,\n          );\n          expect(component4.value, 1);\n          expect(component3.value, 2);\n          expect(component2.value, 3);\n          expect(component1.value, 4);\n          expect(order, 5);\n        },\n      );\n\n      testWithFlameGame(\n        'Components added in onLoad can be accessed in onMount',\n        (game) async {\n          final component = CustomComponent(\n            onLoad: (self) {\n              self.add(Component());\n              self.add(_SlowLoadingComponent());\n              self.add(Component());\n            },\n            onMount: (self) {\n              expect(self.children.length, 3);\n              self.children.elementAt(0).add(Component());\n            },\n          );\n          game.world.add(component);\n          await game.ready();\n\n          expect(component.isMounted, true);\n          expect(component.children.length, 3);\n          expect(component.children.first.children.length, 1);\n        },\n      );\n\n      testWithFlameGame(\n        'Components can be retrieved via a named key',\n        (game) async {\n          final component = _ComponentA(key: ComponentKey.named('A'));\n          game.world.add(component);\n          await game.ready();\n\n          expect(ComponentKey.named('A'), equals(ComponentKey.named('A')));\n\n          final retrieved = game.findByKey(ComponentKey.named('A'));\n          expect(retrieved, equals(component));\n        },\n      );\n\n      testWithFlameGame(\n        'Components can be retrieved via an unique key',\n        (game) async {\n          final key1 = ComponentKey.unique();\n          final key2 = ComponentKey.unique();\n          final component1 = _ComponentA(key: key1);\n          final component2 = _ComponentA(key: key2);\n\n          game.world.add(component1);\n          game.world.add(component2);\n          await game.ready();\n\n          expect(key1, isNot(equals(key2)));\n\n          final retrieved1 = game.findByKey(key1);\n          expect(retrieved1, equals(component1));\n\n          final retrieved2 = game.findByKey(key2);\n          expect(retrieved2, equals(component2));\n\n          expect(retrieved1, isNot(equals(component2)));\n        },\n      );\n\n      testWithFlameGame(\n        'Components can be retrieved via their name',\n        (game) async {\n          final component = _ComponentA(key: ComponentKey.named('A'));\n          game.world.add(component);\n          await game.ready();\n\n          final retrieved = game.findByKeyName('A');\n          expect(retrieved, equals(component));\n        },\n      );\n\n      testWithFlameGame(\n        'findByKey returns null if no component is found',\n        (game) async {\n          await game.ready();\n\n          expect(game.findByKey(ComponentKey.unique()), isNull);\n        },\n      );\n\n      testWithFlameGame(\n        'findByKey returns null when the component is removed',\n        (game) async {\n          final key = ComponentKey.unique();\n          final component = _ComponentA(key: key);\n\n          game.world.add(component);\n          await game.ready();\n\n          final retrieved1 = game.findByKey(key);\n          expect(retrieved1, equals(component));\n\n          component.removeFromParent();\n          await game.ready();\n\n          final retrieved2 = game.findByKey(key);\n          expect(retrieved2, isNull);\n        },\n      );\n\n      testWithFlameGame(\n        'Removed keys can be reused by components',\n        (game) async {\n          final key = ComponentKey.named('A');\n          final parent1 = Component(children: [_ComponentA(key: key)]);\n\n          game.world.add(parent1);\n          await game.ready();\n\n          parent1.removeFromParent();\n          await game.ready();\n\n          final component = _ComponentA(key: key);\n          final parent2 = Component(children: [component]);\n\n          game.world.add(parent2);\n          await game.ready();\n\n          final retrieved1 = game.findByKey(key);\n          expect(retrieved1, equals(component));\n        },\n      );\n\n      testWithFlameGame(\n        'Throws assertion error when registering a component with the same key',\n        (game) async {\n          final component = _ComponentA(key: ComponentKey.named('A'));\n          final component2 = _ComponentA(key: ComponentKey.named('A'));\n\n          game.world.add(component);\n          game.world.add(component2);\n\n          await expectLater(\n            () => game.ready(),\n            throwsA(\n              isA<AssertionError>().having(\n                (e) => e.message,\n                'message',\n                'Key ${ComponentKey.named('A')} is already registered',\n              ),\n            ),\n          );\n        },\n      );\n    });\n  });\n}\n\nclass _ComponentA extends Component {\n  _ComponentA({super.key});\n}\n\nclass _ComponentB extends Component {\n  _ComponentB();\n}\n\nclass _ComponentWithSizeHistory extends Component {\n  List<Vector2> history = [];\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    history.add(size.clone());\n  }\n}\n\nclass _Visitor extends Component {\n  bool visited = false;\n}\n\nclass _IntComponent extends Component {\n  int value = 0;\n}\n\nclass _TwoChildrenComponent extends Component {\n  _TwoChildrenComponent({super.children});\n\n  late final Component child1;\n  late final Component child2;\n\n  @override\n  Future<void> onLoad() async {\n    child1 = Component();\n    child2 = Component();\n    add(child1);\n    add(child2);\n  }\n}\n\nclass _LifecycleComponent extends Component {\n  final List<String> events = [];\n  final String name;\n\n  _LifecycleComponent([this.name = '']);\n\n  int countEvents(String event) {\n    return events.where((e) => e == event).length;\n  }\n\n  @override\n  Future<void> onLoad() async {\n    expect(isLoading, true);\n    expect(isLoaded, false);\n    expect(isMounted, false);\n    expect(isRemoving, false);\n    events.add('onLoad');\n  }\n\n  @override\n  void onMount() {\n    expect(isLoading, false);\n    expect(isLoaded, true);\n    expect(isMounted, false);\n    expect(isRemoving, false);\n    events.add('onMount');\n  }\n\n  @override\n  void onRemove() {\n    expect(isLoaded, true);\n    expect(isMounted, true);\n    events.add('onRemove');\n    super.onRemove();\n  }\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    events.add('onGameResize $size');\n  }\n\n  @override\n  String toString() => 'LifecycleComponent($name)';\n}\n\nclass _SlowLoadingComponent extends Component {\n  int onLoadCalledCount = 0;\n\n  @override\n  Future<void> onLoad() async {\n    onLoadCalledCount++;\n    await Future<void>.delayed(const Duration(milliseconds: 10));\n  }\n}\n\nclass _SlowComponent extends Component {\n  _SlowComponent(this.name, this.loadTime);\n  final double loadTime;\n  final String name;\n\n  @override\n  Future<void> onLoad() async {\n    final ms = (loadTime * 1000).toInt();\n    await Future<int?>.delayed(Duration(milliseconds: ms));\n  }\n\n  @override\n  String toString() => 'SlowComponent($name, loadTime=$loadTime)';\n}\n\nclass _SelfRemovingOnLoadComponent extends Component {\n  @override\n  Future<void>? onLoad() {\n    removeFromParent();\n    return null;\n  }\n}\n\nclass _SelfRemovingOnMountComponent extends Component {\n  @override\n  void onMount() {\n    removeFromParent();\n  }\n}\n\nclass _Pair {\n  _Pair(this.component, this.points);\n  final Component component;\n  final List<Vector2> points;\n}\n\nclass _PrepareGame extends FlameGame {\n  late final _ParentOnPrepareComponent prepareParent;\n\n  @override\n  Future<void> onLoad() async {\n    await add(prepareParent = _ParentOnPrepareComponent());\n  }\n}\n\nclass _OnPrepareComponent extends Component {\n  int onMountRuns = 0;\n\n  @override\n  void onMount() {\n    super.onMount();\n    onMountRuns++;\n  }\n}\n\nclass _ParentOnPrepareComponent extends _OnPrepareComponent {\n  @override\n  Future<void> onLoad() async {\n    await add(_OnPrepareComponent());\n  }\n}\n\nclass _IdentifiableComponent extends Component {\n  final int id;\n\n  _IdentifiableComponent(this.id);\n}\n\nclass _AsyncLoadingChild extends Component {\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    return Future.value();\n  }\n}\n\nclass _GameResizeComponent extends PositionComponent {\n  _GameResizeComponent(this.name) : super(size: Vector2.all(2.0));\n\n  String name;\n  late Vector2 gameSize;\n\n  @override\n  void onGameResize(Vector2 gameSize) {\n    super.onGameResize(gameSize);\n    this.gameSize = gameSize;\n  }\n}\n\nclass _OnChildrenChangedComponent extends PositionComponent {\n  int onChangedChildrenRuns = 0;\n  ChildrenChangeType? lastChangeType;\n\n  @override\n  void onChildrenChanged(Component child, ChildrenChangeType type) {\n    onChangedChildrenRuns++;\n    lastChangeType = type;\n  }\n}\n\nclass _RemoveWhereComponent extends Component {\n  @override\n  Future<void> onLoad() async {\n    add(Component());\n    removeWhere((_) => true);\n  }\n}\n\nclass _Wrapper extends StatefulWidget {\n  const _Wrapper({\n    required this.child,\n    this.open = false,\n  });\n\n  final Widget child;\n  final bool open;\n\n  @override\n  State<_Wrapper> createState() => _WrapperState();\n}\n\nclass _WrapperState extends State<_Wrapper> {\n  late bool _open;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _open = widget.open;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: Scaffold(\n        body: Column(\n          children: [\n            if (_open) Expanded(child: widget.child),\n            ElevatedButton(\n              child: const Text('Toggle'),\n              onPressed: () {\n                setState(() => _open = !_open);\n              },\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _DetachableFlameGame extends FlameGame {\n  bool onAttachCalled = false;\n  bool onDetachCalled = false;\n\n  void resetValues() {\n    onAttachCalled = false;\n    onDetachCalled = false;\n  }\n\n  @override\n  void onAttach() {\n    super.onAttach();\n    onAttachCalled = true;\n  }\n\n  @override\n  void onDetach() {\n    super.onDetach();\n    onDetachCalled = true;\n  }\n}\n\nFlameTester<_DetachableFlameGame> _myDetachableGame({required bool open}) {\n  return FlameTester(\n    _DetachableFlameGame.new,\n    pumpWidget: (gameWidget, tester) async {\n      await tester.pumpWidget(_Wrapper(open: open, child: gameWidget));\n    },\n  );\n}\n\nclass _ComponentWithChildrenRemoveAll extends Component {\n  @override\n  void onMount() {\n    super.onMount();\n\n    add(Component());\n    removeAll(children);\n  }\n}\n\nclass _RemoveAllChildrenComponent extends Component {\n  @override\n  void onRemove() {\n    super.onRemove();\n    removeAll(children);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/components/components_notifier_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nclass _Enemy extends PositionComponent with Notifier {}\n\nvoid main() {\n  group('ComponentsNotifier', () {\n    testWithFlameGame('correctly have the initial value', (game) async {\n      await game.ensureAdd(_Enemy());\n      await game.ensureAdd(_Enemy());\n\n      final notifier = game.componentsNotifier<_Enemy>();\n\n      expect(notifier.components.length, equals(2));\n    });\n\n    testWithFlameGame('notifies when a component is added', (game) async {\n      var called = 0;\n\n      game.componentsNotifier<_Enemy>().addListener(() => called++);\n\n      await game.ensureAdd(_Enemy());\n\n      expect(called, equals(1));\n    });\n\n    testWithFlameGame('notifies when a component is added', (game) async {\n      var called = 0;\n\n      final component = _Enemy();\n      await game.ensureAdd(component);\n\n      game.componentsNotifier<_Enemy>().addListener(() => called++);\n\n      component.removeFromParent();\n      await game.ready();\n\n      expect(called, equals(1));\n    });\n\n    testWithFlameGame(\n      'notifies when a component is manually notified',\n      (game) async {\n        var called = 0;\n\n        final component = _Enemy();\n        await game.ensureAdd(component);\n\n        game.componentsNotifier<_Enemy>().addListener(() => called++);\n\n        component.notifyListeners();\n\n        expect(called, equals(1));\n      },\n    );\n\n    testWithFlameGame(\n      'lazy initializes the notifier',\n      (game) async {\n        expect(game.notifiers, isEmpty);\n        game.componentsNotifier<_Enemy>();\n        expect(game.notifiers, isNotEmpty);\n      },\n    );\n\n    testWithFlameGame(\n      'do not add the same type',\n      (game) async {\n        expect(game.notifiers, isEmpty);\n        game.componentsNotifier<_Enemy>();\n        game.componentsNotifier<_Enemy>();\n        expect(game.notifiers.length, equals(1));\n      },\n    );\n\n    testWithFlameGame('can listen to parent classes', (game) async {\n      var parentCalled = 0;\n      var called = 0;\n\n      game.componentsNotifier<PositionComponent>().addListener(\n        () => parentCalled++,\n      );\n      game.componentsNotifier<_Enemy>().addListener(() => called++);\n\n      expect(game.notifiers.length, equals(2));\n\n      await game.ensureAdd(_Enemy());\n\n      expect(called, equals(1));\n      expect(parentCalled, equals(1));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/custom_painter_component_test.dart",
    "content": "import 'package:canvas_test/canvas_test.dart';\nimport 'package:flame/components.dart';\nimport 'package:flutter/material.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nclass _MockCustomPainter extends Mock implements CustomPainter {}\n\nvoid main() {\n  group('CustomPainterComponent', () {\n    test('correctly calls the paint method of the painter', () {\n      final painter = _MockCustomPainter();\n      final component = CustomPainterComponent(\n        painter: painter,\n      )..size = Vector2.all(100);\n\n      final canvas = MockCanvas();\n      component.render(canvas);\n\n      verify(() => painter.paint(canvas, const Size(100, 100)));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/element_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/text.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('ElementComponent', () {\n    test('document size can be specified via the size parameter', () {\n      final c = TextElementComponent.fromDocument(\n        document: DocumentRoot([]),\n        size: Vector2(100, 200),\n      );\n      expect(c.documentSize, equals(Vector2(100, 200)));\n      expect(c.size, equals(Vector2.zero()));\n    });\n\n    test('document size can be specified via the style', () {\n      final c = TextElementComponent.fromDocument(\n        document: DocumentRoot([]),\n        style: DocumentStyle(width: 100, height: 200),\n      );\n      expect(c.documentSize, equals(Vector2(100, 200)));\n      expect(c.size, equals(Vector2.zero()));\n    });\n\n    test('document size can be super-specified if matching', () {\n      final c = TextElementComponent.fromDocument(\n        document: DocumentRoot([]),\n        style: DocumentStyle(width: 100, height: 200),\n        size: Vector2(100, 200),\n      );\n      expect(c.documentSize, equals(Vector2(100, 200)));\n      expect(c.size, equals(Vector2.zero()));\n    });\n\n    test('document size must be specified', () {\n      expect(\n        () {\n          TextElementComponent.fromDocument(\n            document: DocumentRoot([]),\n            style: DocumentStyle(),\n          );\n        },\n        throwsA(\n          predicate((e) {\n            return e is ArgumentError &&\n                e.message == 'Either style.width or size.x must be provided.';\n          }),\n        ),\n      );\n    });\n\n    test('size cannot be over-specified if mismatched', () {\n      expect(\n        () {\n          TextElementComponent.fromDocument(\n            document: DocumentRoot([]),\n            style: DocumentStyle(width: 100, height: 200),\n            size: Vector2(100, 300),\n          );\n        },\n        throwsA(\n          predicate((e) {\n            return e is ArgumentError &&\n                e.message ==\n                    'style.height and size.y, if both provided, must match.';\n          }),\n        ),\n      );\n    });\n\n    test('size is computed from the element bounding box', () {\n      final c = TextElementComponent.fromDocument(\n        document: DocumentRoot([\n          ParagraphNode.simple('line 1'),\n          ParagraphNode.simple('line 2'),\n        ]),\n        size: Vector2(800, 160), // oversized\n      );\n      expect(c.documentSize, equals(Vector2(800, 160)));\n      expect(c.size, equals(Vector2(96, 44)));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/fps_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  const diff = 0.0000000000001;\n\n  group('FPSComponent', () {\n    testWithFlameGame('reports correct FPS for 1 frames', (game) async {\n      final fpsComponent = FpsComponent();\n      await game.ensureAdd(fpsComponent);\n      expect(fpsComponent.fps, 0);\n      game.update(1 / 60);\n\n      expect(fpsComponent.fps, closeTo(60, diff));\n    });\n\n    testWithFlameGame('reports correct FPS with full window', (game) async {\n      const windowSize = 30;\n      final fpsComponent = FpsComponent(windowSize: windowSize);\n      await game.ensureAdd(fpsComponent);\n      for (var i = 0; i < windowSize; i++) {\n        game.update(1 / 60);\n      }\n\n      expect(fpsComponent.fps, closeTo(60, diff));\n    });\n\n    testWithFlameGame('reports correct FPS with slid window', (game) async {\n      const windowSize = 30;\n      final fpsComponent = FpsComponent(windowSize: windowSize);\n      await game.ensureAdd(fpsComponent);\n      for (var i = 0; i < 2 * windowSize; i++) {\n        game.update(1 / 60);\n      }\n\n      expect(fpsComponent.fps, closeTo(60, diff));\n    });\n\n    testWithFlameGame('reports correct FPS with varying dt', (game) async {\n      final fpsComponent = FpsComponent();\n      await game.ensureAdd(fpsComponent);\n      for (var i = 0; i < fpsComponent.windowSize; i++) {\n        // Alternating between 50 and 100 FPS\n        final dt = i.isEven ? 1 / 100 : 1 / 50;\n        game.update(dt);\n      }\n\n      expect(fpsComponent.fps, closeTo(100 / 1.5, diff));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/has_auto_batched_children_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass _MockCanvas extends Mock implements Canvas {}\n\nclass _FakeImage extends Fake implements Image {}\n\nclass _AutoBatchGroup extends PositionComponent with HasAutoBatchedChildren {}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  late Image sharedImage;\n\n  setUpAll(() async {\n    registerFallbackValue(_FakeImage());\n    registerFallbackValue(Paint());\n    registerFallbackValue(Rect.zero);\n    sharedImage = await generateImage(16, 16);\n  });\n\n  group('HasAutoBatchedChildren', () {\n    group('batchingEnabled', () {\n      testWithFlameGame(\n        'batches eligible SpriteComponent children into one drawAtlas call',\n        (game) async {\n          final group = _AutoBatchGroup();\n          await game.ensureAdd(group);\n\n          final sprite = Sprite(sharedImage, srcSize: Vector2.all(16));\n          for (var i = 0; i < 3; i++) {\n            await group.ensureAdd(\n              SpriteComponent(\n                sprite: sprite,\n                size: Vector2.all(16),\n                position: Vector2(i * 20.0, 0),\n              ),\n            );\n          }\n\n          final canvas = _MockCanvas();\n          group.renderTree(canvas);\n\n          verify(\n            () => canvas.drawAtlas(\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n            ),\n          ).called(1);\n          verifyNever(\n            () => canvas.drawImageRect(any(), any(), any(), any()),\n          );\n        },\n      );\n\n      testWithFlameGame(\n        'renders each sprite individually when batchingEnabled is false',\n        (game) async {\n          final group = _AutoBatchGroup()..batchingEnabled = false;\n          await game.ensureAdd(group);\n\n          final sprite = Sprite(sharedImage, srcSize: Vector2.all(16));\n          for (var i = 0; i < 3; i++) {\n            await group.ensureAdd(\n              SpriteComponent(\n                sprite: sprite,\n                size: Vector2.all(16),\n                position: Vector2(i * 20.0, 0),\n              ),\n            );\n          }\n\n          final canvas = _MockCanvas();\n          group.renderTree(canvas);\n\n          verifyNever(\n            () => canvas.drawAtlas(\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n            ),\n          );\n          verify(\n            () => canvas.drawImageRect(any(), any(), any(), any()),\n          ).called(3);\n        },\n      );\n\n      testWithFlameGame(\n        'toggling batchingEnabled off then on resumes batching',\n        (game) async {\n          final group = _AutoBatchGroup();\n          await game.ensureAdd(group);\n\n          final sprite = Sprite(sharedImage, srcSize: Vector2.all(16));\n          await group.ensureAdd(\n            SpriteComponent(sprite: sprite, size: Vector2.all(16)),\n          );\n          await group.ensureAdd(\n            SpriteComponent(\n              sprite: sprite,\n              size: Vector2.all(16),\n              position: Vector2(20, 0),\n            ),\n          );\n\n          final canvas = _MockCanvas();\n\n          group.renderTree(canvas); // frame 1: on  → 1 drawAtlas\n          group.batchingEnabled = false;\n          group.renderTree(canvas); // frame 2: off → 2 drawImageRect\n          group.batchingEnabled = true;\n          group.renderTree(canvas); // frame 3: on again → 1 drawAtlas\n\n          verify(\n            () => canvas.drawAtlas(\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n            ),\n          ).called(2); // frames 1 and 3\n          verify(\n            () => canvas.drawImageRect(any(), any(), any(), any()),\n          ).called(2); // frame 2 only\n        },\n      );\n    });\n\n    group('eligibility', () {\n      testWithFlameGame(\n        'component with children falls back to individual rendering',\n        (game) async {\n          final group = _AutoBatchGroup();\n          await game.ensureAdd(group);\n\n          // Adding a child before mounting — it will be queued and mounted\n          // once the parent is in the tree.\n          final sprite = Sprite(sharedImage, srcSize: Vector2.all(16));\n          final parentComp = SpriteComponent(\n            sprite: sprite,\n            size: Vector2.all(16),\n          )..add(PositionComponent()); // has children → ineligible\n\n          await group.ensureAdd(parentComp);\n          // Extra tick to ensure the queued PositionComponent child is mounted.\n          await game.ready();\n\n          final canvas = _MockCanvas();\n          group.renderTree(canvas);\n\n          verifyNever(\n            () => canvas.drawAtlas(\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n            ),\n          );\n          verify(\n            () => canvas.drawImageRect(any(), any(), any(), any()),\n          ).called(1);\n        },\n      );\n\n      testWithFlameGame(\n        'SpriteComponent with hitbox-only children is still batch-eligible',\n        (game) async {\n          final group = _AutoBatchGroup();\n          await game.ensureAdd(group);\n\n          final sprite = Sprite(sharedImage, srcSize: Vector2.all(16));\n          for (var i = 0; i < 2; i++) {\n            // Simulate a bullet/enemy: sprite + physics hitbox child.\n            final comp = SpriteComponent(\n              sprite: sprite,\n              size: Vector2.all(16),\n              position: Vector2(i * 20.0, 0),\n            )..add(CircleHitbox());\n            await group.ensureAdd(comp);\n          }\n          await game.ready(); // ensure hitbox is mounted before rendering\n\n          final canvas = _MockCanvas();\n          group.renderTree(canvas);\n\n          // Both sprites should be batched despite each having a hitbox child.\n          verify(\n            () => canvas.drawAtlas(\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n            ),\n          ).called(1);\n          verifyNever(\n            () => canvas.drawImageRect(any(), any(), any(), any()),\n          );\n        },\n      );\n\n      testWithFlameGame(\n        'non-uniform scale falls back to individual rendering',\n        (game) async {\n          final group = _AutoBatchGroup();\n          await game.ensureAdd(group);\n\n          await group.ensureAdd(\n            SpriteComponent(\n              sprite: Sprite(sharedImage, srcSize: Vector2.all(16)),\n              size: Vector2.all(16),\n              scale: Vector2(1, 2), // non-uniform → ineligible\n            ),\n          );\n\n          final canvas = _MockCanvas();\n          group.renderTree(canvas);\n\n          verifyNever(\n            () => canvas.drawAtlas(\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n            ),\n          );\n          verify(\n            () => canvas.drawImageRect(any(), any(), any(), any()),\n          ).called(1);\n        },\n      );\n\n      testWithFlameGame(\n        '''mismatched size-to-source aspect ratio falls back to individual rendering''',\n        (game) async {\n          final group = _AutoBatchGroup();\n          await game.ensureAdd(group);\n\n          // Source is 16×16 (1:1); size is 16×32 (1:2) → ratio mismatch.\n          await group.ensureAdd(\n            SpriteComponent(\n              sprite: Sprite(sharedImage, srcSize: Vector2.all(16)),\n              size: Vector2(16, 32),\n            ),\n          );\n\n          final canvas = _MockCanvas();\n          group.renderTree(canvas);\n\n          verifyNever(\n            () => canvas.drawAtlas(\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n            ),\n          );\n          verify(\n            () => canvas.drawImageRect(any(), any(), any(), any()),\n          ).called(1);\n        },\n      );\n\n      testWithFlameGame(\n        'component size larger than source rect is still batch-eligible',\n        (game) async {\n          final group = _AutoBatchGroup();\n          await game.ensureAdd(group);\n\n          // Source is 8×8, display size is 16×16 → 2× stretch, but 1:1 ratio.\n          final sprite = Sprite(sharedImage, srcSize: Vector2.all(8));\n          await group.ensureAdd(\n            SpriteComponent(\n              sprite: sprite,\n              size: Vector2.all(16),\n            ),\n          );\n          await group.ensureAdd(\n            SpriteComponent(\n              sprite: sprite,\n              size: Vector2.all(16),\n              position: Vector2(20, 0),\n            ),\n          );\n\n          final canvas = _MockCanvas();\n          group.renderTree(canvas);\n\n          verify(\n            () => canvas.drawAtlas(\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n            ),\n          ).called(1);\n          verifyNever(\n            () => canvas.drawImageRect(any(), any(), any(), any()),\n          );\n        },\n      );\n    });\n\n    group('batching', () {\n      testWithFlameGame(\n        'separate priority groups produce separate drawAtlas calls',\n        (game) async {\n          final group = _AutoBatchGroup();\n          await game.ensureAdd(group);\n\n          final sprite = Sprite(sharedImage, srcSize: Vector2.all(16));\n          // priority=0 — 2 sprites\n          await group.ensureAdd(\n            SpriteComponent(\n              sprite: sprite,\n              size: Vector2.all(16),\n              priority: 0,\n            ),\n          );\n          await group.ensureAdd(\n            SpriteComponent(\n              sprite: sprite,\n              size: Vector2.all(16),\n              priority: 0,\n            ),\n          );\n          // priority=1 — 2 sprites\n          await group.ensureAdd(\n            SpriteComponent(\n              sprite: sprite,\n              size: Vector2.all(16),\n              priority: 1,\n            ),\n          );\n          await group.ensureAdd(\n            SpriteComponent(\n              sprite: sprite,\n              size: Vector2.all(16),\n              priority: 1,\n            ),\n          );\n\n          final canvas = _MockCanvas();\n          group.renderTree(canvas);\n\n          // One drawAtlas call per priority group.\n          verify(\n            () => canvas.drawAtlas(\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n            ),\n          ).called(2);\n          verifyNever(\n            () => canvas.drawImageRect(any(), any(), any(), any()),\n          );\n        },\n      );\n\n      testWithFlameGame(\n        'different images produce separate drawAtlas calls',\n        (game) async {\n          final image2 = await generateImage(8, 8);\n          final group = _AutoBatchGroup();\n          await game.ensureAdd(group);\n\n          await group.ensureAdd(\n            SpriteComponent(\n              sprite: Sprite(sharedImage, srcSize: Vector2.all(16)),\n              size: Vector2.all(16),\n            ),\n          );\n          await group.ensureAdd(\n            SpriteComponent(\n              sprite: Sprite(image2, srcSize: Vector2.all(8)),\n              size: Vector2.all(8),\n            ),\n          );\n\n          final canvas = _MockCanvas();\n          group.renderTree(canvas);\n\n          verify(\n            () => canvas.drawAtlas(\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n            ),\n          ).called(2);\n        },\n      );\n\n      testWithFlameGame(\n        'batches eligible SpriteAnimationComponent children',\n        (game) async {\n          final group = _AutoBatchGroup();\n          await game.ensureAdd(group);\n\n          final animation = SpriteAnimation.spriteList(\n            [\n              Sprite(\n                sharedImage,\n                srcPosition: Vector2.zero(),\n                srcSize: Vector2.all(8),\n              ),\n              Sprite(\n                sharedImage,\n                srcPosition: Vector2(8, 0),\n                srcSize: Vector2.all(8),\n              ),\n            ],\n            stepTime: 1.0,\n          );\n\n          await group.ensureAdd(\n            SpriteAnimationComponent(\n              animation: animation,\n              size: Vector2.all(8),\n            ),\n          );\n          await group.ensureAdd(\n            SpriteAnimationComponent(\n              animation: animation,\n              size: Vector2.all(8),\n              position: Vector2(10, 0),\n            ),\n          );\n\n          final canvas = _MockCanvas();\n          group.renderTree(canvas);\n\n          verify(\n            () => canvas.drawAtlas(\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n            ),\n          ).called(1);\n          verifyNever(\n            () => canvas.drawImageRect(any(), any(), any(), any()),\n          );\n        },\n      );\n\n      testWithFlameGame(\n        'accumulators are correctly reset between render frames',\n        (game) async {\n          final group = _AutoBatchGroup();\n          await game.ensureAdd(group);\n\n          final sprite = Sprite(sharedImage, srcSize: Vector2.all(16));\n          await group.ensureAdd(\n            SpriteComponent(sprite: sprite, size: Vector2.all(16)),\n          );\n          await group.ensureAdd(\n            SpriteComponent(\n              sprite: sprite,\n              size: Vector2.all(16),\n              position: Vector2(20, 0),\n            ),\n          );\n\n          final canvas = _MockCanvas();\n          group.renderTree(canvas); // frame 1\n          group.renderTree(canvas); // frame 2\n\n          // Each frame → exactly one drawAtlas call.\n          verify(\n            () => canvas.drawAtlas(\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n            ),\n          ).called(2);\n        },\n      );\n    });\n\n    group('render order', () {\n      testWithFlameGame(\n        '''eligible children flush before a non-eligible sibling at same priority''',\n        (game) async {\n          final group = _AutoBatchGroup();\n          await game.ensureAdd(group);\n\n          final sprite = Sprite(sharedImage, srcSize: Vector2.all(16));\n\n          // Eligible first (lower insertion order → rendered before)\n          final eligibleSprite = SpriteComponent(\n            sprite: sprite,\n            size: Vector2.all(16),\n            priority: 0,\n          );\n\n          // Non-eligible: has a child component\n          final nonEligible = SpriteComponent(\n            sprite: sprite,\n            size: Vector2.all(16),\n            priority: 0,\n          )..add(PositionComponent());\n\n          await group.ensureAdd(eligibleSprite);\n          await group.ensureAdd(nonEligible);\n          await game.ready(); // Ensure nested child of nonEligible is mounted.\n\n          final drawOrder = <String>[];\n          final canvas = _MockCanvas();\n          when(\n            () => canvas.drawAtlas(\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n            ),\n          ).thenAnswer((_) => drawOrder.add('atlas'));\n          when(\n            () => canvas.drawImageRect(any(), any(), any(), any()),\n          ).thenAnswer((_) => drawOrder.add('imageRect'));\n\n          group.renderTree(canvas);\n\n          // The eligible sprite is accumulated first; when the non-eligible\n          // sibling is encountered, the pending batch is flushed (atlas), then\n          // the non-eligible renders its own sprite (imageRect).\n          expect(drawOrder, ['atlas', 'imageRect']);\n        },\n      );\n\n      testWithFlameGame(\n        'non-eligible child renders before a subsequent eligible batch',\n        (game) async {\n          final group = _AutoBatchGroup();\n          await game.ensureAdd(group);\n\n          final sprite = Sprite(sharedImage, srcSize: Vector2.all(16));\n\n          // Non-eligible first (has child)\n          final nonEligible = SpriteComponent(\n            sprite: sprite,\n            size: Vector2.all(16),\n            priority: 0,\n          )..add(PositionComponent());\n\n          // Eligible after\n          final eligibleSprite = SpriteComponent(\n            sprite: sprite,\n            size: Vector2.all(16),\n            priority: 0,\n          );\n\n          await group.ensureAdd(nonEligible);\n          await group.ensureAdd(eligibleSprite);\n          await game.ready();\n\n          final drawOrder = <String>[];\n          final canvas = _MockCanvas();\n          when(\n            () => canvas.drawAtlas(\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n              any(),\n            ),\n          ).thenAnswer((_) => drawOrder.add('atlas'));\n          when(\n            () => canvas.drawImageRect(any(), any(), any(), any()),\n          ).thenAnswer((_) => drawOrder.add('imageRect'));\n\n          group.renderTree(canvas);\n\n          // Non-eligible renders immediately (imageRect), then eligible is\n          // accumulated and flushed at afterChildrenRendered (atlas).\n          expect(drawOrder, ['imageRect', 'atlas']);\n        },\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/hud_button_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/src/events/flame_game_mixins/multi_tap_dispatcher.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('HudButtonComponent', () {\n    testWithFlameGame('correctly registers taps', (game) async {\n      var pressedTimes = 0;\n      var releasedTimes = 0;\n      var cancelledTimes = 0;\n      final initialGameSize = Vector2.all(100);\n      final componentSize = Vector2.all(10);\n      const margin = EdgeInsets.only(bottom: 10, right: 10);\n      late final HudButtonComponent button;\n      game.onGameResize(initialGameSize);\n      await game.ensureAdd(\n        button = HudButtonComponent(\n          button: RectangleComponent(size: componentSize),\n          onPressed: () => pressedTimes++,\n          onReleased: () => releasedTimes++,\n          onCancelled: () => cancelledTimes++,\n          size: componentSize,\n          margin: margin,\n        ),\n      );\n\n      expect(pressedTimes, 0);\n      expect(releasedTimes, 0);\n      expect(cancelledTimes, 0);\n      final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n      tapDispatcher.handleTapDown(1, TapDownDetails());\n      expect(pressedTimes, 0);\n      expect(releasedTimes, 0);\n      expect(cancelledTimes, 0);\n\n      tapDispatcher.handleTapUp(\n        1,\n        createTapUpDetails(\n          globalPosition: button.positionOfAnchor(Anchor.center).toOffset(),\n        ),\n      );\n      expect(pressedTimes, 0);\n      expect(releasedTimes, 0);\n      expect(cancelledTimes, 0);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(\n          globalPosition:\n              initialGameSize.toOffset() +\n              margin.bottomRight -\n              const Offset(1, 1),\n        ),\n      );\n      expect(pressedTimes, 1);\n      expect(releasedTimes, 0);\n      expect(cancelledTimes, 0);\n\n      tapDispatcher.handleTapUp(\n        1,\n        createTapUpDetails(\n          globalPosition: button.positionOfAnchor(Anchor.center).toOffset(),\n        ),\n      );\n      expect(pressedTimes, 1);\n      expect(releasedTimes, 1);\n      expect(cancelledTimes, 0);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(\n          globalPosition:\n              initialGameSize.toOffset() +\n              margin.bottomRight -\n              const Offset(1, 1),\n        ),\n      );\n      tapDispatcher.handleTapCancel(1);\n      expect(pressedTimes, 2);\n      expect(releasedTimes, 1);\n      expect(cancelledTimes, 1);\n    });\n\n    testWithFlameGame('correctly registers taps onGameResize', (game) async {\n      var pressedTimes = 0;\n      var releasedTimes = 0;\n      var cancelledTimes = 0;\n      final initialGameSize = Vector2.all(100);\n      final componentSize = Vector2.all(10);\n      const margin = EdgeInsets.only(bottom: 10, right: 10);\n      late final HudButtonComponent button;\n      game.onGameResize(initialGameSize);\n      await game.ensureAdd(\n        button = HudButtonComponent(\n          button: RectangleComponent(size: componentSize),\n          onPressed: () => pressedTimes++,\n          onReleased: () => releasedTimes++,\n          onCancelled: () => cancelledTimes++,\n          size: componentSize,\n          margin: margin,\n        ),\n      );\n      final previousPosition = button\n          .positionOfAnchor(Anchor.center)\n          .toOffset();\n      game.onGameResize(initialGameSize * 2);\n      final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: previousPosition),\n      );\n      tapDispatcher.handleTapUp(\n        1,\n        createTapUpDetails(globalPosition: previousPosition),\n      );\n      expect(pressedTimes, 0);\n      expect(releasedTimes, 0);\n      expect(cancelledTimes, 0);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(\n          globalPosition:\n              game.size.toOffset() + margin.bottomRight - const Offset(1, 1),\n        ),\n      );\n      expect(pressedTimes, 1);\n      expect(releasedTimes, 0);\n      expect(cancelledTimes, 0);\n\n      tapDispatcher.handleTapUp(\n        1,\n        createTapUpDetails(\n          globalPosition:\n              game.size.toOffset() + margin.bottomRight - const Offset(1, 1),\n        ),\n      );\n      expect(pressedTimes, 1);\n      expect(releasedTimes, 1);\n      expect(cancelledTimes, 0);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(\n          globalPosition:\n              game.size.toOffset() + margin.bottomRight - const Offset(1, 1),\n        ),\n      );\n      tapDispatcher.handleTapCancel(1);\n      expect(pressedTimes, 2);\n      expect(releasedTimes, 1);\n      expect(cancelledTimes, 1);\n    });\n\n    testWithFlameGame('can set button and buttonDown in onLoad', (game) async {\n      expect(\n        () => game.ensureAdd(_CustomHudButtonComponent()),\n        returnsNormally,\n      );\n    });\n  });\n}\n\nclass _CustomHudButtonComponent extends HudButtonComponent {\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    button = RectangleComponent(size: Vector2.all(10));\n    buttonDown = CircleComponent(radius: 10);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/components/hud_margin_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('HudMarginComponent test', () {\n    testWithFlameGame(\n      'position set from margin should change onGameResize',\n      (game) async {\n        final marginComponent = HudMarginComponent(\n          margin: const EdgeInsets.only(right: 10, bottom: 20),\n          size: Vector2.all(20),\n        );\n        await game.ensureAdd(marginComponent);\n        // The position should be (770, 560) since the game size is (800, 600)\n        // and the component has its anchor in the top left corner (which then\n        // is were the margin will be calculated from).\n        // (800, 600) - size(20, 20) - position(10, 20) = (770, 560)\n        expect(marginComponent.position, closeToVector(Vector2(770, 560)));\n        game.onGameResize(Vector2.all(1000));\n        game.update(0);\n        // After resizing the game, the component should still be 30 pixels from\n        // the right edge and 40 pixels from the bottom.\n        expect(marginComponent.position, closeToVector(Vector2(970, 960)));\n        // After the size of the component is changed the position is also\n        // changed.\n        marginComponent.size.add(Vector2.all(10));\n        expect(marginComponent.position, closeToVector(Vector2(960, 950)));\n      },\n    );\n\n    testWithFlameGame(\n      'position is still correct after a game resize',\n      (game) async {\n        final marginComponent = HudMarginComponent(\n          margin: const EdgeInsets.only(right: 10, bottom: 20),\n          size: Vector2.all(20),\n        );\n\n        await game.camera.viewport.ensureAdd(marginComponent);\n        // The position should be (770, 560) since the game size is (800, 600)\n        // and the component has its anchor in the top left corner (which then\n        // is were the margin will be calculated from).\n        // (800, 600) - size(20, 20) - position(10, 20) = (770, 560)\n        expect(marginComponent.position, closeToVector(Vector2(770, 560)));\n        game.update(0);\n        game.onGameResize(Vector2.all(500));\n        game.update(0);\n        expect(marginComponent.position, closeToVector(Vector2(470, 460)));\n      },\n    );\n\n    testWithFlameGame(\n      'position is still correct after parent resize and CameraComponent zoom',\n      (game) async {\n        final world = World();\n        final cameraComponent = CameraComponent(world: world);\n        game.ensureAddAll([world, cameraComponent]);\n\n        final parent = PositionComponent(\n          position: Vector2(10, 20),\n          size: Vector2(50, 40),\n        );\n        await world.ensureAdd(parent);\n\n        final marginComponent = HudMarginComponent(\n          margin: const EdgeInsets.only(right: 10, bottom: 20),\n          size: Vector2.all(20),\n        );\n\n        await parent.ensureAdd(marginComponent);\n        expect(marginComponent.position, closeToVector(Vector2(20, 00)));\n        parent.size = Vector2.all(500);\n        expect(marginComponent.position, closeToVector(Vector2(470, 460)));\n        cameraComponent.viewfinder.zoom = 2.0;\n        game.update(0);\n        expect(marginComponent.position, closeToVector(Vector2(470, 460)));\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/icon_component_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/widgets.dart' show IconData;\nimport 'package:flutter_test/flutter_test.dart';\n\n// A minimal IconData for testing (uses a basic Unicode code point).\nconst _testIcon = IconData(0x41, fontFamily: 'Roboto'); // 'A'\n\nvoid main() {\n  group('IconComponent', () {\n    test('defaults size to iconSize', () {\n      final component = IconComponent(icon: _testIcon, iconSize: 48);\n      expect(component.size, Vector2.all(48));\n    });\n\n    test('uses explicit size when provided', () {\n      final component = IconComponent(\n        icon: _testIcon,\n        iconSize: 48,\n        size: Vector2(100, 200),\n      );\n      expect(component.size, Vector2(100, 200));\n    });\n\n    test('default iconSize is 64', () {\n      final component = IconComponent(icon: _testIcon);\n      expect(component.iconSize, 64);\n      expect(component.size, Vector2.all(64));\n    });\n\n    test('accepts a custom paint', () {\n      final customPaint = Paint()..color = const Color(0xFFFF0000);\n      final component = IconComponent(\n        icon: _testIcon,\n        paint: customPaint,\n      );\n      expect(component.paint.color, const Color(0xFFFF0000));\n    });\n\n    test('icon getter and setter', () {\n      final component = IconComponent(icon: _testIcon);\n      expect(component.icon, _testIcon);\n\n      const newIcon = IconData(0x42, fontFamily: 'Roboto');\n      component.icon = newIcon;\n      expect(component.icon, newIcon);\n    });\n\n    test('iconSize getter and setter', () {\n      final component = IconComponent(icon: _testIcon, iconSize: 32);\n      expect(component.iconSize, 32);\n\n      component.iconSize = 128;\n      expect(component.iconSize, 128);\n    });\n\n    testWithFlameGame('supports fontPackage', (game) async {\n      const iconWithPackage = IconData(\n        0x41,\n        fontFamily: 'MyFont',\n        fontPackage: 'my_package',\n      );\n      final component = IconComponent(\n        icon: iconWithPackage,\n        iconSize: 32,\n      );\n      await game.ensureAdd(component);\n\n      expect(component.image, isNotNull);\n    });\n\n    testWithFlameGame('rasterizes icon on load', (game) async {\n      final component = IconComponent(\n        icon: _testIcon,\n        iconSize: 32,\n      );\n      await game.ensureAdd(component);\n\n      expect(component.image, isNotNull);\n    });\n\n    testWithFlameGame('re-rasterizes when icon changes', (game) async {\n      final component = IconComponent(\n        icon: _testIcon,\n        iconSize: 32,\n      );\n      await game.ensureAdd(component);\n\n      final originalImage = component.image;\n      expect(originalImage, isNotNull);\n\n      const newIcon = IconData(0x42, fontFamily: 'Roboto');\n      component.icon = newIcon;\n      game.update(0);\n\n      // After update, the image should eventually be replaced.\n      // We need to pump the event loop for the async rasterization.\n      await Future<void>.delayed(Duration.zero);\n\n      expect(component.image, isNotNull);\n    });\n\n    testWithFlameGame('re-rasterizes when iconSize changes', (game) async {\n      final component = IconComponent(\n        icon: _testIcon,\n        iconSize: 32,\n      );\n      await game.ensureAdd(component);\n\n      final originalImage = component.image;\n      expect(originalImage, isNotNull);\n\n      component.iconSize = 64;\n      game.update(0);\n\n      await Future<void>.delayed(Duration.zero);\n\n      expect(component.image, isNotNull);\n    });\n\n    testWithFlameGame('disposes image on remove', (game) async {\n      final component = IconComponent(\n        icon: _testIcon,\n        iconSize: 32,\n      );\n      await game.ensureAdd(component);\n\n      expect(component.image, isNotNull);\n\n      component.removeFromParent();\n      game.update(0);\n\n      expect(component.image, isNull);\n    });\n\n    testWithFlameGame('asserts icon is set on mount', (game) async {\n      final component = IconComponent();\n      expect(\n        () async => game.ensureAdd(component),\n        throwsAssertionError,\n      );\n    });\n\n    testWithFlameGame('supports tint', (game) async {\n      final component = IconComponent(\n        icon: _testIcon,\n        iconSize: 32,\n      );\n      await game.ensureAdd(component);\n\n      component.tint(const Color(0xFFFF0000));\n      expect(component.paint.colorFilter, isNotNull);\n    });\n\n    testWithFlameGame('supports setOpacity', (game) async {\n      final component = IconComponent(\n        icon: _testIcon,\n        iconSize: 32,\n      );\n      await game.ensureAdd(component);\n\n      component.setOpacity(0.5);\n      expect(component.paint.color.a, closeTo(0.5, 0.01));\n    });\n\n    test('setting same icon does not trigger re-rasterize', () {\n      final component = IconComponent(icon: _testIcon, iconSize: 32);\n      // Access internal state indirectly - setting the same icon shouldn't\n      // trigger re-rasterization.\n      component.icon = _testIcon;\n      // No assertion error or state change expected.\n      expect(component.icon, _testIcon);\n    });\n\n    test('setting same iconSize does not trigger re-rasterize', () {\n      final component = IconComponent(icon: _testIcon, iconSize: 32);\n      component.iconSize = 32;\n      expect(component.iconSize, 32);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/input/sprite_button_component_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/src/anchor.dart';\nimport 'package:flame/src/components/sprite_group_component.dart';\nimport 'package:flame/src/events/flame_game_mixins/multi_tap_dispatcher.dart';\nimport 'package:flame/src/sprite_sheet.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nenum _ButtonState {\n  up,\n  down,\n}\n\nFuture<void> main() async {\n  // Generate an image\n  final image = await generateImage();\n\n  group('SpriteButtonComponent', () {\n    testWithFlameGame('correctly registers taps', (game) async {\n      final initialGameSize = Vector2.all(100);\n      final componentSize = Vector2.all(10);\n      final buttonPosition = Vector2.all(100);\n      late final SpriteButtonComponent button;\n      game.onGameResize(initialGameSize);\n\n      final buttonSheet = SpriteSheet.fromColumnsAndRows(\n        image: image,\n        columns: 1,\n        rows: 2,\n      );\n\n      final component = SpriteGroupComponent<_ButtonState>(\n        sprites: {\n          _ButtonState.up: buttonSheet.getSpriteById(0),\n          _ButtonState.down: buttonSheet.getSpriteById(1),\n        },\n      );\n\n      component.current = _ButtonState.down;\n\n      await game.ensureAdd(\n        button = SpriteButtonComponent(\n          button: buttonSheet.getSpriteById(0),\n          buttonDown: buttonSheet.getSpriteById(1),\n          onPressed: () => component.current = _ButtonState.up,\n          position: buttonPosition,\n          size: componentSize,\n        ),\n      );\n\n      final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n      tapDispatcher.handleTapDown(1, TapDownDetails());\n      expect(component.current, _ButtonState.down);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(\n          globalPosition: buttonPosition.toOffset(),\n        ),\n      );\n      expect(component.current, _ButtonState.down);\n\n      tapDispatcher.handleTapUp(\n        1,\n        TapUpDetails(\n          kind: PointerDeviceKind.touch,\n          globalPosition: button.positionOfAnchor(Anchor.center).toOffset(),\n        ),\n      );\n      expect(component.current, _ButtonState.up);\n    });\n\n    testWithFlameGame('correctly registers taps onGameResize', (game) async {\n      final initialGameSize = Vector2.all(100);\n      final componentSize = Vector2.all(10);\n      final buttonPosition = Vector2.all(100);\n      late final SpriteButtonComponent button;\n      game.onGameResize(initialGameSize);\n\n      final buttonSheet = SpriteSheet.fromColumnsAndRows(\n        image: image,\n        columns: 1,\n        rows: 2,\n      );\n\n      final component = SpriteGroupComponent<_ButtonState>(\n        sprites: {\n          _ButtonState.up: buttonSheet.getSpriteById(0),\n          _ButtonState.down: buttonSheet.getSpriteById(1),\n        },\n      );\n      component.current = _ButtonState.down;\n\n      await game.ensureAdd(\n        button = SpriteButtonComponent(\n          button: buttonSheet.getSpriteById(0),\n          buttonDown: buttonSheet.getSpriteById(1),\n          onPressed: () => component.current = _ButtonState.up,\n          position: buttonPosition,\n          size: componentSize,\n        ),\n      );\n\n      final previousPosition = button\n          .positionOfAnchor(Anchor.center)\n          .toOffset();\n      game.onGameResize(initialGameSize * 2);\n      final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(\n          globalPosition: previousPosition,\n        ),\n      );\n      expect(component.current, _ButtonState.down);\n\n      tapDispatcher.handleTapUp(\n        1,\n        TapUpDetails(\n          kind: PointerDeviceKind.touch,\n          globalPosition: button.positionOfAnchor(Anchor.center).toOffset(),\n        ),\n      );\n      expect(component.current, _ButtonState.up);\n    });\n\n    testWidgets(\n      'Button can be pressed while the engine is paused',\n      (tester) async {\n        final game = FlameGame();\n\n        final buttonSheet = SpriteSheet.fromColumnsAndRows(\n          image: image,\n          columns: 1,\n          rows: 2,\n        );\n\n        final component = SpriteGroupComponent<_ButtonState>(\n          sprites: {\n            _ButtonState.up: buttonSheet.getSpriteById(0),\n            _ButtonState.down: buttonSheet.getSpriteById(1),\n          },\n        );\n        component.current = _ButtonState.down;\n\n        game.add(\n          SpriteButtonComponent(\n            button: buttonSheet.getSpriteById(0),\n            buttonDown: buttonSheet.getSpriteById(1),\n            onPressed: () {\n              game.pauseEngine();\n              game.overlays.add('pause-menu');\n            },\n            position: Vector2(400, 300),\n            size: Vector2.all(10),\n          ),\n        );\n        await tester.pumpWidget(\n          GameWidget(\n            game: game,\n            overlayBuilderMap: {\n              'pause-menu': (context, _) {\n                return _SimpleStatelessWidget(\n                  build: (context) {\n                    return Center(\n                      child: OutlinedButton(\n                        onPressed: () {\n                          game.overlays.remove('pause-menu');\n                          game.resumeEngine();\n                        },\n                        child: const Text('Resume'),\n                      ),\n                    );\n                  },\n                );\n              },\n            },\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n\n        await tester.tapAt(const Offset(400, 300));\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.paused, true);\n\n        await tester.tapAt(const Offset(400, 300));\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.paused, false);\n      },\n    );\n\n    testWithFlameGame('correctly registers taps (custom button)', (game) async {\n      final initialGameSize = Vector2.all(100);\n      final componentSize = Vector2.all(10);\n      final buttonPosition = Vector2.all(100);\n      late final _CustomSpriteButtonComponent button;\n      game.onGameResize(initialGameSize);\n\n      final buttonSheet = SpriteSheet.fromColumnsAndRows(\n        image: image,\n        columns: 1,\n        rows: 2,\n      );\n\n      final component = SpriteGroupComponent<_ButtonState>(\n        sprites: {\n          _ButtonState.up: buttonSheet.getSpriteById(0),\n          _ButtonState.down: buttonSheet.getSpriteById(1),\n        },\n      );\n      component.current = _ButtonState.down;\n\n      await game.ensureAdd(\n        button = _CustomSpriteButtonComponent(\n          customButton: buttonSheet.getSpriteById(0),\n          customButtonDown: buttonSheet.getSpriteById(1),\n          customOnPressed: () => component.current = _ButtonState.up,\n          customPosition: buttonPosition,\n          customSize: componentSize,\n        ),\n      );\n\n      final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n      tapDispatcher.handleTapDown(1, TapDownDetails());\n      expect(component.current, _ButtonState.down);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(\n          globalPosition: buttonPosition.toOffset(),\n        ),\n      );\n      expect(component.current, _ButtonState.down);\n\n      tapDispatcher.handleTapUp(\n        1,\n        TapUpDetails(\n          kind: PointerDeviceKind.touch,\n          globalPosition: button.positionOfAnchor(Anchor.center).toOffset(),\n        ),\n      );\n      expect(component.current, _ButtonState.up);\n    });\n\n    testWithFlameGame('correctly registers taps onGameResize (custom button)', (\n      game,\n    ) async {\n      final initialGameSize = Vector2.all(100);\n      final componentSize = Vector2.all(10);\n      final buttonPosition = Vector2.all(100);\n      late final _CustomSpriteButtonComponent button;\n      game.onGameResize(initialGameSize);\n\n      final buttonSheet = SpriteSheet.fromColumnsAndRows(\n        image: image,\n        columns: 1,\n        rows: 2,\n      );\n\n      final component = SpriteGroupComponent<_ButtonState>(\n        sprites: {\n          _ButtonState.up: buttonSheet.getSpriteById(0),\n          _ButtonState.down: buttonSheet.getSpriteById(1),\n        },\n      );\n      component.current = _ButtonState.down;\n\n      await game.ensureAdd(\n        button = _CustomSpriteButtonComponent(\n          customButton: buttonSheet.getSpriteById(0),\n          customButtonDown: buttonSheet.getSpriteById(1),\n          customOnPressed: () => component.current = _ButtonState.up,\n          customPosition: buttonPosition,\n          customSize: componentSize,\n        ),\n      );\n\n      final previousPosition = button\n          .positionOfAnchor(Anchor.center)\n          .toOffset();\n      game.onGameResize(initialGameSize * 2);\n      final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(\n          globalPosition: previousPosition,\n        ),\n      );\n      expect(component.current, _ButtonState.down);\n\n      tapDispatcher.handleTapUp(\n        1,\n        TapUpDetails(\n          kind: PointerDeviceKind.touch,\n          globalPosition: button.positionOfAnchor(Anchor.center).toOffset(),\n        ),\n      );\n      expect(component.current, _ButtonState.up);\n    });\n\n    testWidgets(\n      'Button can be pressed while the engine is paused (custom button)',\n      (tester) async {\n        final game = FlameGame();\n\n        final buttonSheet = SpriteSheet.fromColumnsAndRows(\n          image: image,\n          columns: 1,\n          rows: 2,\n        );\n\n        final component = SpriteGroupComponent<_ButtonState>(\n          sprites: {\n            _ButtonState.up: buttonSheet.getSpriteById(0),\n            _ButtonState.down: buttonSheet.getSpriteById(1),\n          },\n        );\n        component.current = _ButtonState.down;\n\n        game.add(\n          _CustomSpriteButtonComponent(\n            customButton: buttonSheet.getSpriteById(0),\n            customButtonDown: buttonSheet.getSpriteById(1),\n            customOnPressed: () {\n              game.pauseEngine();\n              game.overlays.add('pause-menu');\n            },\n            customPosition: Vector2(400, 300),\n            customSize: Vector2.all(10),\n          ),\n        );\n        await tester.pumpWidget(\n          GameWidget(\n            game: game,\n            overlayBuilderMap: {\n              'pause-menu': (context, _) {\n                return _SimpleStatelessWidget(\n                  build: (context) {\n                    return Center(\n                      child: OutlinedButton(\n                        onPressed: () {\n                          game.overlays.remove('pause-menu');\n                          game.resumeEngine();\n                        },\n                        child: const Text('Resume'),\n                      ),\n                    );\n                  },\n                );\n              },\n            },\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n\n        await tester.tapAt(const Offset(400, 300));\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.paused, true);\n\n        await tester.tapAt(const Offset(400, 300));\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.paused, false);\n      },\n    );\n  });\n\n  testWithFlameGame('can change button sprites', (game) async {\n    final buttonSheet = SpriteSheet.fromColumnsAndRows(\n      image: image,\n      columns: 1,\n      rows: 2,\n    );\n\n    final button = SpriteButtonComponent(\n      button: buttonSheet.getSpriteById(0),\n      buttonDown: buttonSheet.getSpriteById(1),\n    );\n\n    await game.ensureAdd(button);\n\n    expect(\n      button.sprites,\n      {\n        ButtonState.up: buttonSheet.getSpriteById(0),\n        ButtonState.down: buttonSheet.getSpriteById(1),\n      },\n    );\n\n    button.button = buttonSheet.getSpriteById(1);\n    button.buttonDown = buttonSheet.getSpriteById(0);\n\n    expect(\n      button.sprites,\n      {\n        ButtonState.up: buttonSheet.getSpriteById(1),\n        ButtonState.down: buttonSheet.getSpriteById(0),\n      },\n    );\n  });\n\n  testWithFlameGame('can change button sprites (custom button)', (game) async {\n    final buttonSheet = SpriteSheet.fromColumnsAndRows(\n      image: image,\n      columns: 1,\n      rows: 2,\n    );\n\n    final button = _CustomSpriteButtonComponent(\n      customButton: buttonSheet.getSpriteById(0),\n      customButtonDown: buttonSheet.getSpriteById(1),\n    );\n\n    await game.ensureAdd(button);\n\n    expect(\n      button.sprites,\n      {\n        ButtonState.up: buttonSheet.getSpriteById(0),\n        ButtonState.down: buttonSheet.getSpriteById(1),\n      },\n    );\n\n    button.button = buttonSheet.getSpriteById(1);\n    button.buttonDown = buttonSheet.getSpriteById(0);\n\n    expect(\n      button.sprites,\n      {\n        ButtonState.up: buttonSheet.getSpriteById(1),\n        ButtonState.down: buttonSheet.getSpriteById(0),\n      },\n    );\n  });\n}\n\n/// This is used to test [SpriteButtonComponent] without using the constructor\n/// and setting properties in [onLoad] of an extending class instead.\nclass _CustomSpriteButtonComponent extends SpriteButtonComponent {\n  final Sprite customButton;\n  final Sprite customButtonDown;\n  final void Function()? customOnPressed;\n  final Vector2? customPosition;\n  final Vector2? customSize;\n\n  _CustomSpriteButtonComponent({\n    required this.customButton,\n    required this.customButtonDown,\n    this.customOnPressed,\n    this.customPosition,\n    this.customSize,\n  });\n\n  @override\n  Future<void> onLoad() async {\n    position = customPosition ?? Vector2(0.0, 0.0);\n    button = customButton;\n    buttonDown = customButtonDown;\n    onPressed = customOnPressed;\n  }\n}\n\nclass _SimpleStatelessWidget extends StatelessWidget {\n  const _SimpleStatelessWidget({\n    required Widget Function(BuildContext) build,\n  }) : _build = build;\n\n  final Widget Function(BuildContext) _build;\n\n  @override\n  Widget build(BuildContext context) => _build(context);\n}\n"
  },
  {
    "path": "packages/flame/test/components/isometric_tile_map_component_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/sprite.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nclass _MockImage extends Mock implements Image {\n  _MockImage(this.size);\n\n  final Vector2 size;\n\n  @override\n  int get width => size.x.toInt();\n\n  @override\n  int get height => size.y.toInt();\n}\n\nclass _MockSpriteSheet extends Mock implements SpriteSheet {\n  _MockSpriteSheet(this.tileSize);\n\n  final Vector2 tileSize;\n\n  @override\n  Image get image => _MockImage(tileSize);\n}\n\nvoid main() {\n  group('isometric tile map', () {\n    final map = [\n      [1, 1],\n      [1, 1],\n    ];\n\n    test('conversions work back and forth', () {\n      final tileSize = Vector2.all(32.0);\n      final c = IsometricTileMapComponent(\n        _MockSpriteSheet(tileSize),\n        map,\n        destTileSize: tileSize,\n        tileHeight: 8.0,\n      );\n\n      final scales = [Vector2.all(0.1), Vector2.all(1), Vector2.all(2.0)];\n      const blocks = [Block(0, 0), Block(1, 1), Block(-1, 0), Block(2, -10)];\n      for (final scale in scales) {\n        c.scale = scale;\n        for (final block in blocks) {\n          expect(c.getBlockRenderedAt(c.getBlockRenderPosition(block)), block);\n          expect(c.getBlock(c.getBlockCenterPosition(block)), block);\n        }\n      }\n    });\n\n    test('center position', () {\n      final tileSize = Vector2.all(32.0);\n      final c = IsometricTileMapComponent(\n        _MockSpriteSheet(tileSize),\n        map,\n        destTileSize: tileSize,\n        tileHeight: 8.0,\n      );\n\n      expect(\n        c.getBlockCenterPosition(const Block(0, 0)),\n        closeToVector(Vector2(32, 8)),\n      );\n    });\n\n    test('height scaling', () {\n      final tileSize = Vector2(156, 181);\n      final c = IsometricTileMapComponent(\n        _MockSpriteSheet(tileSize),\n        map,\n        destTileSize: tileSize,\n      );\n      //expect the block to be directly below\n      final blockAbove = c.getBlockRenderPositionInts(0, 0);\n      expect(\n        c.getBlockRenderPositionInts(1, 1),\n        closeToVector(blockAbove + Vector2(0, 181 / 2), 1e-13),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/joystick_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/src/events/flame_game_mixins/multi_drag_dispatcher.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('JoystickDirection tests', () {\n    testWithFlameGame(\n      'can convert angle to JoystickDirection',\n      (game) async {\n        final joystick = JoystickComponent(\n          knob: CircleComponent(radius: 5.0),\n          size: 20,\n          margin: const EdgeInsets.only(left: 20, bottom: 20),\n        );\n        await game.ensureAdd(joystick);\n\n        expect(joystick.direction, JoystickDirection.idle);\n        joystick.delta.setValues(1.0, 0.0);\n        expect(joystick.direction, JoystickDirection.right);\n        joystick.delta.setValues(0.0, -1.0);\n        expect(joystick.direction, JoystickDirection.up);\n        joystick.delta.setValues(1.0, -1.0);\n        expect(joystick.direction, JoystickDirection.upRight);\n        joystick.delta.setValues(-1.0, -1.0);\n        expect(joystick.direction, JoystickDirection.upLeft);\n        joystick.delta.setValues(-1.0, 0.0);\n        expect(joystick.direction, JoystickDirection.left);\n        joystick.delta.setValues(0.0, 1.0);\n        expect(joystick.direction, JoystickDirection.down);\n        joystick.delta.setValues(1.0, 1.0);\n        expect(joystick.direction, JoystickDirection.downRight);\n        joystick.delta.setValues(-1.0, 1.0);\n        expect(joystick.direction, JoystickDirection.downLeft);\n      },\n    );\n\n    testWithFlameGame(\n      'properly re-positions onGameSize',\n      (game) async {\n        game.onGameResize(Vector2(100, 200));\n        final joystick = JoystickComponent(\n          knob: CircleComponent(radius: 5.0),\n          size: 20,\n          margin: const EdgeInsets.only(left: 20, bottom: 20),\n        );\n        await game.ensureAdd(joystick);\n        game.onGameResize(Vector2(200, 100));\n        expect(joystick.position, Vector2(30, 70));\n      },\n    );\n\n    testWithFlameGame(\n      'properly re-positions with FixedResolutionViewport',\n      (game) async {\n        game.camera = CameraComponent.withFixedResolution(\n          width: 100,\n          height: 200,\n        );\n        game.onGameResize(Vector2(100, 200));\n        final joystick = JoystickComponent(\n          knob: CircleComponent(radius: 5.0),\n          size: 20,\n          margin: const EdgeInsets.only(left: 20, bottom: 20),\n        );\n        await game.camera.viewport.ensureAdd(joystick);\n        expect(joystick.position, Vector2(30, 170));\n        game.onGameResize(Vector2(200, 100));\n        expect(joystick.position, Vector2(30, 170));\n      },\n    );\n  });\n\n  group('Joystick input tests', () {\n    testWithFlameGame(\n      'knob should stay on correct side when the total delta is larger than '\n      'the size and then the knob is moved slightly back again',\n      (game) async {\n        final joystick = JoystickComponent(\n          knob: CircleComponent(radius: 5.0),\n          size: 20,\n          margin: const EdgeInsets.only(left: 20, top: 20),\n        );\n        await game.add(joystick);\n        await game.ready();\n        expect(joystick.knob!.position, closeToVector(Vector2(10, 10)));\n        final dragDispatcher = game.firstChild<MultiDragDispatcher>()!;\n        // Start dragging the joystick\n        dragDispatcher.handleDragStart(\n          1,\n          DragStartDetails(\n            localPosition: const Offset(20, 20),\n            globalPosition: const Offset(20, 20),\n          ),\n        );\n        game.update(0);\n        // Drag the knob outside of the size of the joystick in X-axis\n        dragDispatcher.handleDragUpdate(\n          1,\n          DragUpdateDetails(\n            localPosition: const Offset(60, 20),\n            globalPosition: const Offset(60, 20),\n            delta: const Offset(40, 0),\n          ),\n        );\n        game.update(0);\n        expect(joystick.knob!.position, closeToVector(Vector2(20, 10)));\n        // Drag the knob back towards it's base position\n        dragDispatcher.handleDragUpdate(\n          1,\n          DragUpdateDetails(\n            localPosition: const Offset(40, 20),\n            globalPosition: const Offset(40, 20),\n            delta: const Offset(-20, 0),\n          ),\n        );\n        game.update(0);\n        expect(joystick.knob!.position, closeToVector(Vector2(20, 10)));\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/keyboard_listener_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nabstract class _KeyCallStub {\n  bool onCall(Set<LogicalKeyboardKey> keysPressed);\n}\n\nclass _KeyCallStubImpl extends Mock implements _KeyCallStub {}\n\nclass _MockKeyUpEvent extends Mock implements KeyUpEvent {\n  @override\n  String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {\n    return super.toString();\n  }\n}\n\nKeyUpEvent _mockKeyUp(LogicalKeyboardKey key) {\n  final event = _MockKeyUpEvent();\n  when(() => event.logicalKey).thenReturn(key);\n  return event;\n}\n\nvoid main() {\n  group('KeyboardListenerComponent', () {\n    test('calls registered handlers', () {\n      final stub = _KeyCallStubImpl();\n      when(() => stub.onCall(any())).thenReturn(true);\n\n      final input = KeyboardListenerComponent(\n        keyUp: {\n          LogicalKeyboardKey.arrowUp: stub.onCall,\n        },\n      );\n\n      input.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.arrowUp), {});\n      verify(() => stub.onCall({})).called(1);\n    });\n\n    test(\n      'returns false the handler return value',\n      () {\n        final stub = _KeyCallStubImpl();\n        when(() => stub.onCall(any())).thenReturn(false);\n\n        final input = KeyboardListenerComponent(\n          keyUp: {\n            LogicalKeyboardKey.arrowUp: stub.onCall,\n          },\n        );\n\n        expect(\n          input.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.arrowUp), {}),\n          isFalse,\n        );\n      },\n    );\n\n    test(\n      'returns true (allowing event to bubble) when no handler is registered',\n      () {\n        final stub = _KeyCallStubImpl();\n        when(() => stub.onCall(any())).thenReturn(true);\n\n        final input = KeyboardListenerComponent();\n\n        expect(\n          input.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.arrowUp), {}),\n          isTrue,\n        );\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/mixins/component_viewport_margin_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('ComponentViewportMargin', () {\n    testWithGame(\n      'margin should stay same after GameResize',\n      _TestGame.new,\n      (game) async {\n        final initialGameSize = Vector2.all(100);\n        final componentSize = Vector2.all(10);\n        const margin = EdgeInsets.only(bottom: 10, right: 10);\n\n        final component = _ComponentWithViewportMargin()\n          ..size = componentSize\n          ..margin = margin;\n\n        game.onGameResize(initialGameSize);\n\n        await game.ensureAdd(component);\n\n        final initialMargin = component.margin;\n\n        game.onGameResize(initialGameSize * 2);\n\n        final marginAfterGameResize = component.margin;\n\n        final actualNewPosition = component.position.toOffset();\n\n        final expectedNewPosition =\n            game.size.toOffset() +\n            margin.bottomRight -\n            componentSize.toOffset();\n\n        expect(initialMargin, equals(marginAfterGameResize));\n\n        expect(actualNewPosition, equals(expectedNewPosition));\n      },\n    );\n  });\n}\n\nclass _TestGame extends FlameGame<World> {}\n\nclass _ComponentWithViewportMargin extends PositionComponent\n    with HasGameReference<_TestGame>, ComponentViewportMargin<_TestGame> {}\n"
  },
  {
    "path": "packages/flame/test/components/mixins/gesture_hitboxes_test.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('GestureHitboxes', () {\n    testWithFlameGame('component with hitbox contains point', (game) async {\n      final component = _HitboxComponent();\n      component.position.setValues(1.0, 1.0);\n      component.anchor = Anchor.topLeft;\n      component.size.setValues(2.0, 2.0);\n\n      final hitbox = PolygonHitbox([\n        Vector2(1, 0),\n        Vector2(0, -1),\n        Vector2(-1, 0),\n        Vector2(0, 1),\n      ]);\n\n      component.add(hitbox);\n      await game.ensureAdd(component);\n\n      final point = component.position + component.size / 4;\n      expect(component.containsPoint(point), isTrue);\n    });\n\n    testWithFlameGame(\n      'hitbox is larger than the component and the point is outside '\n      \"of the component's size\",\n      (game) async {\n        final component = _HitboxComponent();\n        component.position.setValues(1.0, 1.0);\n        component.anchor = Anchor.topLeft;\n        component.size.setValues(2.0, 2.0);\n\n        final hitbox = PolygonHitbox([\n          Vector2(10, 0),\n          Vector2(0, -10),\n          Vector2(-10, 0),\n          Vector2(0, 10),\n        ]);\n\n        component.add(hitbox);\n        await game.ensureAdd(component);\n\n        final point = Vector2(9, 0);\n        expect(component.containsPoint(point), isTrue);\n      },\n    );\n\n    testWithFlameGame('CircleHitbox is displaced within the parent component', (\n      game,\n    ) async {\n      final component = _HitboxComponent();\n      component.position.setAll(10);\n      component.size.setValues(2.0, 2.0);\n\n      final hitbox = CircleHitbox(\n        position: Vector2(1.0, 1.5),\n        radius: 0.5,\n        anchor: Anchor.center,\n      );\n\n      component.add(hitbox);\n      await game.ensureAdd(component);\n\n      final point = component.position + hitbox.position;\n      expect(component.containsPoint(point), isTrue);\n      final outsidePoint = component.position + Vector2(1.0, 0.99);\n      expect(component.containsPoint(outsidePoint), isFalse);\n    });\n\n    testWithFlameGame(\n      'RectangleHitbox is displaced within the parent component',\n      (game) async {\n        final component = _HitboxComponent();\n        component.position.setAll(10);\n        component.size.setValues(2.0, 2.0);\n\n        final hitbox = RectangleHitbox(\n          position: Vector2(1.0, 1.5),\n          size: Vector2.all(1.0),\n          anchor: Anchor.center,\n        );\n\n        component.add(hitbox);\n        await game.ensureAdd(component);\n\n        final point = component.position + hitbox.position;\n        expect(component.containsPoint(point), isTrue);\n        final outsidePoint = component.position + Vector2(1.0, 0.99);\n        expect(component.containsPoint(outsidePoint), isFalse);\n      },\n    );\n\n    testWithFlameGame('get component hitboxes', (game) async {\n      final component = _HitboxComponent();\n      component.position.setValues(1.0, 1.0);\n      component.anchor = Anchor.topLeft;\n      component.size.setValues(2.0, 2.0);\n\n      final polygonHitBox = PolygonHitbox([\n        Vector2(1, 0),\n        Vector2(0, -1),\n        Vector2(-1, 0),\n        Vector2(0, 1),\n      ]);\n\n      final circleHitBox = CircleHitbox();\n      final rectangleHitBox = RectangleHitbox();\n\n      component.addAll([polygonHitBox, circleHitBox, rectangleHitBox]);\n      await game.ensureAdd(component);\n\n      expect(component.hitboxes.length, 3);\n      expect(component.hitboxes.contains(polygonHitBox), isTrue);\n      expect(component.hitboxes.contains(circleHitBox), isTrue);\n      expect(component.hitboxes.contains(rectangleHitBox), isTrue);\n    });\n  });\n}\n\nclass _HitboxComponent extends PositionComponent with GestureHitboxes {}\n"
  },
  {
    "path": "packages/flame/test/components/mixins/has_ancestor_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('HasAncestor', () {\n    testWithFlameGame('successfully sets the ancestor link', (game) async {\n      final ancestor = _AncestorComponent()..addToParent(game);\n      final inBetween = _InBetweenComponent()..addToParent(ancestor);\n      final component = _TestComponent()..addToParent(inBetween);\n      await game.ready();\n\n      expect(component.isMounted, true);\n      expect(component.ancestor, isA<_AncestorComponent>());\n    });\n\n    testWithFlameGame(\n      'throws assertion error if the wrong ancestor is used',\n      (game) async {\n        final ancestor = _DifferentComponent()..addToParent(game);\n        final inBetween = _InBetweenComponent()..addToParent(ancestor);\n        await game.ready();\n\n        expect(\n          () {\n            inBetween.add(_TestComponent());\n            game.update(0);\n          },\n          failsAssert(\n            '''An ancestor must be of type _AncestorComponent in the component tree''',\n          ),\n        );\n      },\n    );\n  });\n}\n\nclass _AncestorComponent extends Component {}\n\nclass _InBetweenComponent extends Component {}\n\nclass _DifferentComponent extends Component {}\n\nclass _TestComponent extends Component with HasAncestor<_AncestorComponent> {}\n"
  },
  {
    "path": "packages/flame/test/components/mixins/has_decorator_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/rendering.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('HasDecorator', () {\n    testGolden(\n      'Component rendering with and without a Decorator',\n      (game, tester) async {\n        await game.add(\n          _DecoratedComponent(\n            position: Vector2.all(25),\n            size: Vector2.all(40),\n          ),\n        );\n        await game.add(\n          _DecoratedComponent(\n            position: Vector2(75, 25),\n            size: Vector2.all(40),\n          )..decorator.addLast(PaintDecorator.grayscale()..addBlur(2)),\n        );\n      },\n      size: Vector2(100, 50),\n      goldenFile: '../../_goldens/has_decorator_1.png',\n    );\n  });\n}\n\nclass _DecoratedComponent extends PositionComponent {\n  _DecoratedComponent({super.position, super.size})\n    : super(anchor: Anchor.center);\n\n  final paint = Paint()..color = const Color(0xff30ccd2);\n\n  @override\n  void render(Canvas canvas) {\n    final radius = size.x / 2;\n    canvas.drawCircle(Offset(radius, radius), radius, paint);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/components/mixins/has_game_ref_test.dart",
    "content": "// ignore_for_file: deprecated_member_use_from_same_package\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nclass _MyGame extends FlameGame {\n  bool calledFoo = false;\n  void foo() {\n    calledFoo = true;\n  }\n}\n\nclass _FooComponent extends Component with HasGameRef<_MyGame> {\n  void foo() {\n    gameRef.foo();\n  }\n}\n\nclass _BarComponent extends Component with HasGameRef<_MyGame> {}\n\nclass _MockFlameGame extends Mock implements _MyGame {}\n\nvoid main() {\n  group('HasGameRef', () {\n    testWithGame<_MyGame>('simple test', _MyGame.new, (game) async {\n      final c = _FooComponent();\n      game.add(c);\n      c.foo();\n      expect(game.calledFoo, true);\n    });\n\n    testWithGame<_MyGame>('gameRef can be mocked', _MyGame.new, (game) async {\n      final component = _BarComponent();\n      await game.ensureAdd(component);\n\n      component.game = _MockFlameGame();\n\n      expect(component.gameRef, isA<_MockFlameGame>());\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/mixins/has_paint_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('HasPaint', () {\n    test('paint returns the default paint', () {\n      final component = _MyComponent();\n\n      expect(component.paint, component.getPaint());\n    });\n\n    test(\n      'paint setter sets the main paint',\n      () {\n        final component = _MyComponent();\n\n        const color = Color(0xFFE5E5E5);\n        component.paint = Paint()..color = color;\n\n        expectColor(component.getPaint().color, color);\n      },\n    );\n\n    test('paintLayers defaults to empty list', () {\n      final component = _MyComponent();\n\n      const color = Color(0xFFE5E5E5);\n      component.paint = Paint()..color = color;\n\n      expect(\n        component.paintLayers,\n        equals(<Paint>[]),\n      );\n    });\n\n    test('paintLayers returns correct colors', () {\n      const firstColor = Color(0xFFE5E5E5);\n      const secondColor = Color(0xFF123456);\n      const thirdColor = Color(0xFFABABAB);\n      final firstPaint = Paint()..color = firstColor;\n      final secondPaint = Paint()..color = secondColor;\n      final thirdPaint = Paint()..color = thirdColor;\n\n      final circle = CircleComponent(\n        radius: 10,\n        paint: firstPaint,\n        paintLayers: [secondPaint, thirdPaint],\n      );\n\n      expect(\n        circle.paintLayers,\n        equals([secondPaint, thirdPaint]),\n      );\n      expect(\n        circle.paint,\n        equals(firstPaint),\n      );\n    });\n\n    test('can clear paintLayers', () {\n      const firstColor = Color(0xFFE5E5E5);\n      const secondColor = Color(0xFF123456);\n      const thirdColor = Color(0xFFABABAB);\n      final firstPaint = Paint()..color = firstColor;\n      final secondPaint = Paint()..color = secondColor;\n      final thirdPaint = Paint()..color = thirdColor;\n\n      final circle = CircleComponent(\n        radius: 10,\n        paint: firstPaint,\n        paintLayers: [secondPaint, thirdPaint],\n      );\n\n      circle.paintLayers.clear();\n\n      expect(\n        circle.paintLayers,\n        equals(<Paint>[]),\n      );\n      expect(\n        circle.paint,\n        equals(firstPaint),\n      );\n    });\n\n    test(\n      'getPaint throws exception when retrieving a paint that does not exist',\n      () {\n        final component = _MyComponentWithType();\n\n        expect(\n          () => component.getPaint(_MyComponentKeys.background),\n          throwsArgumentError,\n        );\n      },\n    );\n\n    test(\n      'setPaint sets a paint',\n      () {\n        final component = _MyComponentWithType();\n\n        const color = Color(0xFFA9A9A9);\n        component.setPaint(_MyComponentKeys.background, Paint()..color = color);\n\n        expectColor(\n          component.getPaint(_MyComponentKeys.background).color,\n          color,\n        );\n      },\n    );\n\n    test(\n      'deletePaint removes a paint from the map',\n      () {\n        final component = _MyComponentWithType();\n\n        component.setPaint(_MyComponentKeys.foreground, Paint());\n        component.deletePaint(_MyComponentKeys.foreground);\n\n        expect(\n          () => component.getPaint(_MyComponentKeys.foreground),\n          throwsArgumentError,\n        );\n      },\n    );\n\n    test(\n      'append paint to paintLayers',\n      () {\n        final component = _MyComponent();\n\n        const color = Color(0xFFE5E5E5);\n        component.paintLayers.add(Paint()..color = color);\n\n        expectColor(component.paintLayers[0].color, color);\n      },\n    );\n\n    test(\n      'use setPaintLayers to set multiple paintIds in paintLayers',\n      () {\n        final component = _MyComponent();\n\n        const color = Color(0xFFE5E5E5);\n        const anotherColor = Color(0xFFABABAB);\n        const thirdColor = Color(0xFF123456);\n        component.setPaint('test', Paint()..color = color);\n        component.setPaint('anotherTest', Paint()..color = anotherColor);\n        component.setPaint('thirdTest', Paint()..color = thirdColor);\n\n        component.paintLayers = [\n          component.getPaint('thirdTest'),\n          component.getPaint('test'),\n        ];\n        expectColor(component.paintLayers[0].color, thirdColor);\n        expectColor(component.paintLayers[1].color, color);\n      },\n    );\n\n    test(\n      'makeTransparent sets opacity to 0 on the main when paintId is omitted',\n      () {\n        final component = _MyComponent();\n        component.makeTransparent();\n\n        expect(component.paint.color.a, 0);\n      },\n    );\n\n    test(\n      'makeTransparent sets opacity to 0 on informed paintId',\n      () {\n        final component = _MyComponentWithType();\n        component.setPaint(_MyComponentKeys.background, Paint());\n        component.makeTransparent(paintId: _MyComponentKeys.background);\n\n        expect(\n          component.getPaint(_MyComponentKeys.background).color.a,\n          0,\n        );\n      },\n    );\n\n    test(\n      'makeOpaque sets opacity to 1 on the main when paintId is omitted',\n      () {\n        final component = _MyComponent();\n        component.makeTransparent();\n        component.makeOpaque();\n\n        expect(component.paint.color.a, 1);\n      },\n    );\n\n    test(\n      'makeOpaque sets opacity to 1 on informed paintId',\n      () {\n        final component = _MyComponentWithType();\n        component.setPaint(\n          _MyComponentKeys.background,\n          Paint()..color = const Color(0x00E5E5E5),\n        );\n        component.makeOpaque(paintId: _MyComponentKeys.background);\n\n        expect(\n          component.getPaint(_MyComponentKeys.background).color.a,\n          1,\n        );\n      },\n    );\n\n    test(\n      'setOpacity sets opacity of the main when paintId is omitted',\n      () {\n        final component = _MyComponent();\n        component.setOpacity(0.2);\n\n        expect(component.paint.color.a, closeTo(0.2, 0.00001));\n      },\n    );\n\n    test(\n      'setOpacity sets opacity of the informed paintId',\n      () {\n        final component = _MyComponentWithType();\n        component.setPaint(_MyComponentKeys.background, Paint());\n        component.setOpacity(0.2, paintId: _MyComponentKeys.background);\n\n        expect(\n          component.getPaint(_MyComponentKeys.background).color.a,\n          closeTo(0.2, 0.00001),\n        );\n      },\n    );\n\n    test(\n      'throws error if opacity is less than 0',\n      () {\n        final component = _MyComponent();\n\n        expect(\n          () => component.setOpacity(-0.5),\n          throwsArgumentError,\n        );\n      },\n    );\n\n    test(\n      'throws error if opacity is greater than 1',\n      () {\n        final component = _MyComponent();\n\n        expect(\n          () => component.setOpacity(1.1),\n          throwsArgumentError,\n        );\n      },\n    );\n\n    test(\n      'setColor sets color of the main when paintId is omitted',\n      () {\n        final component = _MyComponent();\n        const color = Color(0xFFE5E5E5);\n        component.setColor(color);\n\n        expectColor(component.paint.color, color);\n      },\n    );\n\n    test(\n      'setOpacity sets opacity of the informed paintId',\n      () {\n        final component = _MyComponentWithType();\n        const color = Color(0xFFE5E5E5);\n        component.setPaint(_MyComponentKeys.background, Paint());\n        component.setColor(color, paintId: _MyComponentKeys.background);\n\n        expectColor(\n          component.getPaint(_MyComponentKeys.background).color,\n          color,\n        );\n      },\n    );\n\n    test(\n      'tint sets the correct color filter of the main when paintId is omitted',\n      () {\n        final component = _MyComponent();\n        const color = Color(0xFFE5E5E5);\n        component.tint(color);\n\n        expect(\n          component.paint.colorFilter,\n          const ColorFilter.mode(color, BlendMode.srcATop),\n        );\n      },\n    );\n\n    test(\n      'setOpacity sets opacity of the informed paintId',\n      () {\n        final component = _MyComponentWithType();\n        const color = Color(0xFFE5E5E5);\n        component.setPaint(_MyComponentKeys.background, Paint());\n        component.tint(color, paintId: _MyComponentKeys.background);\n\n        expect(\n          component.getPaint(_MyComponentKeys.background).colorFilter,\n          const ColorFilter.mode(color, BlendMode.srcATop),\n        );\n      },\n    );\n  });\n}\n\nclass _MyComponent extends PositionComponent with HasPaint {}\n\nenum _MyComponentKeys {\n  background,\n  foreground,\n}\n\nclass _MyComponentWithType extends PositionComponent\n    with HasPaint<_MyComponentKeys> {}\n"
  },
  {
    "path": "packages/flame/test/components/mixins/has_time_scale_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('HasTimeScale', () {\n    testWithGame<_GameWithTimeScale>(\n      'delta time scales correctly',\n      _GameWithTimeScale.new,\n      (game) async {\n        final component = _MovingComponent();\n        await game.add(component);\n        await game.ready();\n        const stepTime = 10.0;\n        var distance = 0.0;\n        final offset = stepTime * component.speed;\n\n        game.timeScale = 0.5;\n        distance = component.x;\n        game.update(stepTime);\n        expect(component.x, distance + game.timeScale * offset);\n\n        game.timeScale = 1.0;\n        distance = component.x;\n        game.update(stepTime);\n        expect(component.x, distance + game.timeScale * offset);\n\n        game.timeScale = 1.5;\n        distance = component.x;\n        game.update(stepTime);\n        expect(component.x, distance + game.timeScale * offset);\n\n        game.timeScale = 2.0;\n        distance = component.x;\n        game.update(stepTime);\n        expect(component.x, distance + game.timeScale * offset);\n      },\n    );\n\n    testWithGame(\n      'cascading time scale',\n      _GameWithTimeScale.new,\n      (game) async {\n        final component1 = _ComponentWithTimeScale();\n        final component2 = _MovingComponent();\n        await component1.add(component2);\n        await game.add(component1);\n        await game.ready();\n        const stepTime = 10.0;\n        var distance = 0.0;\n        final offset = stepTime * component2.speed;\n\n        game.timeScale = 0.5;\n        component1.timeScale = 0.5;\n        distance = component2.x;\n        game.update(stepTime);\n        expect(\n          component2.x,\n          distance + game.timeScale * component1.timeScale * offset,\n        );\n\n        game.timeScale = 1.0;\n        distance = component2.x;\n        game.update(stepTime);\n        expect(\n          component2.x,\n          distance + game.timeScale * component1.timeScale * offset,\n        );\n\n        component1.timeScale = 1.5;\n        distance = component2.x;\n        game.update(stepTime);\n        expect(\n          component2.x,\n          distance + game.timeScale * component1.timeScale * offset,\n        );\n\n        game.timeScale = 2.0;\n        distance = component2.x;\n        game.update(stepTime);\n        expect(\n          component2.x,\n          distance + game.timeScale * component1.timeScale * offset,\n        );\n      },\n    );\n\n    testWithGame(\n      'pausing and resuming',\n      _GameWithTimeScale.new,\n      (game) async {\n        final component = _MovingComponent();\n        await game.add(component);\n        await game.ready();\n        const stepTime = 10.0;\n        var distance = 0.0;\n        final offset = stepTime * component.speed;\n\n        game.pause();\n        distance = component.x;\n        game.update(stepTime);\n        expect(component.x, distance);\n\n        game.resume();\n        distance = component.x;\n        game.update(stepTime);\n        expect(component.x, distance + offset);\n\n        game.pause();\n        distance = component.x;\n        game.update(stepTime);\n        expect(component.x, distance);\n\n        game.resume(newTimeScale: 0.5);\n        distance = component.x;\n        game.update(stepTime);\n        expect(component.x, distance + 0.5 * offset);\n      },\n    );\n\n    testWithGame(\n      'resume does not modify timeScale if not paused',\n      _GameWithTimeScale.new,\n      (game) async {\n        final component = _MovingComponent();\n        await game.add(component);\n        await game.ready();\n        const stepTime = 10.0;\n        var distance = 0.0;\n        final offset = stepTime * component.speed;\n\n        game.pause();\n        distance = component.x;\n        game.update(stepTime);\n        expect(component.x, distance + game.timeScale * offset);\n\n        game.timeScale = 0.5;\n        game.resume();\n        distance = component.x;\n        game.update(stepTime);\n        expect(component.x, distance + game.timeScale * offset);\n      },\n    );\n  });\n}\n\nclass _GameWithTimeScale extends FlameGame with HasTimeScale {}\n\nclass _ComponentWithTimeScale extends Component with HasTimeScale {}\n\nclass _MovingComponent extends PositionComponent {\n  final speed = 1.0;\n  @override\n  void update(double dt) => position.setValues(position.x + speed * dt, 0);\n}\n"
  },
  {
    "path": "packages/flame/test/components/mixins/has_visibility_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../_resources/load_image.dart';\n\nvoid main() {\n  group('HasVisibility', () {\n    testGolden(\n      'Render a Component with isVisible set to false',\n      (game, tester) async {\n        game.add(_MyComponent()..mySprite.isVisible = false);\n      },\n      size: Vector2(300, 400),\n      goldenFile: '../../_goldens/visibility_test_1.png',\n    );\n  });\n}\n\nclass _MySpriteComponent extends PositionComponent with HasVisibility {\n  late final Sprite sprite;\n\n  @override\n  Future<void> onLoad() async {\n    sprite = Sprite(await loadImage('flame.png'));\n  }\n\n  @override\n  void render(Canvas canvas) {\n    sprite.render(canvas, anchor: Anchor.center);\n  }\n}\n\n/// This component contains a [_MySpriteComponent]. It first\n/// renders a rectangle, and then the children will render.\n/// In this test the visibility of [mySprite] is set to\n/// false, so only the rectangle is expected to be rendered.\nclass _MyComponent extends PositionComponent {\n  _MyComponent() : super(size: Vector2(300, 400)) {\n    mySprite = _MySpriteComponent()..position = Vector2(150, 200);\n    add(mySprite);\n  }\n  late final _MySpriteComponent mySprite;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(\n      Rect.fromLTRB(25, 25, size.x - 25, size.y - 25),\n      Paint()\n        ..color = const Color(0xffffffff)\n        ..style = PaintingStyle.stroke\n        ..strokeWidth = 2,\n    );\n    super.render(canvas);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/components/mixins/parent_is_a_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('ParentIsA', () {\n    testWithFlameGame('successfully sets the parent link', (game) async {\n      final parent = _ParentComponent()..addToParent(game);\n      final component = _TestComponent()..addToParent(parent);\n      await game.ready();\n\n      expect(component.isMounted, true);\n      expect(component.parent, isA<_ParentComponent>());\n    });\n\n    testWithFlameGame(\n      'throws assertion error if the wrong parent is used',\n      (game) async {\n        final parent = _DifferentComponent()..addToParent(game);\n        await game.ready();\n\n        expect(\n          () {\n            parent.add(_TestComponent());\n            game.update(0);\n          },\n          failsAssert('Parent must be of type _ParentComponent'),\n        );\n      },\n    );\n  });\n}\n\nclass _ParentComponent extends Component {}\n\nclass _DifferentComponent extends Component {}\n\nclass _TestComponent extends Component with ParentIsA<_ParentComponent> {}\n"
  },
  {
    "path": "packages/flame/test/components/mixins/snapshot_test.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group(\n    'Snapshot',\n    () {\n      testWithGame<_SnapshotTestGame>(\n        'Snapshot should be created once',\n        _SnapshotTestGame.new,\n        (game) async {\n          await game.ready();\n          final snapshotComponent = game.snapshotComponent;\n\n          // Wait a few frames\n          final canvas = Canvas(PictureRecorder());\n          for (var i = 0; i < 5; i++) {\n            game.render(canvas);\n          }\n\n          // Check that the rendering has only happened once\n          expect(snapshotComponent.renderCalled, equals(1));\n          expect(snapshotComponent.parentRenderTreeCalled, equals(1));\n          // And that a snapshot has been generated\n          expect(snapshotComponent.takeSnapshotCalled, equals(1));\n          expect(snapshotComponent.hasSnapshot, equals(true));\n          expect(snapshotComponent.snapshot, isA<Picture>());\n        },\n      );\n\n      testWithGame<_SnapshotTestGame>(\n        'Should render normally when renderSnapshot is false',\n        () => _SnapshotTestGame(renderSnapshot: false),\n        (game) async {\n          await game.ready();\n          final snapshotComponent = game.snapshotComponent;\n\n          // Wait a few frames\n          const framesToWait = 5;\n          final canvas = Canvas(PictureRecorder());\n          for (var i = 0; i < framesToWait; i++) {\n            game.render(canvas);\n          }\n\n          // Check that render tree has been called multiple times\n          expect(\n            snapshotComponent.renderTreeCalled,\n            greaterThanOrEqualTo(framesToWait),\n          );\n          // And that rendering is happening normally\n          expect(\n            snapshotComponent.renderCalled,\n            greaterThanOrEqualTo(framesToWait),\n          );\n          expect(\n            snapshotComponent.parentRenderTreeCalled,\n            greaterThanOrEqualTo(framesToWait),\n          );\n          // And that a snapshot has not been generated\n          expect(snapshotComponent.takeSnapshotCalled, equals(0));\n          expect(snapshotComponent.hasSnapshot, equals(false));\n        },\n      );\n\n      testWidgets(\n        'Should generate a snapshot when takeSnapshot is called',\n        (tester) async {\n          final game = _SnapshotTestGame(renderSnapshot: false);\n          const framesToWait = 5;\n          late final _MockSnapshotComponent snapshotComponent;\n\n          await tester.runAsync(() async {\n            final widget = GameWidget(game: game);\n            await tester.pumpWidget(widget);\n            await tester.pump();\n            await game.ready();\n            snapshotComponent = game.snapshotComponent;\n\n            // Wait a few frames\n            final recorder = PictureRecorder();\n            final canvas = Canvas(recorder);\n            for (var i = 0; i < framesToWait; i++) {\n              game.render(canvas);\n            }\n\n            // Take snapshot\n            snapshotComponent.takeSnapshot();\n\n            // Wait a few more frames\n            for (var i = 0; i < framesToWait; i++) {\n              game.render(canvas);\n            }\n            recorder.endRecording();\n\n            await tester.pump();\n          });\n\n          // Check that a snapshot has been generated\n          expect(snapshotComponent.takeSnapshotCalled, equals(1));\n          expect(snapshotComponent.hasSnapshot, equals(true));\n          // Check golden\n          await expectLater(\n            snapshotComponent.snapshotAsImage(200, 200),\n            matchesGoldenFile('../../_goldens/snapshot_test_1.png'),\n          );\n        },\n      );\n\n      testWidgets(\n        'Should generate a transformed image',\n        (tester) async {\n          final game = _SnapshotTestGame(renderSnapshot: false);\n          late final _MockSnapshotComponent snapshotComponent;\n\n          await tester.runAsync(() async {\n            final widget = GameWidget(game: game);\n            await tester.pumpWidget(widget);\n            await tester.pump();\n            await game.ready();\n            snapshotComponent = game.snapshotComponent;\n\n            // Force a frame\n            final canvas = Canvas(PictureRecorder());\n            game.render(canvas);\n\n            // Take snapshot\n            snapshotComponent.takeSnapshot();\n\n            await tester.pump();\n          });\n\n          // prepare transform\n          final matrix = Matrix4.identity();\n          matrix.translateByDouble(100.0, 100.0, 0.0, 1.0);\n          matrix.rotateZ(-pi / 4);\n          matrix.scaleByDouble(1.5, 1.5, 1.0, 1.0);\n          matrix.translateByDouble(-100.0, -100.0, 0.0, 1.0);\n\n          // Check that a snapshot has been generated\n          expect(snapshotComponent.takeSnapshotCalled, equals(1));\n          expect(snapshotComponent.hasSnapshot, equals(true));\n          // Check transformed image against golden\n          await expectLater(\n            snapshotComponent.snapshotAsImage(200, 200, transform: matrix),\n            matchesGoldenFile('../../_goldens/snapshot_test_2.png'),\n          );\n        },\n      );\n\n      testGolden(\n        'Should respect transforms',\n        game: _SnapshotTestGame(),\n        size: Vector2(200, 200),\n        (game, tester) async {\n          final snapshotComponent =\n              (game as _SnapshotTestGame).snapshotComponent;\n\n          // Apply transforms\n          snapshotComponent.scale = Vector2(0.75, 0.75);\n          snapshotComponent.angle = pi / 4; // 45 degrees\n          snapshotComponent.position += Vector2(-25, -25);\n        },\n        goldenFile: '../../_goldens/snapshot_test_3.png',\n      );\n    },\n  );\n}\n\nclass _SnapshotTestGame extends FlameGame {\n  late final _MockSnapshotComponent snapshotComponent;\n  bool renderSnapshot;\n\n  _SnapshotTestGame({this.renderSnapshot = true});\n\n  @override\n  Future<void> onLoad() async {\n    // Add a snapshot-enabled component that has it's own rendered content\n    snapshotComponent = _MockSnapshotComponent()\n      ..size = Vector2(200, 200)\n      ..anchor = Anchor.center\n      ..position = Vector2(100, 100)\n      ..renderSnapshot = renderSnapshot;\n    add(snapshotComponent);\n\n    // Also adds a child to the snapshot-enabled component tree\n    snapshotComponent.add(\n      _generateCircle(\n        const Color(0x99ff3300),\n        x: 50,\n        y: 100,\n      ),\n    );\n    snapshotComponent.add(\n      _generateCircle(\n        const Color(0x9933ff00),\n        x: 150,\n        y: 100,\n      ),\n    );\n    snapshotComponent.add(\n      _generateCircle(\n        const Color(0x990033ff),\n        x: 100,\n        y: 50,\n      ),\n    );\n    snapshotComponent.add(\n      _generateCircle(\n        const Color(0x99ff33ff),\n        x: 100,\n        y: 150,\n      ),\n    );\n  }\n}\n\nclass _MockSnapshotComponent extends _MockComponentSuper with Snapshot {\n  int renderCalled = 0;\n  int renderTreeCalled = 0;\n  int takeSnapshotCalled = 0;\n\n  @override\n  void render(Canvas canvas) {\n    renderCalled++;\n    super.render(canvas);\n  }\n\n  @override\n  void renderTree(Canvas canvas) {\n    renderTreeCalled++;\n    super.renderTree(canvas);\n  }\n\n  @override\n  Picture takeSnapshot() {\n    takeSnapshotCalled++;\n    return super.takeSnapshot();\n  }\n}\n\n/// Mock a superclass just so we can count how many times super.renderTree has\n/// been called\nclass _MockComponentSuper extends PositionComponent {\n  int parentRenderTreeCalled = 0;\n\n  @override\n  void renderTree(Canvas canvas) {\n    parentRenderTreeCalled++;\n    super.renderTree(canvas);\n  }\n}\n\n/// Need a few circles, so this class helps\nCircleComponent _generateCircle(\n  Color color, {\n  double radius = 50,\n  double x = 0,\n  double y = 0,\n}) {\n  return CircleComponent(\n    radius: radius,\n    position: Vector2(x, y),\n    anchor: Anchor.center,\n    paint: Paint()..color = color,\n  );\n}\n"
  },
  {
    "path": "packages/flame/test/components/parallax_test.dart",
    "content": "import 'package:flame/cache.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/parallax.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nclass _ParallaxGame extends FlameGame {\n  late final ParallaxComponent parallaxComponent;\n  late final Vector2? parallaxSize;\n\n  _ParallaxGame({this.parallaxSize}) {\n    onGameResize(Vector2.all(500));\n  }\n\n  @override\n  Future<void> onLoad() async {\n    parallaxComponent = await loadParallaxComponent(\n      [],\n      size: parallaxSize,\n      baseVelocity: Vector2(20, 0),\n      velocityMultiplierDelta: Vector2(1.8, 1.0),\n    );\n    camera.viewport.add(parallaxComponent);\n  }\n}\n\nclass _MockImages extends Mock implements Images {}\n\nclass _MockImage extends Mock implements Image {\n  @override\n  int get height => 100;\n\n  @override\n  int get width => 100;\n}\n\nclass _SlowLoadParallaxGame extends FlameGame {\n  late final ParallaxComponent parallaxComponent;\n  late final Vector2? parallaxSize;\n\n  _SlowLoadParallaxGame({this.parallaxSize}) {\n    onGameResize(Vector2.all(500));\n  }\n\n  @override\n  Future<void> onLoad() async {\n    final mockImageCache = _MockImages();\n\n    void createMockAnswer(int imageNumber, int time) {\n      when(() => mockImageCache.load('$imageNumber.png')).thenAnswer(\n        (_) {\n          return Future<Image>.delayed(\n            Duration(milliseconds: time * 100),\n            () => Future.value(_MockImage()),\n          );\n        },\n      );\n    }\n\n    // [3, 5, 1, 6, 2]\n    List<void>.generate(\n      5,\n      (i) => createMockAnswer(i, ((i % 2) + 1) * 3 - i % 3),\n    );\n\n    final imagesData = List.generate(5, (i) => ParallaxImageData('$i.png'));\n\n    parallaxComponent = await loadParallaxComponent(\n      imagesData,\n      size: parallaxSize,\n      baseVelocity: Vector2(20, 0),\n      velocityMultiplierDelta: Vector2(1.8, 1.0),\n      images: mockImageCache,\n    );\n    add(parallaxComponent);\n  }\n}\n\nvoid main() {\n  group('parallax test', () {\n    testWithGame<_ParallaxGame>(\n      'can have non-fullscreen ParallaxComponent',\n      _ParallaxGame.new,\n      (game) async {\n        final parallaxSize = Vector2.all(100);\n        game.onGameResize(parallaxSize);\n        expect(game.parallaxComponent.size, parallaxSize);\n      },\n    );\n\n    testWithGame<_ParallaxGame>(\n      'can have fullscreen ParallaxComponent',\n      _ParallaxGame.new,\n      (game) async {\n        expect(game.parallaxComponent.size, game.size);\n      },\n    );\n\n    testWithGame<_SlowLoadParallaxGame>(\n      'can have layers with different loading times',\n      _SlowLoadParallaxGame.new,\n      (game) async {\n        final parallax = game.parallaxComponent.parallax!;\n        var lastLength = 0.0;\n        for (final layer in parallax.layers) {\n          final velocityLength = layer.velocityMultiplier.length;\n          expect(velocityLength > lastLength, isTrue);\n          lastLength = velocityLength;\n        }\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/particle_system_component_test.dart",
    "content": "import 'package:canvas_test/canvas_test.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/particles.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nclass _MockParticle extends Mock implements Particle {}\n\nvoid main() {\n  group('ParticleSystem', () {\n    test('returns the progress of its particle', () {\n      final particle = _MockParticle();\n      when(() => particle.progress).thenReturn(0.2);\n\n      final progress = ParticleSystemComponent(particle: particle).progress;\n      expect(progress, equals(0.2));\n    });\n\n    test('returns the progress of its particle', () {\n      final particle = _MockParticle();\n\n      final canvas = MockCanvas();\n      ParticleSystemComponent(particle: particle).render(canvas);\n\n      verify(() => particle.render(canvas)).called(1);\n    });\n\n    test('updates its particle', () {\n      final particle = _MockParticle();\n      when(() => particle.shouldRemove).thenReturn(false);\n\n      ParticleSystemComponent(particle: particle).update(0.1);\n      verify(() => particle.update(0.1)).called(1);\n    });\n\n    testWithFlameGame(\n      'is removed when its particle is finished',\n      (FlameGame game) async {\n        final particle = _MockParticle();\n        when(() => particle.shouldRemove).thenReturn(true);\n\n        final component = ParticleSystemComponent(particle: particle);\n        await game.ensureAdd(component);\n\n        game.update(1);\n\n        expect(component.isRemoving, true);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/position_component_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:canvas_test/canvas_test.dart';\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('PositionComponent', () {\n    group('Properties getters and setters', () {\n      test('get/set x/y or position', () {\n        final component = PositionComponent();\n        component.position.setValues(2.2, 3.4);\n        expect(component.x, closeTo(2.2, toleranceFloat32(2.2)));\n        expect(component.y, closeTo(3.4, toleranceFloat32(3.4)));\n\n        component.position = Vector2(1.0, 0.0);\n        expect(component.x, 1.0);\n        expect(component.y, 0.0);\n\n        component.x = 3.1;\n        component.y = -2.2;\n        final value = Vector2(3.1, -2.2);\n        expect(\n          component.position,\n          closeToVector(value, toleranceVector2Float32(value)),\n        );\n      });\n\n      test('get/set width/height or size', () {\n        final component = PositionComponent();\n        component.size.setValues(2.2, 3.4);\n        expect(component.size.x, closeTo(2.2, toleranceFloat32(2.2)));\n        expect(component.size.y, closeTo(3.4, toleranceFloat32(3.4)));\n\n        component.size = Vector2(1.0, 0.0);\n        expect(component.width, 1.0);\n        expect(component.height, 0.0);\n\n        component.width = 2.1;\n        component.height = 3.3;\n        final value = Vector2(2.1, 3.3);\n        expect(\n          component.size,\n          closeToVector(value, toleranceVector2Float32(value)),\n        );\n      });\n\n      test('get/set rect', () {\n        final component = PositionComponent(\n          position: Vector2(0, 1),\n          size: Vector2.all(2),\n        );\n        final rect = component.toRect();\n        expect(rect, const Rect.fromLTWH(0, 1, 2, 2));\n\n        component.setByRect(const Rect.fromLTWH(10.0, 10.0, 1.0, 1.0));\n        expect(component.x, 10.0);\n        expect(component.y, 10.0);\n        expect(component.width, 1.0);\n        expect(component.height, 1.0);\n      });\n\n      test('get/set rect with anchor', () {\n        final component = PositionComponent(\n          position: Vector2(0, 1),\n          size: Vector2.all(2),\n          anchor: Anchor.center,\n        );\n        final rect = component.toRect();\n        expect(rect.left, -1.0);\n        expect(rect.top, 0.0);\n        expect(rect.width, 2.0);\n        expect(rect.height, 2.0);\n        expect(component.topLeftPosition, Vector2(-1.0, 0));\n\n        component.setByRect(const Rect.fromLTWH(10.0, 10.0, 1.0, 1.0));\n        expect(component.x, 10.5);\n        expect(component.y, 10.5);\n        expect(component.width, 1.0);\n        expect(component.height, 1.0);\n      });\n    });\n\n    group('Contains point', () {\n      test('inside point', () {\n        final component = PositionComponent()\n          ..position = Vector2(2.0, 2.0)\n          ..size = Vector2(4.0, 4.0)\n          ..angle = 0.0\n          ..anchor = Anchor.center;\n\n        final point = Vector2(2.0, 2.0);\n        expect(component.containsPoint(point), true);\n      });\n\n      test('point on edge', () {\n        final component = PositionComponent();\n        component.position.setValues(2.0, 2.0);\n        component.size.setValues(2.0, 2.0);\n        component.angle = 0.0;\n        component.anchor = Anchor.center;\n\n        final point = Vector2(1.0, 1.0);\n        expect(component.containsPoint(point), true);\n      });\n\n      test('points outside', () {\n        final component = PositionComponent();\n        component.position.setValues(2.0, 2.0);\n        component.size.setValues(2.0, 2.0);\n        component.angle = 0.0;\n        component.anchor = Anchor.center;\n\n        expect(component.containsPoint(Vector2(4.0, 1.0)), false);\n        expect(component.containsPoint(Vector2(1.0, 4.0)), false);\n      });\n\n      test('overlapping with angle', () {\n        final component = PositionComponent();\n        component.position.setValues(2.0, 2.0);\n        component.size.setValues(2.0, 2.0);\n        component.angle = pi / 4;\n        component.anchor = Anchor.center;\n\n        final point = Vector2(3.1, 2.0);\n        expect(component.containsPoint(point), true);\n      });\n\n      test('not overlapping with angle', () {\n        final component = PositionComponent();\n        component.position.setValues(2.0, 2.0);\n        component.size.setValues(2.0, 2.0);\n        component.angle = pi / 4;\n        component.anchor = Anchor.center;\n\n        final point = Vector2(1.0, 0.1);\n        expect(component.containsPoint(point), false);\n      });\n\n      test('overlapping with angle and topLeft anchor', () {\n        final component = PositionComponent();\n        component.position.setValues(1.0, 1.0);\n        component.size.setValues(2.0, 2.0);\n        component.angle = pi / 4;\n        component.anchor = Anchor.topLeft;\n\n        final point = Vector2(1.0, 3.1);\n        expect(component.containsPoint(point), true);\n      });\n\n      testWithFlameGame('component with hitbox contains point', (game) async {\n        final component = _MyHitboxComponent();\n        component.position.setValues(1.0, 1.0);\n        component.anchor = Anchor.topLeft;\n        component.size.setValues(2.0, 2.0);\n        final hitbox = PolygonHitbox([\n          Vector2(1, 0),\n          Vector2(0, -1),\n          Vector2(-1, 0),\n          Vector2(0, 1),\n        ]);\n        component.add(hitbox);\n        await game.ensureAdd(component);\n\n        final point = component.position + component.size / 4;\n        expect(component.containsPoint(point), true);\n      });\n\n      testWithFlameGame('component with hitbox with position contains point', (\n        game,\n      ) async {\n        final component = _MyHitboxComponent();\n        component.position.setValues(1.0, 1.0);\n        component.anchor = Anchor.topLeft;\n        component.size.setValues(2.0, 2.0);\n        final hitbox = PolygonHitbox(\n          [\n            Vector2(1, 0),\n            Vector2(0, -1),\n            Vector2(-1, 0),\n            Vector2(0, 1),\n          ],\n          position: Vector2(5, 6),\n        );\n        component.add(hitbox);\n        await game.ensureAdd(component);\n\n        final point =\n            component.position + (component.size / 4) + hitbox.position;\n        expect(component.containsPoint(point), true);\n      });\n\n      testWithFlameGame(\n        'component with hitbox with position just misses point',\n        (game) async {\n          final component = _MyHitboxComponent();\n          component.position.setValues(1.0, 1.0);\n          component.anchor = Anchor.topLeft;\n          component.size.setValues(2.0, 2.0);\n          final hitbox = PolygonHitbox(\n            [\n              Vector2(1, 0),\n              Vector2(0, -1),\n              Vector2(-1, 0),\n              Vector2(0, 1),\n            ],\n            position: Vector2(5, 6),\n          );\n          component.add(hitbox);\n          await game.ensureAdd(component);\n\n          final point =\n              component.position +\n              (component.size / 4) -\n              Vector2(0.01, 0) +\n              hitbox.position;\n          expect(component.containsPoint(point), false);\n        },\n      );\n\n      testWithFlameGame(\n        'component with anchor topLeft contains point on edge',\n        (game) async {\n          final component = _MyHitboxComponent();\n          component.position.setValues(-1, -1);\n          component.anchor = Anchor.topLeft;\n          component.size.setValues(2.0, 2.0);\n          component.add(RectangleHitbox());\n          await game.ensureAdd(component);\n\n          expect(\n            component.containsPoint(\n              Vector2(prevFloat32(1.0), prevFloat32(1.0)),\n            ),\n            isTrue,\n          );\n          expect(\n            component.containsPoint(\n              Vector2(prevFloat32(1.0), nextFloat32(-1.0)),\n            ),\n            isTrue,\n          );\n          expect(\n            component.containsPoint(\n              Vector2(nextFloat32(-1.0), nextFloat32(-1.0)),\n            ),\n            isTrue,\n          );\n          expect(\n            component.containsPoint(\n              Vector2(nextFloat32(-1.0), prevFloat32(1.0)),\n            ),\n            isTrue,\n          );\n        },\n      );\n\n      testWithFlameGame(\n        'component with anchor bottomRight contains point close to edge',\n        (game) async {\n          final component = _MyHitboxComponent();\n          component.position.setValues(1, 1);\n          component.anchor = Anchor.bottomRight;\n          component.size.setValues(2.0, 2.0);\n          component.add(RectangleHitbox());\n          await game.ensureAdd(component);\n\n          expect(\n            component.containsPoint(\n              Vector2(\n                prevFloat32(1.0),\n                prevFloat32(1.0),\n              ),\n            ),\n            isTrue,\n          );\n          expect(\n            component.containsPoint(\n              Vector2(prevFloat32(1.0), nextFloat32(-1.0)),\n            ),\n            isTrue,\n          );\n          expect(\n            component.containsPoint(\n              Vector2(nextFloat32(-1.0), nextFloat32(-1.0)),\n            ),\n            isTrue,\n          );\n          expect(\n            component.containsPoint(\n              Vector2(nextFloat32(-1.0), prevFloat32(1.0)),\n            ),\n            isTrue,\n          );\n        },\n      );\n\n      test('component with anchor topRight does not contain close points', () {\n        final component = _MyHitboxComponent();\n        component.position.setValues(1, 1);\n        component.anchor = Anchor.topLeft;\n        component.size.setValues(2.0, 2.0);\n        final hitbox = RectangleHitbox();\n        component.add(hitbox);\n\n        expect(component.containsPoint(Vector2(0.0, 0.0)), false);\n        expect(component.containsPoint(Vector2(0.9, 0.9)), false);\n        expect(component.containsPoint(Vector2(3.1, 3.1)), false);\n        expect(component.containsPoint(Vector2(1.1, 3.1)), false);\n      });\n\n      testWithFlameGame(\n        'component with hitbox does not contain point',\n        (game) async {\n          final component = _MyHitboxComponent()\n            ..addToParent(game)\n            ..position.setValues(1.0, 1.0)\n            ..anchor = Anchor.topLeft\n            ..size.setValues(2.0, 2.0)\n            ..add(\n              PolygonHitbox([\n                Vector2(1, 0),\n                Vector2(0, 1),\n                Vector2(1, 2),\n                Vector2(2, 1),\n              ]),\n            );\n          await game.ready();\n          expect(component.children.length, 1);\n\n          expect(component.containsPoint(Vector2.all(1.1)), false);\n          expect(component.containsPoint(Vector2.all(1.4)), false);\n          expect(component.containsPoint(Vector2.all(2.0)), true);\n          expect(component.containsPoint(Vector2.all(2.6)), false);\n          expect(component.containsPoint(Vector2.all(2.9)), false);\n          expect(component.containsPoint(Vector2(2.6, 1.5)), false);\n        },\n      );\n\n      test('component with zero size does not contain point', () {\n        final component = PositionComponent();\n        component.position.setValues(2.0, 2.0);\n        component.size.setValues(0.0, 0.0);\n        component.angle = 0.0;\n        component.anchor = Anchor.center;\n\n        final point = Vector2(2.0, 2.0);\n        expect(component.containsPoint(point), false);\n      });\n\n      test('scaled component contains point', () {\n        final component = PositionComponent();\n        component.anchor = Anchor.center;\n        component.position = Vector2.all(10.0);\n        component.size = Vector2.all(5.0);\n\n        final topLeftPoint = component.position - component.size / 2;\n        final bottomRightPoint = component.position + component.size / 2;\n        final topRightPoint = Vector2(bottomRightPoint.x, topLeftPoint.y);\n        final bottomLeftPoint = Vector2(topLeftPoint.x, bottomRightPoint.y);\n        final epsilon = Vector2.all(0.0001);\n        void checkOutsideCorners({\n          required bool expectedResult,\n          bool? topLeftResult,\n          bool? bottomRightResult,\n          bool? topRightResult,\n          bool? bottomLeftResult,\n        }) {\n          expect(\n            component.containsPoint(topLeftPoint - epsilon),\n            topLeftResult ?? expectedResult,\n          );\n          expect(\n            component.containsPoint(bottomRightPoint + epsilon),\n            bottomRightResult ?? expectedResult,\n          );\n          expect(\n            component.containsPoint(\n              topRightPoint\n                ..x += epsilon.x\n                ..y -= epsilon.y,\n            ),\n            topRightResult ?? expectedResult,\n          );\n          expect(\n            component.containsPoint(\n              bottomLeftPoint\n                ..x -= epsilon.x\n                ..y += epsilon.y,\n            ),\n            bottomLeftResult ?? expectedResult,\n          );\n        }\n\n        checkOutsideCorners(expectedResult: false);\n        component.scale = Vector2.all(1.0001);\n        checkOutsideCorners(expectedResult: true);\n        component.angle = 1;\n        checkOutsideCorners(expectedResult: false);\n        component.angle = 0;\n        component.anchor = Anchor.topLeft;\n        checkOutsideCorners(expectedResult: false, bottomRightResult: true);\n        component.anchor = Anchor.bottomRight;\n        checkOutsideCorners(expectedResult: false, topLeftResult: true);\n        component.anchor = Anchor.topRight;\n        checkOutsideCorners(expectedResult: false, bottomLeftResult: true);\n        component.anchor = Anchor.bottomLeft;\n        checkOutsideCorners(expectedResult: false, topRightResult: true);\n      });\n    });\n\n    group('Anchor points', () {\n      test('component with anchor center has the same center and position', () {\n        final component = PositionComponent();\n        component.position.setValues(2.0, 1.0);\n        component.size.setValues(3.0, 1.0);\n        component.anchor = Anchor.center;\n\n        expect(component.center, component.position);\n        expect(component.absoluteCenter, component.position);\n        expect(\n          component.topLeftPosition,\n          component.position - component.size / 2,\n        );\n      });\n\n      test('component with anchor topLeft has the correct center', () {\n        final component = PositionComponent();\n        component.position.setValues(2.0, 1.0);\n        component.size.setValues(3.0, 1.0);\n        component.angle = 0.0;\n        component.anchor = Anchor.topLeft;\n\n        expect(component.center, component.position + component.size / 2);\n        expect(\n          component.absoluteCenter,\n          component.position + component.size / 2,\n        );\n      });\n\n      testWithFlameGame(\n        'component with parent has the correct center',\n        (game) async {\n          final parent = PositionComponent();\n          parent.position.setValues(2.0, 1.0);\n          parent.anchor = Anchor.topLeft;\n          final child = PositionComponent();\n          child.position.setValues(2.0, 1.0);\n          child.size.setValues(3.0, 1.0);\n          child.angle = 0.0;\n          child.anchor = Anchor.topLeft;\n          parent.add(child);\n          game.add(parent);\n          await game.ready();\n\n          expect(\n            child.absoluteTopLeftPosition,\n            child.position + parent.position,\n          );\n          expect(\n            child.absoluteTopLeftPosition,\n            child.topLeftPosition + parent.topLeftPosition,\n          );\n          expect(child.absoluteCenter, parent.position + child.center);\n        },\n      );\n    });\n\n    group('Coordinates transforms', () {\n      final game = FlameGame();\n      game.onGameResize(Vector2.all(100));\n\n      test('width and height', () {\n        final component = PositionComponent(size: Vector2.all(3));\n        component.scale = Vector2(5, -7);\n        expect(component.width, 3);\n        expect(component.height, 3);\n        expect(component.scaledSize.x, 15);\n        expect(component.scaledSize.y, 21);\n        component.width = 1;\n        component.height = 2;\n        expect(component.width, 1);\n        expect(component.height, 2);\n        expect(component.scaledSize.x, 5);\n        expect(component.scaledSize.y, 14);\n        // Changing scaledSize won't have any effect...\n        component.scaledSize.setValues(1, 1);\n        expect(component.scaledSize.x, 5);\n        expect(component.scaledSize.y, 14);\n      });\n\n      test('positionOf', () {\n        final component = PositionComponent()\n          ..size = Vector2(50, 100)\n          ..position = Vector2(500, 700)\n          ..scale = Vector2(2, 1)\n          ..anchor = Anchor.center;\n        expect(component.positionOfAnchor(Anchor.center), Vector2(500, 700));\n        expect(component.positionOfAnchor(Anchor.topLeft), Vector2(450, 650));\n        expect(component.positionOfAnchor(Anchor.topCenter), Vector2(500, 650));\n        expect(component.positionOfAnchor(Anchor.topRight), Vector2(550, 650));\n        expect(\n          component.positionOfAnchor(Anchor.centerLeft),\n          Vector2(450, 700),\n        );\n        expect(\n          component.positionOfAnchor(Anchor.centerRight),\n          Vector2(550, 700),\n        );\n        expect(\n          component.positionOfAnchor(Anchor.bottomLeft),\n          Vector2(450, 750),\n        );\n        expect(\n          component.positionOfAnchor(Anchor.bottomCenter),\n          Vector2(500, 750),\n        );\n        expect(\n          component.positionOfAnchor(Anchor.bottomRight),\n          Vector2(550, 750),\n        );\n        expect(component.positionOf(Vector2(-3, 2)), Vector2(444, 652));\n        expect(component.positionOf(Vector2(7, 16)), Vector2(464, 666));\n      });\n\n      test('local<->parent transforms', () {\n        final component = PositionComponent()\n          ..size = Vector2(10, 10)\n          ..position = Vector2(50, 20)\n          ..anchor = Anchor.center;\n\n        expect(component.positionOf(Vector2(0, 0)), Vector2(45, 15));\n        expect(component.positionOf(Vector2(5, 5)), Vector2(50, 20));\n        expect(component.positionOf(Vector2(10, 0)), Vector2(55, 15));\n        expect(component.positionOf(Vector2(0, 10)), Vector2(45, 25));\n\n        expect(component.toLocal(Vector2(0, 0)), Vector2(-45, -15));\n        expect(component.toLocal(Vector2(50, 20)), Vector2(5, 5));\n        expect(component.toLocal(Vector2(55, 25)), Vector2(10, 10));\n        expect(component.toLocal(Vector2(100, 100)), Vector2(55, 85));\n      });\n\n      test('flips', () {\n        final component = PositionComponent()\n          ..size = Vector2(10, 10)\n          ..position = Vector2(50, 20)\n          ..anchor = const Anchor(0.6, 0.8);\n\n        expect(component.positionOf(Vector2(6, 8)), Vector2(50, 20));\n        expect(component.positionOf(Vector2(0, 0)), Vector2(44, 12));\n        component.flipHorizontally();\n        expect(component.positionOf(Vector2(6, 8)), Vector2(50, 20));\n        expect(component.positionOf(Vector2(0, 0)), Vector2(56, 12));\n        component.flipVertically();\n        expect(component.positionOf(Vector2(6, 8)), Vector2(50, 20));\n        expect(component.positionOf(Vector2(0, 0)), Vector2(56, 28));\n        component.flipHorizontally();\n        expect(component.positionOf(Vector2(6, 8)), Vector2(50, 20));\n        expect(component.positionOf(Vector2(0, 0)), Vector2(44, 28));\n        component.flipVertically();\n        expect(component.positionOf(Vector2(6, 8)), Vector2(50, 20));\n        expect(component.positionOf(Vector2(0, 0)), Vector2(44, 12));\n      });\n\n      test('center flips', () {\n        final component = PositionComponent()\n          ..size = Vector2(10, 10)\n          ..position = Vector2(50, 20)\n          ..anchor = const Anchor(0.6, 0.8);\n\n        expect(component.positionOf(Vector2(6, 8)), Vector2(50, 20));\n        expect(component.positionOf(Vector2(0, 0)), Vector2(44, 12));\n        expect(component.positionOf(Vector2(10, 0)), Vector2(54, 12));\n        component.flipHorizontallyAroundCenter();\n        expect(component.positionOf(Vector2(6, 8)), Vector2(48, 20));\n        expect(component.positionOf(Vector2(0, 0)), Vector2(54, 12));\n        expect(component.positionOf(Vector2(10, 0)), Vector2(44, 12));\n        component.flipVerticallyAroundCenter();\n        expect(component.positionOf(Vector2(6, 8)), Vector2(48, 14));\n        expect(component.positionOf(Vector2(0, 0)), Vector2(54, 22));\n        expect(component.positionOf(Vector2(10, 0)), Vector2(44, 22));\n      });\n\n      test('double center flips', () {\n        final startPosition = Vector2(50, 20);\n        final component = PositionComponent()\n          ..size = Vector2(10, 20)\n          ..angle = 2\n          ..scale = Vector2(2.0, 3.0)\n          ..position = startPosition;\n        final centerPosition = component.center;\n\n        component.flipVerticallyAroundCenter();\n        // Same position after one vertical flip.\n        expect(\n          component.center,\n          closeToVector(\n            centerPosition,\n            toleranceVector2Float32(centerPosition),\n          ),\n        );\n\n        component.flipVerticallyAroundCenter();\n        // Same position after flipping back the vertical flip.\n        expect(\n          component.center,\n          closeToVector(\n            centerPosition,\n            toleranceVector2Float32(centerPosition),\n          ),\n        );\n\n        component.flipHorizontallyAroundCenter();\n        // Same position after one horizontal flip.\n        expect(\n          component.center,\n          closeToVector(\n            centerPosition,\n            toleranceVector2Float32(centerPosition),\n          ),\n        );\n\n        component.flipHorizontallyAroundCenter();\n        // Same position after flipping back the horizontal flip.\n        expect(\n          component.center,\n          closeToVector(\n            centerPosition,\n            toleranceVector2Float32(centerPosition),\n          ),\n        );\n\n        component.flipVerticallyAroundCenter();\n        component.flipHorizontallyAroundCenter();\n        // Same position after flipping both vertically and horizontally.\n        expect(\n          component.center,\n          closeToVector(\n            centerPosition,\n            toleranceVector2Float32(centerPosition),\n          ),\n        );\n\n        component.flipVerticallyAroundCenter();\n        component.flipHorizontallyAroundCenter();\n        // Same position after flipping back both vertically and horizontally.\n        expect(\n          component.center,\n          closeToVector(\n            centerPosition,\n            toleranceVector2Float32(centerPosition),\n          ),\n        );\n\n        component.flipHorizontallyAroundCenter();\n        component.flipVerticallyAroundCenter();\n        // Same position after flipping both horizontally and vertically.\n        expect(\n          component.center,\n          closeToVector(\n            centerPosition,\n            toleranceVector2Float32(centerPosition),\n          ),\n        );\n\n        component.flipVerticallyAroundCenter();\n        component.flipHorizontallyAroundCenter();\n        // Same position after flipping back both horizontally and vertically in\n        // the reverse order.\n        expect(\n          component.center,\n          closeToVector(\n            centerPosition,\n            toleranceVector2Float32(centerPosition),\n          ),\n        );\n      });\n\n      test('isHorizontallyFlipped', () {\n        final component = PositionComponent()\n          ..size = Vector2(10, 10)\n          ..position = Vector2(50, 20)\n          ..anchor = const Anchor(0.6, 0.8);\n\n        expect(component.isFlippedHorizontally, isFalse);\n        component.flipHorizontally();\n        expect(component.isFlippedHorizontally, isTrue);\n        component.flipHorizontally();\n        expect(component.isFlippedHorizontally, isFalse);\n        component.flipHorizontallyAroundCenter();\n        expect(component.isFlippedHorizontally, isTrue);\n        component.flipHorizontallyAroundCenter();\n        expect(component.isFlippedHorizontally, isFalse);\n      });\n\n      test('isVerticallyFlipped', () {\n        final component = PositionComponent()\n          ..size = Vector2(10, 10)\n          ..position = Vector2(50, 20)\n          ..anchor = const Anchor(0.6, 0.8);\n\n        expect(component.isFlippedVertically, isFalse);\n        component.flipVertically();\n        expect(component.isFlippedVertically, isTrue);\n        component.flipVertically();\n        expect(component.isFlippedVertically, isFalse);\n        component.flipVerticallyAroundCenter();\n        expect(component.isFlippedVertically, isTrue);\n        component.flipVerticallyAroundCenter();\n        expect(component.isFlippedVertically, isFalse);\n      });\n\n      test('rotations', () {\n        final component = PositionComponent()\n          ..size = Vector2(8, 6)\n          ..position = Vector2(50, 20)\n          ..anchor = Anchor.center;\n\n        // Rotate the component in small increments counterclockwise\n        // and track the coordinate of its top-right corner\n        for (var i = 0; i < 30; i++) {\n          component.angle = -i / 10;\n          final cosA = cos(i / 10);\n          final sinA = sin(i / 10);\n          // The component's diagonal is 10, so it's radius (distance from the\n          // center to any vertex) is 5. If we denote φ the angle that the\n          // diagonal makes with a bimedian, then the cosine of that angle is\n          // 0.8 and the sine is 0.6. Thus, when the whole rectangle is rotated\n          // by angle A = i/10, the coordinates of any vertex can be computed\n          // from the trigonometric formulas for `sin(φ + A)` and `cos(φ + A)`,\n          // scaled by the radius of the rectangle.\n          final expectedX = 50 + 5 * (0.8 * cosA - 0.6 * sinA);\n          final expectedY = 20 - 5 * (0.6 * cosA + 0.8 * sinA);\n          final topRight = component.positionOf(Vector2(8, 0));\n          expect(topRight.x, closeTo(expectedX, toleranceFloat32(expectedX)));\n          expect(topRight.y, closeTo(expectedY, toleranceFloat32(expectedY)));\n        }\n      });\n\n      testRandom('random local<->global', (Random rnd) {\n        final parent = PositionComponent()..size = Vector2(50, 25);\n        final child = PositionComponent()\n          ..size = Vector2(10, 8)\n          ..position = Vector2(50, 20)\n          ..anchor = const Anchor(0.1, 0.2);\n        parent.add(child);\n\n        for (var i = 0; i < 100; i++) {\n          child.angle = (rnd.nextDouble() - 0.5) * 10;\n          child.x = rnd.nextDouble() * 100;\n          child.y = rnd.nextDouble() * 70 + 30;\n          parent.angle = (rnd.nextDouble() - 0.5) * 5;\n          parent.x = rnd.nextDouble() * 300;\n          parent.y = rnd.nextDouble() * 1000;\n          if (rnd.nextBool()) {\n            child.flipHorizontally();\n          }\n          if (rnd.nextBool()) {\n            child.flipVertically();\n          }\n          if (rnd.nextDouble() < 0.1) {\n            parent.flipHorizontally();\n          }\n\n          final globalX = (rnd.nextDouble() - 0.3) * 200;\n          final globalY = (rnd.nextDouble() - 0.1) * 200;\n          final localPoint = child.absoluteToLocal(Vector2(globalX, globalY));\n          final globalPoint = child.absolutePositionOf(localPoint);\n          expect(\n            globalPoint,\n            closeToVector(\n              Vector2(globalX, globalY),\n              toleranceVector2Float32(Vector2(globalX, globalY)) +\n                  toleranceVector2Float32(parent.position) +\n                  toleranceVector2Float32(globalPoint) +\n                  toleranceVector2Float32(localPoint),\n            ),\n          );\n        }\n      });\n\n      testRandom('transform matrix', (Random rnd) {\n        final component = PositionComponent()\n          ..size = Vector2(5, 10)\n          ..anchor = Anchor.center;\n\n        for (var i = 0; i < 10; i++) {\n          final x = rnd.nextDouble() * 100;\n          final y = rnd.nextDouble() * 70 + 30;\n          final angle = (rnd.nextDouble() - 0.5) * 10;\n          component.x = x;\n          component.y = y;\n          component.angle = angle;\n\n          final transform = Matrix4.identity()\n            ..translateByDouble(x, y, 0.0, 1.0)\n            ..rotateZ(angle)\n            ..translateByDouble(\n              -component.anchor.x * component.width,\n              -component.anchor.y * component.height,\n              0.0,\n              1.0,\n            );\n          for (var j = 0; j < 16; j++) {\n            expect(component.transformMatrix[j], closeTo(transform[j], 1e-13));\n          }\n        }\n      });\n\n      test('change anchor', () {\n        final component = PositionComponent()\n          ..size = Vector2(10, 10)\n          ..position = Vector2(100, 100)\n          ..anchor = Anchor.center;\n\n        expect(component.toLocal(Vector2(100, 100)), Vector2(5, 5));\n        component.anchor = Anchor.topLeft;\n        expect(component.toLocal(Vector2(100, 100)), Vector2(0, 0));\n        component.anchor = Anchor.topRight;\n        expect(component.toLocal(Vector2(100, 100)), Vector2(10, 0));\n        component.anchor = Anchor.bottomLeft;\n        expect(component.toLocal(Vector2(100, 100)), Vector2(0, 10));\n        component.anchor = Anchor.bottomRight;\n        expect(component.toLocal(Vector2(100, 100)), Vector2(10, 10));\n        component.anchor = const Anchor(0.1, 0.2);\n        expect(component.toLocal(Vector2(100, 100)), Vector2(1, 2));\n      });\n\n      test('distance', () {\n        final parent = PositionComponent(size: Vector2.all(100));\n        final comp1 = PositionComponent(position: Vector2(10, 20));\n        final comp2 = PositionComponent(position: Vector2(40, 60));\n        parent.add(comp1);\n        parent.add(comp2);\n\n        // The distance is the same in both directions\n        expect(comp1.distance(comp2), 50);\n        expect(comp2.distance(comp1), 50);\n\n        // Rotations/rescaling should not affect the distance\n        comp1.angle = 1;\n        comp2.angle = -0.5;\n        comp1.scale = Vector2(2, 1.1);\n        comp2.flipVertically();\n        expect(comp1.distance(comp2), 50);\n\n        // The distance is not affected by parent's rescaling\n        parent.scale = Vector2.all(10);\n        expect(comp1.distance(comp2), 50);\n      });\n\n      testWithFlameGame('deep nested', (game) async {\n        final c1 = PositionComponent()..position = Vector2(10, 20);\n        final c2 = Component();\n        final c3 = PositionComponent()..position = Vector2(-1, -1);\n        final c4 = Component();\n        final c5 = PositionComponent()..position = Vector2(5, 0);\n        c1.add(c2);\n        c2.add(c3);\n        c3.add(c4);\n        c4.add(c5);\n        game.add(c1);\n        await game.ready();\n        // Verify that the absolute coordinate is computed correctly even\n        // if the component is part of a nested tree where not all of\n        // the components are [PositionComponent]s.\n        expect(c5.absoluteToLocal(Vector2(14, 19)), Vector2.zero());\n      });\n\n      testWithFlameGame('auxiliary getters/setters', (game) async {\n        final parent = PositionComponent(position: Vector2(12, 19));\n        final child = PositionComponent(\n          position: Vector2(11, -1),\n          size: Vector2(4, 6),\n        );\n        parent.add(child);\n        game.add(parent);\n        await game.ready();\n\n        expect(child.anchor, Anchor.topLeft);\n        expect(child.topLeftPosition, Vector2(11, -1));\n        expect(child.absoluteTopLeftPosition, Vector2(23, 18));\n        expect(child.center, Vector2(13, 2));\n        expect(child.absoluteCenter, Vector2(25, 21));\n        expect(child.position, Vector2(11, -1));\n        expect(child.absolutePosition, Vector2(23, 18));\n\n        child.center = Vector2(5, 5);\n        expect(child.center, Vector2(5, 5));\n        expect(child.position, Vector2(3, 2));\n        expect(child.absolutePosition, Vector2(15, 21));\n      });\n\n      test('lookAt', () {\n        final component = PositionComponent();\n\n        final targets = [\n          Vector2(0, 1),\n          Vector2.all(2),\n          Vector2(-1, 0),\n          Vector2.all(-50),\n        ];\n        final expectedAngles = [pi, (3 * pi / 4), (-pi / 2), (-pi / 4)];\n\n        for (var i = 0; i < targets.length; ++i) {\n          final target = targets.elementAt(i);\n          final angle = expectedAngles.elementAt(i);\n\n          final result = component.angleTo(target);\n          expectDouble(\n            result,\n            (angle - component.angle).toNormalizedAngle(),\n            epsilon: 1e-10,\n            reason: 'angleTo $i ($angle)',\n          );\n\n          component.lookAt(target);\n          expectDouble(\n            component.angle,\n            angle.toNormalizedAngle(),\n            epsilon: 1e-10,\n            reason: 'lookAt $i ($angle)',\n          );\n        }\n      });\n\n      test('lookAt with native angle', () {\n        final component = PositionComponent(nativeAngle: pi / 2);\n\n        final targets = [\n          Vector2(0, 1),\n          Vector2.all(2),\n          Vector2(-1, 0),\n          Vector2.all(-50),\n        ];\n        final expectedAngles = [pi / 2, pi / 4, pi, -3 * pi / 4];\n\n        for (var i = 0; i < targets.length; ++i) {\n          final target = targets.elementAt(i);\n          final angle = expectedAngles.elementAt(i);\n\n          expectDouble(\n            component.angleTo(target),\n            (angle - component.angle).toNormalizedAngle(),\n            epsilon: 1e-10,\n            reason: 'angleTo $i ($angle)',\n          );\n\n          component.lookAt(target);\n          expectDouble(\n            component.angle,\n            angle.toNormalizedAngle(),\n            epsilon: 1e-10,\n            reason: 'lookAt $i ($angle)',\n          );\n        }\n      });\n\n      test('lookAt with nested components', () {\n        late PositionComponent component;\n\n        PositionComponent(\n          angle: pi / 2,\n          children: [\n            PositionComponent(\n              angle: pi / 2,\n              children: [\n                component = PositionComponent(\n                  nativeAngle: -pi,\n                ),\n              ],\n            ),\n          ],\n        );\n\n        final targets = [\n          Vector2(0, 1),\n          Vector2.all(2),\n          Vector2(-1, 0),\n          Vector2.all(-50),\n        ];\n        final expectedAngles = [pi, (3 * pi / 4), -pi / 2, (-pi / 4)];\n\n        for (var i = 0; i < targets.length; ++i) {\n          final target = targets.elementAt(i);\n          final angle = expectedAngles.elementAt(i);\n\n          expectDouble(\n            component.angleTo(target),\n            (angle - component.angle).toNormalizedAngle(),\n            epsilon: 1e-10,\n            reason: 'angleTo $i ($angle)',\n          );\n\n          component.lookAt(target);\n          expectDouble(\n            component.angle,\n            angle.toNormalizedAngle(),\n            epsilon: 1e-10,\n            reason: 'lookAt $i ($angle)',\n          );\n        }\n      });\n\n      test('lookAt corner cases', () {\n        final component = PositionComponent(position: Vector2(-20, 50));\n        component.lookAt(component.absolutePosition);\n        expectDouble(component.angle, 0, epsilon: 1e-10);\n\n        component.nativeAngle = 3 * pi / 2;\n        component.lookAt(component.absolutePosition);\n        expectDouble(\n          component.angle,\n          -component.nativeAngle.toNormalizedAngle(),\n          epsilon: 1e-10,\n        );\n      });\n\n      test('lookAt with parental flips', () {\n        final child = PositionComponent();\n        final wrapper = PositionComponent(\n          children: [child],\n        );\n\n        final flips = [\n          (Vector2(1, 1), Vector2(1, 1)),\n          (Vector2(1, 1), Vector2(1, -1)),\n          (Vector2(1, 1), Vector2(-1, 1)),\n          (Vector2(1, 1), Vector2(-1, -1)),\n          (Vector2(1, -1), Vector2(1, 1)),\n          (Vector2(1, -1), Vector2(1, -1)),\n          (Vector2(1, -1), Vector2(-1, 1)),\n          (Vector2(1, -1), Vector2(-1, -1)),\n          (Vector2(-1, 1), Vector2(1, 1)),\n          (Vector2(-1, 1), Vector2(1, -1)),\n          (Vector2(-1, 1), Vector2(-1, 1)),\n          (Vector2(-1, 1), Vector2(-1, -1)),\n          (Vector2(-1, -1), Vector2(1, 1)),\n          (Vector2(-1, -1), Vector2(1, -1)),\n          (Vector2(-1, -1), Vector2(-1, 1)),\n          (Vector2(-1, -1), Vector2(-1, -1)),\n        ];\n\n        final notableAngles = List.generate(8, (i) => i * tau / 8);\n        final expectedResults = [\n          0, 1, 2, 3, 4, -3, -2, -1, //\n          -4, -3, -2, -1, 0, 1, 2, 3, //\n          0, 1, 2, 3, 4, -3, -2, -1, //\n          4, -3, -2, -1, 0, 1, 2, 3, //\n          -4, 3, 2, 1, 0, -1, -2, -3, //\n          0, -1, -2, -3, -4, 3, 2, 1, //\n          -4, 3, 2, 1, 0, -1, -2, -3, //\n          0, -1, -2, -3, 4, 3, 2, 1, //\n          0, -1, -2, -3, -4, 3, 2, 1, //\n          4, 3, 2, 1, 0, -1, -2, -3, //\n          0, -1, -2, -3, -4, 3, 2, 1, //\n          4, 3, 2, 1, 0, -1, -2, -3, //\n          4, -3, -2, -1, 0, 1, 2, 3, //\n          0, 1, 2, 3, 4, -3, -2, -1, //\n          4, -3, -2, -1, 0, 1, 2, 3, //\n          0, 1, 2, 3, -4, -3, -2, -1, //\n          0, 1, 2, 3, 4, -3, -2, -1, //\n          -4, -3, -2, -1, 0, 1, 2, 3, //\n          0, 1, 2, 3, 4, -3, -2, -1, //\n          -4, -3, -2, -1, 0, 1, 2, 3, //\n          -4, 3, 2, 1, 0, -1, -2, -3, //\n          0, -1, -2, -3, 4, 3, 2, 1, //\n          -4, 3, 2, 1, 0, -1, -2, -3, //\n          0, -1, -2, -3, 4, 3, 2, 1, //\n          0, -1, -2, -3, 4, 3, 2, 1, //\n          -4, 3, 2, 1, 0, -1, -2, -3, //\n          0, -1, -2, -3, 4, 3, 2, 1, //\n          -4, 3, 2, 1, 0, -1, -2, -3, //\n          -4, -3, -2, -1, 0, 1, 2, 3, //\n          0, 1, 2, 3, 4, -3, -2, -1, //\n          -4, -3, -2, -1, 0, 1, 2, 3, //\n          0, 1, 2, 3, 4, -3, -2, -1, //\n        ];\n        var index = 0;\n        for (final flip in flips) {\n          wrapper.scale = flip.$1;\n          child.scale = flip.$2;\n\n          for (final angle in notableAngles) {\n            final target = Vector2(0, -1)..rotate(angle);\n            expectDouble(\n              child.angleTo(target),\n              expectedResults[index++] * tau / 8,\n              epsilon: 1e-10,\n              reason: 'angleTo with flip $flip, angle $angle, target $target',\n            );\n          }\n        }\n      });\n    });\n\n    group('Rendering', () {\n      test('render in debug mode', () {\n        final component = _MyDebugComponent()\n          ..position = Vector2(23, 17)\n          ..size = Vector2.all(10);\n        final canvas = MockCanvas();\n        component.renderTree(canvas);\n        expect(\n          canvas,\n          MockCanvas()\n            ..translate(23, 17)\n            ..drawRect(const Rect.fromLTWH(0, 0, 10, 10))\n            ..drawLine(const Offset(0, -2), const Offset(0, 2))\n            ..drawLine(const Offset(-2, 0), const Offset(2, 0))\n            ..drawParagraph(null, const Offset(-30, -15))\n            ..drawParagraph(null, const Offset(-20, 10))\n            ..translate(0, 0), // canvas.restore\n        );\n      });\n\n      test('render without coordinates', () {\n        final component = _MyDebugComponent()\n          ..position = Vector2(23, 17)\n          ..size = Vector2.all(10)\n          ..anchor = Anchor.center\n          ..debugCoordinatesPrecision = null;\n        final canvas = MockCanvas();\n        component.renderTree(canvas);\n        expect(\n          canvas,\n          MockCanvas()\n            ..translate(18, 12)\n            ..drawRect(const Rect.fromLTWH(0, 0, 10, 10))\n            ..drawLine(const Offset(5, 3), const Offset(5, 7))\n            ..drawLine(const Offset(3, 5), const Offset(7, 5))\n            ..translate(0, 0), // canvas.restore\n        );\n      });\n\n      test('render without coordinates and then render with coordinates', () {\n        final component = _MyDebugComponent()\n          ..position = Vector2(23, 17)\n          ..size = Vector2.all(10)\n          ..anchor = Anchor.center\n          ..debugCoordinatesPrecision = null;\n        final withoutCoordinatesCanvas = MockCanvas();\n        component.renderTree(withoutCoordinatesCanvas);\n        expect(\n          withoutCoordinatesCanvas,\n          MockCanvas()\n            ..translate(18, 12)\n            ..drawRect(const Rect.fromLTWH(0, 0, 10, 10))\n            ..drawLine(const Offset(5, 3), const Offset(5, 7))\n            ..drawLine(const Offset(3, 5), const Offset(7, 5))\n            ..translate(0, 0), // canvas.restore\n        );\n\n        component.debugCoordinatesPrecision = 0;\n        final withCoordinatesCanvas = MockCanvas();\n        component.renderTree(withCoordinatesCanvas);\n        expect(\n          withCoordinatesCanvas,\n          MockCanvas()\n            ..translate(18, 12)\n            ..drawRect(const Rect.fromLTWH(0, 0, 10, 10))\n            ..drawLine(const Offset(5, 3), const Offset(5, 7))\n            ..drawLine(const Offset(3, 5), const Offset(7, 5))\n            ..drawParagraph(null, const Offset(-30, -15))\n            ..drawParagraph(null, const Offset(-20, 10))\n            ..translate(0, 0), // canvas.restore\n        );\n      });\n    });\n\n    group('Bounding rectangle', () {\n      test('Scale/flip', () {\n        final component = PositionComponent(\n          position: Vector2(5, 5),\n          size: Vector2(4, 2),\n          anchor: Anchor.center,\n        );\n        expect(component.toRect(), const Rect.fromLTWH(3, 4, 4, 2));\n        component.scale = Vector2(0.5, 1);\n        expect(component.toRect(), const Rect.fromLTWH(4, 4, 2, 2));\n        component.flipHorizontally();\n        expect(component.toRect(), const Rect.fromLTWH(4, 4, 2, 2));\n        component.flipVertically();\n        expect(component.toRect(), const Rect.fromLTWH(4, 4, 2, 2));\n      });\n\n      test('flip with non-central anchor', () {\n        final component = PositionComponent(\n          position: Vector2(0, 0),\n          size: Vector2(4, 2),\n        );\n        expect(component.toRect(), const Rect.fromLTWH(0, 0, 4, 2));\n        component.flipHorizontally();\n        expect(component.toRect(), const Rect.fromLTWH(-4, 0, 4, 2));\n        component.flipVertically();\n        expect(component.toRect(), const Rect.fromLTWH(-4, -2, 4, 2));\n      });\n\n      test('rotated component', () {\n        const w = 5.0;\n        const h = 2.0;\n        final component = PositionComponent(size: Vector2(w, h));\n        for (var i = 0; i < 10; i++) {\n          final a = (i / 10) * tau / 4;\n          component.angle = a;\n          final componentRect = component.toRect();\n          expect(\n            componentRect.left,\n            closeTo(\n              -h * sin(a),\n              toleranceFloat32(componentRect.left),\n            ),\n          );\n          expect(\n            componentRect.top,\n            closeTo(\n              0,\n              toleranceFloat32(componentRect.top),\n            ),\n          );\n          expect(\n            componentRect.right,\n            closeTo(\n              w * cos(a),\n              toleranceFloat32(componentRect.right),\n            ),\n          );\n          expect(\n            componentRect.bottom,\n            closeTo(\n              w * sin(a) + h * cos(a),\n              toleranceFloat32(componentRect.bottom),\n            ),\n          );\n        }\n      });\n\n      testWithFlameGame('absolute toRect', (game) async {\n        final parent = PositionComponent(\n          position: Vector2(10, 10),\n          size: Vector2(6, 6),\n        );\n        final child = PositionComponent(\n          position: Vector2(-3, 3),\n          size: Vector2(1, 1),\n        );\n        parent.add(child);\n        game.add(parent);\n        await game.ready();\n        expect(child.toRect(), const Rect.fromLTWH(-3, 3, 1, 1));\n        expect(child.toAbsoluteRect(), const Rect.fromLTWH(7, 13, 1, 1));\n      });\n    });\n\n    group('absoluteAngle', () {\n      test('absoluteAngle with no parent', () {\n        final component = PositionComponent();\n        expect(component.absoluteAngle, 0.0);\n        component.angle = pi / 2;\n        expect(component.absoluteAngle, pi / 2);\n      });\n\n      testWithFlameGame('absoluteAngle with parent', (game) async {\n        final parent = PositionComponent()..angle = pi / 4;\n        final child = PositionComponent();\n        parent.add(child);\n        game.add(parent);\n        await game.ready();\n\n        expect(child.absoluteAngle, pi / 4);\n        child.angle = pi / 2;\n        expect(child.absoluteAngle, 3 * pi / 4);\n      });\n\n      testWithFlameGame('absoluteAngle with parent and child rotated', (\n        game,\n      ) async {\n        final parent = PositionComponent()..angle = pi / 8;\n        final child = PositionComponent()..angle = pi / 8;\n        parent.add(child);\n        game.add(parent);\n        await game.ready();\n\n        expect(child.absoluteAngle, pi / 4);\n        parent.angle = pi / 4;\n        child.angle = pi / 2;\n        expect(child.absoluteAngle, 3 * pi / 4);\n      });\n\n      testWithFlameGame('absoluteAngle with flipped parent', (game) async {\n        final parent = _MyDebugComponent(name: 'parent')..angle = pi / 4;\n        final child = _MyDebugComponent(name: 'child');\n        parent.add(child);\n        game.add(parent);\n        await game.ready();\n\n        expect(child.absoluteAngle, pi / 4);\n        parent.flipHorizontally();\n        expect(child.absoluteAngle, -pi / 4);\n        parent.flipVertically();\n        expect(child.absoluteAngle, (pi / 4 + pi).toNormalizedAngle());\n      });\n\n      testWithFlameGame('absoluteAngle with flipped child', (game) async {\n        final parent = PositionComponent();\n        final child = PositionComponent()..angle = pi / 4;\n        parent.add(child);\n        game.add(parent);\n        await game.ready();\n\n        expect(child.absoluteAngle, pi / 4);\n        child.flipHorizontally();\n        expect(child.absoluteAngle, -pi / 4);\n        child.flipVertically();\n        expect(child.absoluteAngle, (pi / 4 + pi).toNormalizedAngle());\n      });\n\n      testWithFlameGame('absoluteAngle with flipped child and parent', (\n        game,\n      ) async {\n        final parent = PositionComponent()..angle = pi / 8;\n        final child = PositionComponent()..angle = pi / 8;\n        parent.add(child);\n        game.add(parent);\n        await game.ready();\n\n        expect(child.absoluteAngle, pi / 4);\n        child.flipHorizontally();\n        expect(child.absoluteAngle, -pi / 4);\n        parent.flipVertically();\n        expect(child.absoluteAngle, (pi / 4 + pi).toNormalizedAngle());\n      });\n    });\n  });\n}\n\nclass _MyHitboxComponent extends PositionComponent with GestureHitboxes {}\n\nclass _MyDebugComponent extends PositionComponent {\n  _MyDebugComponent({this.name});\n\n  final String? name;\n\n  @override\n  bool get debugMode => true;\n\n  @override\n  String toString() {\n    return name != null\n        ? '$name(angle: $angle, scale: $scale)'\n        : super.toString();\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/components/post_process_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/post_process.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nFuture<void> main() async {\n  group('PostProcessComponent', () {\n    testWithFlameGame('renders post process with explicit size', (game) async {\n      final postProcess = PostProcessComponent(\n        postProcess: PostProcessGroup(postProcesses: []),\n        size: Vector2.all(100),\n      );\n      await game.ensureAdd(postProcess);\n\n      expect(game.children, contains(postProcess));\n      expect(postProcess.size, Vector2.all(100));\n    });\n\n    testWithFlameGame(\n      'renders post process with the bounding box of the children',\n      (game) async {\n        final postProcess = PostProcessComponent(\n          postProcess: PostProcessGroup(postProcesses: []),\n          children: [\n            PositionComponent(size: Vector2.all(50)),\n            PositionComponent(\n              position: Vector2.all(100),\n              size: Vector2.all(50),\n            ),\n          ],\n        );\n        await game.ensureAdd(postProcess);\n\n        expect(game.children, contains(postProcess));\n        expect(postProcess.size, Vector2.all(150));\n      },\n    );\n\n    testWithFlameGame('changes size when children change', (game) async {\n      final componentA = PositionComponent(size: Vector2.all(50));\n      final componentB = PositionComponent(\n        position: Vector2.all(100),\n        size: Vector2.all(50),\n      );\n      final postProcess = PostProcessComponent(\n        postProcess: PostProcessGroup(postProcesses: []),\n        children: [componentA, componentB],\n      );\n      await game.ensureAdd(postProcess);\n\n      expect(game.children, contains(postProcess));\n      expect(postProcess.size, Vector2.all(150));\n      componentB.removeFromParent();\n      game.update(0);\n      expect(postProcess.size, Vector2.all(50));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/priority_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:ordered_set/mapping_ordered_set.dart';\nimport 'package:ordered_set/ordered_set.dart';\nimport 'package:test/test.dart';\n\nimport '../custom_component.dart';\n\nvoid main() {\n  void componentsSorted(Iterable<Component> components) {\n    final priorities = components.map<int>((c) => c.priority).toList();\n    expect(priorities.toList(), orderedEquals(priorities..sort()));\n  }\n\n  group('priority test', () {\n    testWithFlameGame(\n      'components with different priorities are sorted in the list',\n      (game) async {\n        final priorityComponents = List.generate(10, _PriorityComponent.new);\n        priorityComponents.shuffle();\n        await game.ensureAddAll(priorityComponents);\n        componentsSorted(game.children);\n      },\n    );\n\n    testWithFlameGame(\n      'changing priority with the priority setter should reorder the list',\n      (game) async {\n        final firstComponent = _PriorityComponent(-1);\n        final priorityComponents = List.generate(10, _PriorityComponent.new)\n          ..add(firstComponent);\n        priorityComponents.shuffle();\n        final components = game.world.children;\n        await game.world.ensureAddAll(priorityComponents);\n        componentsSorted(components);\n        expect(components.first, firstComponent);\n        firstComponent.priority = 11;\n        game.update(0);\n        expect(components.last, firstComponent);\n      },\n    );\n\n    testWithFlameGame(\n      'changing priorities should reorder component list',\n      (game) async {\n        final priorityComponents = List.generate(10, _PriorityComponent.new);\n        priorityComponents.shuffle();\n        final components = game.children;\n        await game.ensureAddAll(priorityComponents);\n        componentsSorted(components);\n        final first = components.first;\n        final last = components.last;\n        first.priority = 20;\n        last.priority = -1;\n        expect(components.first, first);\n        expect(components.last, last);\n        game.update(0);\n        expect(components.first, last);\n        expect(components.last, first);\n      },\n    );\n\n    testWithFlameGame(\n      'changing child priority should reorder component list',\n      (game) async {\n        final parentComponent = _PriorityComponent(0);\n        final priorityComponents = List.generate(10, _PriorityComponent.new);\n        priorityComponents.shuffle();\n        await game.ensureAdd(parentComponent);\n        await parentComponent.ensureAddAll(priorityComponents);\n        final children = parentComponent.children;\n        componentsSorted(children);\n        final first = children.first;\n        first.priority = 20;\n        expect(children.last, isNot(first));\n        game.update(0);\n        expect(children.last, first);\n      },\n    );\n\n    testWithFlameGame(\n      'changing child priorities should reorder component list',\n      (game) async {\n        final parentComponent = _PriorityComponent(0);\n        final priorityComponents = List.generate(10, _PriorityComponent.new);\n        priorityComponents.shuffle();\n        await game.ensureAdd(parentComponent);\n        await parentComponent.ensureAddAll(priorityComponents);\n        final children = parentComponent.children;\n        componentsSorted(children);\n        final first = children.first;\n        final last = children.last;\n        first.priority = 20;\n        last.priority = -1;\n        expect(children.first, first);\n        expect(children.last, last);\n        game.update(0);\n        expect(children.first, last);\n        expect(children.last, first);\n      },\n    );\n\n    testWithFlameGame(\n      'changing grand child priority should reorder component list',\n      (game) async {\n        final grandParentComponent = _PriorityComponent(0);\n        final parentComponent = _PriorityComponent(0);\n        final priorityComponents = List.generate(10, _PriorityComponent.new);\n        priorityComponents.shuffle();\n        await game.ensureAdd(grandParentComponent);\n        await grandParentComponent.ensureAdd(parentComponent);\n        await parentComponent.ensureAddAll(priorityComponents);\n        final children = parentComponent.children;\n        componentsSorted(children);\n        final first = children.first;\n        first.priority = 20;\n        expect(children.last, isNot(first));\n        game.update(0);\n        expect(children.last, first);\n      },\n    );\n\n    testWithFlameGame(\n      '#reorderChildren is only called once per parent per tick',\n      (game) async {\n        final a = _ParentWithReorderSpy(1);\n        final a1 = _PriorityComponent(1);\n        final a2 = _PriorityComponent(2);\n        a.addAll([a1, a2]);\n\n        final b = _ParentWithReorderSpy(3);\n        final b1 = _PriorityComponent(1);\n        b.add(b1);\n\n        final c = _ParentWithReorderSpy(2);\n        final c1 = _PriorityComponent(1);\n        final c2 = _PriorityComponent(0);\n        final c3 = _PriorityComponent(-1);\n        c.addAll([c1, c2, c3]);\n\n        await game.ensureAddAll([a, b, c]);\n        componentsSorted(game.children);\n        componentsSorted(a.children);\n        componentsSorted(b.children);\n        componentsSorted(c.children);\n        a.assertCalled(0);\n        b.assertCalled(0);\n        c.assertCalled(0);\n\n        a.priority = 10;\n        game.update(0);\n        await game.ready();\n\n        componentsSorted(game.children);\n        componentsSorted(a.children);\n        componentsSorted(b.children);\n        componentsSorted(c.children);\n        a.assertCalled(0);\n        b.assertCalled(0);\n        c.assertCalled(0);\n\n        // Change priority multiple times on c and once on a (and zero on b).\n        c3.priority = 2;\n        c1.priority = 10;\n        a2.priority = 0;\n        game.update(0);\n        await game.ready();\n\n        a.assertCalled(1);\n        b.assertCalled(0);\n        c.assertCalled(1);\n\n        componentsSorted(game.children);\n        componentsSorted(a.children);\n        componentsSorted(b.children);\n        componentsSorted(c.children);\n\n        // Change of b now.\n        b1.priority = 2;\n        a1.priority = 1; // no-op!\n        game.update(0);\n        await game.ready();\n\n        a.assertCalled(0);\n        b.assertCalled(1);\n        c.assertCalled(0);\n\n        componentsSorted(game.children);\n        componentsSorted(a.children);\n        componentsSorted(b.children);\n        componentsSorted(c.children);\n      },\n    );\n\n    testWithFlameGame('child can update priority of its parent', (game) async {\n      final renderEvents = <String>[];\n\n      final parent = CustomComponent(\n        priority: 0,\n        onRender: (self, canvas) {\n          renderEvents.add('render:parent');\n        },\n      );\n      final child = CustomComponent(\n        onUpdate: (self, dt) {\n          self.parent!.priority = 10;\n        },\n      );\n      parent.add(child);\n      game.add(parent);\n      game.add(\n        CustomComponent(\n          priority: 1,\n          onRender: (self, canvas) {\n            renderEvents.add('render:another');\n          },\n        ),\n      );\n      await game.ready();\n\n      expect(parent.priority, 0);\n      expect(child.priority, 0);\n\n      game.update(0.1);\n      await game.ready();\n\n      expect(parent.priority, 10);\n      expect(child.priority, 0);\n      expect(renderEvents, isEmpty);\n      game.render(Canvas(PictureRecorder()));\n      expect(renderEvents, ['render:another', 'render:parent']);\n    });\n  });\n}\n\nclass _SpyComponentSet extends MappingOrderedSet<num, Component> {\n  int callCount = 0;\n\n  _SpyComponentSet() : super((e) => e.priority);\n\n  @override\n  void rebalanceAll() {\n    callCount++;\n    super.rebalanceAll();\n  }\n}\n\nclass _PriorityComponent extends Component {\n  _PriorityComponent(int priority) : super(priority: priority);\n}\n\nclass _ParentWithReorderSpy extends Component {\n  _ParentWithReorderSpy(int priority) : super(priority: priority);\n\n  @override\n  OrderedSet<Component> createComponentSet() => _SpyComponentSet();\n\n  void assertCalled(int n) {\n    final componentSet = children as _SpyComponentSet;\n    expect(componentSet.callCount, n);\n    componentSet.callCount = 0;\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/components/rectangle_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('RectangleComponent.size', () {\n    group(\n      'refreshes vertices',\n      () {\n        test(\n          'default constructed',\n          () {\n            final component = RectangleComponent(\n              position: Vector2.all(20),\n              size: Vector2(20, 40),\n            );\n\n            final size1 = [\n              Vector2(0, 0),\n              Vector2(0, 20),\n              Vector2(10, 20),\n              Vector2(10, 0),\n            ];\n            final size2 = [\n              Vector2(0, 0),\n              Vector2(0, 40),\n              Vector2(20, 40),\n              Vector2(20, 0),\n            ];\n\n            component.size /= 2;\n            expect(component.vertices, size1);\n\n            component.size *= 2;\n            expect(component.vertices, size2);\n          },\n        );\n\n        test(\n          'square',\n          () {\n            final component = RectangleComponent.square(\n              position: Vector2(60, 20),\n              size: 30,\n            );\n            final size1 = [\n              Vector2(0, 0),\n              Vector2(0, 15),\n              Vector2(15, 15),\n              Vector2(15, 0),\n            ];\n            final size2 = [\n              Vector2(0, 0),\n              Vector2(0, 30),\n              Vector2(30, 30),\n              Vector2(30, 0),\n            ];\n\n            component.size /= 2;\n            expect(component.vertices, size1);\n\n            component.size *= 2;\n            expect(component.vertices, size2);\n          },\n        );\n\n        test(\n          'fromRect',\n          () {\n            final component = RectangleComponent.fromRect(\n              const Rect.fromLTWH(110, 20, 50, 60),\n            );\n            final size1 = [\n              Vector2(0, 0),\n              Vector2(0, 30),\n              Vector2(25, 30),\n              Vector2(25, 0),\n            ];\n            final size2 = [\n              Vector2(0, 0),\n              Vector2(0, 60),\n              Vector2(50, 60),\n              Vector2(50, 0),\n            ];\n\n            component.size /= 2;\n            expect(component.vertices, size1);\n            component.size *= 2;\n            expect(component.vertices, size2);\n          },\n        );\n\n        test(\n          'relative',\n          () {\n            final component = RectangleComponent.relative(\n              Vector2.all(0.5),\n              parentSize: Vector2(180, 60),\n              position: Vector2(180, 100),\n            );\n            final size1 = [\n              Vector2(0, 0),\n              Vector2(0, 15),\n              Vector2(45, 15),\n              Vector2(45, 0),\n            ];\n            final size2 = [\n              Vector2(0, 0),\n              Vector2(0, 30),\n              Vector2(90, 30),\n              Vector2(90, 0),\n            ];\n\n            component.size /= 2;\n            expect(component.vertices, size1);\n            component.size *= 2;\n            expect(component.vertices, size2);\n          },\n        );\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/route_test.dart",
    "content": "import 'dart:async';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/rendering.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Route', () {\n    testWithFlameGame('Route without a builder', (game) async {\n      final router = RouterComponent(\n        initialRoute: 'start',\n        routes: {\n          'start': Route(Component.new),\n          'new': Route(null),\n        },\n      )..addToParent(game);\n      await game.ready();\n      expect(\n        () => router.pushNamed('new'),\n        failsAssert(\n          'Either provide `builder` in the constructor, or override the '\n          'build() method',\n        ),\n      );\n    });\n\n    testWithFlameGame('onPush and onPop methods', (game) async {\n      var onPushCalled = 0;\n      var onPopCalled = 0;\n      var buildCalled = 0;\n      Route? previousRoute;\n      final router = RouterComponent(\n        initialRoute: 'start',\n        routes: {\n          'start': Route(Component.new),\n          'new': _CustomRoute(\n            onPush: (self, prevRoute) {\n              onPushCalled++;\n              previousRoute = prevRoute;\n            },\n            onPop: (self, prevRoute) {\n              onPopCalled++;\n              previousRoute = prevRoute;\n            },\n            build: (self) {\n              buildCalled++;\n              return PositionComponent();\n            },\n          ),\n        },\n      )..addToParent(game);\n      await game.ready();\n\n      router.pushNamed('new');\n      expect(buildCalled, 1);\n      await game.ready();\n      expect(router.currentRoute.name, 'new');\n      expect(router.currentRoute.children.first, isA<PositionComponent>());\n      expect(onPushCalled, 1);\n      expect(onPopCalled, 0);\n      expect(previousRoute!.name, 'start');\n\n      previousRoute = null;\n      router.pop();\n      await game.ready();\n      expect(onPushCalled, 1);\n      expect(onPopCalled, 1);\n      expect(previousRoute!.name, 'start');\n      expect(router.currentRoute.name, 'start');\n    });\n\n    testWithFlameGame('maintainState true', (game) async {\n      var onPushCalled = 0;\n      var onPopCalled = 0;\n      var buildFirstCalled = 0;\n      Route? previousRoute;\n      final router = RouterComponent(\n        initialRoute: 'start',\n        routes: {\n          'start': Route(Component.new),\n          'first': _CustomRoute(\n            onPush: (self, prevRoute) {\n              onPushCalled++;\n              previousRoute = prevRoute;\n            },\n            onPop: (self, prevRoute) {\n              onPopCalled++;\n              previousRoute = prevRoute;\n            },\n            build: (self) {\n              buildFirstCalled++;\n              return PositionComponent();\n            },\n          ),\n          'second': _CustomRoute(\n            onPush: (self, prevRoute) {\n              onPushCalled++;\n              previousRoute = prevRoute;\n            },\n            onPop: (self, prevRoute) {\n              onPopCalled++;\n              previousRoute = prevRoute;\n            },\n            build: (self) {\n              return PositionComponent();\n            },\n          ),\n        },\n      )..addToParent(game);\n      await game.ready();\n\n      // Go to first route\n      router.pushNamed('first');\n      expect(buildFirstCalled, 1);\n      await game.ready();\n      expect(router.currentRoute.name, 'first');\n      expect(router.currentRoute.children.first, isA<PositionComponent>());\n      expect(onPushCalled, 1);\n      expect(onPopCalled, 0);\n      expect(previousRoute!.name, 'start');\n\n      previousRoute = null;\n      router.pop();\n      await game.ready();\n      expect(onPushCalled, 1);\n      expect(onPopCalled, 1);\n      expect(previousRoute!.name, 'start');\n      expect(router.currentRoute.name, 'start');\n\n      // Go to second route after first route is popped\n      router.pushNamed('second');\n      expect(buildFirstCalled, 1);\n      await game.ready();\n      expect(router.currentRoute.name, 'second');\n      expect(router.currentRoute.children.first, isA<PositionComponent>());\n      expect(onPushCalled, 2);\n      expect(onPopCalled, 1);\n      expect(previousRoute!.name, 'start');\n\n      previousRoute = null;\n      router.pop();\n      await game.ready();\n      expect(onPushCalled, 2);\n      expect(onPopCalled, 2);\n      expect(previousRoute!.name, 'start');\n      expect(router.currentRoute.name, 'start');\n\n      // Go to first route again after popping second route\n      router.pushNamed('first');\n      expect(buildFirstCalled, 1);\n      await game.ready();\n      expect(router.currentRoute.name, 'first');\n      expect(router.currentRoute.children.first, isA<PositionComponent>());\n      expect(onPushCalled, 3);\n      expect(onPopCalled, 2);\n      expect(previousRoute!.name, 'start');\n    });\n\n    testWithFlameGame('maintainState false', (game) async {\n      var onPushCalled = 0;\n      var onPopCalled = 0;\n      var buildFirstCalled = 0;\n      var buildSecondCalled = 0;\n      Route? previousRoute;\n      final router = RouterComponent(\n        initialRoute: 'start',\n        routes: {\n          'start': Route(Component.new),\n          'first': _CustomRoute(\n            maintainState: false,\n            onPush: (self, prevRoute) {\n              onPushCalled++;\n              previousRoute = prevRoute;\n            },\n            onPop: (self, prevRoute) {\n              onPopCalled++;\n              previousRoute = prevRoute;\n            },\n            build: (self) {\n              buildFirstCalled++;\n              return PositionComponent();\n            },\n          ),\n          'second': _CustomRoute(\n            onPush: (self, prevRoute) {\n              onPushCalled++;\n              previousRoute = prevRoute;\n            },\n            onPop: (self, prevRoute) {\n              onPopCalled++;\n              previousRoute = prevRoute;\n            },\n            build: (self) {\n              buildSecondCalled++;\n              return PositionComponent();\n            },\n          ),\n        },\n      )..addToParent(game);\n      await game.ready();\n\n      // Go to first route\n      router.pushNamed('first');\n      expect(buildFirstCalled, 1);\n      await game.ready();\n      expect(router.currentRoute.name, 'first');\n      expect(router.currentRoute.children.first, isA<PositionComponent>());\n      expect(onPushCalled, 1);\n      expect(onPopCalled, 0);\n      expect(buildFirstCalled, 1);\n      expect(previousRoute!.name, 'start');\n\n      previousRoute = null;\n      router.pop();\n      await game.ready();\n      expect(onPushCalled, 1);\n      expect(onPopCalled, 1);\n      expect(previousRoute!.name, 'start');\n      expect(router.currentRoute.name, 'start');\n\n      // Go to second route after first route is popped\n      router.pushNamed('second');\n      expect(buildFirstCalled, 1);\n      await game.ready();\n      expect(router.currentRoute.name, 'second');\n      expect(router.currentRoute.children.first, isA<PositionComponent>());\n      expect(onPushCalled, 2);\n      expect(onPopCalled, 1);\n      expect(buildSecondCalled, 1);\n      expect(previousRoute!.name, 'start');\n\n      previousRoute = null;\n      router.pop();\n      await game.ready();\n      expect(onPushCalled, 2);\n      expect(onPopCalled, 2);\n      expect(previousRoute!.name, 'start');\n      expect(router.currentRoute.name, 'start');\n\n      // Go to first route again after popping second route\n      // Expect the build method to be called again\n      router.pushNamed('first');\n      expect(buildFirstCalled, 2);\n      await game.ready();\n      expect(router.currentRoute.name, 'first');\n      expect(router.currentRoute.children.first, isA<PositionComponent>());\n      expect(onPushCalled, 3);\n      expect(onPopCalled, 2);\n      expect(previousRoute!.name, 'start');\n    });\n\n    testWithFlameGame('Stop and resume time', (game) async {\n      final router = RouterComponent(\n        initialRoute: 'start',\n        routes: {\n          'start': Route(_TimerComponent.new),\n          'pause': _CustomRoute(\n            builder: Component.new,\n            onPush: (self, route) => route?.stopTime(),\n            onPop: (self, route) => route.resumeTime(),\n          ),\n        },\n      )..addToParent(game);\n      await game.ready();\n\n      final timer = router.currentRoute.children.first as _TimerComponent;\n      expect(timer.elapsedTime, 0);\n\n      game.update(1);\n      expect(timer.elapsedTime, 1);\n\n      router.pushNamed('pause');\n      await game.ready();\n      expect(router.currentRoute.name, 'pause');\n      expect(router.previousRoute!.timeScale, 0);\n\n      game.update(10);\n      expect(timer.elapsedTime, 1);\n\n      router.previousRoute!.timeScale = 0.1;\n      game.update(10);\n      expect(timer.elapsedTime, 2);\n\n      router.pop();\n      await game.ready();\n      expect(router.currentRoute.name, 'start');\n      expect(router.currentRoute.timeScale, 1);\n\n      game.update(10);\n      expect(timer.elapsedTime, 12);\n    });\n\n    testGolden(\n      'Rendering of opaque routes',\n      (game, tester) async {\n        final router = RouterComponent(\n          initialRoute: 'initial',\n          routes: {\n            'initial': Route(\n              () => _ColoredComponent(\n                color: const Color(0xFFFF0000),\n                size: Vector2.all(100),\n              ),\n            ),\n            'green': Route(\n              () => _ColoredComponent(\n                color: const Color(0x8800FF00),\n                position: Vector2.all(10),\n                size: Vector2.all(80),\n              ),\n            ),\n          },\n        )..addToParent(game);\n        await game.ready();\n        router.pushNamed('green');\n      },\n      size: Vector2(100, 100),\n      goldenFile: '../_goldens/route_opaque.png',\n    );\n\n    testGolden(\n      'Rendering of transparent routes',\n      (game, tester) async {\n        final router = RouterComponent(\n          initialRoute: 'initial',\n          routes: {\n            'initial': Route(\n              () => _ColoredComponent(\n                color: const Color(0xFFFF0000),\n                size: Vector2.all(100),\n              ),\n            ),\n            'green': Route(\n              () => _ColoredComponent(\n                color: const Color(0x8800FF00),\n                position: Vector2.all(10),\n                size: Vector2.all(80),\n              ),\n              transparent: true,\n            ),\n          },\n        )..addToParent(game);\n        await game.ready();\n        router.pushNamed('green');\n      },\n      size: Vector2(100, 100),\n      goldenFile: '../_goldens/route_transparent.png',\n    );\n\n    testGolden(\n      'Rendering of transparent routes with decorators',\n      (game, tester) async {\n        final router = RouterComponent(\n          initialRoute: 'initial',\n          routes: {\n            'initial': Route(\n              () => _ColoredComponent(\n                color: const Color(0xFFFF0000),\n                size: Vector2.all(100),\n              ),\n            ),\n            'green': _CustomRoute(\n              builder: () => _ColoredComponent(\n                color: const Color(0x8800FF00),\n                position: Vector2.all(10),\n                size: Vector2.all(80),\n              ),\n              transparent: true,\n              onPush: (self, route) =>\n                  route!.addRenderEffect(PaintDecorator.grayscale()),\n              onPop: (self, route) => route.removeRenderEffect(),\n            ),\n          },\n        )..addToParent(game);\n        await game.ready();\n        router.pushNamed('green');\n      },\n      size: Vector2(100, 100),\n      goldenFile: '../_goldens/route_with_decorators.png',\n    );\n\n    testGolden(\n      'Rendering effect can be removed',\n      (game, tester) async {\n        final router = RouterComponent(\n          initialRoute: 'initial',\n          routes: {\n            'initial': Route(\n              () => _ColoredComponent(\n                color: const Color(0xFFFF0000),\n                size: Vector2.all(100),\n              ),\n            ),\n            'green': _CustomRoute(\n              builder: () => _ColoredComponent(\n                color: const Color(0x8800FF00),\n                position: Vector2.all(10),\n                size: Vector2.all(80),\n              ),\n              transparent: true,\n              onPush: (self, route) =>\n                  route!.addRenderEffect(PaintDecorator.grayscale()),\n              onPop: (self, route) => route.removeRenderEffect(),\n            ),\n          },\n        )..addToParent(game);\n        await game.ready();\n        router.pushNamed('green');\n        await game.ready();\n        router.pop();\n      },\n      size: Vector2(100, 100),\n      goldenFile: '../_goldens/route_decorator_removed.png',\n    );\n\n    testWithFlameGame('componentsAtPoint for opaque route', (game) async {\n      final initialComponent = PositionComponent(size: Vector2.all(100));\n      final newComponent = PositionComponent(size: Vector2.all(100));\n      final router = RouterComponent(\n        initialRoute: 'initial',\n        routes: {\n          'initial': Route(\n            () => initialComponent,\n          ),\n          'new': Route(\n            () => newComponent,\n          ),\n        },\n      )..addToParent(game);\n      await game.ready();\n\n      router.pushNamed('new');\n      await game.ready();\n      expect(\n        game.componentsAtPoint(Vector2(50, 50)).contains(newComponent),\n        isTrue,\n      );\n      expect(\n        game.componentsAtPoint(Vector2(50, 50)).contains(initialComponent),\n        isFalse,\n      );\n    });\n\n    testWithFlameGame(\n      'componentsAtPoint for transparent route',\n      (game) async {\n        final initialComponent = PositionComponent(size: Vector2.all(100));\n        final newComponent = PositionComponent(size: Vector2.all(100));\n        final router = RouterComponent(\n          initialRoute: 'initial',\n          routes: {\n            'initial': Route(\n              () => initialComponent,\n            ),\n            'new': Route(\n              () => newComponent,\n              transparent: true,\n            ),\n          },\n        )..addToParent(game);\n        await game.ready();\n\n        router.pushNamed('new');\n        await game.ready();\n        expect(\n          game.componentsAtPoint(Vector2(50, 50)).contains(newComponent),\n          isTrue,\n        );\n        expect(\n          game.componentsAtPoint(Vector2(50, 50)).contains(initialComponent),\n          isTrue,\n        );\n      },\n    );\n    testWithFlameGame('Route with loading', (game) async {\n      final loadingComponent = PositionComponent(size: Vector2.all(100));\n      final pageComponent = _HeavyComponent()..size = Vector2.all(100);\n      final router = RouterComponent(\n        initialRoute: 'new',\n        routes: {\n          'start': Route(\n            Component.new,\n          ),\n          'new': Route(\n            () {\n              return pageComponent;\n            },\n            loadingBuilder: () {\n              return loadingComponent;\n            },\n          ),\n        },\n      );\n      game.add(router);\n      await game.ready();\n      expect(\n        pageComponent.isMounted,\n        isFalse,\n      );\n      expect(loadingComponent.isMounted, isTrue);\n      pageComponent.completer.complete();\n      await game.ready();\n      expect(\n        pageComponent.isMounted,\n        isTrue,\n      );\n      expect(\n        loadingComponent.isRemoved,\n        isTrue,\n      );\n    });\n  });\n}\n\nclass _CustomRoute extends Route {\n  _CustomRoute({\n    Component Function()? builder,\n    super.transparent,\n    super.maintainState,\n    void Function(Route, Route?)? onPush,\n    void Function(Route, Route)? onPop,\n    Component Function(Route)? build,\n  }) : _onPush = onPush,\n       _onPop = onPop,\n       _build = build,\n       super(builder);\n\n  final void Function(Route, Route?)? _onPush;\n  final void Function(Route, Route)? _onPop;\n  final Component Function(Route)? _build;\n\n  @override\n  void onPush(Route? route) => _onPush?.call(this, route);\n\n  @override\n  void onPop(Route route) => _onPop?.call(this, route);\n\n  @override\n  Component build() => _build?.call(this) ?? super.build();\n}\n\nclass _TimerComponent extends Component {\n  double elapsedTime = 0;\n\n  @override\n  void update(double dt) {\n    elapsedTime += dt;\n  }\n}\n\nclass _ColoredComponent extends PositionComponent {\n  _ColoredComponent({\n    required Color color,\n    super.position,\n    super.size,\n  }) : _paint = Paint()..color = color;\n\n  final Paint _paint;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(size.toRect(), _paint);\n  }\n}\n\nclass _HeavyComponent extends PositionComponent {\n  Duration dummyTime = const Duration(seconds: 3);\n  Completer<void> completer = Completer();\n  @override\n  FutureOr<void> onLoad() async {\n    await completer.future;\n    return super.onLoad();\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/components/router_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/widgets.dart' hide Route, OverlayRoute;\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('RouterComponent', () {\n    testWithFlameGame('normal route pushing/popping', (game) async {\n      final router = RouterComponent(\n        routes: {\n          'A': Route(_ComponentA.new),\n          'B': Route(_ComponentB.new),\n          'C': Route(_ComponentC.new),\n        },\n        initialRoute: 'A',\n      );\n      game.add(router);\n      await game.ready();\n\n      expect(router.routes.length, 3);\n      expect(router.currentRoute.name, 'A');\n      expect(router.currentRoute.children.length, 1);\n      expect(router.currentRoute.children.first, isA<_ComponentA>());\n\n      router.pushNamed('B');\n      await game.ready();\n      expect(router.currentRoute.name, 'B');\n      expect(router.currentRoute.children.length, 1);\n      expect(router.currentRoute.children.first, isA<_ComponentB>());\n      expect(router.stack.length, 2);\n\n      router.pop();\n      await game.ready();\n      expect(router.currentRoute.name, 'A');\n      expect(router.stack.length, 1);\n\n      router.pushRoute(Route(_ComponentD.new), name: 'Dee');\n      await game.ready();\n      expect(router.routes.length, 4);\n      expect(router.currentRoute.name, 'Dee');\n      expect(router.currentRoute.children.length, 1);\n      expect(router.currentRoute.children.first, isA<_ComponentD>());\n      expect(router.stack.length, 2);\n\n      router.pushReplacementNamed('B');\n      await game.ready();\n      expect(router.routes.length, 4);\n      expect(router.currentRoute.name, 'B');\n      expect(router.currentRoute.children.length, 1);\n      expect(router.currentRoute.children.first, isA<_ComponentB>());\n\n      router.pushReplacement(Route(_ComponentE.new), name: 'E');\n      await game.ready();\n      expect(router.routes.length, 5);\n      expect(router.currentRoute.name, 'E');\n      expect(router.currentRoute.children.length, 1);\n      expect(router.currentRoute.children.first, isA<_ComponentE>());\n      expect(router.stack.length, 2);\n    });\n\n    testWithFlameGame('replacing with 2 routes', (game) async {\n      final router = RouterComponent(\n        routes: {\n          'A': Route(_ComponentA.new),\n          'B': Route(_ComponentB.new),\n        },\n        initialRoute: 'A',\n      );\n      game.add(router);\n      await game.ready();\n\n      expect(router.routes.length, 2);\n      expect(router.currentRoute.name, 'A');\n      expect(router.currentRoute.children.length, 1);\n      expect(router.currentRoute.children.first, isA<_ComponentA>());\n\n      router.pushReplacementNamed('B');\n      await game.ready();\n      expect(router.currentRoute.name, 'B');\n      expect(router.currentRoute.children.length, 1);\n      expect(router.currentRoute.children.first, isA<_ComponentB>());\n      expect(router.stack.length, 1);\n    });\n\n    testWithFlameGame(\n      'didPop/onPop/didPush/onPush was called correctly',\n      (game) async {\n        const initialRouteName = 'A';\n        final router = RouterComponent(\n          routes: {\n            'A': _TestRoute(_ComponentA.new),\n            'B': _TestRoute(_ComponentB.new),\n            'C': _TestRoute(_ComponentC.new),\n          },\n          initialRoute: initialRouteName,\n        );\n        game.add(router);\n        await game.ready();\n\n        expect(\n          router.routes.values.whereType<_TestRoute>().every(\n            (e) =>\n                e.onPopTimes == 0 &&\n                e.didPopTimes == 0 &&\n                (e.name == initialRouteName ||\n                    (e.onPushTimes == 0 && e.didPushTimes == 0)),\n          ),\n          isTrue,\n        );\n\n        final routeA = router.routes[initialRouteName]! as _TestRoute;\n        expect(routeA.onPushTimes, 1);\n        expect(routeA.didPushTimes, 1);\n        expect(routeA.lastDidPushPreviousRoute, isNull);\n        expect(routeA.lastOnPushPreviousRoute, isNull);\n        expect(routeA.lastDidPopNextRoute, isNull);\n        expect(routeA.lastOnPopNextRoute, isNull);\n\n        router.pushNamed('B');\n        await game.ready();\n\n        final routeB = router.routes['B']! as _TestRoute;\n        expect(routeB.onPopTimes, 0);\n        expect(routeB.didPopTimes, 0);\n        expect(routeB.onPushTimes, 1);\n        expect(routeB.didPushTimes, 1);\n\n        expect(routeA.lastDidPushPreviousRoute, isNull);\n        expect(routeA.lastOnPushPreviousRoute, isNull);\n        expect(routeA.lastDidPopNextRoute, isNull);\n        expect(routeA.lastOnPopNextRoute, isNull);\n\n        expect(routeB.lastDidPushPreviousRoute, routeA.name);\n        expect(routeB.lastOnPushPreviousRoute, routeA.name);\n        expect(routeB.lastDidPopNextRoute, isNull);\n        expect(routeB.lastOnPopNextRoute, isNull);\n\n        router.pop();\n        expect(routeB.onPopTimes, 1);\n        expect(routeB.didPopTimes, 1);\n        expect(routeB.onPushTimes, 1);\n        expect(routeB.didPushTimes, 1);\n\n        expect(routeB.lastDidPushPreviousRoute, routeA.name);\n        expect(routeB.lastOnPushPreviousRoute, routeA.name);\n        expect(routeB.lastDidPopNextRoute, routeA.name);\n        expect(routeB.lastOnPopNextRoute, routeA.name);\n\n        // Check that all other still hasn't been called.\n        expect(\n          router.routes.values\n              .whereType<_TestRoute>()\n              .where((e) => e.name != 'B')\n              .every(\n                (e) =>\n                    e.onPopTimes == 0 &&\n                    e.didPopTimes == 0 &&\n                    (e.name == initialRouteName ||\n                        (e.onPushTimes == 0 && e.didPushTimes == 0)),\n              ),\n          isTrue,\n        );\n        await game.ready();\n\n        final routeD = _TestRoute(_ComponentD.new);\n        router.pushReplacement(routeD, name: 'D');\n        expect(routeD.onPopTimes, 0);\n        expect(routeD.didPopTimes, 0);\n        expect(routeD.onPushTimes, 1);\n        expect(routeD.didPushTimes, 1);\n\n        expect(routeA.onPopTimes, 1);\n        expect(routeA.didPopTimes, 1);\n        expect(routeA.onPushTimes, 1);\n        expect(routeA.didPushTimes, 1);\n\n        expect(routeA.lastDidPushPreviousRoute, isNull);\n        expect(routeA.lastOnPushPreviousRoute, isNull);\n        expect(routeA.lastDidPopNextRoute, routeD.name);\n        expect(routeA.lastOnPopNextRoute, routeD.name);\n\n        expect(routeD.lastDidPushPreviousRoute, routeA.name);\n        expect(routeD.lastOnPushPreviousRoute, routeA.name);\n        expect(routeD.lastDidPopNextRoute, isNull);\n        expect(routeD.lastOnPopNextRoute, isNull);\n\n        await game.ready();\n\n        router.pushReplacementNamed('B');\n        expect(routeB.onPopTimes, 1);\n        expect(routeB.didPopTimes, 1);\n        expect(routeB.onPushTimes, 2);\n        expect(routeB.didPushTimes, 2);\n\n        expect(routeD.onPopTimes, 1);\n        expect(routeD.didPopTimes, 1);\n        expect(routeD.onPushTimes, 1);\n        expect(routeD.didPushTimes, 1);\n\n        expect(routeB.lastDidPushPreviousRoute, routeD.name);\n        expect(routeB.lastOnPushPreviousRoute, routeD.name);\n        expect(routeB.lastDidPopNextRoute, routeA.name);\n        expect(routeB.lastOnPopNextRoute, routeA.name);\n\n        expect(routeD.lastDidPushPreviousRoute, routeA.name);\n        expect(routeD.lastOnPushPreviousRoute, routeA.name);\n        expect(routeD.lastDidPopNextRoute, routeB.name);\n        expect(routeD.lastOnPopNextRoute, routeB.name);\n      },\n    );\n\n    testWithFlameGame('Route factories', (game) async {\n      final router = RouterComponent(\n        initialRoute: 'initial',\n        routes: {'initial': Route(_ComponentD.new)},\n        routeFactories: {\n          'a': (arg) => Route(_ComponentA.new),\n          'b': (arg) => Route(_ComponentB.new),\n        },\n      );\n      game.add(router);\n      await game.ready();\n\n      expect(router.currentRoute.name, 'initial');\n\n      router.pushNamed('a/101');\n      await game.ready();\n      expect(router.currentRoute.name, 'a/101');\n      expect(router.currentRoute.children.first, isA<_ComponentA>());\n\n      router.pushNamed('b/something');\n      await game.ready();\n      expect(router.currentRoute.name, 'b/something');\n      expect(router.currentRoute.children.first, isA<_ComponentB>());\n    });\n\n    testWithFlameGame('push an existing route', (game) async {\n      final router = RouterComponent(\n        routes: {\n          'A': Route(_ComponentA.new),\n          'B': Route(_ComponentB.new),\n          'C': Route(_ComponentC.new),\n        },\n        initialRoute: 'A',\n      )..addToParent(game);\n      await game.ready();\n\n      router.pushNamed('A');\n      await game.ready();\n      expect(router.stack.length, 1);\n\n      router.pushNamed('B');\n      await game.ready();\n      router.pushNamed('C');\n      await game.ready();\n      expect(router.stack.length, 3);\n\n      router.pushNamed('B');\n      await game.ready();\n      expect(router.stack.length, 3);\n      router.pushNamed('A');\n      await game.ready();\n      expect(router.stack.length, 3);\n\n      expect(router.children.length, 3);\n      expect((router.children.elementAt(0) as Route).name, 'C');\n      expect((router.children.elementAt(1) as Route).name, 'B');\n      expect((router.children.elementAt(2) as Route).name, 'A');\n    });\n\n    testWithFlameGame('onUnknownRoute', (game) async {\n      final router = RouterComponent(\n        initialRoute: 'home',\n        routes: {'home': Route(_ComponentA.new)},\n        onUnknownRoute: (name) => Route(_ComponentD.new),\n      )..addToParent(game);\n      await game.ready();\n\n      router.pushNamed('hello');\n      await game.ready();\n      expect(router.currentRoute.name, 'hello');\n      expect(router.currentRoute.children.first, isA<_ComponentD>());\n    });\n\n    testWithFlameGame('default unknown route handling', (game) async {\n      final router = RouterComponent(\n        initialRoute: 'home',\n        routes: {'home': Route(_ComponentA.new)},\n      )..addToParent(game);\n      await game.ready();\n\n      expect(\n        () => router.pushNamed('hello'),\n        throwsA(\n          predicate(\n            (e) =>\n                e is ArgumentError &&\n                e.message ==\n                    'Route \"hello\" could not be resolved by the Router',\n          ),\n        ),\n      );\n    });\n\n    testWithFlameGame('cannot pop last remaining route', (game) async {\n      final router = RouterComponent(\n        initialRoute: 'home',\n        routes: {'home': Route(_ComponentA.new)},\n      )..addToParent(game);\n      await game.ready();\n\n      expect(\n        router.pop,\n        failsAssert('Cannot pop the last route from the Router'),\n      );\n    });\n\n    testWithFlameGame('canPop returns correct value', (game) async {\n      final router = RouterComponent(\n        routes: {\n          'A': Route(_ComponentA.new),\n          'B': Route(_ComponentB.new),\n        },\n        initialRoute: 'A',\n      )..addToParent(game);\n      await game.ready();\n\n      // Should return false when only one route is on the stack\n      expect(router.canPop(), false);\n      expect(router.stack.length, 1);\n\n      // Should return true when multiple routes are on the stack\n      router.pushNamed('B');\n      await game.ready();\n      expect(router.canPop(), true);\n      expect(router.stack.length, 2);\n\n      // Should return false again after popping back to one route\n      router.pop();\n      await game.ready();\n      expect(router.canPop(), false);\n      expect(router.stack.length, 1);\n    });\n\n    testWithFlameGame('popUntilNamed', (game) async {\n      final router = RouterComponent(\n        routes: {\n          'A': Route(_ComponentA.new),\n          'B': Route(_ComponentB.new),\n          'C': Route(_ComponentC.new),\n        },\n        initialRoute: 'A',\n      );\n      game.add(router);\n      await game.ready();\n\n      router.pushNamed('B');\n      router.pushNamed('C');\n      await game.ready();\n      expect(router.stack.length, 3);\n      expect(router.children.length, 3);\n\n      router.popUntilNamed('A');\n      await game.ready();\n      expect(router.stack.length, 1);\n      expect(router.children.length, 1);\n      expect(router.currentRoute.name, 'A');\n    });\n\n    testWidgets(\n      'can handle overlays via the Router',\n      (tester) async {\n        const key1 = ValueKey('one');\n        const key2 = ValueKey('two');\n        const key3 = ValueKey('three');\n        final game = FlameGame();\n        final router = RouterComponent(\n          initialRoute: 'home',\n          routes: {'home': Route(Component.new)},\n        )..addToParent(game);\n\n        await tester.pumpWidget(\n          GameWidget(\n            game: game,\n            overlayBuilderMap: {\n              'first!': (_, __) => Container(key: key1),\n              'second': (_, __) => Container(key: key2),\n            },\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n        expect(router.stack.length, 1);\n        expect(router.currentRoute.name, 'home');\n        expect(game.overlays.activeOverlays.isEmpty, true);\n\n        router.pushOverlay('second');\n        await tester.pump();\n        expect(game.overlays.activeOverlays, ['second']);\n        expect(find.byKey(key1), findsNothing);\n        expect(find.byKey(key2), findsOneWidget);\n\n        router.pop();\n        await tester.pump();\n        expect(game.overlays.activeOverlays.isEmpty, true);\n        expect(find.byKey(key1), findsNothing);\n        expect(find.byKey(key2), findsNothing);\n\n        router.pushOverlay('first!');\n        router.pushOverlay('second');\n        await tester.pump();\n        expect(game.overlays.activeOverlays, ['first!', 'second']);\n        expect(find.byKey(key1), findsOneWidget);\n        expect(find.byKey(key2), findsOneWidget);\n        router.pop();\n        await tester.pump();\n        expect(game.overlays.activeOverlays, ['first!']);\n\n        router.pushRoute(\n          OverlayRoute((ctx, game) => Container(key: key3)),\n          name: 'new-route',\n        );\n        await tester.pump();\n        expect(game.overlays.activeOverlays, ['first!', 'new-route']);\n        expect(find.byKey(key1), findsOneWidget);\n        expect(find.byKey(key2), findsNothing);\n        expect(find.byKey(key3), findsOneWidget);\n\n        router.pushReplacementOverlay('second');\n        await tester.pump();\n        expect(game.overlays.activeOverlays, ['first!', 'second']);\n        expect(find.byKey(key1), findsOneWidget);\n        expect(find.byKey(key2), findsOneWidget);\n        expect(find.byKey(key3), findsNothing);\n      },\n    );\n  });\n}\n\nclass _ComponentA extends Component {}\n\nclass _ComponentB extends Component {}\n\nclass _ComponentC extends Component {}\n\nclass _ComponentD extends Component {}\n\nclass _ComponentE extends Component {}\n\nclass _TestRoute extends Route {\n  int onPopTimes = 0;\n  int onPushTimes = 0;\n  int didPopTimes = 0;\n  int didPushTimes = 0;\n  String? lastOnPopNextRoute;\n  String? lastOnPushPreviousRoute;\n  String? lastDidPopNextRoute;\n  String? lastDidPushPreviousRoute;\n\n  _TestRoute(super.builder);\n\n  @override\n  void onPop(Route nextRoute) {\n    super.onPop(nextRoute);\n    onPopTimes++;\n    lastOnPopNextRoute = nextRoute.name;\n  }\n\n  @override\n  void onPush(Route? previousRoute) {\n    super.onPush(previousRoute);\n    onPushTimes++;\n    lastOnPushPreviousRoute = previousRoute?.name;\n  }\n\n  @override\n  void didPop(Route nextRoute) {\n    super.didPop(nextRoute);\n    didPopTimes++;\n    lastDidPopNextRoute = nextRoute.name;\n  }\n\n  @override\n  void didPush(Route? previousRoute) {\n    super.didPush(previousRoute);\n    didPushTimes++;\n    lastDidPushPreviousRoute = previousRoute?.name;\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/components/scroll_text_box_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/text.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nvoid main() {\n  group('ScrollTextBoxComponent', () {\n    testWithFlameGame(\n      'onComplete is called when no scrolling is required',\n      (game) async {\n        final onComplete = _MockOnCompleteCallback();\n\n        when(onComplete.call).thenReturn(null);\n\n        final component = ScrollTextBoxComponent(\n          size: Vector2(200, 100),\n          text: 'Short text',\n          onComplete: onComplete.call,\n        );\n        await game.ensureAdd(component);\n\n        game.update(0.1);\n\n        verify(onComplete.call).called(1);\n      },\n    );\n\n    testWithFlameGame(\n      'onComplete is called when scrolling is required',\n      (game) async {\n        final onComplete = _MockOnCompleteCallback();\n\n        when(onComplete.call).thenReturn(null);\n\n        final component = ScrollTextBoxComponent(\n          size: Vector2(200, 100),\n          text: '''Long text that will definitely require scrolling to be \nfully visible in the given size of the ScrollTextBoxComponent.''',\n          onComplete: onComplete.call,\n        );\n        await game.ensureAdd(component);\n\n        game.update(0.1);\n\n        verify(onComplete.call).called(1);\n      },\n    );\n\n    testWithFlameGame(\n      'Text position moves to <0 when scrolled',\n      (game) async {\n        final scrollComponent = ScrollTextBoxComponent(\n          size: Vector2(50, 50),\n          text: '''This is a test text that is long enough to require scrolling \nto see the entire content. It should test whether the scrolling \nfunctionality properly adjusts the text position.''',\n          onComplete: () {},\n        );\n\n        expect(scrollComponent.children.length, greaterThan(0));\n        expect(scrollComponent.children.first, isA<ClipComponent>());\n        final clipCmp = scrollComponent.children.first as ClipComponent;\n\n        expect(clipCmp.children.length, greaterThan(0));\n        expect(clipCmp.children.first, isA<PositionComponent>());\n        final innerScrollComponent =\n            clipCmp.children.first as PositionComponent;\n\n        expect(innerScrollComponent.position.y, equals(0));\n        await game.ensureAdd(scrollComponent);\n\n        expect(innerScrollComponent.position.y, lessThan(0));\n      },\n    );\n\n    testWithFlameGame('Text notifies if a new line is added', (game) async {\n      var newLineCount = 0;\n      final scrollComponent = ScrollTextBoxComponent(\n        size: Vector2(50, 50),\n        text: '''This \ntest\nhas\nfive\nlines.''',\n        textRenderer: TextPaint(\n          style: const TextStyle(\n            fontSize: 8,\n            fontFamily: 'monospace',\n          ),\n        ),\n      );\n      expect(scrollComponent.newLineNotifier.value, equals(0));\n\n      scrollComponent.newLineNotifier.addListener(() {\n        newLineCount++;\n      });\n      await game.ensureAdd(scrollComponent);\n      expect(newLineCount, equals(5));\n    });\n  });\n}\n\nclass _MockOnCompleteCallback extends Mock {\n  void call();\n}\n"
  },
  {
    "path": "packages/flame/test/components/shape_component_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('ShapeComponent.containsPoint', () {\n    test('circle contains point', () {\n      final component = CircleComponent(\n        radius: 1.0,\n        position: Vector2(1, 1),\n        anchor: Anchor.center,\n      );\n      expect(\n        component.containsPoint(Vector2.all(1.5)),\n        isTrue,\n      );\n    });\n\n    test('rectangle contains point', () {\n      final component = RectangleComponent(\n        position: Vector2(1, 1),\n        size: Vector2(1, 1),\n      );\n      expect(\n        component.containsPoint(Vector2.all(1.5)),\n        isTrue,\n      );\n    });\n\n    test('polygon contains point', () {\n      final component = PolygonComponent(\n        [\n          Vector2(2, 2),\n          Vector2(2, 0),\n          Vector2(1, 1),\n        ],\n      );\n      expect(\n        component.containsPoint(Vector2(1.5, 1.0)),\n        isTrue,\n      );\n    });\n\n    test('polygon contains point in local', () {\n      final polygon = PolygonComponent(\n        [\n          Vector2(0.5, 0.5),\n          Vector2(0.5, 1.5),\n          Vector2(1.5, 1.5),\n          Vector2(1.5, 0.5),\n        ],\n      );\n      expect(polygon.containsLocalPoint(Vector2(0.0, 0.0)), isTrue);\n      expect(polygon.containsLocalPoint(Vector2(0.25, 0.25)), isTrue);\n      expect(polygon.containsLocalPoint(Vector2(0.75, 0.75)), isTrue);\n      expect(polygon.containsLocalPoint(Vector2(1.0, 1.0)), isTrue);\n      expect(polygon.containsLocalPoint(Vector2(1.0, 0.0)), isTrue);\n      expect(polygon.containsLocalPoint(Vector2(0.0, 1.0)), isTrue);\n      expect(polygon.containsLocalPoint(Vector2(0.0, 1.0001)), isFalse);\n      expect(polygon.containsLocalPoint(Vector2(-0.0001, 0.0)), isFalse);\n    });\n\n    test('polygon contains point in local with anchor', () {\n      final polygon = PolygonComponent(\n        [\n          Vector2(0.5, 0.5),\n          Vector2(0.5, 1.5),\n          Vector2(1.5, 1.5),\n          Vector2(1.5, 0.5),\n        ],\n        anchor: Anchor.center,\n      );\n      expect(polygon.containsLocalPoint(Vector2(0.0, 0.0)), isTrue);\n      expect(polygon.containsLocalPoint(Vector2(0.25, 0.25)), isTrue);\n      expect(polygon.containsLocalPoint(Vector2(0.75, 0.75)), isTrue);\n      expect(polygon.containsLocalPoint(Vector2(1.0, 1.0)), isTrue);\n      expect(polygon.containsLocalPoint(Vector2(1.0, 0.0)), isTrue);\n      expect(polygon.containsLocalPoint(Vector2(0.0, 1.0)), isTrue);\n      expect(polygon.containsLocalPoint(Vector2(0.0, 1.0001)), isFalse);\n      expect(polygon.containsLocalPoint(Vector2(-0.0001, 0.0)), isFalse);\n    });\n\n    test('rectangle contains point in local with anchor', () {\n      final rectangle = RectangleComponent(\n        position: Vector2.zero(),\n        size: Vector2.all(10),\n        anchor: Anchor.center,\n      );\n      expect(rectangle.containsLocalPoint(Vector2.zero()), isTrue);\n      expect(rectangle.containsLocalPoint(Vector2.all(5)), isTrue);\n      expect(rectangle.containsLocalPoint(Vector2.all(10)), isTrue);\n      expect(rectangle.containsLocalPoint(Vector2(10, 0)), isTrue);\n      expect(rectangle.containsLocalPoint(Vector2(0, 10)), isTrue);\n    });\n\n    test('angled rectangle contains point in local with anchor', () {\n      final rectangle = RectangleComponent(\n        position: Vector2.zero(),\n        size: Vector2.all(10),\n        angle: pi / 4,\n        anchor: Anchor.center,\n      );\n      expect(rectangle.containsLocalPoint(Vector2.zero()), isTrue);\n      expect(rectangle.containsLocalPoint(Vector2.all(5)), isTrue);\n      expect(rectangle.containsLocalPoint(Vector2.all(10)), isTrue);\n      expect(rectangle.containsLocalPoint(Vector2(10, 0)), isTrue);\n      expect(rectangle.containsLocalPoint(Vector2(0, 10)), isTrue);\n    });\n\n    test('rotated circle does not contain point', () {\n      final component = CircleComponent(\n        radius: 1.0,\n        position: Vector2(1, 1),\n        angle: pi / 4,\n        anchor: Anchor.center,\n      );\n      expect(\n        component.containsPoint(Vector2.all(1.9)),\n        isFalse,\n      );\n    });\n\n    test('rotated rectangle does not contain point', () {\n      final component = RectangleComponent(\n        position: Vector2.all(1.0),\n        size: Vector2.all(2.0),\n        anchor: Anchor.center,\n      );\n      expect(component.containsPoint(Vector2.all(2.0)), isTrue);\n      component.angle = pi / 4;\n      expect(component.containsPoint(Vector2.all(2.0)), isFalse);\n    });\n\n    test('rotated polygon does not contain point', () {\n      final component = PolygonComponent(\n        [\n          Vector2(2, 2),\n          Vector2(2, 0),\n          Vector2(1, 1),\n        ],\n        angle: pi / 4,\n        anchor: Anchor.center,\n      );\n      expect(component.containsPoint(Vector2.all(1.9)), isFalse);\n    });\n\n    test('rotated circle contains point', () {\n      final component = CircleComponent(\n        radius: 1.0,\n        position: Vector2(1, 1),\n        angle: pi / 4,\n        anchor: Anchor.center,\n      );\n      expect(\n        component.containsPoint(Vector2(1.0, 1.9)),\n        isTrue,\n      );\n    });\n\n    test('rotated rectangle contains point', () {\n      final component = RectangleComponent(\n        position: Vector2.all(1.0),\n        size: Vector2.all(2.0),\n        angle: pi / 4,\n        anchor: Anchor.center,\n      );\n      expect(\n        component.containsPoint(Vector2(1.0, 2.1)),\n        isTrue,\n      );\n    });\n\n    test('rotated polygon contains point', () {\n      final component = PolygonComponent(\n        [\n          Vector2(2, 2),\n          Vector2(2, 0),\n          Vector2(1, 1),\n        ],\n        position: Vector2(1.5, 1.0),\n        angle: pi / 2,\n        anchor: Anchor.center,\n      );\n      expect(\n        component.containsPoint(Vector2(2.5, 1.5)),\n        isTrue,\n      );\n    });\n\n    test('non-centered normal polygon contains point', () {\n      final component = PolygonComponent.relative(\n        // Top left quadrant\n        [\n          Vector2(-1, -1),\n          Vector2(-1, 0),\n          Vector2(-0.1, -0.1),\n          Vector2(0, -1),\n        ],\n        parentSize: Vector2.all(100),\n      );\n      expect(\n        component.containsPoint(Vector2.all(45)),\n        isTrue,\n      );\n    });\n\n    test('concave polygon contains point', () {\n      final component = PolygonComponent(\n        [\n          Vector2(0, 0),\n          Vector2(-2, -4),\n          Vector2(2, 0),\n          Vector2(-2, 4),\n        ],\n      );\n      expect(\n        component.containsPoint(Vector2(-1, 0)),\n        isFalse,\n      );\n      expect(\n        component.containsPoint(Vector2(-1, 1)),\n        isFalse,\n      );\n      expect(\n        component.containsPoint(Vector2(2, 0)),\n        isTrue,\n      );\n      expect(\n        component.containsPoint(Vector2(1, 1)),\n        isTrue,\n      );\n    });\n\n    test('horizontally flipped rectangle contains point', () {\n      final component = RectangleComponent(\n        position: Vector2.all(1.0),\n        size: Vector2.all(2.0),\n        anchor: Anchor.center,\n      )..flipVerticallyAroundCenter();\n      expect(\n        component.containsPoint(Vector2(2.0, 2.0)),\n        isTrue,\n      );\n    });\n\n    test('initially rotated CircleComponent does not contain point', () {\n      final component = CircleComponent(\n        radius: 1.0,\n        position: Vector2(1, 1),\n        angle: pi / 4,\n        anchor: Anchor.center,\n      );\n      expect(\n        component.containsPoint(Vector2.all(1.9)),\n        isFalse,\n      );\n    });\n\n    test('initially rotated Rectangle does not contain point', () {\n      final component = RectangleComponent(\n        position: Vector2.all(1.0),\n        size: Vector2.all(2.0),\n        angle: pi / 4,\n        anchor: Anchor.topLeft,\n      );\n      expect(\n        component.containsPoint(Vector2(3.0, 1.0)),\n        isFalse,\n      );\n    });\n\n    test('initially rotated PolygonComponent does not contain point', () {\n      final component = PolygonComponent(\n        [\n          Vector2(2, 2),\n          Vector2(3, 1),\n          Vector2(2, 0),\n          Vector2(1, 1),\n        ],\n        angle: pi / 4,\n        anchor: Anchor.center,\n      );\n      expect(\n        component.containsPoint(Vector2.all(1.9)),\n        isFalse,\n      );\n    });\n\n    test('rotated PolygonComponent contains point', () {\n      final component = PolygonComponent(\n        [\n          Vector2(2, 2),\n          Vector2(3, 1),\n          Vector2(2, 0),\n          Vector2(1, 1),\n        ],\n        anchor: Anchor.center,\n      );\n      component.angle = pi / 4;\n      expect(\n        component.containsPoint(Vector2(2.7, 1.7)),\n        isTrue,\n      );\n    });\n\n    test('moved CircleComponent contains point', () {\n      final component = CircleComponent(\n        radius: 1.0,\n        position: Vector2(2, 2),\n        anchor: Anchor.center,\n      );\n      expect(\n        component.containsPoint(Vector2.all(2.1)),\n        isTrue,\n      );\n    });\n\n    test('moved Rectangle contains point', () {\n      final component = RectangleComponent(\n        position: Vector2(2, 2),\n        size: Vector2(1, 1),\n        anchor: Anchor.center,\n      );\n      expect(\n        component.containsPoint(Vector2.all(2.1)),\n        isTrue,\n      );\n    });\n\n    test('moved PolygonComponent contains point', () {\n      final component = PolygonComponent(\n        [\n          Vector2(2, 0),\n          Vector2(1, 1),\n          Vector2(2, 2),\n          Vector2(3, 1),\n        ],\n        anchor: Anchor.center,\n      )..position = Vector2.all(1.0);\n      expect(\n        component.containsPoint(Vector2(0.9, 1.0)),\n        isTrue,\n      );\n    });\n\n    test('sized up CircleComponent does not contain point', () {\n      final component = CircleComponent(\n        radius: 1.0,\n        position: Vector2(1, 1),\n        anchor: Anchor.center,\n      );\n      component.size += Vector2.all(1.0);\n      expect(\n        component.containsPoint(Vector2.all(2.1)),\n        isFalse,\n      );\n    });\n\n    test('sized up Rectangle does not contain point', () {\n      final component = RectangleComponent(\n        position: Vector2(1, 1),\n        size: Vector2(2, 2),\n        anchor: Anchor.center,\n      );\n      expect(\n        component.containsPoint(Vector2.all(2.1)),\n        isFalse,\n      );\n    });\n\n    test('sized PolygonComponent does not contain point', () {\n      final component = PolygonComponent(\n        [\n          Vector2(2, 0),\n          Vector2(1, 1),\n          Vector2(2, 2),\n          Vector2(3, 1),\n        ],\n        anchor: Anchor.center,\n      );\n      component.size += Vector2.all(1.0);\n      expect(\n        component.containsPoint(Vector2(2.0, 2.6)),\n        isFalse,\n      );\n    });\n\n    test(\n      'rotated CircleComponent with default anchor (topLeft) contains point',\n      () {\n        final component = CircleComponent(\n          radius: 1.0,\n          position: Vector2.all(1.0),\n          angle: pi / 4,\n        );\n        expect(\n          component.containsPoint(Vector2(0.9, 2.0)),\n          isTrue,\n        );\n      },\n    );\n\n    test(\n      'rotated Rectangle with default anchor (topLeft) contains point',\n      () {\n        final component = RectangleComponent(\n          position: Vector2.all(1.0),\n          size: Vector2.all(1.0),\n          angle: pi / 4,\n        );\n        expect(\n          component.containsPoint(Vector2(0.9, 2.0)),\n          isTrue,\n        );\n      },\n    );\n\n    test(\n      'rotated PolygonComponent with default anchor (topLeft) contains point',\n      () {\n        final component = PolygonComponent(\n          [\n            Vector2(2, 0),\n            Vector2(1, 1),\n            Vector2(2, 2),\n            Vector2(3, 1),\n          ],\n          angle: pi / 4,\n        );\n        expect(\n          component.containsPoint(Vector2(1.0, 0.99)),\n          isTrue,\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'CircleComponent with multiple parents contains point',\n      (game) async {\n        PositionComponent createParent() {\n          return PositionComponent(\n            position: Vector2.all(1.0),\n            size: Vector2.all(2.0),\n            angle: pi / 2,\n          );\n        }\n\n        final component = CircleComponent(\n          radius: 1.0,\n          position: Vector2.all(1.0),\n          anchor: Anchor.center,\n        );\n        final grandParent = createParent();\n        final parent = createParent();\n        grandParent.add(parent);\n        parent.add(component);\n        game.add(grandParent);\n        await game.ready();\n        expect(\n          component.containsPoint(Vector2(-1.0, 1.0)),\n          isTrue,\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'Rectangle with multiple parents contains point',\n      (game) async {\n        PositionComponent createParent() {\n          return PositionComponent(\n            position: Vector2.all(1.0),\n            size: Vector2.all(2.0),\n            angle: pi / 2,\n          );\n        }\n\n        final component = RectangleComponent(\n          size: Vector2.all(1.0),\n          position: Vector2.all(1.0),\n          anchor: Anchor.center,\n        );\n        final grandParent = createParent();\n        final parent = createParent();\n        grandParent.add(parent);\n        parent.add(component);\n        game.add(grandParent);\n        await game.ready();\n        expect(\n          component.containsPoint(Vector2(-1.0, 1.0)),\n          isTrue,\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'PolygonComponent with multiple parents contains point',\n      (game) async {\n        PositionComponent createParent() {\n          return PositionComponent(\n            position: Vector2.all(1.0),\n            size: Vector2.all(2.0),\n            angle: pi / 2,\n          );\n        }\n\n        final component = PolygonComponent.relative(\n          [\n            Vector2(1, 1),\n            Vector2(1, -1),\n            Vector2(-1, -1),\n            Vector2(-1, 1),\n          ],\n          parentSize: Vector2.all(1.0),\n          position: Vector2.all(1.0),\n        );\n        final grandParent = createParent();\n        final parent = createParent();\n        grandParent.add(parent);\n        parent.add(component);\n        game.add(grandParent);\n        await game.ready();\n        expect(\n          component.containsPoint(Vector2(-2.0, 0.01)),\n          isTrue,\n        );\n        component.angle = pi / 2;\n        expect(\n          component.containsPoint(Vector2(-1.0, 0.01)),\n          isTrue,\n        );\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/spawn_component_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('SpawnComponent', () {\n    testWithFlameGame('Spawns components within rectangle', (game) async {\n      final random = Random(0);\n      final shape = Rectangle.fromCenter(\n        center: Vector2(100, 200),\n        size: Vector2.all(200),\n      );\n      final spawn = SpawnComponent(\n        factory: (_) => PositionComponent(),\n        period: 1,\n        area: shape,\n        random: random,\n      );\n      final world = game.world;\n      await world.ensureAdd(spawn);\n      game.update(0.5);\n      expect(world.children.length, 1);\n      game.update(0.5);\n      game.update(0.0);\n      expect(world.children.length, 2);\n      game.update(1.0);\n      game.update(0.0);\n      expect(world.children.length, 3);\n\n      for (var i = 0; i < 1000; i++) {\n        game.update(random.nextDouble());\n      }\n      expect(\n        world.children.query<PositionComponent>().every(\n          (c) => shape.containsPoint(c.position),\n        ),\n        isTrue,\n      );\n    });\n\n    testWithFlameGame(\n      'Spawns multiple components within rectangle',\n      (game) async {\n        final random = Random(0);\n        final shape = Rectangle.fromCenter(\n          center: Vector2(100, 200),\n          size: Vector2.all(200),\n        );\n        final spawn = SpawnComponent(\n          multiFactory: (_) => [\n            PositionComponent(),\n            PositionComponent(),\n            PositionComponent(),\n          ],\n          period: 1,\n          area: shape,\n          random: random,\n        );\n        final world = game.world;\n        await world.ensureAdd(spawn);\n        game.update(0.5);\n        expect(world.children.length, 1); //1 being the spawnComponent\n        game.update(0.5);\n        game.update(0.0);\n        expect(world.children.length, 4); //1+3 spawned components\n        game.update(1.0);\n        game.update(0.0);\n        expect(world.children.length, 7); //1+2*3 spawned components\n\n        for (var i = 0; i < 1000; i++) {\n          game.update(random.nextDouble());\n        }\n        expect(\n          world.children.query<PositionComponent>().every(\n            (c) => shape.containsPoint(c.position),\n          ),\n          isTrue,\n        );\n      },\n    );\n\n    testWithFlameGame('Spawns components within circle', (game) async {\n      final random = Random(0);\n      final shape = Circle(Vector2(100, 200), 100);\n      expect(shape.containsPoint(Vector2.all(200)), isTrue);\n      final spawn = SpawnComponent(\n        factory: (_) => PositionComponent(),\n        period: 1,\n        area: shape,\n        random: random,\n      );\n      final world = game.world;\n      await world.ensureAdd(spawn);\n      game.update(0.5);\n      expect(world.children.length, 1);\n      game.update(0.5);\n      game.update(0.0);\n      expect(world.children.length, 2);\n      game.update(1.0);\n      game.update(0.0);\n      expect(world.children.length, 3);\n\n      for (var i = 0; i < 1000; i++) {\n        game.update(random.nextDouble());\n      }\n      expect(\n        world.children.query<PositionComponent>().every(\n          (c) => shape.containsPoint(c.position),\n        ),\n        isTrue,\n      );\n    });\n\n    testWithFlameGame('Spawns components within polygon', (game) async {\n      final random = Random(0);\n      final shape = Polygon(\n        [\n          Vector2(100, 100),\n          Vector2(200, 100),\n          Vector2(150, 200),\n        ],\n      );\n      expect(shape.containsPoint(Vector2.all(150)), isTrue);\n      final spawn = SpawnComponent(\n        factory: (_) => PositionComponent(),\n        period: 1,\n        area: shape,\n        random: random,\n      );\n      final world = game.world;\n      await world.ensureAdd(spawn);\n      game.update(0.5);\n      expect(world.children.length, 1);\n      game.update(0.5);\n      game.update(0.0);\n      expect(world.children.length, 2);\n      game.update(1.0);\n      game.update(0.0);\n      expect(world.children.length, 3);\n\n      for (var i = 0; i < 1000; i++) {\n        game.update(random.nextDouble());\n      }\n      expect(\n        world.children.query<PositionComponent>().every(\n          (c) => shape.containsPoint(c.position),\n        ),\n        isTrue,\n      );\n    });\n\n    testWithFlameGame('Can self position', (game) async {\n      final random = Random(0);\n      final spawn = SpawnComponent(\n        factory: (_) => PositionComponent(position: Vector2.all(1000)),\n        period: 1,\n        selfPositioning: true,\n        random: random,\n      );\n      final world = game.world;\n      await world.ensureAdd(spawn);\n      game.update(0.5);\n      expect(world.children.length, 1);\n      game.update(0.5);\n      game.update(0.0);\n      expect(world.children.length, 2);\n      game.update(1.0);\n      game.update(0.0);\n      expect(world.children.length, 3);\n\n      for (var i = 0; i < 1000; i++) {\n        game.update(random.nextDouble());\n      }\n      expect(\n        world.children.query<PositionComponent>().every(\n          (c) => c.position == Vector2.all(1000),\n        ),\n        isTrue,\n      );\n    });\n\n    testWithFlameGame('Can self position multiple components', (game) async {\n      final random = Random(0);\n      final spawn = SpawnComponent(\n        multiFactory: (_) => [\n          PositionComponent(position: Vector2.all(1000)),\n          PositionComponent(position: Vector2.all(1000)),\n          PositionComponent(position: Vector2.all(1000)),\n        ],\n        period: 1,\n        selfPositioning: true,\n        random: random,\n      );\n      final world = game.world;\n      await world.ensureAdd(spawn);\n      game.update(0.5);\n      expect(world.children.length, 1); //1 spawned component\n      game.update(0.5);\n      game.update(0.0);\n      expect(world.children.length, 4); //1+3 spawned components\n      game.update(1.0);\n      game.update(0.0);\n      expect(world.children.length, 7); //1+2*3 spawned components\n\n      for (var i = 0; i < 1000; i++) {\n        game.update(random.nextDouble());\n      }\n      expect(\n        world.children.query<PositionComponent>().every(\n          (c) => c.position == Vector2.all(1000),\n        ),\n        isTrue,\n      );\n    });\n\n    testWithFlameGame('Does not spawns when auto start is false', (game) async {\n      final random = Random(0);\n      final shape = Rectangle.fromCenter(\n        center: Vector2(100, 200),\n        size: Vector2.all(200),\n      );\n      final spawn = SpawnComponent(\n        factory: (_) => PositionComponent(),\n        period: 1,\n        area: shape,\n        random: random,\n        autoStart: false,\n      );\n      final world = game.world;\n      await world.ensureAdd(spawn);\n      game.update(1.5);\n      await game.ready();\n      expect(world.children.length, 1);\n\n      spawn.timer.start();\n\n      game.update(1);\n      await game.ready();\n      expect(world.children.length, 2);\n    });\n\n    testWithFlameGame(\n      'Does not spawns right away when spawnWhenLoaded is false (default)',\n      (game) async {\n        final random = Random(0);\n        final shape = Rectangle.fromCenter(\n          center: Vector2(100, 200),\n          size: Vector2.all(200),\n        );\n        final spawn = SpawnComponent(\n          factory: (_) => PositionComponent(),\n          period: 1,\n          area: shape,\n          random: random,\n        );\n        final world = game.world;\n        await world.ensureAdd(spawn);\n        expect(world.children.whereType<PositionComponent>(), isEmpty);\n        game.update(1.5);\n        await game.ready();\n        expect(\n          world.children.whereType<PositionComponent>(),\n          hasLength(1),\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'Spawns right away when spawnWhenLoaded is true',\n      (game) async {\n        final random = Random(0);\n        final shape = Rectangle.fromCenter(\n          center: Vector2(100, 200),\n          size: Vector2.all(200),\n        );\n        final spawn = SpawnComponent(\n          factory: (_) => PositionComponent(),\n          period: 1,\n          area: shape,\n          random: random,\n          spawnWhenLoaded: true,\n        );\n        final world = game.world;\n        await world.ensureAdd(spawn);\n        expect(world.children.whereType<PositionComponent>(), hasLength(1));\n        game.update(1.5);\n        await game.ready();\n        expect(\n          world.children.whereType<PositionComponent>(),\n          hasLength(2),\n        );\n      },\n    );\n\n    testWithFlameGame('Spawns components within irregular period', (\n      game,\n    ) async {\n      final random = Random(0);\n      // The first two periods will be ~4.3 and ~3.85\n      final spawn = SpawnComponent.periodRange(\n        factory: (_) => PositionComponent(),\n        minPeriod: 1.0,\n        maxPeriod: 5.0,\n        random: random,\n      );\n      final world = game.world;\n      await world.ensureAdd(spawn);\n      expect(world.children.length, 1);\n      game.update(0.3);\n      game.update(0);\n      expect(world.children.length, 1);\n      game.update(4.31);\n      game.update(0);\n      expect(world.children.length, 2);\n      game.update(3.86);\n      game.update(0);\n      expect(world.children.length, 3);\n    });\n\n    testWithFlameGame(\n      'Stops spawning after reaching spawnCount',\n      (game) async {\n        final random = Random(0);\n        final spawn = SpawnComponent(\n          factory: (_) => _CountComponent(),\n          period: 1,\n          spawnCount: 3,\n          random: random,\n        );\n        final world = game.world;\n        await world.ensureAdd(spawn);\n\n        // Simulate updates to reach the spawnCount limit\n        game.update(1.0);\n        game.update(0.0);\n        expect(\n          world.children.whereType<_CountComponent>().toList(),\n          hasLength(1),\n        );\n        game.update(1.0);\n        game.update(0.0);\n        expect(\n          world.children.whereType<_CountComponent>().toList(),\n          hasLength(2),\n        );\n        game.update(1.0);\n        game.update(0.0);\n        expect(\n          world.children.whereType<_CountComponent>().toList(),\n          hasLength(3),\n        );\n\n        // Ensure no more components are spawned after reaching the limit\n        game.update(1.0);\n        game.update(0.0);\n        expect(\n          world.children.whereType<_CountComponent>().toList(),\n          hasLength(3),\n        );\n        expect(spawn.isMounted, isFalse);\n      },\n    );\n\n    testWithFlameGame(\n      'Stops spawning multiple components after reaching spawnCount',\n      (game) async {\n        final random = Random(0);\n        final spawn = SpawnComponent(\n          multiFactory: (_) => [\n            _CountComponent(),\n            _CountComponent(),\n            _CountComponent(),\n          ],\n          period: 1,\n          spawnCount: 6,\n          random: random,\n        );\n        final world = game.world;\n        await world.ensureAdd(spawn);\n\n        // Simulate updates to reach the spawnCount limit\n        game.update(1.0);\n        game.update(0.0);\n        expect(\n          world.children.whereType<_CountComponent>().toList(),\n          hasLength(3),\n        );\n        game.update(1.0);\n        game.update(0.0);\n        expect(\n          world.children.whereType<_CountComponent>().toList(),\n          hasLength(6),\n        );\n\n        // Ensure no more components are spawned after reaching the limit\n        game.update(1.0);\n        game.update(0.0);\n        expect(\n          world.children.whereType<_CountComponent>().toList(),\n          hasLength(6),\n        );\n        expect(spawn.isMounted, isFalse);\n      },\n    );\n\n    group('With target', () {\n      testWithFlameGame(\n        'Spawns within target bounds',\n        (game) async {\n          final target = PositionComponent(\n            size: Vector2(200, 200),\n            position: Vector2(50, 50),\n          );\n          final spawn = SpawnComponent(\n            factory: (_) => PositionComponent(),\n            period: 1,\n            target: target,\n          );\n          await game.ensureAdd(target);\n          await game.ensureAdd(spawn);\n\n          game.update(1.0);\n          game.update(0.0);\n          expect(target.children.whereType<PositionComponent>(), hasLength(1));\n          expect(\n            target.children.query<PositionComponent>().every(\n              (c) => target.toAbsoluteRect().containsPoint(c.position),\n            ),\n            isTrue,\n          );\n\n          game.update(1.0);\n          game.update(0.0);\n          expect(target.children.whereType<PositionComponent>(), hasLength(2));\n          expect(\n            target.children.query<PositionComponent>().every(\n              (c) => target.toAbsoluteRect().containsPoint(c.position),\n            ),\n            isTrue,\n          );\n        },\n      );\n\n      testWithFlameGame(\n        'Spawns multiple components within target bounds',\n        (game) async {\n          final target = PositionComponent(\n            size: Vector2(200, 200),\n            position: Vector2(50, 50),\n          );\n          final spawn = SpawnComponent(\n            multiFactory: (_) => [\n              PositionComponent(),\n              PositionComponent(),\n              PositionComponent(),\n            ],\n            period: 1,\n            target: target,\n          );\n          await game.ensureAdd(target);\n          await game.ensureAdd(spawn);\n\n          game.update(1.0);\n          game.update(0.0);\n          expect(target.children.whereType<PositionComponent>(), hasLength(3));\n          expect(\n            target.children.query<PositionComponent>().every(\n              (c) => target.toAbsoluteRect().containsPoint(c.position),\n            ),\n            isTrue,\n          );\n\n          game.update(1.0);\n          game.update(0.0);\n          expect(target.children.whereType<PositionComponent>(), hasLength(6));\n          expect(\n            target.children.query<PositionComponent>().every(\n              (c) => target.toAbsoluteRect().containsPoint(c.position),\n            ),\n            isTrue,\n          );\n        },\n      );\n\n      testWithFlameGame(\n        'Spawns components with selfPositioning within target bounds',\n        (game) async {\n          final target = PositionComponent(\n            size: Vector2(200, 200),\n            position: Vector2(50, 50),\n          );\n          final spawn = SpawnComponent(\n            factory: (_) => PositionComponent(position: Vector2(100, 100)),\n            period: 1,\n            target: target,\n            selfPositioning: true,\n          );\n          await game.ensureAdd(target);\n          await game.ensureAdd(spawn);\n\n          game.update(1.0);\n          game.update(0.0);\n          expect(target.children.whereType<PositionComponent>(), hasLength(1));\n          expect(\n            target.children.query<PositionComponent>().every(\n              (c) => c.position == Vector2(100, 100),\n            ),\n            isTrue,\n          );\n\n          game.update(1.0);\n          game.update(0.0);\n          expect(target.children.whereType<PositionComponent>(), hasLength(2));\n          expect(\n            target.children.query<PositionComponent>().every(\n              (c) => c.position == Vector2(100, 100),\n            ),\n            isTrue,\n          );\n        },\n      );\n\n      testWithFlameGame(\n        'Throws assertion when target is set without area',\n        (game) async {\n          final target = Component();\n\n          expectLater(\n            game.ensureAdd(\n              SpawnComponent(\n                factory: (_) => PositionComponent(),\n                period: 1,\n                target: target,\n              ),\n            ),\n            throwsA(isA<AssertionError>()),\n          );\n        },\n      );\n    });\n  });\n}\n\nclass _CountComponent extends PositionComponent {}\n"
  },
  {
    "path": "packages/flame/test/components/sprite_animation_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nFuture<void> main() async {\n  // Generate an image\n  final image = await generateImage();\n\n  group('SpriteAnimationComponent clone and reversed', () {\n    test(\n      'clone creates independent copy',\n      () {\n        final animation = SpriteAnimation.spriteList(\n          List.filled(5, Sprite(image)),\n          stepTime: 0.1,\n          loop: false,\n        );\n        final copy = animation.clone();\n        final ticker1 = animation.createTicker();\n        final ticker2 = copy.createTicker();\n\n        expect(copy.loop, animation.loop);\n\n        ticker1.update(0.1);\n        expect(ticker1.currentIndex, 1);\n        expect(ticker2.currentIndex, 0);\n\n        ticker2.update(0.2);\n        expect(ticker1.currentIndex, 1);\n        expect(ticker2.currentIndex, 2);\n      },\n    );\n    test(\n      'reversed creates independent copy',\n      () {\n        final animation = SpriteAnimation.spriteList(\n          List.filled(5, Sprite(image)),\n          stepTime: 0.1,\n          loop: false,\n        );\n        final copy = animation.reversed();\n        final ticker1 = animation.createTicker();\n        final ticker2 = copy.createTicker();\n\n        expect(copy.loop, animation.loop);\n\n        ticker1.update(0.1);\n        expect(ticker1.currentIndex, 1);\n        expect(ticker2.currentIndex, 0);\n\n        ticker2.update(0.2);\n        expect(ticker1.currentIndex, 1);\n        expect(ticker2.currentIndex, 2);\n      },\n    );\n  });\n\n  group('SpriteAnimationComponent removal', () {\n    testWithFlameGame(\n      'removeOnFinish is true and animation#loop is false',\n      (game) async {\n        final animation = SpriteAnimation.spriteList(\n          [\n            Sprite(image),\n            Sprite(image),\n          ],\n          stepTime: 1,\n          loop: false,\n        );\n        final component = SpriteAnimationComponent(\n          animation: animation,\n          removeOnFinish: true,\n        );\n\n        final world = game.world;\n        await world.ensureAdd(component);\n\n        expect(world.children.length, 1);\n        game.update(2);\n\n        // runs a cycle to remove the component\n        game.update(0.1);\n        expect(world.children.length, 0);\n      },\n    );\n\n    testWithFlameGame(\n      'removeOnFinish is true and animation#loop is true',\n      (game) async {\n        final animation = SpriteAnimation.spriteList(\n          [\n            Sprite(image),\n            Sprite(image),\n          ],\n          stepTime: 1,\n          // ignore: avoid_redundant_argument_values\n          loop: true,\n        );\n        final component = SpriteAnimationComponent(\n          animation: animation,\n          removeOnFinish: true,\n        );\n\n        final world = game.world;\n        await world.ensureAdd(component);\n\n        expect(component.parent, world);\n        expect(world.children.length, 1);\n\n        game.update(2);\n        expect(component.parent, world);\n\n        // runs a cycle to remove the component, but failed\n        game.update(0.1);\n        expect(world.children.length, 1);\n      },\n    );\n\n    testWithFlameGame(\n      'removeOnFinish is false and animation#loop is false',\n      (game) async {\n        final animation = SpriteAnimation.spriteList(\n          [\n            Sprite(image),\n            Sprite(image),\n          ],\n          stepTime: 1,\n          loop: false,\n        );\n        final component = SpriteAnimationComponent(\n          animation: animation,\n          // ignore: avoid_redundant_argument_values\n          removeOnFinish: false,\n        );\n\n        final world = game.world;\n        await world.ensureAdd(component);\n\n        expect(component.parent, world);\n        expect(world.children.length, 1);\n\n        game.update(2);\n        expect(component.parent, world);\n\n        // runs a cycle to remove the component, but failed\n        game.update(0.1);\n        expect(world.children.length, 1);\n      },\n    );\n\n    testWithFlameGame(\n      'removeOnFinish is false and animation#loop is true',\n      (game) async {\n        final animation = SpriteAnimation.spriteList(\n          [\n            Sprite(image),\n            Sprite(image),\n          ],\n          stepTime: 1,\n          // ignore: avoid_redundant_argument_values\n          loop: true,\n        );\n        final component = SpriteAnimationComponent(\n          animation: animation,\n          // ignore: avoid_redundant_argument_values\n          removeOnFinish: false,\n        );\n\n        final world = game.world;\n        await world.ensureAdd(component);\n\n        expect(component.parent, world);\n        expect(world.children.length, 1);\n\n        game.update(2);\n        expect(component.parent, world);\n\n        // runs a cycle to remove the component, but failed\n        game.update(0.1);\n        expect(world.children.length, 1);\n      },\n    );\n\n    testWithFlameGame(\n      \"component isn't removed if it is not playing\",\n      (game) async {\n        final animation = SpriteAnimation.spriteList(\n          [\n            Sprite(image),\n            Sprite(image),\n          ],\n          stepTime: 1,\n          loop: false,\n        );\n        final component = SpriteAnimationComponent(\n          animation: animation,\n          removeOnFinish: true,\n          playing: false,\n        );\n\n        final world = game.world;\n        await world.ensureAdd(component);\n\n        expect(component.parent, world);\n        expect(world.children.length, 1);\n\n        game.update(2);\n        expect(component.parent, world);\n\n        // runs a cycle to potentially remove the component\n        game.update(0.1);\n        expect(world.children.length, 1);\n      },\n    );\n\n    testWithFlameGame(\n      'resetOnRemove is resets animation ticker after it has been removed',\n      (game) async {\n        final animation = SpriteAnimation.spriteList(\n          [\n            Sprite(image),\n            Sprite(image),\n          ],\n          stepTime: 1,\n          loop: false,\n        );\n        final component = SpriteAnimationComponent(\n          animation: animation,\n          removeOnFinish: true,\n          resetOnRemove: true,\n        );\n\n        await game.world.ensureAdd(component);\n\n        expect(component.animationTicker?.elapsed, 0);\n        game.update(2);\n\n        expect(component.animationTicker?.elapsed, 2);\n        component.removeFromParent();\n        // runs a cycle to remove the component\n        game.update(1.0);\n        expect(component.animationTicker?.elapsed, 0);\n      },\n    );\n  });\n\n  group('SpriteAnimation timing of animation frames', () {\n    test('Can move to last frame programmatically', () {\n      // Non-looping animation, with the expected total duration of 0.500 s\n      final animation = SpriteAnimation.spriteList(\n        List.filled(5, Sprite(image)),\n        stepTime: 0.1,\n        loop: false,\n      ).createTicker();\n      var callbackInvoked = 0;\n      animation.onComplete = () {\n        callbackInvoked++;\n      };\n      animation.setToLast();\n      expect(animation.currentIndex, 4);\n      expect(animation.elapsed, 0.5);\n      expect(animation.done(), true);\n      expect(callbackInvoked, 1);\n    });\n    // See https://github.com/flame-engine/flame/issues/895\n    testWithFlameGame('Last animation frame is not skipped', (game) async {\n      // Non-looping animation, with the expected total duration of 0.500 s\n      final animation = SpriteAnimation.spriteList(\n        List.filled(5, Sprite(image)),\n        stepTime: 0.1,\n        loop: false,\n      );\n      final component = SpriteAnimationComponent(animation: animation);\n      final ticker = component.animationTicker!;\n      var callbackInvoked = 0;\n      ticker.onComplete = () {\n        callbackInvoked++;\n      };\n      await game.ensureAdd(component);\n\n      game.update(0.01);\n      expect(ticker.currentIndex, 0);\n      game.update(0.1);\n      expect(ticker.currentIndex, 1);\n      game.update(0.3);\n      expect(ticker.currentIndex, 4);\n      game.update(0.089);\n      // At this point we're still on the last frame, which has\n      // almost finished. Total clock time = 0.499 s\n      expect(ticker.currentIndex, 4);\n      expect(ticker.clock, closeTo(0.099, 1e-10));\n      expect(ticker.done(), false);\n      expect(callbackInvoked, 0);\n      // This last tick moves the total clock to 0.5001 s,\n      // completing the last animation frame.\n      game.update(0.0011);\n      expect(callbackInvoked, 1);\n      expect(ticker.currentIndex, 4);\n      expect(ticker.done(), true);\n      // Now move the timer forward again, and verify that the callback won't be\n      // invoked multiple times.\n      for (var i = 0; i < 10; i++) {\n        game.update(1);\n      }\n      expect(callbackInvoked, 1);\n      expect(ticker.currentIndex, 4);\n      expect(ticker.done(), true);\n      // Lastly, let's reset the animation and see if it still works properly\n      callbackInvoked = 0;\n      ticker.reset();\n      expect(ticker.currentIndex, 0);\n      expect(ticker.done(), false);\n      game.update(100);\n      expect(callbackInvoked, 1);\n      expect(ticker.currentIndex, 4);\n      expect(ticker.done(), true);\n    });\n  });\n\n  group('SpriteAnimationComponent.autoResize', () {\n    test('mutual exclusive with size while construction', () {\n      expect(\n        () => SpriteAnimationComponent(autoResize: true, size: Vector2.all(2)),\n        throwsAssertionError,\n      );\n\n      expect(\n        () => SpriteAnimationComponent(autoResize: false),\n        throwsAssertionError,\n      );\n    });\n\n    test('default value set correctly when not provided explicitly', () {\n      final component1 = SpriteAnimationComponent();\n      final component2 = SpriteAnimationComponent(size: Vector2.all(2));\n\n      expect(component1.autoResize, true);\n      expect(component2.autoResize, false);\n    });\n\n    test('resizes on animation update', () {\n      final spriteList = [\n        Sprite(image, srcSize: Vector2.all(1)),\n        Sprite(image, srcSize: Vector2.all(2)),\n        Sprite(image, srcSize: Vector2.all(3)),\n      ];\n      final animation = SpriteAnimation.spriteList(\n        spriteList,\n        stepTime: 1,\n        loop: false,\n      );\n\n      final component = SpriteAnimationComponent(animation: animation);\n      expect(component.size, spriteList[0].srcSize);\n\n      component.update(1);\n      expect(component.size, spriteList[1].srcSize);\n\n      component.update(1);\n      expect(component.size, spriteList[2].srcSize);\n    });\n\n    test('resizes only when true', () {\n      final spriteList = [\n        Sprite(image, srcSize: Vector2.all(1)),\n        Sprite(image, srcSize: Vector2.all(2)),\n      ];\n      final animation = SpriteAnimation.spriteList(\n        spriteList,\n        stepTime: 1,\n        loop: false,\n      );\n      final component = SpriteAnimationComponent(animation: animation)\n        ..autoResize = false;\n\n      component.update(1);\n      expect(component.size, spriteList[0].srcSize);\n\n      component.autoResize = true;\n      expect(component.size, spriteList[1].srcSize);\n    });\n\n    test('stop autoResizing on external size modifications', () {\n      final spriteList = [\n        Sprite(image, srcSize: Vector2.all(1)),\n        Sprite(image, srcSize: Vector2.all(2)),\n      ];\n      final animation = SpriteAnimation.spriteList(\n        spriteList,\n        stepTime: 1,\n        loop: false,\n      );\n      final testSize = Vector2(83, 100);\n      final component = SpriteAnimationComponent();\n\n      // NOTE: Sequence of modifications is important here. Changing the size\n      // first disables the auto-resizing. So even if animation is changed\n      // later, the component should still maintain testSize.\n      component\n        ..size = testSize\n        ..animation = animation;\n\n      expectDouble(component.size.x, testSize.x);\n      expectDouble(component.size.y, testSize.y);\n    });\n\n    test('modify size only if changed while auto-resizing', () {\n      final spriteList = [\n        Sprite(image, srcSize: Vector2.all(1)),\n        Sprite(image, srcSize: Vector2.all(1)),\n        Sprite(image, srcSize: Vector2(2, 1)),\n      ];\n      final animation = SpriteAnimation.spriteList(spriteList, stepTime: 1);\n      final component = SpriteAnimationComponent(animation: animation);\n\n      var sizeChangeCounter = 0;\n      component.size.addListener(() => ++sizeChangeCounter);\n\n      component.update(1);\n      expect(sizeChangeCounter, equals(0));\n\n      component.update(0.5);\n      expect(sizeChangeCounter, equals(0));\n\n      component.update(0.5);\n      expect(sizeChangeCounter, equals(1));\n\n      component.update(1);\n      expect(sizeChangeCounter, equals(2));\n\n      for (var i = 0; i < 15; ++i) {\n        component.update(1);\n      }\n      expect(sizeChangeCounter, equals(12));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/sprite_animation_group_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nenum _AnimationState {\n  idle,\n  running,\n}\n\nFuture<void> main() async {\n  // Generate a image\n  final image = await generateImage();\n\n  group('SpriteAnimationGroupComponent', () {\n    test('returns the correct animation according to its state', () {\n      final animation1 = SpriteAnimation.spriteList(\n        [\n          Sprite(image),\n          Sprite(image),\n        ],\n        stepTime: 1,\n      );\n      final animation2 = SpriteAnimation.spriteList(\n        [\n          Sprite(image),\n          Sprite(image),\n        ],\n        stepTime: 1,\n      );\n      final component = SpriteAnimationGroupComponent<_AnimationState>(\n        animations: {\n          _AnimationState.idle: animation1,\n          _AnimationState.running: animation2,\n        },\n      );\n\n      // No state was specified so nothing is playing\n      expect(component.animation, null);\n\n      // Setting the idle state, we need to see the animation1\n      component.current = _AnimationState.idle;\n      expect(component.animation, animation1);\n\n      // Setting the running state, we need to see the animation2\n      component.current = _AnimationState.running;\n      expect(component.animation, animation2);\n    });\n\n    test('Asserts that map contains key', () {\n      expect(\n        () {\n          SpriteAnimationGroupComponent<String>(animations: {}).current =\n              'non-existent-key';\n        },\n        failsAssert('Animation not found for key: non-existent-key'),\n      );\n    });\n  });\n\n  group('SpriteAnimationGroupComponent.shouldRemove', () {\n    testWithFlameGame('removeOnFinish is true and there is no any state yet', (\n      game,\n    ) async {\n      final animation = SpriteAnimation.spriteList(\n        [\n          Sprite(image),\n          Sprite(image),\n        ],\n        stepTime: 1,\n        loop: false,\n      );\n      final component = SpriteAnimationGroupComponent<_AnimationState>(\n        animations: {_AnimationState.idle: animation},\n        removeOnFinish: {_AnimationState.idle: true},\n      );\n\n      final world = game.world;\n      await world.ensureAdd(component);\n      expect(component.parent, world);\n      expect(world.children.length, 1);\n\n      game.update(2);\n      expect(component.parent, world);\n\n      // runs a cycle and the component should still be there\n      game.update(0.1);\n      expect(world.children.length, 1);\n    });\n\n    testWithFlameGame(\n      'removeOnFinish is true and current state animation#loop is false',\n      (game) async {\n        final animation = SpriteAnimation.spriteList(\n          [\n            Sprite(image),\n            Sprite(image),\n          ],\n          stepTime: 1,\n          loop: false,\n        );\n        final component = SpriteAnimationGroupComponent<_AnimationState>(\n          animations: {_AnimationState.idle: animation},\n          removeOnFinish: {_AnimationState.idle: true},\n          current: _AnimationState.idle,\n        );\n\n        final world = game.world;\n        await world.ensureAdd(component);\n        expect(world.children.length, 1);\n\n        game.update(2);\n\n        // runs a cycle to remove the component\n        game.update(0.1);\n        expect(world.children.length, 0);\n      },\n    );\n\n    testWithFlameGame(\n      'removeOnFinish is true and current animation#loop is true',\n      (game) async {\n        final animation = SpriteAnimation.spriteList(\n          [\n            Sprite(image),\n            Sprite(image),\n          ],\n          stepTime: 1,\n          // ignore: avoid_redundant_argument_values\n          loop: true,\n        );\n        final component = SpriteAnimationGroupComponent<_AnimationState>(\n          animations: {_AnimationState.idle: animation},\n          removeOnFinish: {_AnimationState.idle: true},\n          current: _AnimationState.idle,\n        );\n\n        final world = game.world;\n        await world.ensureAdd(component);\n        expect(component.parent, world);\n        expect(world.children.length, 1);\n\n        game.update(2);\n        expect(component.parent, world);\n\n        // runs a cycle to remove the component, but failed\n        game.update(0.1);\n        expect(world.children.length, 1);\n      },\n    );\n\n    testWithFlameGame(\n      'removeOnFinish is false and current animation#loop is false',\n      (game) async {\n        final animation = SpriteAnimation.spriteList(\n          [\n            Sprite(image),\n            Sprite(image),\n          ],\n          stepTime: 1,\n          loop: false,\n        );\n        final component = SpriteAnimationGroupComponent<_AnimationState>(\n          animations: {_AnimationState.idle: animation},\n          current: _AnimationState.idle,\n          // when omitted, removeOnFinish is false for all states\n        );\n\n        final world = game.world;\n        await world.ensureAdd(component);\n        expect(component.parent, world);\n        expect(world.children.length, 1);\n\n        game.update(2);\n        expect(component.parent, world);\n\n        // runs a cycle to remove the component, but failed\n        game.update(0.1);\n        expect(world.children.length, 1);\n      },\n    );\n\n    testWithFlameGame(\n      'removeOnFinish is false and current animation#loop is true',\n      (game) async {\n        final animation = SpriteAnimation.spriteList(\n          [\n            Sprite(image),\n            Sprite(image),\n          ],\n          stepTime: 1,\n          // ignore: avoid_redundant_argument_values\n          loop: true,\n        );\n        final component = SpriteAnimationGroupComponent<_AnimationState>(\n          animations: {_AnimationState.idle: animation},\n          // when omitted, removeOnFinish is false for all states\n          current: _AnimationState.idle,\n        );\n\n        final world = game.world;\n        await world.ensureAdd(component);\n\n        expect(component.parent, world);\n        expect(world.children.length, 1);\n\n        game.update(2);\n        expect(component.parent, world);\n\n        // runs a cycle to remove the component, but failed\n        game.update(0.1);\n        expect(world.children.length, 1);\n      },\n    );\n  });\n\n  group('SpriteAnimationGroupComponent.currentAnimationNotifier', () {\n    test('notifies when the current animation changes', () {\n      final sprite1 = Sprite(image, srcSize: Vector2.all(76));\n      final sprite2 = Sprite(image, srcSize: Vector2.all(15));\n      final animation1 = SpriteAnimation.spriteList(\n        List.filled(5, sprite1),\n        stepTime: 1,\n        loop: false,\n      );\n      final animation2 = SpriteAnimation.spriteList(\n        [sprite2, sprite1],\n        stepTime: 1,\n      );\n      final animationsMap = {\n        _AnimationState.idle: animation1,\n        _AnimationState.running: animation2,\n      };\n      final component = SpriteAnimationGroupComponent<_AnimationState>(\n        animations: animationsMap,\n      );\n      var animationChangeCounter = 0;\n      component.currentAnimationNotifier.addListener(\n        () => animationChangeCounter++,\n      );\n\n      component.current = _AnimationState.running;\n      expect(animationChangeCounter, equals(1));\n\n      component.current = _AnimationState.idle;\n      expect(animationChangeCounter, equals(2));\n\n      component.update(1);\n      expect(animationChangeCounter, equals(2));\n\n      component.current = _AnimationState.running;\n      expect(animationChangeCounter, equals(3));\n\n      component.update(1);\n      expect(animationChangeCounter, equals(3));\n\n      component.current = _AnimationState.running;\n      component.update(1);\n      expect(animationChangeCounter, equals(3));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/sprite_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nFuture<void> main() async {\n  final image = await generateImage();\n\n  group('SpriteComponent', () {\n    test('check sizing of SpriteComponent', () {\n      expect(image.width, 1);\n      expect(image.height, 1);\n\n      final component1 = SpriteComponent.fromImage(image);\n      expect(component1.size, Vector2(1, 1));\n\n      final component2 = SpriteComponent.fromImage(image, size: Vector2(5, 10));\n      expect(component2.size, Vector2(5, 10));\n\n      final component3 = SpriteComponent.fromImage(\n        image,\n        srcSize: Vector2(4, 3),\n      );\n      expect(component3.size, Vector2(4, 3));\n\n      final component4 = SpriteComponent.fromImage(\n        image,\n        size: Vector2(40, 30),\n        srcSize: Vector2(4, 3),\n      );\n      expect(component4.size, Vector2(40, 30));\n\n      final sprite = Sprite(image, srcSize: Vector2(2, 2));\n      final component5 = SpriteComponent(sprite: sprite, size: Vector2(10, 10));\n      expect(component5.size, Vector2(10, 10));\n\n      final component6 = SpriteComponent(sprite: sprite);\n      expect(component6.size, Vector2(2, 2));\n    });\n  });\n\n  group('SpriteComponent.autoResize', () {\n    test('mutual exclusive with size while construction', () {\n      expect(\n        () => SpriteComponent(autoResize: true, size: Vector2.all(2)),\n        throwsAssertionError,\n      );\n      expect(\n        () => SpriteComponent.fromImage(\n          image,\n          autoResize: true,\n          size: Vector2.all(2),\n        ),\n        throwsAssertionError,\n      );\n\n      expect(() => SpriteComponent(autoResize: false), throwsAssertionError);\n      expect(\n        () => SpriteComponent.fromImage(image, autoResize: false),\n        throwsAssertionError,\n      );\n    });\n\n    test('default value set correctly when not provided explicitly', () {\n      final component1 = SpriteComponent();\n      final component2 = SpriteComponent(size: Vector2.all(2));\n\n      expect(component1.autoResize, true);\n      expect(component2.autoResize, false);\n    });\n\n    test('resizes on sprite change', () {\n      final sprite1 = Sprite(image);\n      final sprite2 = Sprite(image, srcSize: Vector2.all(13));\n\n      final component = SpriteComponent()..sprite = sprite1;\n      expect(component.size, sprite1.srcSize);\n\n      component.sprite = sprite2;\n      expect(component.size, sprite2.srcSize);\n    });\n\n    test('resizes only when true', () {\n      final sprite1 = Sprite(image);\n      final sprite2 = Sprite(image, srcSize: Vector2.all(13));\n      final component = SpriteComponent(sprite: sprite1)..autoResize = false;\n\n      component.sprite = sprite2;\n      expect(component.size, sprite1.srcSize);\n\n      component.autoResize = true;\n      expect(component.size, sprite2.srcSize);\n    });\n\n    test('stop autoResizing on external size modifications', () {\n      final testSize = Vector2(83, 100);\n      final sprite = Sprite(image, srcSize: Vector2.all(13));\n      final component = SpriteComponent();\n\n      // NOTE: Sequence of modifications is important here. Changing the size\n      // first disables the auto-resizing. So even if sprite is changed later,\n      // the component should still maintain testSize.\n      component\n        ..size = testSize\n        ..sprite = sprite;\n\n      expectDouble(component.size.x, testSize.x);\n      expectDouble(component.size.y, testSize.y);\n    });\n\n    test('modify size only if changed while auto-resizing', () {\n      final sprite1 = Sprite(image, srcSize: Vector2.all(13));\n      final sprite2 = Sprite(image, srcSize: Vector2.all(13));\n      final sprite3 = Sprite(image, srcSize: Vector2(13, 14));\n      final component = SpriteComponent();\n\n      var sizeChangeCounter = 0;\n      component.size.addListener(() => ++sizeChangeCounter);\n\n      component.sprite = sprite1;\n      expect(sizeChangeCounter, equals(1));\n\n      component.sprite = sprite2;\n      expect(sizeChangeCounter, equals(1));\n\n      component.sprite = sprite3;\n      expect(sizeChangeCounter, equals(2));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/sprite_group_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nenum _SpriteState {\n  idle,\n  running,\n  flying,\n}\n\nFuture<void> main() async {\n  // Generate a image\n  final image = await generateImage();\n\n  group('SpriteGroupComponent', () {\n    test('returns the correct sprite according to its state', () {\n      final sprite1 = Sprite(image);\n      final sprite2 = Sprite(image);\n      final component = SpriteGroupComponent<_SpriteState>(\n        sprites: {\n          _SpriteState.idle: sprite1,\n          _SpriteState.running: sprite2,\n        },\n      );\n\n      // No state was specified so nothing is playing\n      expect(component.sprite, null);\n\n      // Setting the idle state, we need to see the sprite1\n      component.current = _SpriteState.idle;\n      expect(component.sprite, sprite1);\n\n      // Setting the running state, we need to see the sprite2\n      component.current = _SpriteState.running;\n      expect(component.sprite, sprite2);\n    });\n\n    test('Asserts that map contains key', () {\n      expect(\n        () {\n          SpriteGroupComponent<String>(sprites: {}).current =\n              'non-existent-key';\n        },\n        failsAssert('Sprite not found for key: non-existent-key'),\n      );\n    });\n  });\n\n  group('SpriteComponent.autoResize', () {\n    test('mutual exclusive with size while construction', () {\n      expect(\n        () => SpriteGroupComponent<_SpriteState>(\n          autoResize: true,\n          size: Vector2.all(2),\n        ),\n        throwsAssertionError,\n      );\n\n      expect(\n        () => SpriteGroupComponent<_SpriteState>(autoResize: false),\n        throwsAssertionError,\n      );\n    });\n\n    test('default value set correctly when not provided explicitly', () {\n      final component1 = SpriteGroupComponent<_SpriteState>();\n      final component2 = SpriteGroupComponent<_SpriteState>(\n        size: Vector2.all(2),\n      );\n\n      expect(component1.autoResize, true);\n      expect(component2.autoResize, false);\n    });\n\n    test('resizes on current state change', () {\n      final sprite1 = Sprite(image);\n      final sprite2 = Sprite(image, srcSize: Vector2.all(15));\n\n      final component = SpriteGroupComponent<_SpriteState>(\n        sprites: {_SpriteState.idle: sprite1, _SpriteState.running: sprite2},\n        current: _SpriteState.idle,\n      );\n      expect(component.size, sprite1.srcSize);\n\n      component.current = _SpriteState.running;\n      expect(component.size, sprite2.srcSize);\n    });\n\n    test('resizes only when true', () {\n      final sprite1 = Sprite(image);\n      final sprite2 = Sprite(image, srcSize: Vector2.all(15));\n      final component = SpriteGroupComponent<_SpriteState>(\n        sprites: {_SpriteState.idle: sprite1, _SpriteState.running: sprite2},\n        current: _SpriteState.idle,\n      )..autoResize = false;\n\n      component.current = _SpriteState.running;\n      expect(component.size, sprite1.srcSize);\n\n      component.autoResize = true;\n      expect(component.size, sprite2.srcSize);\n    });\n\n    test('stop autoResizing on external size modifications', () {\n      final testSize = Vector2(83, 100);\n      final spritesMap = {\n        _SpriteState.idle: Sprite(image),\n        _SpriteState.running: Sprite(image, srcSize: Vector2.all(15)),\n      };\n      final component = SpriteGroupComponent<_SpriteState>();\n\n      // NOTE: Sequence of modifications is important here. Changing the size\n      // first disables the auto-resizing. So even if sprites map is changed\n      // later, the component should still maintain testSize.\n      component\n        ..size = testSize\n        ..sprites = spritesMap\n        ..current = _SpriteState.running;\n\n      expectDouble(component.size.x, testSize.x);\n      expectDouble(component.size.y, testSize.y);\n    });\n\n    test('modify size only if changed while auto-resizing', () {\n      final spritesMap = {\n        _SpriteState.idle: Sprite(image, srcSize: Vector2.all(15)),\n        _SpriteState.running: Sprite(image, srcSize: Vector2.all(15)),\n        _SpriteState.flying: Sprite(image, srcSize: Vector2(15, 12)),\n      };\n      final component = SpriteGroupComponent<_SpriteState>(sprites: spritesMap);\n\n      var sizeChangeCounter = 0;\n      component.size.addListener(() => ++sizeChangeCounter);\n\n      component.current = _SpriteState.running;\n      expect(sizeChangeCounter, equals(1));\n\n      component.current = _SpriteState.idle;\n      expect(sizeChangeCounter, equals(1));\n\n      component.current = _SpriteState.flying;\n      expect(sizeChangeCounter, equals(2));\n    });\n  });\n\n  group('SpriteGroupComponent.currentSpriteNotifier', () {\n    test('notifies when the current sprite changes', () {\n      final spritesMap = {\n        _SpriteState.idle: Sprite(image, srcSize: Vector2.all(15)),\n        _SpriteState.running: Sprite(image, srcSize: Vector2.all(15)),\n        _SpriteState.flying: Sprite(image, srcSize: Vector2(15, 12)),\n      };\n      final component = SpriteGroupComponent<_SpriteState>(\n        sprites: spritesMap,\n      );\n      var spriteChangeCounter = 0;\n      component.currentSpriteNotifier.addListener(\n        () => spriteChangeCounter++,\n      );\n\n      component.current = _SpriteState.running;\n      expect(spriteChangeCounter, equals(1));\n\n      component.current = _SpriteState.idle;\n      expect(spriteChangeCounter, equals(2));\n\n      component.update(1);\n      expect(spriteChangeCounter, equals(2));\n\n      component.current = _SpriteState.running;\n      expect(spriteChangeCounter, equals(3));\n\n      component.update(1);\n      expect(spriteChangeCounter, equals(3));\n\n      component.current = _SpriteState.running;\n      component.update(1);\n      expect(spriteChangeCounter, equals(3));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/text_box_component_test.dart",
    "content": "import 'package:canvas_test/canvas_test.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/painting.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('TextBoxComponent', () {\n    test('size is properly computed', () {\n      final c = TextBoxComponent(\n        text: 'The quick brown fox jumps over the lazy dog.',\n        boxConfig: const TextBoxConfig(\n          maxWidth: 100.0,\n        ),\n      );\n\n      expect(c.size.x, 100 + 2 * 8);\n      expect(c.size.y, greaterThan(1));\n    });\n\n    test('size is properly computed with new line character', () {\n      final c = TextBoxComponent(\n        text: 'The quick brown fox \\n jumps over the lazy dog.',\n        boxConfig: const TextBoxConfig(\n          maxWidth: 100.0,\n        ),\n      );\n\n      expect(c.size.x, 100 + 2 * 8);\n      expect(c.lines.length, greaterThan(2));\n      expect(c.size.y, greaterThan(50));\n    });\n\n    test('lines are properly computed with new line character', () {\n      final c = TextBoxComponent(\n        text: 'The quick brown fox \\n jumps over the lazy dog.',\n        boxConfig: const TextBoxConfig(\n          maxWidth: 400.0,\n        ),\n      );\n\n      // TextPainter preserves trailing spaces before line breaks\n      expect(\n        c.lines,\n        ['The quick brown ', 'fox ', ' jumps over the ', 'lazy dog.'],\n      );\n    });\n\n    test('Chinese text wraps correctly without spaces', () {\n      const maxWidth = 100.0;\n      final c = TextBoxComponent(\n        text:\n            '''编写这些测试的人明显是一个天才（而且非常谦虚）。他们值得终生免费咖啡。这些测试需要奥妙的智慧、无穷的耐心和可能太多的咖啡因。''',\n        textRenderer: TextPaint(\n          style: const TextStyle(fontSize: 16),\n        ),\n        boxConfig: const TextBoxConfig(\n          maxWidth: maxWidth,\n        ),\n      );\n\n      // With the fix, Chinese text should wrap across multiple lines\n      // Without the fix, it would be a single long line\n      expect(c.lines.length, greaterThan(1));\n\n      // Each line should be shorter than maxWidth\n      for (final line in c.lines) {\n        final lineMetrics = c.textRenderer.getLineMetrics(line);\n        expect(\n          lineMetrics.width,\n          lessThanOrEqualTo(maxWidth),\n          reason: 'Line \"$line\" exceeds maxWidth',\n        );\n      }\n    });\n\n    test('Japanese text wraps correctly without spaces', () {\n      const maxWidth = 100.0;\n      final c = TextBoxComponent(\n        text:\n            '''これらのテストを書いた人は明らかに天才です（そしてとても謙虚です）。彼らは生涉無料コーヒーを受けるに値します。これらのテストは絶妙な知恵、無限の忍耐、そしておそらくカフェインの取りすぎが必要でした。''',\n        textRenderer: TextPaint(\n          style: const TextStyle(fontSize: 16),\n        ),\n        boxConfig: const TextBoxConfig(\n          maxWidth: maxWidth,\n        ),\n      );\n\n      // Japanese text should also wrap across multiple lines\n      expect(c.lines.length, greaterThan(1));\n\n      // Each line should be shorter than maxWidth\n      for (final line in c.lines) {\n        final lineMetrics = c.textRenderer.getLineMetrics(line);\n        expect(\n          lineMetrics.width,\n          lessThanOrEqualTo(maxWidth),\n          reason: 'Line \"$line\" exceeds maxWidth',\n        );\n      }\n    });\n\n    test('Korean text wraps correctly without spaces', () {\n      const maxWidth = 100.0;\n      final c = TextBoxComponent(\n        text:\n            '''이테스트를작성한사람은분명천재입니다（그리고매우겸손합니다）。그들은평생무료커피를받을자격이있습니다。이테스트들은놀라운지혜와무한한인내심그리고아마도너무많은카페인이필요했습니다。''',\n        textRenderer: TextPaint(\n          style: const TextStyle(fontSize: 16),\n        ),\n        boxConfig: const TextBoxConfig(\n          maxWidth: maxWidth,\n        ),\n      );\n\n      // Korean text should also wrap across multiple lines\n      expect(c.lines.length, greaterThan(1));\n\n      // Each line should be shorter than maxWidth\n      for (final line in c.lines) {\n        final lineMetrics = c.textRenderer.getLineMetrics(line);\n        expect(\n          lineMetrics.width,\n          lessThanOrEqualTo(maxWidth),\n          reason: 'Line \"$line\" exceeds maxWidth',\n        );\n      }\n    });\n\n    test('boxConfig gets set', () {\n      const firstConfig = TextBoxConfig(maxWidth: 400, timePerChar: 0.1);\n      const secondConfig = TextBoxConfig(maxWidth: 300, timePerChar: 0.2);\n      final c = TextBoxComponent(\n        text: 'The quick brown fox jumps over the lazy dog.',\n        boxConfig: firstConfig,\n      );\n      expect(\n        c.boxConfig,\n        firstConfig,\n      );\n      c.boxConfig = secondConfig;\n      expect(\n        c.boxConfig,\n        secondConfig,\n      );\n    });\n\n    test('maxWidth causes text reflow', () async {\n      const firstConfig = TextBoxConfig();\n      const secondConfig = TextBoxConfig(maxWidth: 640);\n      final c = TextBoxComponent(\n        text: 'The quick brown fox jumps over the lazy dog.',\n        boxConfig: firstConfig,\n        textRenderer: TextPaint(\n          style: const TextStyle(fontSize: 10),\n        ),\n      );\n      c.boxConfig = secondConfig;\n      expect(c.lines.length, 1);\n    });\n\n    test('skip method sets boxConfig timePerChar to 0', () {\n      const firstConfig = TextBoxConfig(maxWidth: 400, timePerChar: 0.1);\n      final c = TextBoxComponent(\n        text: 'The quick brown fox jumps over the lazy dog.',\n        boxConfig: firstConfig,\n      );\n      expect(\n        c.boxConfig,\n        firstConfig,\n      );\n      c.skip();\n      expect(c.boxConfig.timePerChar, 0);\n      // other props are preserved\n      expect(c.boxConfig.maxWidth, 400);\n    });\n\n    testWithFlameGame(\n      'setting dismissDelay removes component when finished',\n      (game) async {\n        final component = TextBoxComponent(\n          text: 'foo bar',\n          boxConfig: const TextBoxConfig(\n            dismissDelay: 10.0,\n            timePerChar: 1.0,\n          ),\n        );\n\n        await game.ensureAdd(component);\n        game.update(8);\n        expect(component.isMounted, isTrue);\n        game.update(9);\n        expect(component.finished, isTrue);\n        expect(component.isRemoving, isTrue);\n        game.update(0);\n        expect(component.isMounted, isFalse);\n      },\n    );\n\n    testWithFlameGame('onLoad waits for cache to be done', (game) async {\n      final c = TextBoxComponent(text: 'foo bar');\n\n      await game.ensureAdd(c);\n\n      final canvas = MockCanvas();\n      game.render(canvas); // this should render the cache\n      expect(\n        canvas,\n        MockCanvas(mode: AssertionMode.containsAnyOrder)..drawImage(\n          null,\n          Offset.zero,\n          BasicPalette.white.paint(),\n        ),\n      );\n    });\n\n    testWithFlameGame(\n      'internal image is disposed when component is removed',\n      (game) async {\n        final c = TextBoxComponent(text: 'foo bar');\n\n        await game.ensureAdd(c);\n        final imageCache = c.cache;\n\n        final canvas = MockCanvas();\n        game.render(canvas);\n        game.remove(c);\n        game.update(0);\n        expect(imageCache, isNotNull);\n        expect(imageCache!.debugDisposed, isTrue);\n        expect(c.cache, null);\n      },\n    );\n\n    testWithFlameGame(\n      'internal image is redrawn when component is re-added',\n      (game) async {\n        final c = TextBoxComponent(text: 'foo bar');\n\n        await game.ensureAdd(c);\n        game.remove(c);\n        game.update(0);\n        await game.ensureAdd(c);\n        expect(c.isMounted, true);\n\n        await null;\n        expect(c.cache, isNotNull);\n        expect(c.cache!.debugDisposed, isFalse);\n      },\n    );\n\n    testWithFlameGame(\n      'TextBoxComponent notifies if a new line is added and requires space',\n      (game) async {\n        var lineSize = 0.0;\n        final textBoxComponent = TextBoxComponent(\n          size: Vector2(50, 50),\n          text: '''This \ntest\nhas\nfive\nlines.''',\n        );\n        expect(textBoxComponent.newLinePositionNotifier.value, equals(0));\n\n        textBoxComponent.newLinePositionNotifier.addListener(() {\n          lineSize += textBoxComponent.newLinePositionNotifier.value;\n        });\n        await game.ensureAdd(textBoxComponent);\n        expect(lineSize, greaterThan(0));\n      },\n    );\n\n    testWithFlameGame('TextBoxComponent skips to the end of text', (\n      game,\n    ) async {\n      final textBoxComponent1 = TextBoxComponent(\n        text: 'aaa',\n        boxConfig: const TextBoxConfig(timePerChar: 1.0),\n      );\n      await game.ensureAdd(textBoxComponent1);\n      // forward time by 2.5 seconds\n      game.update(2.5);\n      expect(textBoxComponent1.finished, false);\n      // flush\n      game.update(0.6);\n      expect(textBoxComponent1.finished, true);\n\n      // reset\n      await game.ensureRemove(textBoxComponent1);\n\n      final textBoxComponent2 = TextBoxComponent(\n        text: 'aaa',\n        boxConfig: const TextBoxConfig(timePerChar: 1.0),\n      );\n      await game.ensureAdd(textBoxComponent2);\n      expect(textBoxComponent2.finished, false);\n      // Simulate running 0.5 seconds before skipping\n      game.update(0.5);\n      textBoxComponent2.skip();\n      expect(textBoxComponent2.finished, true);\n    });\n\n    testWithFlameGame(\n      'TextBoxComponent resets animation to the start of text',\n      (\n        game,\n      ) async {\n        final textBoxComponent1 = TextBoxComponent(\n          text: 'aaa',\n          boxConfig: const TextBoxConfig(timePerChar: 1.0),\n        );\n        await game.ensureAdd(textBoxComponent1);\n        // forward time by 2.5 seconds\n        game.update(2.5);\n        expect(textBoxComponent1.finished, false);\n        // flush\n        game.update(0.6);\n        expect(textBoxComponent1.finished, true);\n        // reset animation\n        textBoxComponent1.resetAnimation();\n        // 'finished' state should reset immediately\n        expect(textBoxComponent1.finished, false);\n        expect(textBoxComponent1.currentChar, 0);\n        expect(textBoxComponent1.currentLine, 0);\n        game.update(2.5);\n        expect(textBoxComponent1.finished, false);\n        game.update(0.6);\n        expect(textBoxComponent1.finished, true);\n      },\n    );\n\n    testGolden(\n      'Alignment options',\n      (game, tester) async {\n        game.addAll([\n          _FramedTextBox(\n            text: 'I strike quickly, being moved.',\n            position: Vector2(10.5, 10),\n            size: Vector2(390, 100),\n            align: Anchor.topLeft,\n          ),\n          _FramedTextBox(\n            text: 'But thou art not quickly moved to strike.',\n            position: Vector2(10, 120),\n            size: Vector2(390, 115),\n            align: Anchor.topCenter,\n          ),\n          _FramedTextBox(\n            text: 'A dog of the house of Montague moves me.',\n            position: Vector2(10, 245),\n            size: Vector2(390, 115),\n            align: Anchor.topRight,\n          ),\n          _FramedTextBox(\n            // cSpell:ignore runn'st (old english)\n            text:\n                'To move is to stir, and to be valiant is to stand. '\n                \"Therefore, if thou art moved, thou runn'st away.\",\n            position: Vector2(10, 370),\n            size: Vector2(390, 220),\n            align: Anchor.bottomRight,\n          ),\n          _FramedTextBox(\n            text:\n                'A dog of that house shall move me to stand. I will take '\n                'the wall of any man or maid of Montague‘s.',\n            position: Vector2(410, 10),\n            size: Vector2(380, 300),\n            align: Anchor.center,\n          ),\n          _FramedTextBox(\n            text:\n                'That shows thee a weak slave; for the weakest goes to the '\n                'wall.',\n            position: Vector2(410, 320) + Vector2(380, 270),\n            size: Vector2(380, 270),\n            align: Anchor.centerRight,\n            anchor: Anchor.bottomRight,\n          ),\n        ]);\n      },\n      goldenFile: '../_goldens/text_box_component_test_1.png',\n    );\n  });\n\n  testGolden(\n    'Big upscale',\n    (game, tester) async {\n      game.addAll([\n        TextBoxComponent(\n          text: 'quickly',\n          pixelRatio: 8,\n        ),\n      ]);\n    },\n    size: Vector2(512, 64),\n    goldenFile: '../_goldens/text_box_component_test_2.png',\n  );\n}\n\nclass _FramedTextBox extends TextBoxComponent {\n  _FramedTextBox({\n    required String super.text,\n    super.align,\n    super.position,\n    super.size,\n    super.anchor,\n  }) : super(\n         textRenderer: DebugTextRenderer(fontSize: 22),\n       );\n\n  final Paint _borderPaint = Paint()\n    ..style = PaintingStyle.stroke\n    ..strokeWidth = 2\n    ..color = const Color(0xff00ff00);\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRRect(\n      RRect.fromRectAndRadius(size.toRect(), const Radius.circular(5)),\n      _borderPaint,\n    );\n    super.render(canvas);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/components/text_component_test.dart",
    "content": "import 'package:flame/src/components/text_component.dart';\nimport 'package:test/test.dart';\nimport 'package:vector_math/vector_math.dart';\n\nvoid main() {\n  group('TextComponent', () {\n    test('sets the size of the text component', () {\n      final t = TextComponent(text: 'foobar');\n      expect(t.size, isNot(equals(Vector2.zero())));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/timer_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _MyTimerComponent extends TimerComponent {\n  int count = 0;\n\n  _MyTimerComponent()\n    : super(\n        period: 1,\n        repeat: true,\n        removeOnFinish: false,\n      );\n\n  @override\n  void onTick() {\n    count++;\n  }\n}\n\nclass _MyTickOnLoadTimerComponent extends TimerComponent {\n  int count = 0;\n\n  _MyTickOnLoadTimerComponent()\n    : super(\n        period: 1,\n        repeat: true,\n        removeOnFinish: false,\n        tickWhenLoaded: true,\n      );\n\n  @override\n  void onTick() {\n    count++;\n  }\n}\n\nclass _NonRepeatingTimerComponent extends TimerComponent {\n  _NonRepeatingTimerComponent()\n    : super(\n        period: 1,\n        repeat: false,\n        removeOnFinish: true,\n      );\n}\n\nvoid main() {\n  group('TimerComponent', () {\n    testWithFlameGame('runs the tick method', (game) async {\n      final timer = _MyTimerComponent();\n      game.add(timer);\n      await game.ready();\n      game.update(0);\n\n      game.update(1.2);\n\n      game.update(0);\n      expect(timer.count, equals(1));\n    });\n\n    testWithFlameGame('never remove from the game when is repeating', (\n      game,\n    ) async {\n      final world = game.world;\n      world.add(_MyTimerComponent());\n      await game.ready();\n      game.update(0);\n\n      game.update(1.2);\n\n      game.update(0);\n      expect(world.children.length, equals(1));\n    });\n\n    testWithFlameGame('is removed from the game when is finished', (\n      game,\n    ) async {\n      final world = game.world;\n      world.add(_NonRepeatingTimerComponent());\n      game.update(0);\n\n      game.update(1.2);\n\n      game.update(0);\n      expect(world.children.length, equals(0));\n    });\n\n    testWithFlameGame('calls onTick when provided', (game) async {\n      var called = false;\n      game.add(\n        TimerComponent(\n          period: 1,\n          onTick: () {\n            called = true;\n          },\n        ),\n      );\n      await game.ready();\n      game.update(0);\n      game.update(1.2);\n\n      expect(called, isTrue);\n    });\n\n    testWithFlameGame(\n      'runs the tick method on load when tickWhenLoaded is true',\n      (game) async {\n        final timer = _MyTickOnLoadTimerComponent();\n        game.add(timer);\n        await game.ready();\n        expect(timer.count, equals(1));\n\n        game.update(0);\n\n        game.update(1.2);\n\n        game.update(0);\n        expect(timer.count, equals(2));\n      },\n    );\n\n    testWithFlameGame(\n      'when tickCount is provided, stops after that many times',\n      (game) async {\n        var x = 0;\n        final timer = TimerComponent(\n          period: 1,\n          repeat: true,\n          tickCount: 2,\n          onTick: () {\n            x++;\n          },\n        );\n        game.add(timer);\n        await game.ready();\n        game.update(0);\n\n        game.update(1.2);\n\n        game.update(0);\n        expect(x, equals(1));\n\n        game.update(1.2);\n\n        game.update(0);\n        expect(x, equals(2));\n\n        game.update(1.2);\n\n        game.update(0);\n        expect(x, equals(2));\n\n        await game.ready();\n\n        expect(timer.timer.isRunning(), isFalse);\n        expect(timer.isRemoved, isFalse);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/components/toogle_button_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/src/events/flame_game_mixins/multi_tap_dispatcher.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('ToggleButtonComponent', () {\n    testWithFlameGame('correctly registers taps', (game) async {\n      var pressedTimes = 0;\n      final initialGameSize = Vector2.all(200);\n      final componentSize = Vector2.all(10);\n      final buttonPosition = Vector2.all(100);\n      late final ToggleButtonComponent button;\n      game.onGameResize(initialGameSize);\n      await game.ensureAdd(\n        button = ToggleButtonComponent(\n          defaultSkin: RectangleComponent(size: componentSize),\n          defaultSelectedSkin: RectangleComponent(size: componentSize),\n          onPressed: () => pressedTimes++,\n          position: buttonPosition,\n          size: componentSize,\n        ),\n      );\n\n      expect(pressedTimes, 0);\n      final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n      tapDispatcher.handleTapDown(1, TapDownDetails());\n      expect(pressedTimes, 0);\n\n      tapDispatcher.handleTapUp(\n        1,\n        createTapUpDetails(\n          globalPosition: button.positionOfAnchor(Anchor.center).toOffset(),\n        ),\n      );\n      expect(pressedTimes, 0);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: buttonPosition.toOffset()),\n      );\n      expect(pressedTimes, 1);\n\n      tapDispatcher.handleTapUp(\n        1,\n        createTapUpDetails(globalPosition: buttonPosition.toOffset()),\n      );\n      expect(pressedTimes, 1);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: buttonPosition.toOffset()),\n      );\n      tapDispatcher.handleTapCancel(1);\n      expect(pressedTimes, 2);\n    });\n\n    testWithFlameGame('correctly registers taps onGameResize', (game) async {\n      var pressedTimes = 0;\n      final initialGameSize = Vector2.all(100);\n      final componentSize = Vector2.all(10);\n      final buttonPosition = Vector2.all(100);\n      late final ToggleButtonComponent button;\n      game.onGameResize(initialGameSize);\n      await game.ensureAdd(\n        button = ToggleButtonComponent(\n          defaultSkin: RectangleComponent(size: componentSize),\n          defaultSelectedSkin: RectangleComponent(size: componentSize),\n          onPressed: () => pressedTimes++,\n          position: buttonPosition,\n          size: componentSize,\n        ),\n      );\n      final previousPosition = button\n          .positionOfAnchor(Anchor.center)\n          .toOffset();\n      game.onGameResize(initialGameSize * 2);\n      final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: previousPosition),\n      );\n      expect(pressedTimes, 1);\n\n      tapDispatcher.handleTapUp(\n        1,\n        createTapUpDetails(globalPosition: previousPosition),\n      );\n      expect(pressedTimes, 1);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: previousPosition),\n      );\n      tapDispatcher.handleTapCancel(1);\n      expect(pressedTimes, 2);\n    });\n\n    testWithFlameGame('correctly work isDisabled', (game) async {\n      var pressedTimes = 0;\n      final initialGameSize = Vector2.all(100);\n      final componentSize = Vector2.all(10);\n      final buttonPosition = Vector2.all(100);\n      late final ToggleButtonComponent button;\n      game.onGameResize(initialGameSize);\n      await game.ensureAdd(\n        button = ToggleButtonComponent(\n          defaultSkin: RectangleComponent(size: componentSize),\n          defaultSelectedSkin: RectangleComponent(size: componentSize),\n          onPressed: () => pressedTimes++,\n          position: buttonPosition,\n          size: componentSize,\n        ),\n      );\n      button.isDisabled = true;\n      final previousPosition = button\n          .positionOfAnchor(Anchor.center)\n          .toOffset();\n      game.onGameResize(initialGameSize * 2);\n      final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: previousPosition),\n      );\n      expect(pressedTimes, 0);\n\n      tapDispatcher.handleTapUp(\n        1,\n        createTapUpDetails(globalPosition: previousPosition),\n      );\n      expect(pressedTimes, 0);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: previousPosition),\n      );\n      tapDispatcher.handleTapCancel(1);\n      expect(pressedTimes, 0);\n    });\n\n    testWithFlameGame('toggle works correctly', (game) async {\n      var pressedTimes = 0;\n      final initialGameSize = Vector2.all(100);\n      final componentSize = Vector2.all(10);\n      final buttonPosition = Vector2.all(100);\n      late final ToggleButtonComponent button;\n      game.onGameResize(initialGameSize);\n      await game.ensureAdd(\n        button = ToggleButtonComponent(\n          defaultSkin: RectangleComponent(size: componentSize),\n          defaultSelectedSkin: RectangleComponent(size: componentSize),\n          onPressed: () => pressedTimes++,\n          position: buttonPosition,\n          size: componentSize,\n        ),\n      );\n      final previousPosition = button\n          .positionOfAnchor(Anchor.center)\n          .toOffset();\n      game.onGameResize(initialGameSize * 2);\n      final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: previousPosition),\n      );\n      expect(button.isSelected, false);\n\n      tapDispatcher.handleTapUp(\n        1,\n        createTapUpDetails(globalPosition: previousPosition),\n      );\n      expect(button.isSelected, true);\n\n      tapDispatcher.handleTapDown(\n        1,\n        TapDownDetails(globalPosition: previousPosition),\n      );\n      expect(button.isSelected, true);\n\n      tapDispatcher.handleTapUp(\n        1,\n        createTapUpDetails(globalPosition: previousPosition),\n      );\n      expect(button.isSelected, false);\n    });\n\n    testWidgets(\n      '[#1723] can be pressed while the engine is paused',\n      (tester) async {\n        final game = FlameGame();\n        game.add(\n          ToggleButtonComponent(\n            defaultSkin: CircleComponent(radius: 40),\n            downSkin: CircleComponent(radius: 40),\n            defaultSelectedSkin: CircleComponent(radius: 40),\n            position: Vector2(400, 300),\n            anchor: Anchor.center,\n            onPressed: () {\n              game.pauseEngine();\n              game.overlays.add('pause-menu');\n            },\n          ),\n        );\n        await tester.pumpWidget(\n          GameWidget(\n            game: game,\n            overlayBuilderMap: {\n              'pause-menu': (context, _) {\n                return _SimpleStatelessWidget(\n                  build: (context) {\n                    return Center(\n                      child: OutlinedButton(\n                        onPressed: () {\n                          game.overlays.remove('pause-menu');\n                          game.resumeEngine();\n                        },\n                        child: const Text('Resume'),\n                      ),\n                    );\n                  },\n                );\n              },\n            },\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n\n        await tester.tapAt(const Offset(400, 300));\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.paused, true);\n\n        await tester.tapAt(const Offset(400, 300));\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.paused, false);\n      },\n    );\n  });\n}\n\nclass _SimpleStatelessWidget extends StatelessWidget {\n  const _SimpleStatelessWidget({\n    required Widget Function(BuildContext) build,\n  }) : _build = build;\n\n  final Widget Function(BuildContext) _build;\n\n  @override\n  Widget build(BuildContext context) => _build(context);\n}\n"
  },
  {
    "path": "packages/flame/test/components/value_route_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('ValueRoute', () {\n    testWidgets('can return a value', (tester) async {\n      final router = RouterComponent(\n        initialRoute: '/',\n        routes: {'/': Route(Component.new)},\n      );\n      await tester.pumpWidget(\n        GameWidget(\n          game: FlameGame(children: [router]),\n        ),\n      );\n      await tester.pump();\n      await tester.pump();\n      expect(router.currentRoute.name, '/');\n\n      final future = router.pushAndWait(\n        _CustomValueRoute<int>(\n          defaultValue: 100,\n          builder: (route) {\n            return _CustomComponent(\n              onUpdate: (double dt) {\n                Future.microtask(() => route.completeWith(5));\n              },\n            );\n          },\n        ),\n      );\n      await tester.pump();\n      expect(await future, 5);\n    });\n\n    testWidgets('default return value', (tester) async {\n      final router = RouterComponent(\n        initialRoute: '/',\n        routes: {'/': Route(Component.new)},\n      );\n      await tester.pumpWidget(\n        GameWidget(\n          game: FlameGame(children: [router]),\n        ),\n      );\n      await tester.pump();\n      await tester.pump();\n      expect(router.currentRoute.name, '/');\n\n      final future = router.pushAndWait(\n        _CustomValueRoute<int>(\n          defaultValue: 100,\n          builder: (route) => Component(),\n        ),\n      );\n      await tester.pump();\n      router.pop();\n      await tester.pump();\n      expect(await future, 100);\n\n      final future2 = router.pushAndWait(\n        _CustomValueRoute<int>(\n          defaultValue: 17,\n          builder: (route) {\n            return _CustomComponent(\n              onUpdate: (double dt) {\n                Future.microtask(() => route.complete());\n              },\n            );\n          },\n        ),\n      );\n      await tester.pump();\n      expect(await future2, 17);\n    });\n\n    testWidgets('routes over the ValueRoute', (tester) async {\n      final router = RouterComponent(\n        initialRoute: '/',\n        routes: {'/': Route(Component.new)},\n      );\n      await tester.pumpWidget(\n        GameWidget(\n          game: FlameGame(children: [router]),\n        ),\n      );\n      await tester.pump();\n      await tester.pump();\n      expect(router.currentRoute.name, '/');\n\n      final customRoute = _CustomValueRoute<String>(\n        defaultValue: '',\n        builder: (route) => Component(),\n      );\n      final future = router.pushAndWait(customRoute);\n      router.pushRoute(Route(Component.new), name: 'one');\n      router.pushRoute(Route(Component.new), name: 'two');\n      await tester.pump();\n      expect(router.currentRoute.name, 'two');\n\n      customRoute.completeWith('ok!');\n      await tester.pump();\n      expect(await future, 'ok!');\n      expect(router.currentRoute.name, '/');\n    });\n  });\n}\n\nclass _CustomValueRoute<T> extends ValueRoute<T> {\n  _CustomValueRoute({\n    required T defaultValue,\n    required this.builder,\n  }) : super(value: defaultValue);\n\n  final Component Function(_CustomValueRoute<T>) builder;\n\n  @override\n  Component build() => builder(this);\n}\n\nclass _CustomComponent extends Component {\n  _CustomComponent({this.onUpdate});\n\n  final void Function(double dt)? onUpdate;\n\n  @override\n  void update(double dt) => onUpdate?.call(dt);\n}\n"
  },
  {
    "path": "packages/flame/test/components/world_route_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _TestWorld1 extends World {\n  int value = 0;\n}\n\nclass _TestWorld2 extends World {}\n\nvoid main() {\n  group('WorldRoute', () {\n    testWidgets('can set a new world', (tester) async {\n      final router = RouterComponent(\n        initialRoute: '/',\n        routes: {\n          '/': Route(Component.new),\n          '/world': WorldRoute(_TestWorld1.new),\n        },\n      );\n      final game = FlameGame(children: [router]);\n      await tester.pumpWidget(GameWidget(game: game));\n      await tester.pump();\n      await tester.pump();\n      expect(router.currentRoute.name, '/');\n      expect(game.world, isNot(isA<_TestWorld1>));\n\n      router.pushNamed('/world');\n      await tester.pump();\n      expect(router.currentRoute.name, '/world');\n      expect(game.world, isA<_TestWorld1>());\n    });\n\n    testWidgets('changes back to previous world on pop', (tester) async {\n      final router = RouterComponent(\n        initialRoute: '/world1',\n        routes: {\n          '/world1': WorldRoute(_TestWorld1.new),\n          '/world2': WorldRoute(_TestWorld2.new),\n        },\n      );\n      final game = FlameGame(children: [router]);\n      await tester.pumpWidget(GameWidget(game: game));\n      await tester.pump();\n      await tester.pump();\n      expect(router.currentRoute.name, '/world1');\n      expect(game.world, isA<_TestWorld1>());\n\n      router.pushNamed('/world2');\n      expect(router.currentRoute.name, '/world2');\n      expect(game.world, isA<_TestWorld2>());\n\n      router.pop();\n      expect(router.currentRoute.name, '/world1');\n      expect(game.world, isA<_TestWorld1>());\n    });\n\n    testWidgets('retains the state of the world', (tester) async {\n      final router = RouterComponent(\n        initialRoute: '/world1',\n        routes: {\n          '/world1': WorldRoute(_TestWorld1.new),\n          '/world2': WorldRoute(_TestWorld2.new),\n        },\n      );\n      final game = FlameGame(children: [router]);\n      await tester.pumpWidget(GameWidget(game: game));\n      await tester.pump();\n      await tester.pump();\n      expect(router.currentRoute.name, '/world1');\n      expect(game.world, isA<_TestWorld1>());\n      expect((game.world as _TestWorld1).value, isZero);\n      (game.world as _TestWorld1).value = 1;\n\n      router.pushReplacementNamed('/world2');\n      expect(router.currentRoute.name, '/world2');\n      expect(game.world, isA<_TestWorld2>());\n\n      router.pushReplacementNamed('/world1');\n      expect(router.currentRoute.name, '/world1');\n      expect(game.world, isA<_TestWorld1>());\n      expect((game.world as _TestWorld1).value, 1);\n    });\n\n    testWidgets('does not retain the state of world', (tester) async {\n      final router = RouterComponent(\n        initialRoute: '/world1',\n        routes: {\n          '/world1': WorldRoute(_TestWorld1.new, maintainState: false),\n          '/world2': WorldRoute(_TestWorld2.new),\n        },\n      );\n      final game = FlameGame(children: [router]);\n      await tester.pumpWidget(GameWidget(game: game));\n      await tester.pump();\n      await tester.pump();\n      expect(router.currentRoute.name, '/world1');\n      expect(game.world, isA<_TestWorld1>());\n      expect((game.world as _TestWorld1).value, isZero);\n      (game.world as _TestWorld1).value = 1;\n\n      router.pushReplacementNamed('/world2');\n      expect(router.currentRoute.name, '/world2');\n      expect(game.world, isA<_TestWorld2>());\n\n      router.pushReplacementNamed('/world1');\n      expect(router.currentRoute.name, '/world1');\n      expect(game.world, isA<_TestWorld1>());\n      expect((game.world as _TestWorld1).value, isZero);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/custom_component.dart",
    "content": "import 'dart:async';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\n\n/// A component where all event handlers can be provided as parameters in the\n/// constructor.\nclass CustomComponent extends Component {\n  CustomComponent({\n    void Function(CustomComponent, Vector2)? onGameResize,\n    FutureOr<void> Function(CustomComponent)? onLoad,\n    void Function(CustomComponent)? onMount,\n    void Function(CustomComponent)? onRemove,\n    void Function(CustomComponent, double)? onUpdate,\n    void Function(CustomComponent, Canvas)? onRender,\n    super.priority,\n    super.children,\n  }) : _onGameResize = onGameResize,\n       _onLoad = onLoad,\n       _onMount = onMount,\n       _onRemove = onRemove,\n       _onUpdate = onUpdate,\n       _onRender = onRender;\n\n  final void Function(CustomComponent, Vector2)? _onGameResize;\n  final FutureOr<void> Function(CustomComponent)? _onLoad;\n  final void Function(CustomComponent)? _onMount;\n  final void Function(CustomComponent)? _onRemove;\n  final void Function(CustomComponent, double)? _onUpdate;\n  final void Function(CustomComponent, Canvas)? _onRender;\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    _onGameResize?.call(this, size);\n  }\n\n  @override\n  FutureOr<void> onLoad() => _onLoad?.call(this);\n\n  @override\n  void onMount() => _onMount?.call(this);\n\n  @override\n  void onRemove() => _onRemove?.call(this);\n\n  @override\n  void update(double dt) => _onUpdate?.call(this, dt);\n\n  @override\n  void render(Canvas canvas) => _onRender?.call(this, canvas);\n}\n"
  },
  {
    "path": "packages/flame/test/effects/anchor_by_effect_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('AnchorByEffect', () {\n    testWithFlameGame('simple AnchorEffect.by', (game) async {\n      final component = PositionComponent(\n        size: Vector2(100, 40),\n        position: Vector2(12, 98),\n        anchor: Anchor.center,\n      );\n      game.add(component);\n      await game.ready();\n\n      component.add(\n        AnchorEffect.by(Vector2(0.3, 0.5), EffectController(duration: 1)),\n      );\n      for (var t = 0.0; t <= 1.0; t += 0.1) {\n        final value1 = 0.5 + 0.3 * t;\n        final value2 = 0.5 + 0.5 * t;\n        expect(component.anchor.x, closeTo(value1, toleranceFloat32(value1)));\n        expect(component.anchor.y, closeTo(value2, toleranceFloat32(value2)));\n        game.update(0.1);\n      }\n    });\n\n    testWithFlameGame('AnchorByEffect with explicit target', (game) async {\n      final component = PositionComponent(anchor: Anchor.topCenter);\n      game.add(component);\n      game.add(\n        AnchorByEffect(\n          Vector2(-0.2, 0.6),\n          EffectController(duration: 1),\n          target: component,\n        ),\n      );\n      await game.ready();\n\n      for (var t = 0.0; t <= 1.0; t += 0.1) {\n        final value1 = 0.5 - 0.2 * t;\n        final value2 = 0.0 + 0.6 * t;\n        expect(component.anchor.x, closeTo(value1, toleranceFloat32(value1)));\n        expect(component.anchor.y, closeTo(value2, toleranceFloat32(value2)));\n        game.update(0.1);\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/anchor_to_effect_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('AnchorToEffect', () {\n    testWithFlameGame('simple anchor movement', (game) async {\n      final object = PositionComponent()\n        ..position = Vector2(3, 4)\n        ..anchor = Anchor.bottomRight;\n      game.add(object);\n      await game.ready();\n\n      object.add(\n        AnchorToEffect(Anchor.center, EffectController(duration: 1)),\n      );\n      game.update(0.5);\n      expect(object.position, Vector2(3, 4));\n      expect(object.anchor.x, closeTo(0.75, 1e-15));\n      expect(object.anchor.y, closeTo(0.75, 1e-15));\n\n      game.update(0.5);\n      expect(object.anchor.x, closeTo(0.5, 1e-15));\n      expect(object.anchor.y, closeTo(0.5, 1e-15));\n    });\n\n    testWithFlameGame('viewfinder move anchor', (game) async {\n      final world = World()..addToParent(game);\n      final camera = CameraComponent(world: world)..addToParent(game);\n      await game.ready();\n\n      expect(camera.viewfinder.anchor, Anchor.center);\n      camera.viewfinder.add(\n        AnchorToEffect(const Anchor(0.2, 0.6), EffectController(duration: 1)),\n      );\n      for (var t = 0.0; t <= 1.0; t += 0.1) {\n        final value1 = 0.5 - 0.3 * t;\n        final value2 = 0.5 + 0.1 * t;\n        expect(\n          camera.viewfinder.anchor.x,\n          closeTo(\n            value1,\n            toleranceFloat32(value1),\n          ),\n        );\n        expect(\n          camera.viewfinder.anchor.y,\n          closeTo(\n            value2,\n            toleranceFloat32(value2),\n          ),\n        );\n        game.update(0.1);\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/color_effect_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _PaintComponent extends Component with HasPaint {}\n\nvoid main() {\n  group('ColorEffect', () {\n    testWithFlameGame('applies the correct color filter', (game) async {\n      final component = _PaintComponent();\n      await game.ensureAdd(component);\n      const color = Colors.red;\n      await component.add(\n        ColorEffect(color, EffectController(duration: 1)),\n      );\n      game.update(0);\n      expect(\n        component.paint.colorFilter.toString(),\n        equals(\n          'ColorFilter.mode(Color(alpha: 0.0000, red: 0.9569, green: '\n          '0.2627, blue: 0.2118, colorSpace: ColorSpace.sRGB),'\n          ' BlendMode.srcATop)',\n        ),\n      );\n\n      game.update(0.5);\n      expect(\n        component.paint.colorFilter.toString(),\n        equals(\n          'ColorFilter.mode(Color(alpha: 0.5000, red: 0.9569, green: '\n          '0.2627, blue: 0.2118, colorSpace: ColorSpace.sRGB), '\n          'BlendMode.srcATop)',\n        ),\n      );\n    });\n\n    testWithFlameGame(\n      'resets the color filter to the original state',\n      (game) async {\n        final component = _PaintComponent();\n        await game.ensureAdd(component);\n\n        final originalColorFilter = component.paint.colorFilter;\n        const color = Colors.red;\n\n        final effect = ColorEffect(\n          color,\n          EffectController(duration: 1),\n        );\n        await component.add(effect);\n        game.update(0.5);\n\n        expect(\n          originalColorFilter,\n          isNot(equals(component.paint.colorFilter)),\n        );\n\n        effect.reset();\n\n        expect(\n          originalColorFilter,\n          equals(component.paint.colorFilter),\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'resets the color filter to the original state',\n      (game) async {\n        final component = _PaintComponent();\n        await game.ensureAdd(component);\n\n        final originalColorFilter = component.paint.colorFilter;\n        const color = Colors.red;\n\n        final effect = ColorEffect(\n          color,\n          EffectController(duration: 1),\n        );\n        await component.add(effect);\n        game.update(0.5);\n\n        expect(\n          originalColorFilter,\n          isNot(equals(component.paint.colorFilter)),\n        );\n\n        effect.reset();\n\n        expect(\n          originalColorFilter,\n          equals(component.paint.colorFilter),\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'can be re-added in the component tree',\n      (game) async {\n        final component = _PaintComponent();\n        await game.ensureAdd(component);\n\n        final effect = ColorEffect(\n          Colors.red,\n          EffectController(duration: 1),\n        );\n        await component.ensureAdd(effect);\n        game.update(0.5);\n        effect.removeFromParent();\n        game.update(0.0);\n        component.add(effect);\n        expect(\n          () => game.update(0),\n          returnsNormally,\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'will clamp controllers that over or under set the progress value',\n      (game) async {\n        final component = _PaintComponent();\n        await game.ensureAdd(component);\n\n        final effect = ColorEffect(\n          opacityFrom: 1,\n          opacityTo: 0,\n          Colors.black,\n          ZigzagEffectController(\n            period: 1,\n          ),\n        );\n        await component.ensureAdd(effect);\n        expect(\n          () => game.update(0.56),\n          returnsNormally,\n        );\n      },\n    );\n\n    test('Validates opacity values', () {\n      expect(\n        () => ColorEffect(\n          Colors.blue,\n          EffectController(duration: 1),\n          opacityTo: 1.1,\n        ),\n        throwsAssertionError,\n      );\n\n      expect(\n        () => ColorEffect(\n          Colors.blue,\n          EffectController(duration: 1),\n          opacityFrom: 255,\n        ),\n        throwsAssertionError,\n      );\n\n      expect(\n        () => ColorEffect(\n          Colors.blue,\n          EffectController(duration: 1),\n          opacityTo: -254,\n        ),\n        throwsAssertionError,\n      );\n\n      expect(\n        () => ColorEffect(\n          Colors.blue,\n          EffectController(duration: 1),\n          opacityFrom: -0.5,\n        ),\n        throwsAssertionError,\n      );\n\n      expect(\n        () => ColorEffect(\n          Colors.blue,\n          EffectController(duration: 1),\n          opacityFrom: 0.1,\n          opacityTo: 0.9,\n        ),\n        returnsNormally,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/combined_effect_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('CombinedEffect', () {\n    group('properties', () {\n      test('simple', () {\n        final effect = CombinedEffect([\n          MoveEffect.to(Vector2(10, 10), EffectController(duration: 3)),\n          MoveEffect.by(\n            Vector2(1, 0),\n            EffectController(duration: 0.1, repeatCount: 15),\n          ),\n        ]);\n        expect(effect.controller.duration, 3.0);\n        expect(effect.controller.isRandom, false);\n        expect(effect.controller.completed, false);\n      });\n\n      test('alternating', () {\n        final effect = CombinedEffect(\n          [\n            MoveEffect.to(Vector2(10, 10), EffectController(duration: 3)),\n          ],\n          alternate: true,\n        );\n        expect(effect.controller.duration, 6);\n        expect(effect.controller.isRandom, false);\n      });\n\n      test('errors', () {\n        expect(\n          () => CombinedEffect(<Effect>[]),\n          failsAssert('The list of effects cannot be empty'),\n        );\n        expect(\n          () => CombinedEffect(\n            [MoveEffect.to(Vector2.zero(), EffectController(duration: 1))],\n            infinite: true,\n            repeatCount: 10,\n          ),\n          failsAssert(\n            'Parameters infinite and repeatCount cannot be specified '\n            'simultaneously',\n          ),\n        );\n      });\n    });\n\n    group('combination progression', () {\n      testWithFlameGame('simple combination', (game) async {\n        final effect = CombinedEffect([\n          MoveEffect.by(Vector2(10, 0), EffectController(duration: 1)),\n          ScaleEffect.to(Vector2.all(3), EffectController(duration: 2)),\n        ]);\n        final component = PositionComponent()..add(effect);\n        game.add(component);\n        await game.ready();\n\n        game.update(0.5);\n        expect(component.position, closeToVector(Vector2(5, 0)));\n        expect(component.scale, closeToVector(Vector2(1.5, 1.5)));\n\n        game.update(0.5);\n        expect(component.position, closeToVector(Vector2(10, 0)));\n        expect(component.scale, closeToVector(Vector2(2, 2)));\n\n        game.update(0.5);\n        expect(component.position, closeToVector(Vector2(10, 0)));\n        expect(component.scale, closeToVector(Vector2(2.5, 2.5)));\n\n        game.update(0.5);\n        expect(component.position, closeToVector(Vector2(10, 0)));\n        expect(component.scale, closeToVector(Vector2(3, 3)));\n        expect(effect.controller.completed, true);\n      });\n\n      testWithFlameGame('alternating combination', (game) async {\n        final effect = CombinedEffect(\n          [\n            MoveEffect.by(Vector2(10, 0), EffectController(duration: 1)),\n            ScaleEffect.to(Vector2.all(3), EffectController(duration: 2)),\n          ],\n          alternate: true,\n        );\n        final component = PositionComponent()..add(effect);\n        game.add(component);\n        await game.ready();\n\n        game.update(0.5);\n        expect(component.position, closeToVector(Vector2(5, 0)));\n        expect(component.scale, closeToVector(Vector2(1.5, 1.5)));\n\n        game.update(0.5);\n        expect(component.position, closeToVector(Vector2(10, 0)));\n        expect(component.scale, closeToVector(Vector2(2, 2)));\n        expect(component.position, closeToVector(Vector2(10, 0)));\n\n        game.update(0.5);\n        expect(component.scale, closeToVector(Vector2(2.5, 2.5)));\n\n        game.update(0.5);\n        expect(component.scale, closeToVector(Vector2(3, 3)));\n\n        // Alternating\n        game.update(0.5);\n        expect(component.position, closeToVector(Vector2(5, 0)));\n        expect(component.scale, closeToVector(Vector2(2.5, 2.5)));\n\n        game.update(0.5);\n        expect(component.position, closeToVector(Vector2(0, 0)));\n        expect(component.scale, closeToVector(Vector2(2, 2)));\n\n        game.update(0.5);\n        expect(component.scale, closeToVector(Vector2(1.5, 1.5)));\n\n        game.update(0.5);\n        expect(component.scale, closeToVector(Vector2(1, 1)));\n        expect(effect.controller.completed, true);\n      });\n      testWithFlameGame('infinite alternating combination', (game) async {\n        final effect = CombinedEffect(\n          [\n            MoveEffect.by(Vector2(10, 0), EffectController(duration: 1)),\n            ScaleEffect.to(Vector2.all(3), EffectController(duration: 2)),\n          ],\n          alternate: true,\n          infinite: true,\n        );\n        final component = PositionComponent()..add(effect);\n        game.add(component);\n        await game.ready();\n\n        game.update(1);\n        expect(component.position, closeToVector(Vector2(10, 0)));\n        expect(component.scale, closeToVector(Vector2(2, 2)));\n\n        game.update(1);\n        expect(component.position, closeToVector(Vector2(10, 0)));\n        expect(component.scale, closeToVector(Vector2(3, 3)));\n\n        // Alternating\n        game.update(1);\n        expect(component.position, closeToVector(Vector2(0, 0)));\n        expect(component.scale, closeToVector(Vector2(2, 2)));\n\n        game.update(1);\n        expect(component.position, closeToVector(Vector2(0, 0)));\n        expect(component.scale, closeToVector(Vector2(1, 1)));\n        expect(effect.controller.completed, false);\n\n        // Infinite\n        game.update(1);\n        expect(component.position, closeToVector(Vector2(10, 0)));\n        expect(component.scale, closeToVector(Vector2(2, 2)));\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/controllers/curved_effect_controller_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/src/effects/controllers/curved_effect_controller.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/animation.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('CurvedEffectController', () {\n    const curves = <Curve>[\n      Curves.bounceIn,\n      Curves.bounceInOut,\n      Curves.bounceOut,\n      Curves.decelerate,\n      Curves.ease,\n      Curves.easeIn,\n      Curves.easeInBack,\n      Curves.easeInCirc,\n      Curves.easeInCubic,\n      Curves.easeInExpo,\n      Curves.easeInOut,\n      Curves.easeInOutBack,\n      Curves.easeInOutCirc,\n      Curves.easeInOutCubic,\n      Curves.easeInOutCubicEmphasized,\n      Curves.easeInOutExpo,\n      Curves.easeInOutQuad,\n      Curves.easeInOutQuart,\n      Curves.easeInOutQuint,\n      Curves.easeInOutSine,\n      Curves.easeInQuad,\n      Curves.easeInQuart,\n      Curves.easeInQuint,\n      Curves.easeInSine,\n      Curves.easeInToLinear,\n      Curves.easeOut,\n      Curves.easeOutBack,\n      Curves.easeOutCirc,\n      Curves.easeOutCubic,\n      Curves.easeOutExpo,\n      Curves.easeOutQuad,\n      Curves.easeOutQuart,\n      Curves.easeOutQuint,\n      Curves.easeOutSine,\n      Curves.elasticIn,\n      Curves.elasticInOut,\n      Curves.elasticOut,\n      Curves.fastLinearToSlowEaseIn,\n      Curves.fastOutSlowIn,\n      Curves.linear,\n      Curves.linearToEaseOut,\n      Curves.slowMiddle,\n    ];\n\n    testRandom('normal', (Random random) {\n      final duration = random.nextDouble();\n      final curve = curves[random.nextInt(curves.length)];\n      final ec = CurvedEffectController(duration, curve);\n\n      expect(ec.progress, 0);\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.curve, curve);\n      expect(ec.isInfinite, false);\n      expect(ec.isRandom, false);\n      expect(ec.duration, duration);\n\n      var totalTime = 0.0;\n      while (totalTime < duration) {\n        final dt = random.nextDouble() * 0.01;\n        totalTime += dt;\n        final leftoverTime = ec.advance(dt);\n        if (totalTime > duration) {\n          expect(leftoverTime, closeTo(totalTime - duration, 1e-15));\n          expect(ec.progress, 1);\n        } else {\n          expect(leftoverTime, 0);\n          expect(\n            ec.progress,\n            closeTo(curve.transform(totalTime / duration), 1e-15),\n          );\n        }\n      }\n      expect(ec.progress, 1);\n      expect(ec.completed, true);\n\n      totalTime = duration;\n      while (totalTime > 0) {\n        final dt = random.nextDouble() * 0.01;\n        totalTime -= dt;\n        final leftoverTime = ec.recede(dt);\n        if (totalTime > 0) {\n          expect(leftoverTime, 0);\n          expect(\n            ec.progress,\n            closeTo(curve.transform(totalTime / duration), 1e-15),\n          );\n        } else {\n          expect(leftoverTime, closeTo(-totalTime, 1e-15));\n          expect(ec.progress, 0);\n        }\n      }\n      expect(ec.progress, 0);\n      expect(ec.completed, false);\n    });\n\n    test('errors', () {\n      expect(\n        () => CurvedEffectController(0, Curves.linear),\n        throwsA(isA<AssertionError>()),\n      );\n      expect(\n        () => CurvedEffectController(-1, Curves.linear),\n        throwsA(isA<AssertionError>()),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/controllers/delayed_effect_controller_test.dart",
    "content": "import 'package:flame/src/effects/controllers/delayed_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/linear_effect_controller.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('DelayedEffectController', () {\n    test('normal', () {\n      final ec = DelayedEffectController(LinearEffectController(1), delay: 3);\n      expect(ec.isInfinite, false);\n      expect(ec.isRandom, false);\n      expect(ec.started, false);\n      expect(ec.completed, false);\n      expect(ec.progress, 0);\n      expect(ec.duration, 4);\n\n      for (var i = 0; i < 6; i++) {\n        expect(ec.advance(0.5), 0);\n        expect(ec.started, i == 5);\n        expect(ec.progress, 0);\n      }\n      expect(ec.advance(1), 0);\n      expect(ec.progress, 1);\n      expect(ec.completed, true);\n      expect(ec.advance(1), 1);\n    });\n\n    test('reset', () {\n      final ec = DelayedEffectController(LinearEffectController(1), delay: 3);\n      ec.setToEnd();\n      expect(ec.started, true);\n      expect(ec.completed, true);\n      expect(ec.progress, 1);\n      ec.setToStart();\n      expect(ec.started, false);\n      expect(ec.completed, false);\n      expect(ec.progress, 0);\n    });\n\n    test('advance/recede', () {\n      final ec = DelayedEffectController(LinearEffectController(1), delay: 3);\n\n      expect(ec.advance(1), 0);\n      expect(ec.recede(0.5), 0);\n      expect(ec.advance(0.5), 0);\n      expect(ec.advance(2), 0); // 3/3 + 0/1\n      expect(ec.progress, 0);\n      expect(ec.started, true);\n      expect(ec.recede(0.5), 0); // 2.5/3 + 0/1\n      expect(ec.started, false);\n      expect(ec.advance(1), 0); // 3/3 + 0.5/1\n      expect(ec.started, true);\n      expect(ec.progress, closeTo(0.5, 1e-15));\n      expect(ec.advance(1), closeTo(0.5, 1e-15)); // 3/3 + 1/1\n      expect(ec.completed, true);\n      expect(ec.recede(0.5), 0); // 3/3 + 0.5/1\n      expect(ec.completed, false);\n      expect(ec.started, true);\n      expect(ec.progress, closeTo(0.5, 1e-15));\n      expect(ec.recede(1), 0); // 2.5/3 + 0/1\n      expect(ec.progress, 0);\n      expect(ec.started, false);\n      expect(ec.recede(3), closeTo(0.5, 1e-15));\n    });\n\n    test('errors', () {\n      expect(\n        () => DelayedEffectController(LinearEffectController(1), delay: -1),\n        failsAssert('Delay must be non-negative: -1.0'),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/controllers/effect_controller_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/animation.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nabstract class _Callback {\n  void call();\n}\n\nclass _CallbackMock extends Mock implements _Callback {}\n\nvoid main() {\n  group('EffectController', () {\n    test('forward', () {\n      final ec = EffectController(duration: 1.0);\n      expect(ec.isInfinite, false);\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.progress, 0.0);\n\n      ec.advance(0.5);\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.progress, 0.5);\n\n      ec.advance(0.5);\n      expect(ec.started, true);\n      expect(ec.completed, true);\n      expect(ec.progress, 1.0);\n\n      // Updating controller after it already finished is a no-op\n      ec.advance(0.5);\n      expect(ec.started, true);\n      expect(ec.completed, true);\n      expect(ec.progress, 1.0);\n    });\n\n    test('forward x 2', () {\n      final ec = EffectController(duration: 1, repeatCount: 2);\n      expect(ec.started, true);\n      expect(ec.progress, 0);\n\n      ec.advance(1);\n      expect(ec.progress, 1);\n      ec.advance(1e-10);\n      expect(ec.progress, closeTo(1e-10, 1e-15));\n      ec.advance(1);\n      expect(ec.progress, 1);\n      expect(ec.completed, true);\n    });\n\n    test('forward + delay', () {\n      final ec = EffectController(duration: 1.0, startDelay: 0.2);\n      expect(ec.isInfinite, false);\n\n      // initial delay\n      for (var i = 0; i < 20; i++) {\n        expect(ec.started, false);\n        expect(ec.completed, false);\n        expect(ec.progress, 0);\n        ec.advance(0.01);\n      }\n      expect(ec.progress, closeTo(0, 1e-10));\n\n      // progress from 0 to 1 over 1s\n      for (var i = 0; i < 100; i++) {\n        ec.advance(0.01);\n        expect(ec.started, true);\n        expect(ec.progress, closeTo((i + 1) / 100, 1e-10));\n      }\n\n      // final update, to account for any rounding errors\n      ec.advance(1e-10);\n      expect(ec.completed, true);\n      expect(ec.progress, 1);\n    });\n\n    test('forward + atMax', () {\n      final ec = EffectController(duration: 1, atMaxDuration: 0.5);\n      expect(ec.duration, 1.5);\n      expect(ec.isInfinite, false);\n      expect(ec.progress, 0);\n\n      ec.advance(1.5);\n      expect(ec.progress, 1);\n      expect(ec.started, true);\n      expect(ec.completed, true);\n    });\n\n    test('(forward + reverse) x 5', () {\n      final ec = EffectController(\n        startDelay: 1.0,\n        duration: 2.0,\n        reverseDuration: 1.0,\n        atMaxDuration: 0.2,\n        atMinDuration: 0.5,\n        repeatCount: 5,\n      );\n      expect(ec.isInfinite, false);\n      expect(ec.progress, 0);\n      expect(ec.started, false);\n      expect(ec.completed, false);\n\n      // Initial delay\n      ec.advance(1.0);\n      expect(ec.started, true);\n      expect(ec.progress, 0);\n\n      // 5 iterations\n      for (var iteration = 0; iteration < 5; iteration++) {\n        // forward\n        for (var i = 0; i < 200; i++) {\n          ec.advance(0.01);\n          expect(ec.progress, closeTo((i + 1) / 200, 1e-10));\n        }\n        // atPeak\n        for (var i = 0; i < 20; i++) {\n          ec.advance(0.01);\n          expect(ec.progress, closeTo(1, i == 19 ? 1e-10 : 0));\n        }\n        // reverse\n        for (var i = 0; i < 100; i++) {\n          ec.advance(0.01);\n          expect(ec.progress, closeTo((99 - i) / 100, 1e-10));\n        }\n        // atPit\n        for (var i = 0; i < 50; i++) {\n          ec.advance(0.01);\n          expect(ec.progress, closeTo(0, i == 49 ? 1e-10 : 0));\n        }\n      }\n\n      // In the end, the progress will remain at zero\n      ec.advance(1e-10);\n      expect(ec.started, true);\n      expect(ec.completed, true);\n      expect(ec.progress, 0);\n    });\n\n    testRandom('infinite', (Random random) {\n      const duration = 1.4;\n      final ec = EffectController(duration: duration, infinite: true);\n      expect(ec.isInfinite, true);\n      expect(ec.progress, 0);\n      expect(ec.started, true);\n      expect(ec.completed, false);\n\n      var stageTime = 0.0;\n      for (var i = 0; i < 100; i++) {\n        final dt = random.nextDouble() * 0.3;\n        ec.advance(dt);\n        stageTime += dt;\n        if (stageTime >= duration) {\n          stageTime -= duration;\n        }\n        expect(ec.progress, closeTo(stageTime / duration, 1e-10));\n      }\n\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.isInfinite, true);\n    });\n\n    testWithFlameGame('with speed', (game) async {\n      final ec = EffectController(speed: 1);\n      expect(ec.duration, isNaN);\n\n      final component = PositionComponent();\n      final effect = MoveEffect.by(Vector2(10, 0), ec);\n      component.add(effect);\n      await game.ensureAdd(component);\n      game.update(0);\n      expect(ec.duration, 10);\n    });\n\n    test('curved with speed', () {\n      final ec = EffectController(speed: 1, curve: Curves.ease);\n      expect(ec, isA<SpeedEffectController>());\n      expect(\n        (ec as SpeedEffectController).child,\n        isA<CurvedEffectController>(),\n      );\n    });\n\n    test('reverse speed-1', () {\n      final ec = EffectController(speed: 1, alternate: true);\n      expect(ec, isA<SequenceEffectController>());\n      final seq = (ec as SequenceEffectController).children;\n      expect(seq.length, 2);\n      expect(seq[0], isA<SpeedEffectController>());\n      expect(seq[1], isA<SpeedEffectController>());\n      expect(\n        (seq[0] as SpeedEffectController).child,\n        isA<LinearEffectController>(),\n      );\n      expect(\n        (seq[1] as SpeedEffectController).child,\n        isA<ReverseLinearEffectController>(),\n      );\n    });\n\n    test('reverse speed-2', () {\n      final ec = EffectController(speed: 1, reverseSpeed: 2);\n      expect(ec, isA<SequenceEffectController>());\n      final seq = (ec as SequenceEffectController).children;\n      expect(seq.length, 2);\n      expect(seq[0], isA<SpeedEffectController>());\n      expect(seq[1], isA<SpeedEffectController>());\n      expect((seq[0] as SpeedEffectController).speed, 1);\n      expect((seq[1] as SpeedEffectController).speed, 2);\n    });\n\n    test('reset', () {\n      final ec = EffectController(duration: 1.23);\n      expect(ec.started, true);\n      expect(ec.progress, 0);\n\n      ec.advance(0.4);\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.progress, closeTo(0.4 / 1.23, 1e-10));\n\n      ec.setToStart();\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.progress, 0);\n\n      ec.advance(0.5);\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.progress, closeTo(0.5 / 1.23, 1e-10));\n\n      ec.advance(1);\n      expect(ec.started, true);\n      expect(ec.completed, true);\n      expect(ec.progress, 1);\n\n      ec.setToStart();\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.progress, 0);\n    });\n\n    test('curve', () {\n      const curve = Curves.easeIn;\n      final ec = EffectController(\n        duration: 1,\n        curve: curve,\n        reverseDuration: 0.8,\n      );\n      expect(ec.started, true);\n      expect(ec.duration, 1.8);\n\n      for (var i = 0; i < 100; i++) {\n        ec.advance(0.01);\n        // Precision is less for final iteration, because it may flip over\n        // to the backwards curve.\n        final epsilon = i == 99 ? 1e-6 : 1e-10;\n        expect(ec.progress, closeTo(curve.transform((i + 1) / 100), epsilon));\n      }\n      for (var i = 0; i < 80; i++) {\n        ec.advance(0.01);\n        expect(\n          ec.progress,\n          closeTo(curve.flipped.transform(1 - (i + 1) / 80), 1e-10),\n        );\n      }\n      ec.advance(1e-10);\n      expect(ec.completed, true);\n      expect(ec.progress, 0);\n    });\n\n    test('reverse curve', () {\n      const curve = Curves.easeInQuad;\n      final ec = EffectController(\n        duration: 1,\n        reverseDuration: 1,\n        reverseCurve: curve,\n      );\n      expect(ec.started, true);\n      expect(ec.duration, 2);\n\n      ec.advance(1);\n      expect(ec.progress, 1);\n      expect(ec.completed, false);\n\n      for (var i = 0; i < 100; i++) {\n        ec.advance(0.01);\n        expect(ec.progress, closeTo(curve.transform(1 - (i + 1) / 100), 1e-10));\n      }\n      ec.advance(1e-10);\n      expect(ec.completed, true);\n    });\n\n    test('reverse curve with speed', () {\n      final ec = EffectController(\n        speed: 1,\n        curve: Curves.easeIn,\n        alternate: true,\n      );\n      expect(ec, isA<SequenceEffectController>());\n      final seq = (ec as SequenceEffectController).children;\n      expect(seq.length, 2);\n      expect(seq[0], isA<SpeedEffectController>());\n      expect(seq[1], isA<SpeedEffectController>());\n      expect(\n        (seq[0] as SpeedEffectController).child,\n        isA<CurvedEffectController>(),\n      );\n      expect(\n        (seq[1] as SpeedEffectController).child,\n        isA<ReverseCurvedEffectController>(),\n      );\n    });\n\n    test('onMax and onMin callbacks', () {\n      final mockOnMax = _CallbackMock();\n      final mockOnMin = _CallbackMock();\n      final ec = EffectController(\n        duration: 1,\n        reverseDuration: 1,\n        onMax: mockOnMax.call,\n        onMin: mockOnMin.call,\n        infinite: true,\n      );\n\n      ec.advance(1);\n      verifyNever(mockOnMax.call);\n      verifyNever(mockOnMin.call);\n\n      ec.advance(0.1);\n      verify(mockOnMax.call).called(1);\n      verifyNever(mockOnMin.call);\n\n      ec.advance(1); // after this call a .setToStart() is performed\n      verifyNever(mockOnMax.call);\n      verify(mockOnMin.call).called(1);\n\n      ec.advance(0.5);\n      verifyNever(mockOnMax.call);\n      verifyNever(mockOnMin.call);\n\n      ec.advance(0.5);\n      verify(mockOnMax.call).called(1);\n      verifyNever(mockOnMin.call);\n    });\n\n    group('errors', () {\n      test('empty', () {\n        expect(\n          EffectController.new,\n          failsAssert('Either duration or speed must be specified'),\n        );\n      });\n\n      test('duration and speed', () {\n        expect(\n          () => EffectController(duration: 1, speed: 1),\n          failsAssert(\n            'Both duration and speed arguments cannot be specified at the same '\n            'time',\n          ),\n        );\n      });\n\n      test('reverseDuration and reverseSpeed', () {\n        expect(\n          () => EffectController(\n            duration: 1,\n            reverseDuration: 1,\n            reverseSpeed: 1,\n          ),\n          failsAssert(\n            'Both duration and speed arguments cannot be specified at the same '\n            'time',\n          ),\n        );\n      });\n\n      test('negative duration', () {\n        expect(\n          () => EffectController(duration: -1),\n          failsAssert('Duration cannot be negative: -1.0'),\n        );\n      });\n\n      test('negative reverse duration', () {\n        expect(\n          () => EffectController(duration: 1, reverseDuration: -1),\n          failsAssert('Reverse duration cannot be negative: -1.0'),\n        );\n      });\n\n      test('zero speed', () {\n        expect(\n          () => EffectController(speed: 0),\n          failsAssert('Speed must be positive: 0.0'),\n        );\n      });\n\n      test('negative speed', () {\n        expect(\n          () => EffectController(speed: -1),\n          failsAssert('Speed must be positive: -1.0'),\n        );\n      });\n\n      test('zero reverseSpeed', () {\n        expect(\n          () => EffectController(speed: 1, reverseSpeed: 0),\n          failsAssert('Reverse speed must be positive: 0.0'),\n        );\n      });\n\n      test('negative reverseSpeed', () {\n        expect(\n          () => EffectController(speed: 1, reverseSpeed: -1),\n          failsAssert('Reverse speed must be positive: -1.0'),\n        );\n      });\n\n      test('zero repeat count', () {\n        expect(\n          () => EffectController(duration: 1, repeatCount: 0),\n          failsAssert('Repeat count must be positive: 0'),\n        );\n      });\n\n      test('negative repeat count', () {\n        expect(\n          () => EffectController(duration: 1, repeatCount: -1),\n          failsAssert('Repeat count must be positive: -1'),\n        );\n      });\n\n      test('repeated and infinite', () {\n        expect(\n          () => EffectController(\n            duration: 1,\n            infinite: true,\n            repeatCount: 3,\n          ),\n          failsAssert('An infinite effect cannot have a repeat count'),\n        );\n      });\n\n      test('negative startDelay', () {\n        expect(\n          () => EffectController(duration: 1, startDelay: -1),\n          failsAssert('Start delay cannot be negative: -1.0'),\n        );\n      });\n\n      test('negative atMinDuration', () {\n        expect(\n          () => EffectController(duration: 1, atMinDuration: -1),\n          failsAssert('At-min duration cannot be negative: -1.0'),\n        );\n      });\n\n      test('negative atMaxDuration', () {\n        expect(\n          () => EffectController(duration: 1, atMaxDuration: -1),\n          failsAssert('At-max duration cannot be negative: -1.0'),\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/controllers/infinite_effect_controller_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/src/effects/controllers/infinite_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/linear_effect_controller.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('InfiniteEffectController', () {\n    test('basic properties', () {\n      final ec = InfiniteEffectController(LinearEffectController(1));\n      expect(ec.isInfinite, true);\n      expect(ec.isRandom, false);\n      expect(ec.duration, double.infinity);\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.progress, 0);\n    });\n\n    test('reset', () {\n      final ec = InfiniteEffectController(LinearEffectController(1));\n      ec.setToEnd();\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.progress, 1);\n      ec.setToStart();\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.progress, 0);\n    });\n\n    testRandom('advance', (Random random) {\n      final ec = InfiniteEffectController(LinearEffectController(1));\n      var totalTime = 0.0;\n      while (totalTime < 10) {\n        final dt = random.nextDouble() * 0.1;\n        totalTime += dt;\n        expect(ec.advance(dt), 0);\n        expect(ec.progress, closeTo(totalTime % 1, 5e-14));\n      }\n      expect(ec.completed, false);\n    });\n\n    testRandom('recede', (Random random) {\n      final ec = InfiniteEffectController(LinearEffectController(1));\n      var totalTime = 0.0;\n      while (totalTime < 10) {\n        final dt = random.nextDouble() * 0.1;\n        totalTime += dt;\n        expect(ec.recede(dt), 0);\n        expect(ec.progress, closeTo((11 - totalTime) % 1, 5e-14));\n      }\n      expect(ec.completed, false);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/controllers/linear_effect_controller_test.dart",
    "content": "import 'package:flame/src/effects/controllers/linear_effect_controller.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('LinearEffectController', () {\n    test('[duration==0]', () {\n      final ec = LinearEffectController(0);\n      expect(ec.duration, 0);\n      expect(ec.isInfinite, false);\n      expect(ec.started, true);\n      expect(ec.completed, true);\n      expect(ec.progress, 1);\n\n      expect(ec.advance(0.1), 0.1);\n      expect(ec.progress, 1);\n    });\n\n    test('[duration==0] reset', () {\n      final ec = LinearEffectController(0);\n      ec.setToStart();\n      expect(ec.completed, true);\n      expect(ec.progress, 1);\n      ec.setToEnd();\n      expect(ec.completed, true);\n      expect(ec.progress, 1);\n    });\n\n    test('[duration==1]', () {\n      final ec = LinearEffectController(1);\n      expect(ec.duration, 1);\n      expect(ec.progress, 0);\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.isInfinite, false);\n\n      expect(ec.advance(0.5), 0);\n      expect(ec.progress, 0.5);\n      expect(ec.completed, false);\n\n      expect(ec.advance(0.5), 0);\n      expect(ec.progress, 1);\n      expect(ec.completed, true);\n\n      expect(ec.advance(0.00001), closeTo(0.00001, 1e-15));\n      expect(ec.progress, 1);\n      expect(ec.completed, true);\n\n      expect(ec.recede(0.5), 0);\n      expect(ec.progress, 0.5);\n\n      expect(ec.recede(0.5), 0);\n      expect(ec.progress, 0);\n\n      expect(ec.recede(0.00001), closeTo(0.00001, 1e-15));\n      expect(ec.progress, 0);\n    });\n\n    test('[duration==2] reset', () {\n      final ec = LinearEffectController(2);\n      expect(ec.advance(3), 1);\n      expect(ec.completed, true);\n      expect(ec.progress, 1);\n\n      ec.setToStart();\n      expect(ec.completed, false);\n      expect(ec.progress, 0);\n\n      expect(ec.advance(1), 0);\n      expect(ec.progress, closeTo(0.5, 1e-15));\n      expect(ec.completed, false);\n\n      expect(ec.advance(1), 0);\n      expect(ec.progress, 1);\n      expect(ec.completed, true);\n\n      expect(ec.advance(1), 1);\n      expect(ec.progress, 1);\n      expect(ec.completed, true);\n\n      ec.setToStart();\n      ec.setToEnd();\n      expect(ec.progress, 1);\n      expect(ec.completed, true);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/controllers/mixins/has_single_child_effect_controller_test.dart",
    "content": "import 'package:flame/effects.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('HasSingleChildEffectController', () {\n    test('child getter should return the wrapped child effect controller', () {\n      final childController = _MockEffectController();\n      final controller = _TestEffectController(childController);\n\n      expect(controller.child, equals(childController));\n    });\n\n    test(\n      'setToStart should call setToStart on the child effect controller',\n      () {\n        final childController = _MockEffectController();\n        final controller = _TestEffectController(childController);\n        controller.setToStart();\n        verify(childController.setToStart).called(1);\n      },\n    );\n\n    test('setToEnd should call setToEnd on the child effect controller', () {\n      final childController = _MockEffectController();\n      final controller = _TestEffectController(childController);\n      controller.setToEnd();\n      verify(childController.setToEnd).called(1);\n    });\n\n    test('onMount should call onMount on the child effect controller', () {\n      final childController = _MockEffectController();\n      final controller = _TestEffectController(childController);\n      final parentEffect = _MockEffect();\n      controller.onMount(parentEffect);\n      verify(() => childController.onMount(parentEffect)).called(1);\n    });\n  });\n}\n\nclass _TestEffectController extends _MockEffectController\n    with HasSingleChildEffectController<_MockEffectController> {\n  _TestEffectController(_MockEffectController child) : _child = child;\n\n  final _MockEffectController _child;\n\n  @override\n  _MockEffectController get child => _child;\n}\n\nclass _MockEffectController extends Mock implements EffectController {}\n\nclass _MockEffect extends Mock implements Effect {}\n"
  },
  {
    "path": "packages/flame/test/effects/controllers/random_effect_controller_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/effects.dart';\nimport 'package:test/test.dart';\n\nclass _MyRandom implements Random {\n  double value = 0.5;\n\n  @override\n  double nextDouble() => value;\n\n  @override\n  bool nextBool() => true;\n\n  @override\n  int nextInt(int max) => 1;\n}\n\nclass _MyRandomVariable extends RandomVariable {\n  _MyRandomVariable() : super(null);\n  double value = 1.23;\n\n  @override\n  double nextValue() => value;\n}\n\nvoid main() {\n  group('RandomEffectController', () {\n    test('custom random', () {\n      final randomVariable = _MyRandomVariable();\n      final ec = RandomEffectController(\n        LinearEffectController(1000),\n        randomVariable,\n      );\n\n      expect(ec.duration, 1.23);\n      expect(ec.isRandom, true);\n      expect(ec.isInfinite, false);\n      expect(ec.progress, 0);\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.advance(1), 0);\n      expect(ec.advance(0.23), 0);\n      expect(ec.completed, true);\n      expect(ec.advance(1), 1);\n      expect(ec.duration, 1.23);\n    });\n\n    test('.uniform', () {\n      final random = _MyRandom();\n      final ec = RandomEffectController.uniform(\n        LinearEffectController(1000),\n        min: 0,\n        max: 10,\n        random: random,\n      );\n      expect(random.nextDouble(), 0.5);\n      expect(ec.duration, 5);\n      random.value = 0;\n      ec.setToStart();\n      expect(ec.duration, 0);\n      random.value = 1;\n      ec.setToStart();\n      expect(ec.duration, 10);\n    });\n\n    test('.exponential', () {\n      const n = 1000;\n      final random = _MyRandom();\n      final ec = RandomEffectController.exponential(\n        LinearEffectController(1e6),\n        beta: 42,\n        random: random,\n      );\n      var sum = 0.0;\n      for (var i = 0; i < n; i++) {\n        random.value = i / n;\n        ec.setToStart();\n        expect(ec.duration! >= 0, true);\n        sum += ec.duration!;\n      }\n      expect(sum / n, closeTo(42, 400 / n));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/controllers/repeated_effect_controller_test.dart",
    "content": "import 'package:flame/src/effects/controllers/infinite_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/linear_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/repeated_effect_controller.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('RepeatedEffectController', () {\n    test('basic properties', () {\n      final ec = RepeatedEffectController(LinearEffectController(1), 5);\n      expect(ec.isInfinite, false);\n      expect(ec.isRandom, false);\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.duration, 5);\n      expect(ec.progress, 0);\n      expect(ec.repeatCount, 5);\n      expect(ec.remainingIterationsCount, 5);\n    });\n\n    test('reset', () {\n      final ec = RepeatedEffectController(LinearEffectController(1), 5);\n      ec.setToEnd();\n      expect(ec.started, true);\n      expect(ec.completed, true);\n      expect(ec.child.completed, true);\n      expect(ec.progress, 1);\n      expect(ec.remainingIterationsCount, 0);\n\n      ec.setToStart();\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.child.completed, false);\n      expect(ec.progress, 0);\n      expect(ec.remainingIterationsCount, 5);\n    });\n\n    test('advance', () {\n      final ec = RepeatedEffectController(LinearEffectController(2), 5);\n      expect(ec.remainingIterationsCount, 5);\n\n      // First iteration\n      expect(ec.advance(1), 0);\n      expect(ec.progress, 0.5);\n      expect(ec.remainingIterationsCount, 5);\n\n      expect(ec.advance(1), 0);\n      expect(ec.progress, 1);\n      expect(ec.remainingIterationsCount, 5);\n\n      // Second iteration\n      expect(ec.advance(1), 0);\n      expect(ec.progress, 0.5);\n      expect(ec.remainingIterationsCount, 4);\n\n      expect(ec.advance(1), 0);\n      expect(ec.progress, 1);\n      expect(ec.remainingIterationsCount, 4);\n\n      // Third iteration\n      expect(ec.advance(1), 0);\n      expect(ec.progress, 0.5);\n      expect(ec.remainingIterationsCount, 3);\n\n      expect(ec.advance(1), 0);\n      expect(ec.progress, 1);\n      expect(ec.remainingIterationsCount, 3);\n\n      // Forth iteration\n      expect(ec.advance(1), 0);\n      expect(ec.progress, 0.5);\n      expect(ec.remainingIterationsCount, 2);\n\n      expect(ec.advance(1), 0);\n      expect(ec.progress, 1);\n      expect(ec.remainingIterationsCount, 2);\n\n      // Fifth iteration\n      expect(ec.advance(1), 0);\n      expect(ec.progress, 0.5);\n      expect(ec.remainingIterationsCount, 1);\n\n      expect(ec.advance(1), 0);\n      expect(ec.progress, 1);\n      // last iteration is consumed immediately\n      expect(ec.remainingIterationsCount, 0);\n      expect(ec.completed, true);\n\n      // Any subsequent time will be spilled over\n      expect(ec.advance(1), 1);\n      expect(ec.progress, 1);\n      expect(ec.completed, true);\n    });\n\n    test('advance 2', () {\n      const n = 5;\n      const dt = 0.17;\n      final nIterations = (n / dt).floor();\n      final ec = RepeatedEffectController(LinearEffectController(1), n);\n      for (var i = 0; i < nIterations; i++) {\n        expect(ec.advance(dt), 0);\n        expect(ec.progress, closeTo((i + 1) * dt % 1, 1e-15));\n      }\n      expect(ec.advance(dt), closeTo((nIterations + 1) * dt - n, 1e-15));\n      expect(ec.completed, true);\n      expect(ec.progress, 1);\n    });\n\n    test('recede', () {\n      final ec = RepeatedEffectController(LinearEffectController(2), 5);\n      ec.setToEnd();\n      expect(ec.completed, true);\n      expect(ec.recede(0), 0);\n      expect(ec.completed, true);\n      expect(ec.remainingIterationsCount, 0);\n\n      // Fifth iteration\n      expect(ec.recede(1), 0);\n      expect(ec.progress, 0.5);\n      expect(ec.completed, false);\n      expect(ec.remainingIterationsCount, 1);\n\n      expect(ec.recede(1), 0);\n      expect(ec.progress, 0);\n      expect(ec.remainingIterationsCount, 1);\n\n      // Forth iteration\n      expect(ec.recede(1), 0);\n      expect(ec.progress, 0.5);\n      expect(ec.remainingIterationsCount, 2);\n\n      expect(ec.recede(1), 0);\n      expect(ec.progress, 0);\n      expect(ec.remainingIterationsCount, 2);\n\n      // Third iteration\n      expect(ec.recede(1), 0);\n      expect(ec.progress, 0.5);\n      expect(ec.remainingIterationsCount, 3);\n\n      expect(ec.recede(1), 0);\n      expect(ec.progress, 0);\n      expect(ec.remainingIterationsCount, 3);\n\n      // Second iteration\n      expect(ec.recede(1), 0);\n      expect(ec.progress, 0.5);\n      expect(ec.remainingIterationsCount, 4);\n\n      expect(ec.recede(1), 0);\n      expect(ec.progress, 0);\n      expect(ec.remainingIterationsCount, 4);\n\n      // First iteration\n      expect(ec.recede(1), 0);\n      expect(ec.progress, 0.5);\n      expect(ec.remainingIterationsCount, 5);\n\n      expect(ec.recede(1), 0);\n      expect(ec.progress, 0);\n      expect(ec.remainingIterationsCount, 5);\n      expect(ec.started, true);\n\n      // Extra iterations\n      expect(ec.recede(1), 1);\n      expect(ec.progress, 0);\n      expect(ec.remainingIterationsCount, 5);\n    });\n\n    test('errors', () {\n      final ec = LinearEffectController(1);\n      expect(\n        () => RepeatedEffectController(InfiniteEffectController(ec), 1),\n        failsAssert('child cannot be infinite'),\n      );\n      expect(\n        () => RepeatedEffectController(ec, 0),\n        failsAssert('repeatCount must be positive'),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/controllers/sequence_effect_controller_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/src/effects/controllers/infinite_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/linear_effect_controller.dart';\nimport 'package:flame/src/effects/controllers/sequence_effect_controller.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('SequenceEffectController', () {\n    test('basic properties', () {\n      final ec = SequenceEffectController([\n        LinearEffectController(1),\n        LinearEffectController(2),\n        LinearEffectController(3),\n      ]);\n      expect(ec.isRandom, false);\n      expect(ec.isInfinite, false);\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.duration, 6);\n      expect(ec.progress, 0);\n      expect(ec.children.length, 3);\n    });\n\n    test('reset', () {\n      final ec = SequenceEffectController([\n        LinearEffectController(1),\n        LinearEffectController(2),\n        LinearEffectController(3),\n      ]);\n      ec.setToEnd();\n      expect(ec.started, true);\n      expect(ec.completed, true);\n      expect(ec.progress, 1);\n      expect(ec.children.every((c) => c.completed), true);\n\n      ec.setToStart();\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.progress, 0);\n      expect(ec.children.every((c) => c.progress == 0), true);\n    });\n\n    testRandom('advance', (Random random) {\n      final ec = SequenceEffectController([\n        LinearEffectController(1),\n        LinearEffectController(2),\n        LinearEffectController(3),\n      ]);\n\n      var totalTime = 0.0;\n      while (totalTime <= 6) {\n        expect(\n          ec.progress,\n          closeTo(\n            switch (totalTime) {\n              <= 1 => totalTime,\n              <= 3 => (totalTime - 1) / 2,\n              _ => (totalTime - 3) / 3,\n            },\n            1e-15,\n          ),\n        );\n        final dt = random.nextDouble();\n        totalTime += dt;\n        ec.advance(dt);\n      }\n      expect(ec.completed, true);\n      expect(ec.progress, 1);\n      expect(ec.children.every((c) => c.completed), true);\n    });\n\n    testRandom('recede', (Random random) {\n      final ec = SequenceEffectController([\n        LinearEffectController(1),\n        LinearEffectController(2),\n        LinearEffectController(3),\n      ]);\n      ec.setToEnd();\n\n      var totalTime = 6.0;\n      while (totalTime >= 0) {\n        expect(\n          ec.progress,\n          closeTo(\n            switch (totalTime) {\n              <= 1 => totalTime,\n              <= 3 => (totalTime - 1) / 2,\n              _ => (totalTime - 3) / 3,\n            },\n            1e-14,\n          ),\n        );\n        final dt = random.nextDouble() * 0.1;\n        totalTime -= dt;\n        ec.recede(dt);\n      }\n      expect(ec.completed, false);\n      expect(ec.progress, 0);\n      expect(ec.children.every((c) => c.progress == 0), true);\n    });\n\n    test('errors', () {\n      expect(\n        () => SequenceEffectController([]),\n        failsAssert('List of controllers cannot be empty'),\n      );\n      expect(\n        () => SequenceEffectController(\n          [InfiniteEffectController(LinearEffectController(1))],\n        ),\n        failsAssert('Children controllers cannot be infinite'),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/controllers/sine_effect_controller_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/effects.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('SineEffectController', () {\n    test('general properties', () {\n      final ec = SineEffectController(period: 1);\n      expect(ec.duration, 1);\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.progress, 0);\n      expect(ec.isRandom, false);\n    });\n\n    test('progression', () {\n      final ec = SineEffectController(period: 3);\n      final expectedProgress = List<double>.generate(\n        101,\n        (i) => sin(i * 0.01 * 2 * pi),\n      );\n      for (final p in expectedProgress) {\n        expect(ec.progress, closeTo(p, 2e-14));\n        ec.advance(0.01 * 3);\n      }\n      expect(ec.completed, true);\n    });\n\n    test('errors', () {\n      expect(\n        () => SineEffectController(period: 0),\n        failsAssert('Period must be positive: 0.0'),\n      );\n      expect(\n        () => SineEffectController(period: -1.1),\n        failsAssert('Period must be positive: -1.1'),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/controllers/speed_effect_controller_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('SpeedEffectController', () {\n    group('properties', () {\n      test('simple properties', () {\n        final ec = SpeedEffectController(LinearEffectController(1), speed: 10);\n        expect(ec.duration, isNaN);\n        expect(ec.progress, 0);\n        expect(ec.isRandom, true);\n        expect(ec.started, true);\n        expect(ec.completed, false);\n      });\n\n      test('assert speed positive', () {\n        expect(\n          () => SpeedEffectController(LinearEffectController(1), speed: 0),\n          failsAssert('Speed must be positive: 0.0'),\n        );\n        expect(\n          () => SpeedEffectController(LinearEffectController(1), speed: -1),\n          failsAssert('Speed must be positive: -1.0'),\n        );\n      });\n\n      test('assert effect measurable', () {\n        expect(\n          () => SizeEffect.by(\n            Vector2.zero(),\n            SpeedEffectController(LinearEffectController(1), speed: 1),\n          ),\n          failsAssert(\n            'SpeedEffectController can only be applied to a MeasurableEffect',\n          ),\n        );\n      });\n    });\n\n    group('applied to various effects', () {\n      testWithFlameGame('speed on MoveEffect', (game) async {\n        final effect = MoveEffect.to(\n          Vector2(8, 12),\n          EffectController(speed: 1),\n        );\n        final component = PositionComponent(position: Vector2(5, 8));\n        component.add(effect);\n        await game.ensureAdd(component);\n        game.update(0);\n\n        expect(effect.controller.duration, 5);\n        game.update(5);\n        expect(component.position, closeToVector(Vector2(8, 12)));\n      });\n\n      testWithFlameGame('speed on MoveEffect with delay', (game) async {\n        final effect = MoveToEffect(\n          Vector2(8, 12),\n          EffectController(speed: 1, startDelay: 1),\n        );\n        final component = PositionComponent(position: Vector2(5, 8));\n        component.add(effect);\n        await game.ensureAdd(component);\n        expect(effect.controller.duration, 6);\n        game.update(1);\n        expect(component.position, closeToVector(Vector2(5, 8)));\n        game.update(5);\n        expect(component.position, closeToVector(Vector2(8, 12)));\n      });\n\n      testWithFlameGame('speed on MoveAlongPathEffect', (game) async {\n        final effect = MoveAlongPathEffect(\n          Path()\n            ..lineTo(30, 40)\n            ..lineTo(30, 20)\n            ..lineTo(10, 35)\n            ..lineTo(10, 30),\n          EffectController(speed: 4),\n          absolute: true,\n        );\n        final component = PositionComponent(position: Vector2(5, 8));\n        component.add(effect);\n        await game.ensureAdd(component);\n        game.update(0);\n\n        expect(effect.controller.duration, 25);\n        game.update(25);\n        expect(component.position, closeToVector(Vector2(10, 30)));\n      });\n\n      testWithFlameGame('speed on RotateEffect', (game) async {\n        final effect = RotateEffect.to(tau, EffectController(speed: 1));\n        final component = PositionComponent(position: Vector2(5, 8));\n        component.add(effect);\n        await game.ensureAdd(component);\n        game.update(0);\n\n        expect(effect.controller.duration, tau);\n        game.update(tau);\n        expect(component.angle, closeTo(tau.toNormalizedAngle(), 1e-15));\n      });\n\n      testWithFlameGame('reset', (game) async {\n        final effect = MoveEffect.to(\n          Vector2(10, 0),\n          SpeedEffectController(LinearEffectController(0), speed: 1),\n        );\n        final component = PositionComponent();\n        component.add(effect..removeOnFinish = false);\n        await game.ensureAdd(component);\n        game.update(0);\n\n        game.update(0);\n        expect(effect.controller.duration, 10);\n        game.update(10);\n        expect(effect.controller.completed, true);\n\n        expect(component.position, closeToVector(Vector2(10, 0)));\n        component.position = Vector2.all(40);\n        effect.reset();\n        game.update(0);\n        expect(effect.controller.duration, 50);\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/controllers/zigzag_effect_controller_test.dart",
    "content": "import 'package:flame/effects.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('ZigzagEffectController', () {\n    test('general properties', () {\n      final ec = ZigzagEffectController(period: 1);\n      expect(ec.duration, 1);\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.progress, 0);\n      expect(ec.isRandom, false);\n    });\n\n    test('progression', () {\n      final ec = ZigzagEffectController(period: 4);\n      final expectedProgress = [\n        for (var i = 0; i < 10; i++) i * 0.1,\n        for (var i = 10; i > 0; i--) i * 0.1,\n        for (var i = 0; i > -10; i--) i * 0.1,\n        for (var i = -10; i <= 0; i++) i * 0.1,\n      ];\n      for (final p in expectedProgress) {\n        expect(ec.progress, closeTo(p, 3e-15));\n        ec.advance(0.1);\n      }\n      expect(ec.completed, true);\n    });\n\n    test('errors', () {\n      expect(\n        () => ZigzagEffectController(period: 0),\n        failsAssert('Period must be positive: 0.0'),\n      );\n      expect(\n        () => ZigzagEffectController(period: -1.1),\n        failsAssert('Period must be positive: -1.1'),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/effect_test.dart",
    "content": "import 'package:flame/src/components/core/component.dart';\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/effect.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _MyEffect extends Effect {\n  _MyEffect(super.controller) {\n    completed.whenComplete(() => ++completedCounter);\n  }\n\n  int completedCounter = 0;\n\n  double x = -1;\n  Function()? onStartCallback;\n\n  @override\n  void apply(double progress) {\n    x = progress;\n  }\n\n  @override\n  void reset() {\n    super.reset();\n    x = -1;\n  }\n\n  @override\n  void onStart() {\n    super.onStart();\n    onStartCallback?.call();\n  }\n}\n\nvoid main() {\n  group('Effect', () {\n    test('pause & resume', () {\n      final effect = _MyEffect(EffectController(duration: 10));\n      expect(effect.x, -1);\n      expect(effect.isPaused, false);\n\n      effect.update(0);\n      expect(effect.x, 0);\n\n      effect.update(1);\n      expect(effect.x, closeTo(0.1, 1e-15));\n\n      effect.update(2);\n      expect(effect.x, closeTo(0.3, 1e-15));\n\n      effect.pause();\n      effect.update(5);\n      effect.update(2);\n      expect(effect.x, closeTo(0.3, 1e-15));\n\n      effect.resume();\n      effect.update(1);\n      expect(effect.x, closeTo(0.4, 1e-15));\n\n      effect.pause();\n      effect.update(1000);\n      expect(effect.isPaused, true);\n\n      effect.reset();\n      expect(effect.isPaused, false);\n      expect(effect.x, -1);\n\n      effect.update(5);\n      expect(effect.x, closeTo(0.5, 1e-15));\n\n      effect.update(5);\n      expect(effect.x, closeTo(1, 1e-15));\n    });\n\n    test(\n      'Completed future return on complete',\n      () async {\n        final effect = _MyEffect(EffectController(duration: 2));\n        final completer = effect.completed;\n\n        effect.update(4);\n        await expectLater(completer, completes);\n      },\n    );\n\n    testWithFlameGame(\n      'removeOnFinish = true',\n      (game) async {\n        final component = Component();\n        game.add(component);\n        final effect = _MyEffect(EffectController(duration: 1));\n        component.add(effect);\n        await game.ready();\n        expect(component.children.length, 1);\n\n        expect(effect.removeOnFinish, true);\n        expect(effect.isMounted, true);\n        game.update(1);\n\n        expect(effect.controller.completed, true);\n        game.update(0);\n        expect(effect.isMounted, false);\n        expect(component.children.length, 0);\n      },\n    );\n\n    testWithFlameGame(\n      'removeOnFinish = false',\n      (game) async {\n        final component = Component();\n        game.add(component);\n        final effect = _MyEffect(EffectController(duration: 1));\n        effect.removeOnFinish = false;\n        component.add(effect);\n        await game.ready();\n        expect(component.children.length, 1);\n\n        expect(effect.removeOnFinish, false);\n        expect(effect.isMounted, true);\n\n        // After the effect completes, it still remains mounted\n        game.update(1);\n        expect(effect.x, 1);\n        expect(effect.controller.completed, true);\n        game.update(0);\n        expect(effect.isMounted, true);\n        expect(component.children.length, 1);\n\n        // Even as more time is passing, the effect remains mounted and in\n        // the completed state\n        game.update(10);\n        expect(effect.x, 1);\n        expect(effect.isMounted, true);\n        expect(effect.controller.completed, true);\n\n        // However, once the effect is reset, it goes to its initial state\n        effect.reset();\n        expect(effect.x, -1);\n        expect(effect.controller.completed, false);\n\n        game.update(0.5);\n        expect(effect.x, 0.5);\n        expect(effect.controller.completed, false);\n\n        // Now the effect completes once again, but still remains mounted\n        game.update(0.5);\n        expect(effect.controller.completed, true);\n        expect(effect.x, 1);\n        game.update(0);\n        expect(effect.isMounted, true);\n      },\n    );\n\n    test('onStart & onFinish', () {\n      var nStarted = 0;\n      var nFinished = 0;\n      final effect = _MyEffect(EffectController(duration: 1))\n        ..onStartCallback = () {\n          nStarted++;\n        }\n        ..onComplete = () {\n          nFinished++;\n        };\n\n      effect.update(0);\n      expect(effect.x, 0);\n      expect(nStarted, 1);\n      expect(nFinished, 0);\n\n      effect.update(0.5);\n      expect(effect.x, 0.5);\n      expect(nStarted, 1);\n      expect(nFinished, 0);\n\n      effect.update(0.5);\n      expect(effect.controller.completed, true);\n      expect(effect.x, 1);\n      expect(nStarted, 1);\n      expect(nFinished, 1);\n\n      // As more time passes, `onStart` and `onFinish` are no longer called\n      effect.update(0.5);\n      expect(effect.x, 1);\n      expect(nStarted, 1);\n      expect(nFinished, 1);\n\n      // However, if we reset the effect, the callbacks will be invoked again\n      effect.reset();\n      effect.update(0);\n      expect(nStarted, 2);\n      expect(nFinished, 1);\n      effect.update(1);\n      expect(nStarted, 2);\n      expect(nFinished, 2);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/function_effect_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/function_effect.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('FunctionEffect', () {\n    testWithFlameGame('applies function correctly', (game) async {\n      final effect = FunctionEffect<PositionComponent>(\n        (target, progress) {\n          target.x = progress * 100;\n        },\n        EffectController(duration: 1),\n      );\n      final component = PositionComponent(children: [effect]);\n      await game.ensureAdd(component);\n\n      effect.update(0);\n      expect(component.x, 0);\n\n      effect.update(0.5);\n      expect(component.x, 50);\n\n      effect.update(0.5);\n      expect(component.x, 100);\n    });\n\n    testWithFlameGame('completes correctly', (game) async {\n      final effect = FunctionEffect<PositionComponent>(\n        (target, progress) {\n          target.x = progress * 100;\n        },\n        EffectController(duration: 1),\n      );\n      final component = PositionComponent(children: [effect]);\n      await game.ensureAdd(component);\n\n      effect.update(1);\n      expect(component.x, 100);\n      expect(effect.controller.completed, true);\n    });\n\n    testWithFlameGame('removes on finish', (game) async {\n      final effect = FunctionEffect<PositionComponent>(\n        (target, progress) {\n          target.x = progress * 100;\n        },\n        EffectController(duration: 1),\n      );\n      final component = PositionComponent(children: [effect]);\n      await game.ensureAdd(component);\n\n      expect(component.children.length, 1);\n      game.update(1);\n      expect(effect.controller.completed, true);\n      game.update(0);\n      expect(component.children.length, 0);\n    });\n\n    testWithFlameGame('does not remove on finish', (game) async {\n      final effect = FunctionEffect<PositionComponent>(\n        (target, progress) {\n          target.x = progress * 100;\n        },\n        EffectController(duration: 1),\n      );\n      effect.removeOnFinish = false;\n      final component = PositionComponent(children: [effect]);\n      await game.ensureAdd(component);\n\n      expect(component.children.length, 1);\n      game.update(1);\n      expect(effect.controller.completed, true);\n      game.update(0);\n      expect(component.children.length, 1);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/glow_effect.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('GlowEffect', () {\n    testWithFlameGame('can apply to component having HasPaint', (game) async {\n      final component = _PaintComponent();\n      await game.ensureAdd(component);\n      await component.add(\n        GlowEffect(1, EffectController(duration: 1)),\n      );\n\n      game.update(0);\n\n      expect(component.children.length, 1);\n      expect(component.paint.maskFilter, isNotNull);\n\n      expect(\n        component.paint.maskFilter.toString(),\n        'MaskFilter.blur(BlurStyle.outer, 0.0)',\n      );\n\n      game.update(1);\n\n      expect(\n        component.paint.maskFilter.toString(),\n        'MaskFilter.blur(BlurStyle.outer, 1.0)',\n      );\n    });\n  });\n}\n\nclass _PaintComponent extends Component with HasPaint {}\n"
  },
  {
    "path": "packages/flame/test/effects/hue_effect_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('HueEffect', () {\n    testWithFlameGame('can apply to component having HasPaint', (game) async {\n      final component = _PaintComponent();\n      await game.ensureAdd(component);\n      await component.add(\n        HueEffect.by(pi, EffectController(duration: 1)),\n      );\n\n      game.update(0);\n\n      expect(component.children.length, 1);\n      // At progress 0, hue is 0, so colorFilter should be null\n      // due to optimization.\n      expect(component.paint.colorFilter, isNull);\n\n      game.update(0.5);\n      final filter05 = component.paint.colorFilter;\n      expect(filter05, isNotNull);\n\n      game.update(0.5);\n      final filter1 = component.paint.colorFilter;\n      expect(filter1, isNotNull);\n      expect(filter1, isNot(equals(filter05)));\n    });\n\n    testWithFlameGame('reset works correctly', (game) async {\n      final component = _PaintComponent();\n      await game.ensureAdd(component);\n      final effect = HueEffect.by(pi, EffectController(duration: 1));\n      await component.add(effect);\n\n      game.update(0.5);\n      expect(component.paint.colorFilter, isNotNull);\n\n      effect.reset();\n      // Incremental effects don't usually clear the target property on reset.\n      // If we want to maintain the old behavior,\n      // we'd need to manually set hue to 0.\n      component.hue = 0;\n      expect(component.paint.colorFilter, isNull);\n    });\n  });\n\n  group('HueToEffect', () {\n    testWithFlameGame('animates hue to target angle', (game) async {\n      final component = _PaintComponent();\n      await game.ensureAdd(component);\n      await component.add(\n        HueEffect.to(pi, EffectController(duration: 1)),\n      );\n\n      game.update(0);\n      expect(component.hue, 0.0);\n\n      game.update(0.5);\n      expect(component.hue, closeTo(pi / 2, 0.001));\n\n      game.update(0.5);\n      expect(component.hue, closeTo(pi, 0.001));\n    });\n\n    testWithFlameGame('computes delta from current hue', (game) async {\n      final component = _PaintComponent();\n      component.hue = pi / 4;\n      await game.ensureAdd(component);\n      await component.add(\n        HueEffect.to(pi, EffectController(duration: 1)),\n      );\n\n      game.update(0);\n      expect(component.hue, closeTo(pi / 4, 0.001));\n\n      game.update(1);\n      expect(component.hue, closeTo(pi, 0.001));\n    });\n\n    testWithFlameGame('applies color filter', (game) async {\n      final component = _PaintComponent();\n      await game.ensureAdd(component);\n      await component.add(\n        HueEffect.to(pi / 2, EffectController(duration: 1)),\n      );\n\n      game.update(0);\n      expect(component.paint.colorFilter, isNull);\n\n      game.update(0.5);\n      expect(component.paint.colorFilter, isNotNull);\n\n      game.update(0.5);\n      expect(component.paint.colorFilter, isNotNull);\n    });\n  });\n}\n\nclass _PaintComponent extends Component with HasPaint {}\n"
  },
  {
    "path": "packages/flame/test/effects/move_along_path_effect_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('MoveAlongPathEffect', () {\n    testWithFlameGame('relative path', (game) async {\n      const x0 = 32.5;\n      const y0 = 14.88;\n      final component = PositionComponent(position: Vector2(x0, y0));\n      game.add(component);\n      game.update(0);\n\n      component.add(\n        MoveAlongPathEffect(\n          Path()\n            ..addOval(Rect.fromCircle(center: const Offset(6, 10), radius: 50)),\n          LinearEffectController(1),\n        ),\n      );\n      game.update(0);\n      for (var i = 0; i < 100; i++) {\n        final a = tau * i / 100;\n        // Apparently, in Flutter circle paths are not truly circles, but only\n        // appear circle-ish to an unsuspecting observer. Which is why the\n        // precision in `closeTo()` is so low: only 0.1 pixels.\n        expect(component.position.x, closeTo(x0 + 6 + 50 * cos(a), 0.1));\n        expect(component.position.y, closeTo(y0 + 10 + 50 * sin(a), 0.1));\n        game.update(0.01);\n      }\n    });\n\n    testWithFlameGame('absolute path', (game) async {\n      final component = PositionComponent(position: Vector2(17, -5));\n      game.add(component);\n      game.update(0);\n\n      component.add(\n        MoveAlongPathEffect(\n          Path()\n            ..moveTo(1000, 300)\n            ..lineTo(1200, 500),\n          EffectController(duration: 1),\n          absolute: true,\n        ),\n      );\n      game.update(0);\n      for (var i = 0; i < 10; i++) {\n        expect(component.position.x, closeTo(1000 + 200 * (i / 10), 1e-10));\n        expect(component.position.y, closeTo(300 + 200 * (i / 10), 1e-10));\n        game.update(0.1);\n      }\n    });\n\n    testWithFlameGame('absolute oriented path', (game) async {\n      final component = PositionComponent(\n        position: Vector2(17, -5),\n        angle: -30.5,\n      );\n      game.add(component);\n      game.update(0);\n\n      component.add(\n        MoveAlongPathEffect(\n          Path() // pythagorean triangle, perimeter=600\n            ..moveTo(200, 200)\n            ..lineTo(290, 80)\n            ..lineTo(450, 200)\n            ..lineTo(200, 200),\n          EffectController(duration: 6),\n          absolute: true,\n          oriented: true,\n        ),\n      );\n      game.update(0);\n      for (var i = 0; i < 60; i++) {\n        if (i <= 15) {\n          expect(component.position.x, closeTo(200 + 6 * i, 1e-10));\n          expect(component.position.y, closeTo(200 - 8 * i, 1e-10));\n          expect(\n            component.angle,\n            closeTo(-asin(0.8) + component.nativeAngle, 1e-7),\n          );\n        } else if (i <= 35) {\n          expect(component.position.x, closeTo(290 + 8 * (i - 15), 1e-10));\n          expect(component.position.y, closeTo(80 + 6 * (i - 15), 1e-10));\n          expect(\n            component.angle,\n            closeTo(asin(0.6) + component.nativeAngle, 1e-7),\n          );\n        } else {\n          expect(component.position.x, closeTo(450 - 10 * (i - 35), 1e-10));\n          expect(component.position.y, closeTo(200, 1e-10));\n          expect(component.angle, closeTo(pi + component.nativeAngle, 1e-7));\n        }\n        game.update(0.1);\n      }\n    });\n\n    testWithFlameGame('absolute oriented path with nativeAngle', (game) async {\n      final component = PositionComponent(\n        position: Vector2(17, -5),\n        angle: -30.5,\n        nativeAngle: 20.5,\n      );\n      game.add(component);\n      game.update(0);\n\n      component.add(\n        MoveAlongPathEffect(\n          Path() // pythagorean triangle, perimeter=600\n            ..moveTo(200, 200)\n            ..lineTo(290, 80)\n            ..lineTo(450, 200)\n            ..lineTo(200, 200),\n          EffectController(duration: 6),\n          absolute: true,\n          oriented: true,\n        ),\n      );\n      game.update(0);\n      for (var i = 0; i < 60; i++) {\n        if (i <= 15) {\n          expect(component.position.x, closeTo(200 + 6 * i, 1e-10));\n          expect(component.position.y, closeTo(200 - 8 * i, 1e-10));\n          expect(\n            component.angle,\n            closeTo(\n              (-asin(0.8) + component.nativeAngle).toNormalizedAngle(),\n              1e-7,\n            ),\n          );\n        } else if (i <= 35) {\n          expect(component.position.x, closeTo(290 + 8 * (i - 15), 1e-10));\n          expect(component.position.y, closeTo(80 + 6 * (i - 15), 1e-10));\n          expect(\n            component.angle,\n            closeTo(\n              (asin(0.6) + component.nativeAngle).toNormalizedAngle(),\n              1e-7,\n            ),\n          );\n        } else {\n          expect(component.position.x, closeTo(450 - 10 * (i - 35), 1e-10));\n          expect(component.position.y, closeTo(200, 1e-10));\n          expect(\n            component.angle,\n            closeTo((pi + component.nativeAngle).toNormalizedAngle(), 1e-7),\n          );\n        }\n        game.update(0.1);\n      }\n    });\n\n    testWithFlameGame(\n      'oriented effect applied to non-orientable target',\n      (game) async {\n        final world = World()..addToParent(game);\n        final camera = CameraComponent(world: world)..addToParent(game);\n        await game.ready();\n        await camera.viewport.add(\n          MoveAlongPathEffect(\n            Path()..lineTo(10, 10),\n            EffectController(duration: 1),\n            oriented: true,\n          ),\n        );\n        expect(\n          () => game.update(0),\n          failsAssert(\n            'An `oriented` MoveAlongPathEffect cannot be applied to a target '\n            'that does not support rotation',\n          ),\n        );\n      },\n    );\n\n    testWithFlameGame('non-absolute oriented path', (game) async {\n      final component = PositionComponent(\n        position: Vector2.zero(),\n        angle: -30.5,\n      );\n      game.add(component);\n      game.update(0);\n\n      component.add(\n        MoveAlongPathEffect(\n          Path() // pythagorean triangle, perimeter=600\n            ..moveTo(200, 200)\n            ..lineTo(290, 80)\n            ..lineTo(450, 200)\n            ..lineTo(200, 200),\n          EffectController(duration: 6),\n          oriented: true,\n        ),\n      );\n      game.update(0);\n      for (var i = 0; i < 60; i++) {\n        if (i <= 15) {\n          expect(component.position.x, closeTo(200 + 6 * i, 1e-10));\n          expect(component.position.y, closeTo(200 - 8 * i, 1e-10));\n          expect(\n            component.angle,\n            closeTo(-asin(0.8) + component.nativeAngle, 1e-7),\n          );\n        } else if (i <= 35) {\n          expect(component.position.x, closeTo(290 + 8 * (i - 15), 1e-10));\n          expect(component.position.y, closeTo(80 + 6 * (i - 15), 1e-10));\n          expect(\n            component.angle,\n            closeTo(asin(0.6) + component.nativeAngle, 1e-7),\n          );\n        } else {\n          expect(component.position.x, closeTo(450 - 10 * (i - 35), 1e-10));\n          expect(component.position.y, closeTo(200, 1e-10));\n          expect(component.angle, closeTo(pi + component.nativeAngle, 1e-7));\n        }\n        game.update(0.1);\n      }\n    });\n\n    testWithFlameGame('non-absolute oriented path with nativeAngle', (\n      game,\n    ) async {\n      final component = PositionComponent(\n        position: Vector2.zero(),\n        angle: -30.5,\n        nativeAngle: 20.5,\n      );\n      game.add(component);\n      game.update(0);\n\n      component.add(\n        MoveAlongPathEffect(\n          Path() // pythagorean triangle, perimeter=600\n            ..moveTo(200, 200)\n            ..lineTo(290, 80)\n            ..lineTo(450, 200)\n            ..lineTo(200, 200),\n          EffectController(duration: 6),\n          oriented: true,\n        ),\n      );\n      game.update(0);\n      for (var i = 0; i < 60; i++) {\n        if (i <= 15) {\n          expect(component.position.x, closeTo(200 + 6 * i, 1e-10));\n          expect(component.position.y, closeTo(200 - 8 * i, 1e-10));\n          expect(\n            component.angle,\n            closeTo(\n              (-asin(0.8) + component.nativeAngle).toNormalizedAngle(),\n              1e-7,\n            ),\n          );\n        } else if (i <= 35) {\n          expect(component.position.x, closeTo(290 + 8 * (i - 15), 1e-10));\n          expect(component.position.y, closeTo(80 + 6 * (i - 15), 1e-10));\n          expect(\n            component.angle,\n            closeTo(\n              (asin(0.6) + component.nativeAngle).toNormalizedAngle(),\n              1e-7,\n            ),\n          );\n        } else {\n          expect(component.position.x, closeTo(450 - 10 * (i - 35), 1e-10));\n          expect(component.position.y, closeTo(200, 1e-10));\n          expect(\n            component.angle,\n            closeTo((pi + component.nativeAngle).toNormalizedAngle(), 1e-7),\n          );\n        }\n        game.update(0.1);\n      }\n    });\n\n    test('errors', () {\n      final controller = LinearEffectController(0);\n      expect(\n        () => MoveAlongPathEffect(Path(), controller),\n        throwsArgumentError,\n      );\n\n      final path2 = Path()\n        ..moveTo(10, 10)\n        ..lineTo(10, 10);\n      expect(\n        () => MoveAlongPathEffect(path2, controller),\n        throwsArgumentError,\n      );\n\n      final path3 = Path()\n        ..addOval(const Rect.fromLTWH(0, 0, 1, 1))\n        ..addOval(const Rect.fromLTWH(2, 2, 1, 1));\n      expect(\n        () => MoveAlongPathEffect(path3, controller),\n        throwsArgumentError,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/move_by_effect_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('MoveByEffect', () {\n    testWithFlameGame('simple linear motion', (game) async {\n      final component = PositionComponent(position: Vector2(3, 4));\n      game.add(component);\n      game.update(0);\n\n      component.add(\n        MoveByEffect(Vector2(5, -1), EffectController(duration: 1)),\n      );\n      game.update(0.5);\n      expect(component.position.x, closeTo(3 + 2.5, 1e-15));\n      expect(component.position.y, closeTo(4 + -0.5, 1e-15));\n      game.update(0.5);\n      expect(component.position.x, closeTo(3 + 5, 1e-15));\n      expect(component.position.y, closeTo(4 + -1, 1e-15));\n    });\n\n    testWithFlameGame('#to', (game) async {\n      final component = PositionComponent(position: Vector2(3, 4));\n      game.add(component);\n      game.update(0);\n\n      component.add(\n        MoveEffect.to(Vector2(5, -1), LinearEffectController(1)),\n      );\n      game.update(0.5);\n      expect(component.position.x, closeTo(3 * 0.5 + 5 * 0.5, 1e-15));\n      expect(component.position.y, closeTo(4 * 0.5 + -1 * 0.5, 1e-15));\n      game.update(0.5);\n      expect(component.position.x, closeTo(5, 1e-15));\n      expect(component.position.y, closeTo(-1, 1e-15));\n    });\n\n    testWithFlameGame('custom target', (game) async {\n      final rectComponent = _RectComponent()\n        ..rect = const Rect.fromLTRB(10, 20, 50, 40)\n        ..addToParent(game);\n      rectComponent.add(\n        MoveEffect.by(\n          Vector2(3, -3),\n          EffectController(duration: 1),\n          target: _TopLeftCorner(rectComponent),\n        ),\n      );\n      await game.ready();\n\n      expect(rectComponent.rect, const Rect.fromLTRB(10, 20, 50, 40));\n      game.update(0.5);\n      expect(rectComponent.rect, const Rect.fromLTRB(11.5, 18.5, 50, 40));\n      game.update(0.5);\n      expect(rectComponent.rect, const Rect.fromLTRB(13, 17, 50, 40));\n    });\n  });\n}\n\nclass _RectComponent extends Component {\n  Rect rect = Rect.zero;\n}\n\nclass _TopLeftCorner implements PositionProvider {\n  _TopLeftCorner(this.target);\n  _RectComponent target;\n\n  @override\n  Vector2 get position => Vector2(target.rect.left, target.rect.top);\n\n  @override\n  set position(Vector2 value) {\n    final rect = target.rect;\n    target.rect = Rect.fromLTRB(value.x, value.y, rect.right, rect.bottom);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/effects/move_to_effect_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('MoveToEffect', () {\n    testWithFlameGame('simple linear movement', (game) async {\n      final component = PositionComponent()..position = Vector2(3, 4);\n      game.add(component);\n      game.update(0);\n\n      component.add(\n        MoveToEffect(Vector2(5, -1), EffectController(duration: 1)),\n      );\n      game.update(0.5);\n      expect(component.position.x, closeTo(3 * 0.5 + 5 * 0.5, 1e-15));\n      expect(component.position.y, closeTo(4 * 0.5 + -1 * 0.5, 1e-15));\n      game.update(0.5);\n      expect(component.position.x, closeTo(5, 1e-15));\n      expect(component.position.y, closeTo(-1, 1e-15));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/opacity_effect_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _PaintComponent extends Component with HasPaint {}\n\nclass _CustomPaintComponent<T extends Object> extends Component\n    with HasPaint<T> {\n  _CustomPaintComponent(Map<T, Paint> paints) {\n    for (final p in paints.entries) {\n      setPaint(p.key, p.value);\n    }\n  }\n}\n\nenum _PaintTypes { paint1, paint2, paint3 }\n\nvoid main() {\n  const epsilon = 0.004; // 1/255, since alpha only holds 8 bits\n\n  group('OpacityEffect', () {\n    testWithFlameGame('relative', (game) async {\n      final component = _PaintComponent();\n      await game.ensureAdd(component);\n\n      component.setOpacity(0.2);\n      await component.add(\n        OpacityEffect.by(0.4, EffectController(duration: 1)),\n      );\n      game.update(0);\n      expect(component.getOpacity(), closeTo(0.2, 0.00001));\n      expect(component.children.length, 1);\n\n      game.update(0.5);\n      expectDouble(component.getOpacity(), 0.4, epsilon: epsilon);\n\n      game.update(0.5);\n      expectDouble(component.getOpacity(), 0.6, epsilon: epsilon);\n      game.update(0);\n      expect(component.children.length, 0);\n      expectDouble(component.getOpacity(), 0.6, epsilon: epsilon);\n    });\n\n    testWithFlameGame('absolute', (game) async {\n      final component = _PaintComponent();\n      await game.ensureAdd(component);\n\n      component.setOpacity(0.2);\n      await component.add(\n        OpacityEffect.to(0.4, EffectController(duration: 1)),\n      );\n      game.update(0);\n      expect(component.getOpacity(), closeTo(0.2, 0.00001));\n      expect(component.children.length, 1);\n\n      game.update(0.5);\n      expectDouble(component.getOpacity(), 0.3, epsilon: epsilon);\n\n      game.update(0.5);\n      expectDouble(component.getOpacity(), 0.4, epsilon: epsilon);\n      game.update(0);\n      expect(component.children.length, 0);\n      expectDouble(component.getOpacity(), 0.4, epsilon: epsilon);\n    });\n\n    testWithFlameGame('reset relative', (game) async {\n      final component = _PaintComponent();\n      await game.ensureAdd(component);\n\n      // Since we'll have to change with multiples of 255 to not get rounding\n      // errors.\n      const step = 10 * 1 / 255;\n      final effect = OpacityEffect.by(\n        -step,\n        EffectController(duration: 1),\n      );\n      component.add(effect..removeOnFinish = false);\n      for (var i = 0; i < 5; i++) {\n        expectDouble(component.getOpacity(), 1.0 - step * i, epsilon: epsilon);\n        // After each reset the object will have its opacity modified by -10/255\n        // relative to its opacity at the start of the effect.\n        effect.reset();\n        game.update(1);\n        expectDouble(\n          component.getOpacity(),\n          1.0 - step * (i + 1),\n          epsilon: epsilon,\n        );\n      }\n    });\n\n    testWithFlameGame('reset absolute', (game) async {\n      final component = _PaintComponent();\n      await game.ensureAdd(component);\n\n      final effect = OpacityEffect.to(\n        0.0,\n        EffectController(duration: 1),\n      );\n      component.add(effect..removeOnFinish = false);\n      for (var i = 0; i < 5; i++) {\n        component.setOpacity(1 - 0.1 * i);\n        // After each reset the object will have an opacity value of 0.0\n        // regardless of its initial opacity.\n        effect.reset();\n        game.update(1);\n        expect(component.getOpacity(), 0.0);\n      }\n    });\n\n    testWithFlameGame('opacity composition', (game) async {\n      final component = _PaintComponent();\n      component.setOpacity(0.0);\n      await game.ensureAdd(component);\n\n      await component.add(\n        OpacityEffect.by(0.5, EffectController(duration: 10)),\n      );\n      await component.add(\n        OpacityEffect.by(\n          0.5,\n          EffectController(\n            duration: 1,\n            reverseDuration: 1,\n            repeatCount: 5,\n          ),\n        ),\n      );\n\n      game.update(1);\n      expectDouble(\n        component.getOpacity(),\n        0.55, // 0.5/10 + 0.5*1\n        epsilon: epsilon,\n      );\n      game.update(1);\n      expectDouble(\n        component.getOpacity(),\n        0.1,\n        epsilon: epsilon,\n      ); // 0.5*2/10 + 0.5*1 - 0.5*1\n      for (var i = 0; i < 10; i++) {\n        game.update(1);\n      }\n      expectDouble(component.getOpacity(), 0.5, epsilon: epsilon);\n      expect(component.children.length, 0);\n    });\n\n    testWithFlameGame(\n      'fade out',\n      (game) async {\n        final rng = Random();\n        final component = _PaintComponent();\n        await game.ensureAdd(component);\n\n        // Repeat the test 3 times\n        for (var i = 0; i < 3; ++i) {\n          await component.add(\n            OpacityEffect.fadeOut(EffectController(duration: 3)),\n          );\n\n          var timeElapsed = 0.0;\n          while (timeElapsed < 3) {\n            final dt = rng.nextDouble() / 60;\n            game.update(dt);\n            timeElapsed += dt;\n          }\n\n          expect(component.getOpacity(), 0.0);\n          component.setOpacity(1.0);\n        }\n      },\n    );\n\n    testWithFlameGame(\n      'infinite fade out',\n      (game) async {\n        final component = _PaintComponent();\n        await game.ensureAdd(component);\n\n        await component.add(\n          OpacityEffect.fadeOut(\n            EffectController(\n              duration: 3,\n              infinite: true,\n            ),\n          ),\n        );\n\n        for (var i = 0; i < 100; ++i) {\n          game.update(3);\n          expectDouble(component.getOpacity(), 0.0);\n        }\n      },\n    );\n\n    testWithFlameGame(\n      'on custom paint',\n      (game) async {\n        final component = _CustomPaintComponent<String>(\n          {'bluePaint': BasicPalette.blue.paint()},\n        );\n        await game.ensureAdd(component);\n\n        await component.add(\n          OpacityEffect.fadeOut(\n            EffectController(duration: 1),\n            target: component.opacityProviderOf('bluePaint'),\n          ),\n        );\n\n        game.update(1);\n\n        expect(component.getPaint('bluePaint').color.a, isZero);\n\n        // RGB components shouldn't be affected after opacity effect.\n        expect(component.getPaint('bluePaint').color.b, 1.0);\n        expect(component.getPaint('bluePaint').color.r, isZero);\n        expect(component.getPaint('bluePaint').color.g, isZero);\n      },\n    );\n\n    testWithFlameGame(\n      'apply on all paints',\n      (game) async {\n        final component = _CustomPaintComponent<_PaintTypes>(\n          {\n            _PaintTypes.paint1: BasicPalette.red.paint(),\n            _PaintTypes.paint2: BasicPalette.green.paint(),\n            _PaintTypes.paint3: BasicPalette.blue.paint(),\n          },\n        );\n        await game.ensureAdd(component);\n\n        await component.add(\n          OpacityEffect.fadeOut(EffectController(duration: 1)),\n        );\n\n        game.update(1);\n\n        // All paints should have the same opacity after the effect completes.\n        expect(component.getPaint().color.a, isZero);\n        expect(component.getPaint(_PaintTypes.paint1).color.a, isZero);\n        expect(component.getPaint(_PaintTypes.paint2).color.a, isZero);\n        expect(component.getPaint(_PaintTypes.paint3).color.a, isZero);\n      },\n    );\n\n    testWithFlameGame(\n      'maintains opacity ratios',\n      (game) async {\n        const redInitialOpacity = 0.9;\n        const greenInitialOpacity = 0.5;\n        const blueInitialOpacity = 0.2;\n        const targetOpacity = 0.5;\n\n        final component = _CustomPaintComponent<_PaintTypes>(\n          {\n            _PaintTypes.paint1: BasicPalette.red.paint()\n              ..color = BasicPalette.green.paint().color.withValues(\n                alpha: redInitialOpacity,\n              ),\n            _PaintTypes.paint2: BasicPalette.green.paint()\n              ..color = BasicPalette.green.paint().color.withValues(\n                alpha: greenInitialOpacity,\n              ),\n            _PaintTypes.paint3: BasicPalette.blue.paint()\n              ..color = BasicPalette.blue.paint().color.withValues(\n                alpha: blueInitialOpacity,\n              ),\n          },\n        );\n        await game.ensureAdd(component);\n\n        await component.add(\n          OpacityEffect.to(\n            targetOpacity,\n            EffectController(duration: 1),\n            target: component.opacityProviderOfList(),\n          ),\n        );\n\n        game.update(1);\n\n        expectDouble(\n          component.getPaint(_PaintTypes.paint1).color.a,\n          redInitialOpacity * targetOpacity,\n        );\n        expectDouble(\n          component.getPaint(_PaintTypes.paint2).color.a,\n          greenInitialOpacity * targetOpacity,\n        );\n        expectDouble(\n          component.getPaint(_PaintTypes.paint3).color.a,\n          blueInitialOpacity * targetOpacity,\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'maintains opacity ratios while ignoring some paints',\n      (game) async {\n        const redInitialOpacity = 0.9;\n        const greenInitialOpacity = 0.5;\n        const blueInitialOpacity = 0.2;\n        const targetOpacity = 1.0;\n\n        final component = _CustomPaintComponent<_PaintTypes>(\n          {\n            _PaintTypes.paint1: BasicPalette.red.paint()\n              ..color = BasicPalette.green.paint().color.withValues(\n                alpha: redInitialOpacity,\n              ),\n            _PaintTypes.paint2: BasicPalette.green.paint()\n              ..color = BasicPalette.green.paint().color.withValues(\n                alpha: greenInitialOpacity,\n              ),\n            _PaintTypes.paint3: BasicPalette.blue.paint()\n              ..color = BasicPalette.blue.paint().color.withValues(\n                alpha: blueInitialOpacity,\n              ),\n          },\n        );\n        await game.ensureAdd(component);\n\n        await component.add(\n          OpacityEffect.fadeIn(\n            EffectController(duration: 1),\n            target: component.opacityProviderOfList(\n              paintIds: const [_PaintTypes.paint1, _PaintTypes.paint2],\n            ),\n          ),\n        );\n\n        game.update(1);\n\n        expectDouble(\n          component.getPaint(_PaintTypes.paint1).color.a,\n          targetOpacity,\n        );\n        expectDouble(\n          component.getPaint(_PaintTypes.paint2).color.a,\n          (greenInitialOpacity / redInitialOpacity) * targetOpacity,\n        );\n\n        // Opacity of this paint shouldn't be changed.\n        expectDouble(\n          component.getPaint(_PaintTypes.paint3).color.a,\n          blueInitialOpacity,\n        );\n      },\n    );\n\n    testRandom('a very long opacity change', (Random rng) async {\n      final game = await initializeFlameGame();\n      final component = _PaintComponent();\n      await game.ensureAdd(component);\n\n      final effect = OpacityEffect.fadeOut(\n        EffectController(\n          duration: 1,\n          reverseDuration: 1,\n          infinite: true,\n        ),\n      );\n      await component.add(effect);\n\n      var totalTime = 0.0;\n      while (totalTime < 999.9) {\n        final dt = rng.nextDouble() * 0.02;\n        totalTime += dt;\n        game.update(dt);\n      }\n      game.update(1000 - totalTime);\n      expectDouble(component.getOpacity(), 1.0, epsilon: epsilon);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/remove_effect_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('RemoveEffect', () {\n    testWithFlameGame('no delay', (game) async {\n      final world = game.world;\n      final component = Component();\n      await world.ensureAdd(component);\n      expect(world.children.length, 1);\n\n      // First `game.update()` invokes the destroy effect and schedules\n      // `component` for deletion; second `game.update()` processes the deletion\n      // queue and actually removes the component.\n      component.add(RemoveEffect());\n      game.update(0);\n      game.update(0);\n      expect(world.children.length, 0);\n    });\n\n    testWithFlameGame('delayed', (game) async {\n      final world = game.world;\n      final component = Component();\n      await world.ensureAdd(component);\n      expect(world.children.length, 1);\n\n      component.add(RemoveEffect(delay: 1));\n      game.update(0.5);\n      game.update(0);\n      expect(world.children.length, 1);\n\n      game.update(0.5);\n      game.update(0);\n      expect(world.children.length, 0);\n    });\n\n    testWithFlameGame('as a part of a sequence', (game) async {\n      final world = game.world;\n      final component = PositionComponent();\n      await world.ensureAdd(component);\n      component.add(\n        SequenceEffect([\n          MoveByEffect(Vector2.all(10), EffectController(duration: 1)),\n          RemoveEffect(),\n        ]),\n      );\n      game.update(0);\n      expect(world.children.length, 1);\n      game.update(0.5);\n      expect(world.children.length, 1);\n      game.update(1.0); // This completes the move effect\n      game.update(0); // This runs the remove effect\n      expect(world.children.length, 0);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/rotate_around_effect_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nconst _epsilon = 1e-13;\n\nvoid main() {\n  group('RotateAroundEffect', () {\n    testWithFlameGame('applies rotation correctly', (game) async {\n      final component = PositionComponent(position: Vector2(50, 0));\n      await game.ensureAdd(component);\n\n      final effect = RotateAroundEffect(\n        tau,\n        center: Vector2(50, 50),\n        EffectController(duration: 1),\n      );\n      component.add(effect);\n\n      game.update(0);\n      expect(component.angle, 0);\n      expect(component.position, Vector2(50, 0));\n\n      game.update(0.5);\n      expect(component.angle, closeTo(pi, _epsilon));\n      expect(component.position, closeToVector(Vector2(50, 100), _epsilon));\n\n      game.update(0.5);\n      expect(component.angle % tau, closeTo(0, _epsilon));\n      expect(component.position, closeToVector(Vector2(50, 0), _epsilon));\n    });\n\n    testWithFlameGame('aligns rotation correctly', (game) async {\n      final component = PositionComponent(position: Vector2(100, 100));\n      await game.ensureAdd(component);\n\n      final effect = RotateAroundEffect(\n        pi,\n        center: Vector2(50, 50),\n        EffectController(duration: 1),\n      );\n      component.add(effect);\n\n      game.update(0);\n      expect(component.angle, 0);\n      expect(component.position, Vector2(100, 100));\n\n      game.update(0.5);\n      expect(component.angle, closeTo(pi / 2, _epsilon));\n      expect(component.position, closeToVector(Vector2(0, 100), _epsilon));\n\n      game.update(0.5);\n      expect(component.angle, closeTo(pi, _epsilon));\n      expect(component.position, closeToVector(Vector2(0, 0), _epsilon));\n    });\n\n    testWithFlameGame('handles infinite rotation', (game) async {\n      final component = PositionComponent(position: Vector2(100, 100));\n      await game.ensureAdd(component);\n\n      final effect = RotateAroundEffect(\n        tau,\n        center: Vector2(50, 50),\n        EffectController(duration: 1, infinite: true),\n      );\n      component.add(effect);\n\n      for (var i = 0; i < 10; i++) {\n        game.update(0.5);\n        expect(component.angle % tau, closeTo(i.isOdd ? 0 : pi, _epsilon));\n        expect(\n          component.position,\n          closeToVector(Vector2.all(i.isOdd ? 100 : 0), _epsilon),\n        );\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/rotate_effect_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/rotate_effect.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('RotateEffect', () {\n    testWithFlameGame('relative', (game) async {\n      final component = PositionComponent();\n      await game.ensureAdd(component);\n\n      component.angle = 1;\n      component.add(\n        RotateEffect.by(1, EffectController(duration: 1)),\n      );\n      game.update(0);\n      expect(component.angle, 1);\n      expect(component.children.length, 1);\n\n      game.update(0.5);\n      expect(component.angle, 1.5);\n\n      game.update(0.5);\n      expect(component.angle, 2);\n      game.update(0);\n      expect(component.children.length, 0);\n      expect(component.angle, 2);\n    });\n\n    testWithFlameGame('absolute', (game) async {\n      game.onGameResize(Vector2(1, 1));\n      final component = PositionComponent();\n      await game.ensureAdd(component);\n\n      component.angle = 1;\n      component.add(\n        RotateEffect.to(3, EffectController(duration: 1)),\n      );\n      game.update(0);\n      expect(component.angle, 1);\n      expect(component.children.length, 1);\n\n      game.update(0.5);\n      expect(component.angle, 2);\n\n      game.update(0.5);\n      expect(component.angle, 3);\n      game.update(0);\n      expect(component.children.length, 0);\n      expect(component.angle, 3);\n    });\n\n    testWithFlameGame('reset relative', (game) async {\n      final component = PositionComponent();\n      await game.ensureAdd(component);\n\n      final effect = RotateEffect.by(1, EffectController(duration: 1));\n      component.add(effect..removeOnFinish = false);\n      for (var i = 0.0; i < 5; i++) {\n        expect(component.angle, i.toNormalizedAngle());\n        // After each reset the object will be rotated by 1 radian relative to\n        // its orientation at the start of the effect\n        effect.reset();\n        game.update(1);\n        expect(component.angle, (i + 1.0).toNormalizedAngle());\n      }\n    });\n\n    testWithFlameGame('reset absolute', (game) async {\n      final component = PositionComponent();\n      await game.ensureAdd(component);\n\n      final effect = RotateEffect.to(1, EffectController(duration: 1));\n      component.add(effect..removeOnFinish = false);\n      for (var i = 0; i < 5; i++) {\n        component.angle = 1 + 4.0 * i;\n        // After each reset the object will be rotated to the value of\n        // `angle == 1`, regardless of its initial orientation.\n        effect.reset();\n        game.update(1);\n        expect(component.angle, 1);\n      }\n    });\n\n    testWithFlameGame('rotation composition', (game) async {\n      final component = PositionComponent();\n      await game.ensureAdd(component);\n\n      component.add(\n        RotateEffect.by(5, EffectController(duration: 10)),\n      );\n      component.add(\n        RotateEffect.by(\n          0.5,\n          EffectController(\n            duration: 1,\n            reverseDuration: 1,\n            repeatCount: 5,\n          ),\n        ),\n      );\n\n      game.update(1);\n      expect(component.angle, closeTo(1, 1e-15)); // 5*1/10 + 0.5*1\n      game.update(1);\n      expect(component.angle, closeTo(1, 1e-15)); // 5*2/10 + 0.5*1 - 0.5*1\n      for (var i = 0; i < 10; i++) {\n        game.update(1);\n      }\n      expect(component.angle, closeTo(5.0.toNormalizedAngle(), 1e-15));\n      expect(component.children.length, 0);\n    });\n\n    testRandom('a very long rotation', (Random rng) async {\n      final game = await initializeFlameGame();\n      final component = PositionComponent();\n      await game.ensureAdd(component);\n\n      final effect = RotateEffect.by(\n        1.0,\n        EffectController(\n          duration: 1,\n          reverseDuration: 1,\n          infinite: true,\n        ),\n      );\n      component.add(effect);\n\n      var totalTime = 0.0;\n      while (totalTime < 999.9) {\n        final dt = rng.nextDouble() * 0.02;\n        totalTime += dt;\n        game.update(dt);\n      }\n      game.update(1000 - totalTime);\n      // Typically, `object.angle` could accumulate numeric discrepancy on the\n      // order of 1e-11 .. 1e-12 by now.\n      expect(component.angle, closeTo(0, 1e-10));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/scale_effect_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/scale_effect.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('ScaleEffect', () {\n    testWithFlameGame('relative', (game) async {\n      final component = PositionComponent();\n      await game.ensureAdd(component);\n\n      await component.add(\n        ScaleEffect.by(Vector2.all(2.0), EffectController(duration: 1)),\n      );\n      game.update(0);\n      expect(component.scale, closeToVector(Vector2(1, 1)));\n      expect(component.children.length, 1);\n\n      game.update(0.5);\n      expect(component.scale, closeToVector(Vector2(1.5, 1.5)));\n\n      game.update(0.5);\n      expect(component.scale, closeToVector(Vector2(2, 2)));\n      game.update(0);\n      expect(component.children.length, 0);\n      expect(component.scale, closeToVector(Vector2(2, 2)));\n    });\n\n    testWithFlameGame('absolute', (game) async {\n      final component = PositionComponent();\n      await game.ensureAdd(component);\n\n      component.scale = Vector2.all(1.0);\n      await component.add(\n        ScaleEffect.to(Vector2.all(3.0), EffectController(duration: 1)),\n      );\n      game.update(0);\n      expect(component.scale, closeToVector(Vector2(1, 1)));\n      expect(component.children.length, 1);\n\n      game.update(0.5);\n      expect(component.scale, closeToVector(Vector2(2, 2)));\n\n      game.update(0.5);\n      expect(component.scale, closeToVector(Vector2(3, 3)));\n      game.update(0);\n      expect(component.children.length, 0);\n      expect(component.scale, closeToVector(Vector2(3, 3)));\n    });\n\n    testWithFlameGame('reset relative', (game) async {\n      final component = PositionComponent();\n      await game.ensureAdd(component);\n\n      final effect = ScaleEffect.by(\n        Vector2.all(2.0),\n        EffectController(duration: 1),\n      );\n      await component.add(effect..removeOnFinish = false);\n      var expectedScale = 1.0;\n      for (var i = 0; i < 5; i++) {\n        // After each reset the object will be scaled up twice\n        // relative to its scale at the start of the effect.\n        effect.reset();\n        game.update(1);\n        expectedScale *= 2;\n        expect(component.scale, closeToVector(Vector2.all(expectedScale)));\n      }\n    });\n\n    testWithFlameGame('reset absolute', (game) async {\n      final component = PositionComponent();\n      await game.ensureAdd(component);\n\n      final effect = ScaleEffect.to(\n        Vector2.all(1.0),\n        EffectController(duration: 1),\n      );\n      await component.add(effect..removeOnFinish = false);\n      for (var i = 0; i < 5; i++) {\n        component.scale = Vector2.all(1 + 4.0 * i);\n        // After each reset the object will be scaled to the value of\n        // `Vector2(1, 1)`, regardless of its initial orientation.\n        effect.reset();\n        game.update(1);\n        expect(component.scale, closeToVector(Vector2.all(1)));\n      }\n    });\n\n    testWithFlameGame('scale composition', (game) async {\n      final component = PositionComponent()..flipVertically();\n      await game.ensureAdd(component);\n\n      await component.add(\n        ScaleEffect.by(Vector2.all(5), EffectController(duration: 10)),\n      );\n      component.add(\n        ScaleEffect.by(\n          Vector2.all(0.5),\n          EffectController(\n            duration: 1,\n            reverseDuration: 1,\n            repeatCount: 5,\n          ),\n        ),\n      );\n\n      game.update(1);\n      // (1 + 0.4) * 0.5\n      expect(component.scale.x, closeTo(0.7, toleranceFloat32(0.7)));\n      expect(component.scale.y, closeTo(-0.7, toleranceFloat32(-0.7)));\n      game.update(1);\n      // (1 + 2*0.4) * 1\n      expect(component.scale.x, closeTo(1.8, toleranceFloat32(1.8)));\n      expect(component.scale.y, closeTo(-1.8, toleranceFloat32(-1.8)));\n      for (var i = 0; i < 8; i++) {\n        game.update(1);\n      }\n      expect(component.scale.x, closeTo(5, toleranceFloat32(5)));\n      expect(component.scale.y, closeTo(-5, toleranceFloat32(-5)));\n      game.update(0);\n      expect(component.children.length, 0);\n    });\n\n    testRandom('a very long scale change', (Random rng) async {\n      final game = await initializeFlameGame();\n      final component = PositionComponent();\n      await game.ensureAdd(component);\n\n      final effect = ScaleEffect.by(\n        Vector2.all(1.0),\n        EffectController(\n          duration: 1,\n          reverseDuration: 1,\n          infinite: true,\n        ),\n      );\n      await component.add(effect);\n\n      var totalTime = 0.0;\n      while (totalTime < 999.9) {\n        final dt = rng.nextDouble() * 0.02;\n        totalTime += dt;\n        game.update(dt);\n      }\n      game.update(1000 - totalTime);\n      // Typically, `component.scale` could accumulate numeric discrepancy on\n      // the order of 1e-11 .. 1e-12 by now.\n      expect(component.scale, closeToVector(Vector2.all(1), 1e-10));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/sequence_effect_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('SequenceEffect', () {\n    group('properties', () {\n      test('simple', () {\n        final effect = SequenceEffect([\n          MoveEffect.to(Vector2(10, 10), EffectController(duration: 3)),\n          MoveEffect.by(\n            Vector2(1, 0),\n            EffectController(duration: 0.1, repeatCount: 15),\n          ),\n        ]);\n        expect(effect.controller.duration, 4.5);\n        expect(effect.controller.isRandom, false);\n        expect(effect.controller.completed, false);\n      });\n\n      test('alternating', () {\n        final effect = SequenceEffect(\n          [\n            MoveEffect.to(Vector2(10, 10), EffectController(duration: 3)),\n          ],\n          alternate: true,\n        );\n        expect(effect.controller.duration, 6);\n        expect(effect.controller.isRandom, false);\n      });\n\n      test('infinite', () {\n        final effect = SequenceEffect(\n          [\n            MoveEffect.to(Vector2.zero(), EffectController(duration: 1)),\n          ],\n          alternate: true,\n          infinite: true,\n        );\n        expect(effect.controller.duration, double.infinity);\n        expect(effect.controller.isRandom, false);\n      });\n\n      test('with random effects', () {\n        final randomEffect = MoveEffect.to(\n          Vector2(10, 10),\n          RandomEffectController.uniform(\n            LinearEffectController(0),\n            min: 1,\n            max: 5,\n          ),\n        );\n        final effect = SequenceEffect(\n          [randomEffect],\n          alternate: true,\n          repeatCount: 1000,\n        );\n        expect(\n          effect.controller.duration,\n          closeTo(randomEffect.controller.duration! * 2000, 1e-15),\n        );\n        expect(effect.controller.isRandom, true);\n      });\n\n      test('errors', () {\n        expect(\n          () => SequenceEffect(<Effect>[]),\n          failsAssert('The list of effects cannot be empty'),\n        );\n        expect(\n          () => SequenceEffect(\n            [MoveEffect.to(Vector2.zero(), EffectController(duration: 1))],\n            infinite: true,\n            repeatCount: 10,\n          ),\n          failsAssert(\n            'Parameters infinite and repeatCount cannot be specified '\n            'simultaneously',\n          ),\n        );\n      });\n    });\n\n    group('sequence progression', () {\n      testWithFlameGame('simple sequence', (game) async {\n        final effect = SequenceEffect([\n          MoveEffect.by(Vector2(10, 0), EffectController(duration: 1)),\n          MoveEffect.by(Vector2(0, 10), EffectController(duration: 2)),\n          MoveEffect.by(Vector2(-10, 0), EffectController(duration: 3)),\n          MoveEffect.by(Vector2(30, 30), EffectController(duration: 4)),\n        ]);\n        final component = PositionComponent()..add(effect);\n        game.add(component);\n        await game.ready();\n\n        // Each point here is spaced 0.1 seconds apart\n        final expectedPositions = <Vector2>[\n          ...List.generate(10, (i) => Vector2(i * 1.0, 0)),\n          ...List.generate(20, (i) => Vector2(10, i * 0.5)),\n          ...List.generate(30, (i) => Vector2(10 - i / 3, 10)),\n          ...List.generate(40, (i) => Vector2(i * 0.75, 10 + i * 0.75)),\n          Vector2(30, 40),\n        ];\n        var tolerance = toleranceVector2Float32(Vector2.zero());\n        for (final p in expectedPositions) {\n          // floating point errors are cumulative\n          tolerance += toleranceVector2Float32(p);\n          expect(\n            component.position,\n            closeToVector(\n              p,\n              tolerance,\n            ),\n          );\n          game.update(0.1);\n        }\n      });\n\n      testWithFlameGame('large step', (game) async {\n        final effect = SequenceEffect([\n          MoveEffect.by(Vector2(10, 0), EffectController(duration: 1)),\n          MoveEffect.by(Vector2(0, 10), EffectController(duration: 2)),\n          MoveEffect.by(Vector2(-10, 0), EffectController(duration: 3)),\n          MoveEffect.by(Vector2(30, 30), EffectController(duration: 4)),\n        ]);\n        final component = PositionComponent()..add(effect);\n        game.add(component);\n        await game.ready();\n\n        game.update(10);\n        expect(component.position, closeToVector(Vector2(30, 40)));\n      });\n\n      testWithFlameGame('k-step sequence', (game) async {\n        final effect = SequenceEffect(\n          [\n            MoveEffect.by(Vector2(10, 0), EffectController(duration: 1)),\n            MoveEffect.by(Vector2(0, 10), EffectController(duration: 1)),\n          ],\n          repeatCount: 5,\n        );\n        final component = PositionComponent()..add(effect);\n        game.add(component);\n        await game.ready();\n\n        for (var i = 0; i < 10; i++) {\n          final x = ((i + 1) ~/ 2) * 10.0;\n          final y = (i ~/ 2) * 10.0;\n          expect(component.position, closeToVector(Vector2(x, y)));\n          expect(effect.isMounted, true);\n          game.update(1);\n        }\n        game.update(5); // Will schedule the `effect` component for deletion\n        game.update(0); // Second update ensures the game deletes the component\n        expect(effect.isMounted, false);\n        expect(component.position, closeToVector(Vector2(50, 50)));\n      });\n\n      testWithFlameGame('alternating sequence', (game) async {\n        final effect = SequenceEffect(\n          [\n            MoveEffect.by(Vector2(10, 0), EffectController(duration: 1)),\n            MoveEffect.by(Vector2(0, 10), EffectController(duration: 1)),\n          ],\n          alternate: true,\n        );\n        expect(effect.controller.duration, 4);\n\n        final component = PositionComponent()..add(effect);\n        game.add(component);\n        await game.ready();\n\n        final expectedPath = <Vector2>[\n          for (var i = 0.0; i < 10; i++) Vector2(i, 0),\n          for (var i = 0.0; i < 10; i++) Vector2(10, i),\n          for (var i = 10.0; i > 0; i--) Vector2(10, i),\n          for (var i = 10.0; i > 0; i--) Vector2(i, 0),\n        ];\n        for (final p in expectedPath) {\n          expect(component.position, closeToVector(p, 1e-14));\n          game.update(0.1);\n        }\n        game.update(0.001);\n        expect(effect.controller.completed, true);\n      });\n\n      testWithFlameGame('sequence of alternates', (game) async {\n        EffectController controller() =>\n            EffectController(duration: 1, alternate: true);\n        final effect = SequenceEffect(\n          [\n            MoveEffect.by(Vector2(1, 0), controller()),\n            MoveEffect.by(Vector2(0, 1), controller()),\n          ],\n          alternate: true,\n        );\n\n        final component = PositionComponent()..add(effect);\n        game.add(component);\n        await game.ready();\n\n        final forwardPath = <Vector2>[\n          for (var i = 0; i < 10; i++) Vector2(i * 0.1, 0),\n          for (var i = 10; i > 0; i--) Vector2(i * 0.1, 0),\n          for (var i = 0; i < 10; i++) Vector2(0, i * 0.1),\n          for (var i = 10; i > 0; i--) Vector2(0, i * 0.1),\n        ];\n        final expectedPath = [\n          ...forwardPath,\n          Vector2.zero(),\n          ...forwardPath.reversed,\n        ];\n        var tolerance = toleranceVector2Float32(Vector2.zero());\n        for (final p in expectedPath) {\n          tolerance += toleranceVector2Float32(p);\n          expect(\n            component.position,\n            closeToVector(\n              p,\n              tolerance,\n            ),\n          );\n          game.update(0.1);\n        }\n        game.update(0.001);\n        expect(\n          component.position,\n          closeToVector(\n            Vector2.zero(),\n            tolerance + toleranceFloat32(0),\n          ),\n        );\n        expect(effect.controller.completed, true);\n      });\n\n      testWithFlameGame('with SpeedEffectController', (game) async {\n        final effect = SequenceEffect([\n          MoveEffect.to(\n            Vector2(10, 0),\n            EffectController(speed: 10.0),\n          ),\n          MoveEffect.by(\n            Vector2(10, 0),\n            EffectController(duration: 1.0),\n          ),\n        ]);\n        await game.ensureAdd(PositionComponent(children: [effect]));\n        game.update(0);\n        expect(effect.controller.duration, 2.0);\n        expect(effect.controller.completed, false);\n      });\n\n      testWithFlameGame('sequence in sequence', (game) async {\n        EffectController duration(double t) => EffectController(duration: t);\n        const dt = 0.01;\n        const x0 = 0.0;\n        const y0 = 0.0;\n        const x1 = 10.0;\n        const y1 = 10.0;\n        const x2 = 20.0;\n        const y2 = 0.0;\n        const x3 = 30.0;\n        const y3 = 10.0;\n        const x4 = 10.0;\n        const y4 = 30.0;\n        const dx5 = 1.6;\n        const dy5 = 0.9;\n\n        final effect = SequenceEffect(\n          [\n            MoveEffect.by(Vector2(x1 - x0, y1 - y0), duration(1)),\n            SequenceEffect(\n              [\n                MoveEffect.to(Vector2(x2, y2), duration(1)),\n                MoveEffect.to(Vector2(x3, y3), duration(1)),\n              ],\n              alternate: true,\n              repeatCount: 2,\n            ),\n            MoveEffect.by(Vector2(x4 - x1, y4 - y1), duration(2)),\n            SequenceEffect(\n              [\n                MoveEffect.by(Vector2(dx5, 0), duration(1)),\n                MoveEffect.by(Vector2(0, dy5), duration(1)),\n              ],\n              repeatCount: 5,\n            ),\n          ],\n          alternate: true,\n        );\n        expect(effect.controller.duration, 42);\n\n        final component = PositionComponent()..add(effect);\n        game.add(component);\n        await game.ready();\n\n        // All points here are spaced `dt = 0.01` apart\n        final forwardPath = <Vector2>[\n          // First MoveEffect\n          for (var t = 0.0; t < 1; t += dt)\n            Vector2(x0 + (x1 - x0) * t, y0 + (y1 - y0) * t),\n          // First SequenceEffect\n          for (var t = 0.0; t < 1; t += dt)\n            Vector2(x1 + (x2 - x1) * t, y1 + (y2 - y1) * t),\n          for (var t = 0.0; t < 1; t += dt)\n            Vector2(x2 + (x3 - x2) * t, y2 + (y3 - y2) * t),\n          for (var t = 1.0; t > 0; t -= dt)\n            Vector2(x2 + (x3 - x2) * t, y2 + (y3 - y2) * t),\n          for (var t = 1.0; t > 0; t -= dt)\n            Vector2(x1 + (x2 - x1) * t, y1 + (y2 - y1) * t),\n          // First SequenceEffect, repeated second time\n          for (var t = 0.0; t < 1; t += dt)\n            Vector2(x1 + (x2 - x1) * t, y1 + (y2 - y1) * t),\n          for (var t = 0.0; t < 1; t += dt)\n            Vector2(x2 + (x3 - x2) * t, y2 + (y3 - y2) * t),\n          for (var t = 1.0; t > 0; t -= dt)\n            Vector2(x2 + (x3 - x2) * t, y2 + (y3 - y2) * t),\n          for (var t = 1.0; t > 0; t -= dt)\n            Vector2(x1 + (x2 - x1) * t, y1 + (y2 - y1) * t),\n          // Second MoveEffect, duration = 2\n          for (var t = 0.0; t < 2; t += dt)\n            Vector2(x1 + (x4 - x1) * t, y1 + (y4 - y1) * t / 2),\n          // Second sequence effect, repeated 5 times\n          for (var j = 0; j < 5; j++)\n            for (var t = 0.0; t < 2; t += dt)\n              Vector2(\n                x4 + min(j + t, j + 1) * dx5,\n                y4 + max(j + t - 1, j) * dy5,\n              ),\n        ];\n        final expectedPath = <Vector2>[\n          ...forwardPath,\n          Vector2(x4 + 5 * dx5, y4 + 5 * dy5),\n          ...forwardPath.reversed,\n        ];\n\n        var tolerance = toleranceVector2Float32(Vector2.zero());\n        for (final p in expectedPath) {\n          tolerance += toleranceVector2Float32(p);\n          expect(\n            component.position,\n            closeToVector(\n              p,\n              // this is the maximum error we can have by the end.\n              tolerance,\n            ),\n          );\n          game.update(dt);\n        }\n        game.update(1e-5);\n        expect(effect.controller.completed, true);\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/effects/size_effect_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('SizeEffect', () {\n    testWithFlameGame('relative', (game) async {\n      final component = _ResizableComponent();\n      await game.ensureAdd(component);\n\n      component.size = Vector2.all(1.0);\n      await component.add(\n        SizeEffect.by(Vector2.all(1.0), EffectController(duration: 1)),\n      );\n      game.update(0);\n      expect(component.size, closeToVector(Vector2(1, 1)));\n      expect(component.children.length, 1);\n\n      game.update(0.5);\n      expect(component.size, closeToVector(Vector2(1.5, 1.5)));\n\n      game.update(0.5);\n      expect(component.size, closeToVector(Vector2(2, 2)));\n      game.update(0);\n      expect(component.children.length, 0);\n      expect(component.size, closeToVector(Vector2(2, 2)));\n    });\n\n    testWithFlameGame('absolute', (game) async {\n      final component = _ResizableComponent();\n      await game.ensureAdd(component);\n\n      component.size = Vector2.all(1.0);\n      await component.add(\n        SizeEffect.to(Vector2.all(3.0), EffectController(duration: 1)),\n      );\n      game.update(0);\n      expect(component.size, closeToVector(Vector2(1, 1)));\n      expect(component.children.length, 1);\n\n      game.update(0.5);\n      expect(component.size, closeToVector(Vector2(2, 2)));\n\n      game.update(0.5);\n      expect(component.size, closeToVector(Vector2(3, 3)));\n      game.update(0);\n      expect(component.children.length, 0);\n      expect(component.size, closeToVector(Vector2(3, 3)));\n    });\n\n    testWithFlameGame('reset relative', (game) async {\n      final component = _ResizableComponent();\n      await game.ensureAdd(component);\n\n      final effect = SizeEffect.by(\n        Vector2.all(1.0),\n        EffectController(duration: 1),\n      );\n      await component.add(effect..removeOnFinish = false);\n      final expectedSize = Vector2.zero();\n      for (var i = 0; i < 5; i++) {\n        // After each reset the object will be sized up by Vector2(1, 1)\n        // relative to its size at the start of the effect.\n        effect.reset();\n        game.update(1);\n        expectedSize.add(Vector2.all(1.0));\n        expect(component.size, closeToVector(expectedSize));\n      }\n    });\n\n    testWithFlameGame('reset absolute', (game) async {\n      final component = _ResizableComponent();\n      await game.ensureAdd(component);\n\n      final effect = SizeEffect.to(\n        Vector2.all(1.0),\n        EffectController(duration: 1),\n      );\n      component.add(effect..removeOnFinish = false);\n      for (var i = 0; i < 5; i++) {\n        component.size = Vector2.all(1 + 4.0 * i);\n        // After each reset the object will be sized to the value of\n        // `Vector2(1, 1)`, regardless of its initial orientation.\n        effect.reset();\n        game.update(1);\n        expect(component.size, closeToVector(Vector2(1, 1)));\n      }\n    });\n\n    testWithFlameGame('size composition', (game) async {\n      final component = _ResizableComponent();\n      await game.ensureAdd(component);\n\n      await component.add(\n        SizeEffect.by(Vector2.all(5), EffectController(duration: 10)),\n      );\n      await component.add(\n        SizeEffect.by(\n          Vector2.all(0.5),\n          EffectController(\n            duration: 1,\n            reverseDuration: 1,\n            repeatCount: 5,\n          ),\n        ),\n      );\n\n      game.update(1);\n      expect(component.size, closeToVector(Vector2(1, 1))); // 5*1/10 + 0.5*1\n      game.update(1);\n      // 5*2/10 + 0.5*1 - 0.5*1\n      expect(component.size, closeToVector(Vector2(1, 1)));\n      for (var i = 0; i < 10; i++) {\n        game.update(1);\n      }\n      expect(component.size, closeToVector(Vector2(5, 5)));\n      expect(component.children.length, 0);\n    });\n\n    testRandom('a very long size change', (Random rng) async {\n      final game = await initializeFlameGame();\n      final component = _ResizableComponent();\n      await game.ensureAdd(component);\n\n      final effect = SizeEffect.by(\n        Vector2.all(1.0),\n        EffectController(\n          duration: 1,\n          reverseDuration: 1,\n          infinite: true,\n        ),\n      );\n      await component.add(effect);\n\n      var totalTime = 0.0;\n      var tolerance = toleranceFloat32(0);\n      while (totalTime < 999.9) {\n        final dt = rng.nextDouble() * 0.02;\n        totalTime += dt;\n        game.update(dt);\n        // cumulative floating point error\n        tolerance += toleranceVector2Float32(component.size);\n      }\n      game.update(1000 - totalTime);\n      // Typically, `component.size` could accumulate numeric discrepancy on the\n      // order of 1e-11 .. 1e-12 by now.\n      expect(\n        component.size,\n        closeToVector(Vector2(0, 0), tolerance),\n      );\n    });\n  });\n}\n\nclass _ResizableComponent extends PositionComponent implements SizeProvider {}\n"
  },
  {
    "path": "packages/flame/test/effects/transform2d_effect_test.dart",
    "content": "import 'package:flame/src/components/position_component.dart';\nimport 'package:flame/src/effects/controllers/effect_controller.dart';\nimport 'package:flame/src/effects/transform2d_effect.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _MyEffect extends Transform2DEffect {\n  _MyEffect(super.controller);\n\n  @override\n  void apply(double progress) {}\n}\n\nvoid main() {\n  group('Transform2DEffect', () {\n    testWithFlameGame(\n      'onMount',\n      (game) async {\n        final component = PositionComponent();\n        game.add(component);\n        await game.ready();\n\n        final effect = _MyEffect(EffectController(duration: 1));\n        component.add(effect);\n        game.update(0);\n        expect(effect.transform, component.transform);\n\n        final effect2 = _MyEffect(EffectController(duration: 1));\n        expect(\n          () => game.ensureAdd(effect2),\n          throwsA(isA<UnsupportedError>()),\n        );\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/events/component_mixins/double_tap_callbacks_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('DoubleTapCallbacks', () {\n    testWidgets(\n      'receives double-tap event',\n      (tester) async {\n        final component = _DoubleTapCallbacksComponent()\n          ..position = Vector2.all(10);\n        await tester.pumpWidget(\n          GameWidget(\n            game: FlameGame(children: [component]),\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n\n        final gesture = await tester.createGesture();\n        await gesture.down(const Offset(10, 10));\n        await tester.pump();\n\n        await gesture.up();\n        expect(component.doubleTapDown, 0);\n        expect(component.doubleTapCancel, 0);\n        expect(component.doubleTap, 0);\n\n        await tester.pump(kDoubleTapMinTime);\n        await gesture.down(const Offset(10, 10));\n\n        expect(component.doubleTapDown, 1);\n        expect(component.doubleTapCancel, 0);\n        expect(component.doubleTap, 0);\n\n        await gesture.up();\n\n        expect(component.doubleTapDown, 1);\n        expect(component.doubleTapCancel, 0);\n        expect(component.doubleTap, 1);\n\n        await tester.pump(kDoubleTapMinTime);\n      },\n    );\n\n    testWidgets(\n      '''does not receive an event when double-tapping a position far from the component''',\n      (tester) async {\n        final component = _DoubleTapCallbacksComponent()\n          ..position = Vector2.all(10);\n        await tester.pumpWidget(\n          GameWidget(\n            game: FlameGame(children: [component]),\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n\n        final gesture = await tester.createGesture();\n        await gesture.down(const Offset(100, 100));\n        await gesture.up();\n\n        await tester.pump(kDoubleTapMinTime);\n        await gesture.down(const Offset(100, 100));\n\n        expect(component.doubleTapDown, 0);\n        expect(component.doubleTapCancel, 0);\n        expect(component.doubleTap, 0);\n\n        await gesture.up();\n\n        expect(component.doubleTapDown, 0);\n        expect(component.doubleTapCancel, 0);\n        expect(component.doubleTap, 0);\n\n        await tester.pump(kDoubleTapMinTime);\n      },\n    );\n\n    testWidgets(\n      'receives a cancel event when gesture is canceled by drag',\n      (tester) async {\n        final component = _DoubleTapCallbacksComponent()\n          ..position = Vector2.all(10);\n        await tester.pumpWidget(\n          GameWidget(\n            game: FlameGame(children: [component]),\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n\n        final gesture = await tester.createGesture();\n        await gesture.down(const Offset(10, 10));\n        await gesture.up();\n\n        await tester.pump(kDoubleTapMinTime);\n        await gesture.down(const Offset(10, 10));\n\n        expect(component.doubleTapDown, 1);\n        expect(component.doubleTapCancel, 0);\n        expect(component.doubleTap, 0);\n\n        await gesture.moveBy(const Offset(100, 100));\n\n        expect(component.doubleTapDown, 1);\n        expect(component.doubleTapCancel, 1);\n        expect(component.doubleTap, 0);\n\n        await tester.pump(kDoubleTapMinTime);\n      },\n    );\n\n    testWidgets(\n      'receives a cancel event when gesture is canceled by cancel',\n      (tester) async {\n        final component = _DoubleTapCallbacksComponent()\n          ..position = Vector2.all(10);\n        await tester.pumpWidget(\n          GameWidget(\n            game: FlameGame(children: [component]),\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n\n        final gesture = await tester.createGesture();\n        await gesture.down(const Offset(10, 10));\n        await gesture.up();\n\n        await tester.pump(kDoubleTapMinTime);\n        await gesture.down(const Offset(10, 10));\n\n        expect(component.doubleTapDown, 1);\n        expect(component.doubleTapCancel, 0);\n        expect(component.doubleTap, 0);\n\n        await gesture.cancel();\n\n        expect(component.doubleTapDown, 1);\n        expect(component.doubleTapCancel, 1);\n        expect(component.doubleTap, 0);\n\n        await tester.pump(kDoubleTapMinTime);\n      },\n    );\n\n    testWithFlameGame(\n      'DoubleTapDispatcher is added to game when the callback is mounted',\n      (game) async {\n        final component = _DoubleTapCallbacksComponent();\n        await game.add(component);\n        await game.ready();\n\n        expect(game.firstChild<DoubleTapDispatcher>(), isNotNull);\n      },\n    );\n  });\n}\n\nclass _DoubleTapCallbacksComponent extends PositionComponent\n    with DoubleTapCallbacks {\n  _DoubleTapCallbacksComponent() {\n    anchor = Anchor.center;\n    size = Vector2.all(10);\n  }\n\n  int doubleTapDown = 0;\n  int doubleTapCancel = 0;\n  int doubleTap = 0;\n\n  @override\n  void onDoubleTapUp(DoubleTapEvent event) {\n    doubleTap++;\n  }\n\n  @override\n  void onDoubleTapCancel(DoubleTapCancelEvent event) {\n    doubleTapCancel++;\n  }\n\n  @override\n  void onDoubleTapDown(DoubleTapDownEvent event) {\n    doubleTapDown++;\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/events/component_mixins/drag_callbacks_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('DragCallbacks', () {\n    testWithFlameGame(\n      'make sure DragCallback components can be added to a FlameGame',\n      (game) async {\n        await game.add(_DragCallbacksComponent());\n        await game.ready();\n        expect(game.children.toList()[2], isA<MultiDragDispatcher>());\n      },\n    );\n\n    testWithFlameGame('drag event start', (game) async {\n      final component = _DragCallbacksComponent()\n        ..x = 10\n        ..y = 10\n        ..width = 10\n        ..height = 10;\n      game.add(component);\n      await game.ready();\n\n      expect(game.children.whereType<MultiDragDispatcher>().length, 1);\n      game.firstChild<MultiDragDispatcher>()!.onDragStart(\n        createDragStartEvents(\n          game: game,\n          localPosition: const Offset(12, 12),\n          globalPosition: const Offset(12, 12),\n        ),\n      );\n      expect(component.containsLocalPoint(Vector2(10, 10)), false);\n    });\n\n    testWithFlameGame('drag event start, update and cancel', (game) async {\n      final component = _DragCallbacksComponent()\n        ..x = 10\n        ..y = 10\n        ..width = 10\n        ..height = 10;\n      await game.ensureAdd(component);\n      final dispatcher = game.firstChild<MultiDragDispatcher>()!;\n\n      dispatcher.onDragStart(\n        createDragStartEvents(\n          game: game,\n          localPosition: const Offset(12, 12),\n          globalPosition: const Offset(12, 12),\n        ),\n      );\n      expect(component.dragStartEvent, 1);\n      expect(component.dragUpdateEvent, 0);\n      expect(component.dragEndEvent, 0);\n\n      dispatcher.onDragUpdate(\n        createDragUpdateEvents(\n          game: game,\n          localPosition: const Offset(15, 15),\n          globalPosition: const Offset(15, 15),\n        ),\n      );\n\n      expect(game.containsLocalPoint(Vector2(9, 9)), isTrue);\n      expect(component.dragUpdateEvent, equals(1));\n\n      dispatcher.onDragEnd(DragEndEvent(1, DragEndDetails()));\n      expect(component.dragEndEvent, equals(1));\n    });\n\n    testWithFlameGame(\n      'drag event update not called without onDragStart',\n      (game) async {\n        final component = _DragCallbacksComponent()\n          ..x = 10\n          ..y = 10\n          ..width = 10\n          ..height = 10;\n        await game.ensureAdd(component);\n        final dispatcher = game.firstChild<MultiDragDispatcher>()!;\n        expect(component.dragStartEvent, equals(0));\n        expect(component.dragUpdateEvent, equals(0));\n\n        dispatcher.onDragUpdate(\n          createDragUpdateEvents(\n            game: game,\n            localPosition: const Offset(15, 15),\n            globalPosition: const Offset(15, 15),\n          ),\n        );\n        expect(component.dragUpdateEvent, equals(0));\n      },\n    );\n\n    testWidgets(\n      'drag correctly registered handled event',\n      (tester) async {\n        final component = _DragCallbacksComponent()\n          ..x = 10\n          ..y = 10\n          ..width = 10\n          ..height = 10;\n        final game = FlameGame(children: [component]);\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n        await tester.pump();\n        expect(game.children.length, equals(4));\n        expect(component.isMounted, isTrue);\n\n        await tester.dragFrom(const Offset(10, 10), const Offset(90, 90));\n        expect(component.dragStartEvent, equals(1));\n        expect(component.dragUpdateEvent, greaterThan(0));\n        expect(component.dragEndEvent, equals(1));\n        expect(component.dragCancelEvent, equals(0));\n      },\n    );\n\n    testWidgets(\n      'drag outside of component is not registered as handled',\n      (tester) async {\n        final component = _DragCallbacksComponent()..size = Vector2.all(100);\n        final game = FlameGame(children: [component]);\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n        await tester.pump();\n        expect(component.isMounted, isTrue);\n\n        await tester.dragFrom(const Offset(110, 110), const Offset(120, 120));\n        expect(component.dragStartEvent, equals(0));\n        expect(component.dragUpdateEvent, equals(0));\n        expect(component.dragEndEvent, equals(0));\n        expect(component.dragCancelEvent, equals(0));\n      },\n    );\n\n    testWithGame(\n      'make sure the FlameGame can registers DragCallback on itself',\n      _DragCallbacksGame.new,\n      (game) async {\n        await game.ready();\n        expect(game.children.length, equals(3));\n        expect(game.children.elementAt(1), isA<MultiDragDispatcher>());\n      },\n    );\n\n    testWidgets(\n      'drag correctly registered handled event directly on FlameGame',\n      (tester) async {\n        final game = _DragCallbacksGame()..onGameResize(Vector2.all(300));\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n        await tester.pump();\n        expect(game.children.length, equals(3));\n        expect(game.isMounted, isTrue);\n\n        await tester.dragFrom(const Offset(10, 10), const Offset(90, 90));\n        expect(game.dragStartEvent, equals(1));\n        expect(game.dragUpdateEvent, greaterThan(0));\n        expect(game.dragEndEvent, equals(1));\n        expect(game.dragCancelEvent, equals(0));\n      },\n    );\n\n    testWidgets(\n      'isDragged is changed',\n      (tester) async {\n        final component = _DragCallbacksComponent()..size = Vector2.all(100);\n        final game = FlameGame(children: [component]);\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n        await tester.pump();\n\n        // Inside component\n        await tester.dragFrom(const Offset(10, 10), const Offset(90, 90));\n        expect(component.isDraggedStateChange, equals(2));\n\n        // Outside component\n        await tester.dragFrom(const Offset(101, 101), const Offset(110, 110));\n        expect(component.isDraggedStateChange, equals(2));\n      },\n    );\n  });\n\n  group('HasDraggableComponents', () {\n    testWidgets(\n      'drags are delivered to DragCallbacks components',\n      (tester) async {\n        var nDragStartCalled = 0;\n        var nDragUpdateCalled = 0;\n        var nDragEndCalled = 0;\n        final game = FlameGame(\n          children: [\n            _DragWithCallbacksComponent(\n              position: Vector2(20, 20),\n              size: Vector2(100, 100),\n              onDragStart: (e) => nDragStartCalled++,\n              onDragUpdate: (e) => nDragUpdateCalled++,\n              onDragEnd: (e) => nDragEndCalled++,\n            ),\n          ],\n        );\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n        await tester.pump(const Duration(milliseconds: 10));\n\n        expect(game.children.length, 4);\n        expect(game.children.elementAt(1), isA<_DragWithCallbacksComponent>());\n        expect(game.children.elementAt(2), isA<MultiDragDispatcher>());\n\n        // regular drag\n        await tester.timedDragFrom(\n          const Offset(50, 50),\n          const Offset(20, 0),\n          const Duration(milliseconds: 100),\n        );\n        expect(nDragStartCalled, 1);\n        expect(nDragUpdateCalled, 8);\n        expect(nDragEndCalled, 1);\n\n        // cancelled drag\n        final gesture = await tester.startGesture(const Offset(50, 50));\n        await gesture.moveBy(const Offset(10, 10));\n        await gesture.cancel();\n        await tester.pump(const Duration(seconds: 1));\n        expect(nDragStartCalled, 2);\n        expect(nDragEndCalled, 2);\n      },\n    );\n\n    testWidgets(\n      'drag event does not affect more than one component',\n      (tester) async {\n        var nEvents = 0;\n        final game = FlameGame(\n          children: [\n            _DragWithCallbacksComponent(\n              size: Vector2.all(100),\n              onDragStart: (e) => nEvents++,\n              onDragUpdate: (e) => nEvents++,\n              onDragEnd: (e) => nEvents++,\n            ),\n            _SimpleDragCallbacksComponent(size: Vector2.all(200)),\n          ],\n        );\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n        await tester.pump();\n        expect(game.children.length, 5);\n        expect(game.children.elementAt(3), isA<MultiDragDispatcher>());\n\n        await tester.timedDragFrom(\n          const Offset(20, 20),\n          const Offset(5, 5),\n          const Duration(seconds: 1),\n        );\n        expect(nEvents, 0);\n      },\n    );\n\n    testWidgets(\n      'drag event can move outside the component bounds and still fire',\n      (tester) async {\n        final points = <Vector2>[];\n        final game = FlameGame(\n          children: [\n            _DragWithCallbacksComponent(\n              size: Vector2.all(95),\n              position: Vector2.all(5),\n              onDragUpdate: (e) => points.add(e.localStartPosition),\n            ),\n          ],\n        );\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n        await tester.pump();\n        expect(game.children.length, 4);\n        expect(game.children.elementAt(2), isA<MultiDragDispatcher>());\n\n        await tester.timedDragFrom(\n          const Offset(80, 80),\n          const Offset(0, 40),\n          const Duration(seconds: 1),\n          frequency: 40,\n        );\n        expect(points.length, 42);\n        expect(points.first, Vector2(75, 75));\n        expect(\n          points.skip(1),\n          List.generate(41, (i) => Vector2(75.0, 75.0 + i)),\n        );\n      },\n    );\n  });\n\n  testWidgets(\n    'drag event delta respects camera & zoom',\n    (tester) async {\n      // canvas size is 800x600 so this means a 10x logical scale across\n      // both dimensions\n      final resolution = Vector2(80, 60);\n      final game = FlameGame(\n        camera: CameraComponent.withFixedResolution(\n          width: resolution.x,\n          height: resolution.y,\n        ),\n      );\n\n      game.camera.viewfinder.zoom = 2;\n\n      final deltas = <Vector2>[];\n      await game.world.add(\n        _DragWithCallbacksComponent(\n          position: Vector2.all(-5),\n          size: Vector2.all(10),\n          onDragUpdate: (event) => deltas.add(event.localDelta),\n        ),\n      );\n      await tester.pumpWidget(GameWidget(game: game));\n      await tester.pump();\n      await tester.pump();\n\n      final canvasSize = game.canvasSize;\n      await tester.dragFrom(\n        (canvasSize / 2).toOffset(),\n        Offset(canvasSize.x / 10, 0),\n      );\n      final totalDelta = deltas.reduce((a, b) => a + b);\n      expect(totalDelta, Vector2(4, 0));\n    },\n  );\n\n  testWidgets(\n    'drag event delta respects widget positioning',\n    (tester) async {\n      // canvas size is 800x600 so this means a 10x logical scale across\n      // both dimensions\n      final resolution = Vector2(80, 60);\n      final game = FlameGame(\n        camera: CameraComponent.withFixedResolution(\n          width: resolution.x,\n          height: resolution.y,\n        ),\n      );\n\n      game.camera.viewfinder.zoom = 1 / 2;\n\n      final deltas = <Vector2>[];\n      await game.world.add(\n        _DragWithCallbacksComponent(\n          position: Vector2.all(-5),\n          size: Vector2.all(10),\n          onDragUpdate: (event) => deltas.add(event.localDelta),\n        ),\n      );\n      await tester.pumpWidget(\n        MaterialApp(\n          home: Stack(\n            children: [\n              Positioned(\n                left: 100.0,\n                top: 200.0,\n                width: 800,\n                height: 600,\n                child: GameWidget(game: game),\n              ),\n            ],\n          ),\n        ),\n      );\n      await tester.pump();\n      await tester.pump();\n\n      final canvasSize = game.canvasSize;\n\n      // no offset\n      await tester.dragFrom(\n        (canvasSize / 2).toOffset(),\n        Offset(canvasSize.x / 10, 0),\n      );\n      expect(deltas, isEmpty);\n\n      // accounting for offset\n      await tester.dragFrom(\n        (canvasSize / 2 + Vector2(100, 200)).toOffset(),\n        Offset(canvasSize.x / 10, 0),\n      );\n      expect(deltas, isNotEmpty);\n      final totalDelta = deltas.reduce((a, b) => a + b);\n      expect(totalDelta, Vector2(16, 0));\n    },\n  );\n}\n\nmixin _DragCounter on DragCallbacks {\n  int dragStartEvent = 0;\n  int dragUpdateEvent = 0;\n  int dragEndEvent = 0;\n  int dragCancelEvent = 0;\n  int isDraggedStateChange = 0;\n\n  bool _wasDragged = false;\n\n  @override\n  void onDragStart(DragStartEvent event) {\n    super.onDragStart(event);\n    expect(event.raw, isNotNull);\n    event.handled = true;\n    dragStartEvent++;\n    if (_wasDragged != isDragged) {\n      ++isDraggedStateChange;\n      _wasDragged = isDragged;\n    }\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    expect(event.raw, isNotNull);\n    event.handled = true;\n    dragUpdateEvent++;\n  }\n\n  @override\n  void onDragEnd(DragEndEvent event) {\n    super.onDragEnd(event);\n    expect(event.raw, isNotNull);\n    event.handled = true;\n    dragEndEvent++;\n    if (_wasDragged != isDragged) {\n      ++isDraggedStateChange;\n      _wasDragged = isDragged;\n    }\n  }\n\n  @override\n  void onDragCancel(DragCancelEvent event) {\n    super.onDragCancel(event);\n    event.handled = true;\n    dragCancelEvent++;\n  }\n}\n\nclass _DragCallbacksComponent extends PositionComponent\n    with DragCallbacks, _DragCounter {}\n\nclass _DragCallbacksGame extends FlameGame with DragCallbacks, _DragCounter {}\n\nclass _DragWithCallbacksComponent extends PositionComponent with DragCallbacks {\n  _DragWithCallbacksComponent({\n    void Function(DragStartEvent)? onDragStart,\n    void Function(DragUpdateEvent)? onDragUpdate,\n    void Function(DragEndEvent)? onDragEnd,\n    super.position,\n    super.size,\n  }) : _onDragStart = onDragStart,\n       _onDragUpdate = onDragUpdate,\n       _onDragEnd = onDragEnd;\n\n  final void Function(DragStartEvent)? _onDragStart;\n  final void Function(DragUpdateEvent)? _onDragUpdate;\n  final void Function(DragEndEvent)? _onDragEnd;\n\n  @override\n  void onDragStart(DragStartEvent event) {\n    super.onDragStart(event);\n    return _onDragStart?.call(event);\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    return _onDragUpdate?.call(event);\n  }\n\n  @override\n  void onDragEnd(DragEndEvent event) {\n    super.onDragEnd(event);\n    return _onDragEnd?.call(event);\n  }\n}\n\nclass _SimpleDragCallbacksComponent extends PositionComponent\n    with DragCallbacks {\n  _SimpleDragCallbacksComponent({super.size});\n}\n"
  },
  {
    "path": "packages/flame/test/events/component_mixins/hover_callbacks_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('HoverCallbacks', () {\n    testWithFlameGame(\n      'make sure HoverCallbacks components can be added to a FlameGame',\n      (game) async {\n        await game.ensureAdd(_HoverCallbacksComponent());\n        await game.ready();\n\n        _hasDispatcher(game);\n      },\n    );\n\n    testWithFlameGame('receive hover events', (game) async {\n      final component = _HoverCallbacksComponent(\n        position: Vector2.all(10),\n        size: Vector2.all(10),\n      );\n      game.add(component);\n      await game.ready();\n\n      _hasDispatcher(game);\n\n      _mouseEvent(game, Vector2.all(12));\n      component.checkHoverEventCounts(enter: 1, exit: 0);\n\n      _mouseEvent(game, Vector2.all(14));\n      component.checkHoverEventCounts(enter: 1, exit: 0);\n\n      _mouseEvent(game, Vector2.all(16));\n      component.checkHoverEventCounts(enter: 1, exit: 0);\n\n      _mouseEvent(game, Vector2.all(18));\n      component.checkHoverEventCounts(enter: 1, exit: 0);\n\n      _mouseEvent(game, Vector2.all(20));\n      component.checkHoverEventCounts(enter: 1, exit: 1);\n\n      _mouseEvent(game, Vector2.all(22));\n      component.checkHoverEventCounts(enter: 1, exit: 1);\n\n      _mouseEvent(game, Vector2.all(18));\n      component.checkHoverEventCounts(enter: 2, exit: 1);\n\n      _mouseEvent(game, Vector2.all(19));\n      component.checkHoverEventCounts(enter: 2, exit: 1);\n\n      _mouseEvent(game, Vector2.all(20));\n      component.checkHoverEventCounts(enter: 2, exit: 2);\n    });\n  });\n}\n\nvoid _mouseEvent(FlameGame game, Vector2 position) {\n  game.firstChild<PointerMoveDispatcher>()!.onMouseMove(\n    createMouseMoveEvent(\n      game: game,\n      position: position,\n    ),\n  );\n}\n\nvoid _hasDispatcher(FlameGame game) {\n  expect(\n    game.children.whereType<PointerMoveDispatcher>(),\n    hasLength(1),\n  );\n}\n\nmixin _HoverInspector on HoverCallbacks {\n  int hoverEnterEvent = 0;\n  int hoverExitEvent = 0;\n\n  void checkHoverEventCounts({required int enter, required int exit}) {\n    expect(\n      hoverEnterEvent,\n      equals(enter),\n      reason: 'Mismatched hover enter event count',\n    );\n    expect(\n      hoverExitEvent,\n      equals(exit),\n      reason: 'Mismatched hover exit event count',\n    );\n  }\n\n  @override\n  void onHoverEnter() {\n    hoverEnterEvent++;\n  }\n\n  @override\n  void onHoverExit() {\n    hoverExitEvent++;\n  }\n}\n\nclass _HoverCallbacksComponent extends PositionComponent\n    with HoverCallbacks, _HoverInspector {\n  _HoverCallbacksComponent({\n    super.position,\n    super.size,\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/events/component_mixins/ignore_events_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('IgnoreEvents', () {\n    testWithFlameGame(\n      'correctly ignores events all the way down the subtree',\n      (game) async {\n        final grandChild = _IgnoreTapCallbacksComponent();\n        final child = _IgnoreTapCallbacksComponent(children: [grandChild]);\n        final component = _IgnoreTapCallbacksComponent(\n          position: Vector2.all(10),\n          children: [child],\n        );\n\n        await game.ensureAdd(component);\n        final dispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n        dispatcher.onTapDown(\n          createTapDownEvents(\n            game: game,\n            localPosition: const Offset(12, 12),\n            globalPosition: const Offset(12, 12),\n          ),\n        );\n        expect(component.tapDownEvent, equals(0));\n        expect(component.tapUpEvent, equals(0));\n        expect(component.tapCancelEvent, equals(0));\n        expect(child.tapDownEvent, equals(0));\n        expect(child.tapUpEvent, equals(0));\n        expect(child.tapCancelEvent, equals(0));\n        expect(grandChild.tapDownEvent, equals(0));\n        expect(grandChild.tapUpEvent, equals(0));\n        expect(grandChild.tapCancelEvent, equals(0));\n\n        // [onTapUp] will call, if there was an [onTapDown] event before\n        dispatcher.onTapUp(\n          createTapUpEvents(\n            game: game,\n            localPosition: const Offset(12, 12),\n            globalPosition: const Offset(12, 12),\n          ),\n        );\n\n        expect(component.tapDownEvent, equals(0));\n        expect(component.tapUpEvent, equals(0));\n        expect(component.tapCancelEvent, equals(0));\n        expect(child.tapDownEvent, equals(0));\n        expect(child.tapUpEvent, equals(0));\n        expect(child.tapCancelEvent, equals(0));\n        expect(grandChild.tapDownEvent, equals(0));\n        expect(grandChild.tapUpEvent, equals(0));\n        expect(grandChild.tapCancelEvent, equals(0));\n      },\n    );\n\n    testWithFlameGame(\n      'correctly accepts events all the way down the subtree when ignoreEvents '\n      'is false',\n      (game) async {\n        final grandChild = _IgnoreTapCallbacksComponent()..ignoreEvents = false;\n        final child = _IgnoreTapCallbacksComponent(children: [grandChild])\n          ..ignoreEvents = false;\n        final component = _IgnoreTapCallbacksComponent(\n          position: Vector2.all(10),\n          children: [child],\n        )..ignoreEvents = false;\n\n        await game.ensureAdd(component);\n        final dispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n        dispatcher.onTapDown(\n          createTapDownEvents(\n            game: game,\n            localPosition: const Offset(12, 12),\n            globalPosition: const Offset(12, 12),\n          ),\n        );\n        expect(component.tapDownEvent, equals(1));\n        expect(component.tapUpEvent, equals(0));\n        expect(component.tapCancelEvent, equals(0));\n        expect(child.tapDownEvent, equals(1));\n        expect(child.tapUpEvent, equals(0));\n        expect(child.tapCancelEvent, equals(0));\n        expect(grandChild.tapDownEvent, equals(1));\n        expect(grandChild.tapUpEvent, equals(0));\n        expect(grandChild.tapCancelEvent, equals(0));\n\n        // [onTapUp] will call, if there was an [onTapDown] event before\n        dispatcher.onTapUp(\n          createTapUpEvents(\n            game: game,\n            localPosition: const Offset(12, 12),\n            globalPosition: const Offset(12, 12),\n          ),\n        );\n\n        expect(component.tapDownEvent, equals(1));\n        expect(component.tapUpEvent, equals(1));\n        expect(component.tapCancelEvent, equals(0));\n        expect(child.tapDownEvent, equals(1));\n        expect(child.tapUpEvent, equals(1));\n        expect(child.tapCancelEvent, equals(0));\n        expect(grandChild.tapDownEvent, equals(1));\n        expect(grandChild.tapUpEvent, equals(1));\n        expect(grandChild.tapCancelEvent, equals(0));\n      },\n    );\n  });\n}\n\nmixin _TapCounter on TapCallbacks {\n  int tapDownEvent = 0;\n  int tapUpEvent = 0;\n  int longTapDownEvent = 0;\n  int tapCancelEvent = 0;\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    expect(event.raw, isNotNull);\n    event.continuePropagation = true;\n    tapDownEvent++;\n  }\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    expect(event.raw, isNotNull);\n    event.continuePropagation = true;\n    tapUpEvent++;\n  }\n\n  @override\n  void onTapCancel(TapCancelEvent event) {\n    event.continuePropagation = true;\n    tapCancelEvent++;\n  }\n}\n\nclass _IgnoreTapCallbacksComponent extends PositionComponent\n    with TapCallbacks, _TapCounter, IgnoreEvents {\n  _IgnoreTapCallbacksComponent({super.position, super.children})\n    : super(size: Vector2.all(10));\n}\n"
  },
  {
    "path": "packages/flame/test/events/component_mixins/pointer_move_callbacks_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('PointerMoveCallbacks', () {\n    testWithFlameGame(\n      'make sure PointerMoveCallbacks components can be added to a FlameGame',\n      (game) async {\n        await game.ensureAdd(_PointerMoveCallbacksComponent());\n        await game.ready();\n\n        _hasDispatcher(game);\n      },\n    );\n\n    testWithFlameGame('receive pointer move events on component', (game) async {\n      final c1 = _PointerMoveCallbacksComponent(\n        position: Vector2.all(10),\n        size: Vector2.all(10),\n      );\n      game.add(c1);\n      final c2 = _PointerMoveCallbacksComponent(\n        position: Vector2.all(15),\n        size: Vector2.all(10),\n      );\n      game.add(c2);\n\n      await game.ready();\n\n      _hasDispatcher(game);\n\n      _mouseEvent(game, Vector2.all(12));\n      expect(c1.removeSingle(), Vector2.all(2));\n      expect(c2.receivedEventsAt, isEmpty);\n\n      _mouseEvent(game, Vector2.all(1));\n      expect(c1.receivedEventsAt, isEmpty);\n      expect(c2.receivedEventsAt, isEmpty);\n\n      _mouseEvent(game, Vector2.all(19));\n      expect(c1.removeSingle(), Vector2.all(9));\n      expect(c2.removeSingle(), Vector2.all(4));\n\n      _mouseEvent(game, Vector2.all(21));\n      expect(c1.receivedEventsAt, isEmpty);\n      expect(c2.removeSingle(), Vector2.all(6));\n    });\n\n    testWithGame(\n      'receive pointer move events on game',\n      _PointerMoveCallbacksGame.new,\n      (game) async {\n        _hasDispatcher(game);\n\n        _mouseEvent(game, Vector2.all(12));\n        expect(game.removeSingle(), Vector2.all(12));\n\n        _mouseEvent(game, Vector2.all(1));\n        expect(game.removeSingle(), Vector2.all(1));\n\n        _mouseEvent(game, Vector2.all(19));\n        expect(game.removeSingle(), Vector2.all(19));\n\n        _mouseEvent(game, Vector2.all(21));\n        expect(game.removeSingle(), Vector2.all(21));\n      },\n    );\n  });\n}\n\nvoid _mouseEvent(FlameGame game, Vector2 position) {\n  game.firstChild<PointerMoveDispatcher>()!.onMouseMove(\n    createMouseMoveEvent(\n      game: game,\n      position: position,\n    ),\n  );\n}\n\nvoid _hasDispatcher(FlameGame game) {\n  expect(\n    game.children.whereType<PointerMoveDispatcher>(),\n    hasLength(1),\n  );\n}\n\nmixin _PointerMoveInspector on PointerMoveCallbacks {\n  List<Vector2> receivedEventsAt = [];\n\n  Vector2 removeSingle() {\n    expect(receivedEventsAt, hasLength(1));\n    return receivedEventsAt.removeAt(0);\n  }\n\n  @override\n  void onPointerMove(PointerMoveEvent event) {\n    expect(event.raw, isNotNull);\n    receivedEventsAt.add(event.localPosition);\n  }\n}\n\nclass _PointerMoveCallbacksComponent extends PositionComponent\n    with PointerMoveCallbacks, _PointerMoveInspector {\n  _PointerMoveCallbacksComponent({\n    super.position,\n    super.size,\n  });\n}\n\nclass _PointerMoveCallbacksGame extends FlameGame\n    with PointerMoveCallbacks, _PointerMoveInspector {}\n"
  },
  {
    "path": "packages/flame/test/events/component_mixins/scale_callbacks_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart' hide PointerMoveEvent;\nimport 'package:flame/game.dart';\nimport 'package:flame/src/events/flame_game_mixins/scale_dispatcher.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/gestures.dart' show PointerAddedEvent, kPrimaryButton;\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('ScaleCallbacks', () {\n    testWithFlameGame(\n      'make sure ScaleCallback components can be added to a FlameGame',\n      (game) async {\n        await game.add(_ScaleCallbacksComponent());\n        await game.ready();\n        expect(game.children.toList()[2], isA<ScaleDispatcher>());\n      },\n    );\n  });\n  testWithFlameGame('scale event start', (game) async {\n    final component = _ScaleCallbacksComponent()\n      ..x = 10\n      ..y = 10\n      ..width = 10\n      ..height = 10;\n    game.add(component);\n    await game.ready();\n\n    expect(game.children.whereType<ScaleDispatcher>().length, 1);\n    game.firstChild<ScaleDispatcher>()!.onScaleStart(\n      createScaleStartEvents(\n        game: game,\n        localFocalPoint: const Offset(12, 12),\n        focalPoint: const Offset(12, 12),\n      ),\n    );\n    expect(component.containsLocalPoint(Vector2(10, 10)), false);\n  });\n\n  testWithFlameGame('scale event start, update and end', (game) async {\n    final component = _ScaleCallbacksComponent()\n      ..x = 10\n      ..y = 10\n      ..width = 10\n      ..height = 10;\n    await game.ensureAdd(component);\n    final dispatcher = game.firstChild<ScaleDispatcher>()!;\n\n    dispatcher.onScaleStart(\n      createScaleStartEvents(\n        game: game,\n        localFocalPoint: const Offset(12, 12),\n        focalPoint: const Offset(12, 12),\n      ),\n    );\n    expect(component.scaleStartEvent, 1);\n    expect(component.scaleUpdateEvent, 0);\n    expect(component.scaleEndEvent, 0);\n\n    dispatcher.onScaleUpdate(\n      createScaleUpdateEvents(\n        game: game,\n        localFocalPoint: const Offset(15, 15),\n        focalPoint: const Offset(15, 15),\n      ),\n    );\n\n    expect(game.containsLocalPoint(Vector2(9, 9)), isTrue);\n    expect(component.scaleUpdateEvent, equals(1));\n\n    dispatcher.onScaleEnd(ScaleEndEvent(1, ScaleEndDetails()));\n    expect(component.scaleEndEvent, equals(1));\n  });\n\n  testWithFlameGame(\n    'scale event update not called without onScaleStart',\n    (game) async {\n      final component = _ScaleCallbacksComponent()\n        ..x = 10\n        ..y = 10\n        ..width = 10\n        ..height = 10;\n      await game.ensureAdd(component);\n      final dispatcher = game.firstChild<ScaleDispatcher>()!;\n      expect(component.scaleStartEvent, equals(0));\n      expect(component.scaleUpdateEvent, equals(0));\n\n      dispatcher.onScaleUpdate(\n        createScaleUpdateEvents(\n          game: game,\n          localFocalPoint: const Offset(15, 15),\n          focalPoint: const Offset(15, 15),\n        ),\n      );\n      expect(component.scaleUpdateEvent, equals(0));\n    },\n  );\n\n  testWidgets('scale correctly registered handled event', (tester) async {\n    final component = _ScaleCallbacksComponent()\n      ..x = 100\n      ..y = 100\n      ..width = 150\n      ..height = 150;\n    final game = FlameGame(children: [component]);\n\n    await tester.pumpWidget(GameWidget(game: game));\n    await tester.pump();\n\n    await _zoomFrom(\n      tester,\n      startLocation1: const Offset(180, 150),\n      offset1: const Offset(15, 2),\n      startLocation2: const Offset(120, 150),\n      offset2: const Offset(-15, -2),\n    );\n\n    expect(game.children.length, equals(4));\n    expect(component.isMounted, isTrue);\n\n    expect(component.scaleStartEvent, equals(2));\n    expect(component.scaleUpdateEvent, greaterThan(0));\n    expect(component.scaleEndEvent, equals(2));\n  });\n\n  testWidgets(\n    'scale outside of component is not registered as handled',\n    (tester) async {\n      final component = _ScaleCallbacksComponent()..size = Vector2.all(100);\n      final game = FlameGame(children: [component]);\n      await tester.pumpWidget(GameWidget(game: game));\n      await tester.pump();\n      await tester.pump();\n      expect(component.isMounted, isTrue);\n\n      await _zoomFrom(\n        tester,\n        startLocation1: const Offset(250, 200),\n        offset1: const Offset(15, 2),\n        startLocation2: const Offset(150, 200),\n        offset2: const Offset(-15, -2),\n      );\n\n      expect(component.scaleStartEvent, equals(0));\n      expect(component.scaleUpdateEvent, equals(0));\n      expect(component.scaleEndEvent, equals(0));\n    },\n  );\n\n  testWithGame(\n    'make sure the FlameGame can registers Scale Callbacks on itself',\n    _ScaleCallbacksGame.new,\n    (game) async {\n      await game.ready();\n      expect(game.children.length, equals(3));\n      expect(game.children.elementAt(1), isA<ScaleDispatcher>());\n    },\n  );\n\n  testWidgets(\n    'scale correctly registered handled event directly on FlameGame',\n    (tester) async {\n      final game = _ScaleCallbacksGame()..onGameResize(Vector2.all(300));\n      await tester.pumpWidget(GameWidget(game: game));\n      await tester.pump();\n      await tester.pump();\n      expect(game.children.length, equals(3));\n      expect(game.isMounted, isTrue);\n\n      await _zoomFrom(\n        tester,\n        startLocation1: const Offset(50, 100),\n        offset1: const Offset(15, 2),\n        startLocation2: const Offset(150, 100),\n        offset2: const Offset(-15, -2),\n      );\n\n      expect(game.scaleStartEvent, equals(2));\n      expect(game.scaleUpdateEvent, greaterThan(0));\n      expect(game.scaleEndEvent, equals(2));\n    },\n  );\n\n  testWidgets(\n    'isScaled is changed',\n    (tester) async {\n      final component = _ScaleCallbacksComponent()\n        ..size = Vector2.all(100)\n        ..x = 100\n        ..y = 100;\n\n      final game = FlameGame(children: [component]);\n      await tester.pumpWidget(GameWidget(game: game));\n      await tester.pump();\n      await tester.pump();\n\n      // Inside component\n      await _zoomFrom(\n        tester,\n        startLocation1: const Offset(180, 100),\n        offset1: const Offset(15, 2),\n        startLocation2: const Offset(120, 100),\n        offset2: const Offset(-15, -2),\n      );\n\n      expect(component.isScaledStateChange, equals(4));\n\n      // Outside component\n      await _zoomFrom(\n        tester,\n        startLocation1: const Offset(330, 300),\n        offset1: const Offset(15, 2),\n        startLocation2: const Offset(270, 300),\n        offset2: const Offset(-15, -2),\n      );\n\n      expect(component.isScaledStateChange, equals(4));\n    },\n  );\n  group('HasScalableComponents', () {\n    testWidgets(\n      'scale event does not affect more than one component',\n      (tester) async {\n        var nEvents = 0;\n        final game = FlameGame(\n          children: [\n            _ScaleWithCallbacksComponent(\n              size: Vector2.all(100),\n              onScaleStart: (e) => nEvents++,\n              onScaleUpdate: (e) => nEvents++,\n              onScaleEnd: (e) => nEvents++,\n            ),\n            _SimpleScaleCallbacksComponent(size: Vector2.all(200))\n              ..priority = 10,\n          ],\n        );\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n        await tester.pump();\n        await _zoomFrom(\n          tester,\n          startLocation1: const Offset(80, 50),\n          offset1: const Offset(15, 2),\n          startLocation2: const Offset(20, 50),\n          offset2: const Offset(-15, -2),\n        );\n        expect(nEvents, 0);\n      },\n    );\n\n    testWidgets(\n      'scale event can move outside the component bounds and still fire',\n      (tester) async {\n        var nEvents = 0;\n        const intervals = 50;\n        final component = _ScaleWithCallbacksComponent(\n          size: Vector2.all(30),\n          position: Vector2.all(100),\n          onScaleUpdate: (e) => nEvents++,\n        );\n        final game = FlameGame(\n          children: [component],\n        );\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n\n        const center = Offset(115, 115);\n        await tester._timedZoomFrom(\n          center.translate(-10, 0),\n          const Offset(-30, 0),\n          center.translate(10, 0),\n          const Offset(30, 0),\n          const Duration(milliseconds: 300),\n          intervals: intervals,\n        );\n\n        expect(nEvents, intervals * 2 + 2);\n      },\n    );\n  });\n\n  testWidgets(\n    'scale event scale respects camera & zoom',\n    (tester) async {\n      final resolution = Vector2(80, 60);\n      final game = FlameGame(\n        camera: CameraComponent.withFixedResolution(\n          width: resolution.x,\n          height: resolution.y,\n        ),\n      );\n      final scales = [];\n\n      game.camera.viewfinder.zoom = 3;\n\n      await game.world.add(\n        _ScaleWithCallbacksComponent(\n          position: Vector2.all(-5),\n          size: Vector2.all(10),\n          onScaleUpdate: (event) {\n            scales.add(event.scale);\n          },\n        ),\n      );\n      await tester.pumpWidget(GameWidget(game: game));\n      await tester.pump();\n      await tester.pump();\n\n      final canvasSize = game.canvasSize;\n\n      final center = (canvasSize / 2).toOffset();\n      await tester._timedZoomFrom(\n        center.translate(-1, 0),\n        const Offset(-20, 0),\n        center.translate(1, 0),\n        const Offset(20, 0),\n        const Duration(milliseconds: 300),\n        intervals: 10,\n      );\n\n      expect(scales.skip(1), List.generate(21, (i) => i + 1));\n    },\n  );\n\n  testWidgets(\n    'scale event rotation respects camera & zoom',\n    (tester) async {\n      final resolution = Vector2(80, 60);\n      final game = FlameGame(\n        camera: CameraComponent.withFixedResolution(\n          width: resolution.x,\n          height: resolution.y,\n        ),\n      );\n      var rotations = [];\n\n      game.camera.viewfinder.zoom = 3;\n\n      await game.world.add(\n        _ScaleWithCallbacksComponent(\n          position: Vector2.all(-5),\n          size: Vector2.all(10),\n          onScaleUpdate: (event) {\n            rotations.add(event.rotation);\n          },\n        ),\n      );\n      await tester.pumpWidget(GameWidget(game: game));\n      await tester.pump();\n      await tester.pump();\n\n      final canvasSize = game.canvasSize;\n\n      final center = (canvasSize / 2).toOffset();\n      await tester._timedZoomFrom(\n        center.translate(-1, 0),\n        const Offset(0, 20),\n        center.translate(1, 0),\n        const Offset(0, -20),\n        const Duration(milliseconds: 300),\n        intervals: 10,\n      );\n\n      // computation of angle using trigonometry with triangle having a size\n      // of length 1 and one of length i.\n      final expected = List.generate(21, (i) => -atan(i));\n\n      // remove the first element that is registered twice in the simulation\n      rotations = rotations.sublist(1);\n      for (var i = 0; i < expected.length; i++) {\n        expect(rotations[i], closeTo(expected[i], 1e-6)); // tolerance\n      }\n    },\n  );\n}\n\nFuture<void> _zoomFrom(\n  WidgetTester tester, {\n  required Offset startLocation1,\n  required Offset offset1,\n  required Offset startLocation2,\n  required Offset offset2,\n}) async {\n  // Start two gestures on opposite sides of that center\n  final gesture1 = await tester.startGesture(startLocation1);\n  final gesture2 = await tester.startGesture(startLocation2);\n\n  await tester.pump();\n\n  await gesture1.moveBy(offset1);\n  await gesture2.moveBy(offset2);\n  await tester.pump();\n\n  // release fingers\n  await gesture1.up();\n  await gesture2.up();\n\n  await tester.pump();\n}\n\nmixin _ScaleCounter on ScaleCallbacks {\n  int scaleStartEvent = 0;\n  int scaleUpdateEvent = 0;\n  int scaleEndEvent = 0;\n\n  int isScaledStateChange = 0;\n\n  bool _wasScaled = false;\n\n  @override\n  void onScaleStart(ScaleStartEvent event) {\n    super.onScaleStart(event);\n    expect(event.raw, isNotNull);\n    event.handled = true;\n    scaleStartEvent++;\n    if (_wasScaled != isScaling) {\n      ++isScaledStateChange;\n      _wasScaled = isScaling;\n    }\n  }\n\n  @override\n  void onScaleUpdate(ScaleUpdateEvent event) {\n    expect(event.raw, isNotNull);\n    event.handled = true;\n    scaleUpdateEvent++;\n  }\n\n  @override\n  void onScaleEnd(ScaleEndEvent event) {\n    super.onScaleEnd(event);\n    expect(event.raw, isNotNull);\n    event.handled = true;\n    scaleEndEvent++;\n    if (_wasScaled != isScaling) {\n      ++isScaledStateChange;\n      _wasScaled = isScaling;\n    }\n  }\n}\n\n// Source - https://stackoverflow.com/a/75171528\n// Posted by Alexander\n// Retrieved 2025-11-19, License - CC BY-SA 4.0\n\nextension _ZoomTesting on WidgetTester {\n  Future<void> _timedZoomFrom(\n    Offset startLocation1,\n    Offset offset1,\n    Offset startLocation2,\n    Offset offset2,\n    Duration duration, {\n    int? pointer,\n    int buttons = kPrimaryButton,\n    int intervals = 30,\n  }) {\n    assert(intervals > 1);\n    pointer ??= nextPointer;\n    final pointer2 = pointer + 1;\n    final timeStamps = <Duration>[\n      for (int t = 0; t <= intervals; t += 1) duration * t ~/ intervals,\n    ];\n    final offsets1 = <Offset>[\n      startLocation1,\n      for (int t = 0; t <= intervals; t += 1)\n        startLocation1 + offset1 * (t / intervals),\n    ];\n    final offsets2 = <Offset>[\n      startLocation2,\n      for (int t = 0; t <= intervals; t += 1)\n        startLocation2 + offset2 * (t / intervals),\n    ];\n    final records = <PointerEventRecord>[\n      PointerEventRecord(Duration.zero, <PointerEvent>[\n        PointerAddedEvent(\n          position: startLocation1,\n        ),\n        PointerAddedEvent(\n          position: startLocation2,\n        ),\n        PointerDownEvent(\n          position: startLocation1,\n          pointer: pointer,\n          buttons: buttons,\n        ),\n        PointerDownEvent(\n          position: startLocation2,\n          pointer: pointer2,\n          buttons: buttons,\n        ),\n      ]),\n      ...<PointerEventRecord>[\n        for (int t = 0; t <= intervals; t += 1)\n          PointerEventRecord(timeStamps[t], <PointerEvent>[\n            PointerMoveEvent(\n              timeStamp: timeStamps[t],\n              position: offsets1[t + 1],\n              delta: offsets1[t + 1] - offsets1[t],\n              pointer: pointer,\n              buttons: buttons,\n            ),\n            PointerMoveEvent(\n              timeStamp: timeStamps[t],\n              position: offsets2[t + 1],\n              delta: offsets2[t + 1] - offsets2[t],\n              pointer: pointer2,\n              buttons: buttons,\n            ),\n          ]),\n      ],\n      PointerEventRecord(duration, <PointerEvent>[\n        PointerUpEvent(\n          timeStamp: duration,\n          position: offsets1.last,\n          pointer: pointer,\n        ),\n        PointerUpEvent(\n          timeStamp: duration,\n          position: offsets2.last,\n          pointer: pointer2,\n        ),\n      ]),\n    ];\n    return TestAsyncUtils.guard<void>(() async {\n      await handlePointerEventRecord(records);\n    });\n  }\n}\n\nclass _ScaleCallbacksComponent extends PositionComponent\n    with ScaleCallbacks, _ScaleCounter {}\n\nclass _ScaleCallbacksGame extends FlameGame\n    with ScaleCallbacks, _ScaleCounter {}\n\nclass _SimpleScaleCallbacksComponent extends PositionComponent\n    with ScaleCallbacks {\n  _SimpleScaleCallbacksComponent({super.size});\n}\n\nclass _ScaleWithCallbacksComponent extends PositionComponent\n    with ScaleCallbacks {\n  _ScaleWithCallbacksComponent({\n    void Function(ScaleStartEvent)? onScaleStart,\n    void Function(ScaleUpdateEvent)? onScaleUpdate,\n    void Function(ScaleEndEvent)? onScaleEnd,\n    super.position,\n    super.size,\n  }) : _onScaleStart = onScaleStart,\n       _onScaleUpdate = onScaleUpdate,\n       _onScaleEnd = onScaleEnd;\n\n  final void Function(ScaleStartEvent)? _onScaleStart;\n  final void Function(ScaleUpdateEvent)? _onScaleUpdate;\n  final void Function(ScaleEndEvent)? _onScaleEnd;\n\n  @override\n  void onScaleStart(ScaleStartEvent event) {\n    super.onScaleStart(event);\n    return _onScaleStart?.call(event);\n  }\n\n  @override\n  void onScaleUpdate(ScaleUpdateEvent event) {\n    return _onScaleUpdate?.call(event);\n  }\n\n  @override\n  void onScaleEnd(ScaleEndEvent event) {\n    super.onScaleEnd(event);\n    return _onScaleEnd?.call(event);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/events/component_mixins/secondary_tap_callbacks_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('SecondaryTapCallbacks', () {\n    testWidgets(\n      'receives secondary tap event',\n      (tester) async {\n        final component = _SecondaryTapCallbacksComponent()\n          ..position = Vector2.all(10);\n        await tester.pumpWidget(\n          GameWidget(\n            game: FlameGame(children: [component]),\n          ),\n        );\n        await tester.pump();\n\n        final gesture = await tester.createGesture(\n          buttons: kSecondaryButton,\n        );\n        await gesture.down(const Offset(10, 10));\n\n        expect(component.secondaryTapDown, 1);\n        expect(component.secondaryTapCancel, 0);\n        expect(component.secondaryTap, 0);\n\n        await gesture.up();\n\n        expect(component.secondaryTapDown, 1);\n        expect(component.secondaryTapCancel, 0);\n        expect(component.secondaryTap, 1);\n\n        await tester.pump(kDoubleTapMinTime);\n      },\n    );\n\n    testWidgets(\n      'primary and secondary are received separately',\n      (tester) async {\n        final component = _BothTapCallbacksComponent()\n          ..position = Vector2.all(10);\n        await tester.pumpWidget(\n          GameWidget(\n            game: FlameGame(children: [component]),\n          ),\n        );\n        await tester.pump();\n\n        final primaryGesture = await tester.createGesture();\n        final secondaryGesture = await tester.createGesture(\n          buttons: kSecondaryButton,\n        );\n\n        await primaryGesture.down(const Offset(10, 10));\n        expect(component.primaryTapDown, 1);\n        expect(component.secondaryTapDown, 0);\n\n        await primaryGesture.up();\n        expect(component.primaryTapUp, 1);\n        expect(component.secondaryTapUp, 0);\n\n        await secondaryGesture.down(const Offset(10, 10));\n        expect(component.primaryTapDown, 1);\n        expect(component.secondaryTapDown, 1);\n\n        await secondaryGesture.up();\n        expect(component.primaryTapUp, 1);\n        expect(component.secondaryTapUp, 1);\n\n        await primaryGesture.down(const Offset(10, 10));\n        expect(component.primaryTapDown, 2);\n        expect(component.secondaryTapDown, 1);\n\n        await secondaryGesture.down(const Offset(10, 10));\n        expect(component.primaryTapDown, 2);\n        expect(component.secondaryTapDown, 2);\n\n        await primaryGesture.up();\n        expect(component.primaryTapUp, 2);\n        expect(component.secondaryTapUp, 1);\n\n        await secondaryGesture.up();\n        expect(component.primaryTapUp, 2);\n        expect(component.secondaryTapUp, 2);\n\n        await tester.pump(kDoubleTapMinTime);\n      },\n    );\n\n    testWidgets(\n      '''does not receive an event when secondary-tapping a position far from the component''',\n      (tester) async {\n        final component = _SecondaryTapCallbacksComponent()\n          ..position = Vector2.all(10);\n        await tester.pumpWidget(\n          GameWidget(\n            game: FlameGame(children: [component]),\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n\n        final gesture = await tester.createGesture();\n        await gesture.down(const Offset(100, 100));\n\n        expect(component.secondaryTapDown, 0);\n        expect(component.secondaryTapCancel, 0);\n        expect(component.secondaryTap, 0);\n\n        await gesture.up();\n\n        expect(component.secondaryTapDown, 0);\n        expect(component.secondaryTapCancel, 0);\n        expect(component.secondaryTap, 0);\n\n        await tester.pump(kDoubleTapMinTime);\n      },\n    );\n\n    testWidgets(\n      'receives a cancel event when gesture is canceled by drag',\n      (tester) async {\n        final component = _SecondaryTapCallbacksComponent()\n          ..position = Vector2.all(10);\n        await tester.pumpWidget(\n          GameWidget(\n            game: FlameGame(children: [component]),\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n\n        final gesture = await tester.createGesture(\n          buttons: kSecondaryButton,\n        );\n        await gesture.down(const Offset(10, 10));\n\n        expect(component.secondaryTapDown, 1);\n        expect(component.secondaryTapCancel, 0);\n        expect(component.secondaryTap, 0);\n\n        await gesture.moveBy(const Offset(100, 100));\n\n        expect(component.secondaryTapDown, 1);\n        expect(component.secondaryTapCancel, 1);\n        expect(component.secondaryTap, 0);\n\n        await tester.pump(kDoubleTapMinTime);\n      },\n    );\n\n    testWidgets(\n      'receives a cancel event when gesture is canceled by cancel',\n      (tester) async {\n        final component = _SecondaryTapCallbacksComponent()\n          ..position = Vector2.all(10);\n        await tester.pumpWidget(\n          GameWidget(\n            game: FlameGame(children: [component]),\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n\n        final gesture = await tester.createGesture(\n          buttons: kSecondaryButton,\n        );\n        await gesture.down(const Offset(10, 10));\n\n        expect(component.secondaryTapDown, 1);\n        expect(component.secondaryTapCancel, 0);\n        expect(component.secondaryTap, 0);\n\n        await gesture.cancel();\n\n        expect(component.secondaryTapDown, 1);\n        expect(component.secondaryTapCancel, 1);\n        expect(component.secondaryTap, 0);\n\n        await tester.pump(kDoubleTapMinTime);\n      },\n    );\n\n    testWithFlameGame(\n      'SecondaryTapDispatcher is added to game when the callback is mounted',\n      (game) async {\n        final component = _SecondaryTapCallbacksComponent();\n        await game.add(component);\n        await game.ready();\n\n        expect(game.firstChild<SecondaryTapDispatcher>(), isNotNull);\n      },\n    );\n  });\n}\n\nclass _SecondaryTapCallbacksComponent extends PositionComponent\n    with SecondaryTapCallbacks {\n  _SecondaryTapCallbacksComponent() {\n    anchor = Anchor.center;\n    size = Vector2.all(10);\n  }\n\n  int secondaryTapDown = 0;\n  int secondaryTapCancel = 0;\n  int secondaryTap = 0;\n\n  @override\n  void onSecondaryTapUp(SecondaryTapUpEvent event) {\n    secondaryTap++;\n  }\n\n  @override\n  void onSecondaryTapCancel(SecondaryTapCancelEvent event) {\n    secondaryTapCancel++;\n  }\n\n  @override\n  void onSecondaryTapDown(SecondaryTapDownEvent event) {\n    secondaryTapDown++;\n  }\n}\n\nclass _BothTapCallbacksComponent extends PositionComponent\n    with TapCallbacks, SecondaryTapCallbacks {\n  _BothTapCallbacksComponent() {\n    anchor = Anchor.center;\n    size = Vector2.all(10);\n  }\n\n  int primaryTapDown = 0;\n  int primaryTapUp = 0;\n  int primaryTapCancel = 0;\n\n  int secondaryTapDown = 0;\n  int secondaryTapUp = 0;\n  int secondaryTapCancel = 0;\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    primaryTapUp++;\n  }\n\n  @override\n  void onTapCancel(TapCancelEvent event) {\n    primaryTapCancel++;\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    primaryTapDown++;\n  }\n\n  @override\n  void onSecondaryTapUp(SecondaryTapUpEvent event) {\n    secondaryTapUp++;\n  }\n\n  @override\n  void onSecondaryTapCancel(SecondaryTapCancelEvent event) {\n    secondaryTapCancel++;\n  }\n\n  @override\n  void onSecondaryTapDown(SecondaryTapDownEvent event) {\n    secondaryTapDown++;\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/events/component_mixins/tap_callbacks_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('TapCallbacks', () {\n    testWithFlameGame(\n      'make sure TapCallback components can be added to a FlameGame',\n      (game) async {\n        await game.ensureAdd(_TapCallbacksComponent());\n        await game.ready();\n        expect(game.children.toList()[2], isA<MultiTapDispatcher>());\n      },\n    );\n\n    testWithFlameGame('tap event start', (game) async {\n      final component = _TapCallbacksComponent()\n        ..x = 10\n        ..y = 10\n        ..width = 10\n        ..height = 10;\n      game.add(component);\n      await game.ready();\n\n      expect(game.children.whereType<MultiTapDispatcher>().length, equals(1));\n      game.firstChild<MultiTapDispatcher>()!.onTapDown(\n        createTapDownEvents(\n          game: game,\n          localPosition: const Offset(12, 12),\n          globalPosition: const Offset(12, 12),\n        ),\n      );\n      expect(component.containsLocalPoint(Vector2(10, 10)), false);\n    });\n\n    testWithFlameGame(\n      'tap up, down event',\n      (game) async {\n        final component = _TapCallbacksComponent()\n          ..x = 10\n          ..y = 10\n          ..width = 10\n          ..height = 10;\n\n        await game.ensureAdd(component);\n        final dispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n        dispatcher.onTapDown(\n          createTapDownEvents(\n            game: game,\n            localPosition: const Offset(12, 12),\n            globalPosition: const Offset(12, 12),\n          ),\n        );\n        expect(component.tapDownEvent, equals(1));\n        expect(component.tapUpEvent, equals(0));\n        expect(component.tapCancelEvent, equals(0));\n\n        // [onTapUp] will call, if there was an [onTapDown] event before\n        dispatcher.onTapUp(\n          createTapUpEvents(\n            game: game,\n            localPosition: const Offset(12, 12),\n            globalPosition: const Offset(12, 12),\n          ),\n        );\n\n        expect(component.tapUpEvent, equals(1));\n      },\n    );\n\n    testWithFlameGame(\n      'longTapDown and tapCancel event',\n      (game) async {\n        final component = _TapCallbacksComponent()\n          ..x = 10\n          ..y = 10\n          ..width = 10\n          ..height = 10;\n\n        await game.ensureAdd(component);\n        final dispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n        dispatcher.onTapDown(\n          createTapDownEvents(\n            game: game,\n            localPosition: const Offset(12, 12),\n            globalPosition: const Offset(12, 12),\n          ),\n        );\n        expect(component.tapDownEvent, equals(1));\n        expect(component.tapUpEvent, equals(0));\n        expect(component.tapCancelEvent, equals(0));\n\n        // [onTapUp] will call, if there was an [onTapDown] event before\n        dispatcher.onLongTapDown(\n          createTapDownEvents(\n            game: game,\n            localPosition: const Offset(12, 12),\n            globalPosition: const Offset(12, 12),\n          ),\n        );\n        expect(component.longTapDownEvent, equals(1));\n\n        dispatcher.onTapCancel(\n          TapCancelEvent(1),\n        );\n        expect(component.tapCancelEvent, equals(1));\n      },\n    );\n\n    testWidgets(\n      'tap correctly registered handled event',\n      (tester) async {\n        final component = _TapCallbacksComponent()\n          ..x = 10\n          ..y = 10\n          ..width = 10\n          ..height = 10;\n        final game = FlameGame(children: [component]);\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n        await tester.pump();\n        expect(game.children.length, 4);\n        expect(component.isMounted, isTrue);\n\n        await tester.tapAt(const Offset(10, 10));\n        await tester.pump(const Duration(seconds: 500));\n        expect(component.tapDownEvent, equals(1));\n        expect(component.tapUpEvent, equals(1));\n        expect(component.tapCancelEvent, equals(0));\n      },\n    );\n\n    testWidgets(\n      'tap outside of component is not registered as handled',\n      (tester) async {\n        final component = _TapCallbacksComponent()..size = Vector2.all(100);\n        final game = FlameGame(children: [component]);\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n        await tester.pump();\n        expect(component.isMounted, isTrue);\n\n        await tester.tapAt(const Offset(110, 110));\n        await tester.pump(const Duration(milliseconds: 500));\n        expect(component.tapDownEvent, equals(0));\n        expect(component.tapUpEvent, equals(0));\n        expect(component.longTapDownEvent, equals(0));\n        expect(component.tapCancelEvent, equals(0));\n      },\n    );\n\n    testWidgets(\n      'tap that starts inside the component and ends outside is cancelled',\n      (tester) async {\n        final component = _TapCallbacksComponent()\n          ..x = 10\n          ..y = 10\n          ..width = 10\n          ..height = 10;\n        final game = FlameGame(children: [component]);\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n        await tester.pump();\n        expect(component.isMounted, isTrue);\n\n        final gesture = await tester.startGesture(const Offset(10, 10));\n        await tester.pump(const Duration(milliseconds: 500));\n        await gesture.moveTo(const Offset(10, 9));\n        await tester.pump(const Duration(milliseconds: 500));\n        await gesture.up();\n\n        await tester.pump();\n\n        expect(component.tapDownEvent, equals(1));\n        expect(component.tapUpEvent, equals(0));\n        expect(component.tapCancelEvent, equals(1));\n      },\n    );\n\n    testWidgets(\n      'tap that starts and ends in different positions'\n      ' inside the component is handled',\n      (tester) async {\n        final component = _TapCallbacksComponent()\n          ..x = 10\n          ..y = 10\n          ..width = 10\n          ..height = 10;\n        final game = FlameGame(children: [component]);\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n        await tester.pump();\n        expect(component.isMounted, isTrue);\n\n        final gesture = await tester.startGesture(const Offset(10, 10));\n        await tester.pump(const Duration(milliseconds: 500));\n        await gesture.moveTo(const Offset(10, 11));\n        await tester.pump(const Duration(milliseconds: 500));\n        await gesture.up();\n\n        await tester.pump();\n\n        expect(component.tapDownEvent, equals(1));\n        expect(component.tapUpEvent, equals(1));\n        expect(component.tapCancelEvent, equals(0));\n      },\n    );\n\n    testWithGame(\n      'make sure the FlameGame can registers TapCallback on itself',\n      _TapCallbacksGame.new,\n      (game) async {\n        await game.ready();\n        expect(game.children.length, equals(3));\n        expect(game.children.elementAt(1), isA<MultiTapDispatcher>());\n      },\n    );\n\n    testWidgets(\n      'tap correctly registered handled event directly on FlameGame',\n      (tester) async {\n        final game = _TapCallbacksGame()..onGameResize(Vector2.all(300));\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n        await tester.pump();\n        expect(game.children.length, equals(3));\n        expect(game.isMounted, isTrue);\n\n        await tester.tapAt(const Offset(10, 10));\n        await tester.pump(const Duration(seconds: 500));\n        expect(game.tapDownEvent, equals(1));\n        expect(game.tapUpEvent, equals(1));\n        expect(game.longTapDownEvent, equals(0));\n        expect(game.tapCancelEvent, equals(0));\n      },\n    );\n\n    testWithFlameGame(\n      'viewport components should get events before world',\n      (game) async {\n        final component = _TapCallbacksComponent()\n          ..x = 10\n          ..y = 10\n          ..width = 10\n          ..height = 10;\n        final hudComponent = _TapCallbacksComponent()\n          ..x = 10\n          ..y = 10\n          ..width = 10\n          ..height = 10;\n        final world = World();\n        final cameraComponent = CameraComponent(world: world)\n          ..viewfinder.anchor = Anchor.topLeft;\n\n        await game.ensureAddAll([world, cameraComponent]);\n        await world.ensureAdd(component);\n        await cameraComponent.viewport.ensureAdd(hudComponent);\n        final dispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n        dispatcher.onTapDown(\n          createTapDownEvents(\n            game: game,\n            localPosition: const Offset(12, 12),\n            globalPosition: const Offset(12, 12),\n          ),\n        );\n\n        expect(hudComponent.tapDownEvent, equals(1));\n        expect(hudComponent.tapUpEvent, equals(0));\n        expect(hudComponent.tapCancelEvent, equals(0));\n\n        expect(component.tapDownEvent, equals(0));\n        expect(component.tapUpEvent, equals(0));\n        expect(component.tapCancelEvent, equals(0));\n\n        dispatcher.onTapUp(\n          createTapUpEvents(\n            game: game,\n            localPosition: const Offset(12, 12),\n            globalPosition: const Offset(12, 12),\n          ),\n        );\n\n        expect(hudComponent.tapUpEvent, equals(1));\n        expect(component.tapUpEvent, equals(0));\n      },\n    );\n  });\n\n  testWidgets(\n    'taps are delivered to a TapCallbacks component',\n    (tester) async {\n      var nTapDown = 0;\n      var nLongTapDown = 0;\n      var nTapCancel = 0;\n      var nTapUp = 0;\n      final game = FlameGame(\n        children: [\n          _TapWithCallbacksComponent(\n            size: Vector2(200, 100),\n            position: Vector2(50, 50),\n            onTapDown: (e) => nTapDown++,\n            onLongTapDown: (e) => nLongTapDown++,\n            onTapCancel: (e) => nTapCancel++,\n            onTapUp: (e) => nTapUp++,\n          ),\n        ],\n      );\n      await tester.pumpWidget(GameWidget(game: game));\n      await tester.pump();\n      await tester.pump(const Duration(milliseconds: 10));\n      expect(game.children.length, 4);\n\n      // regular tap\n      await tester.tapAt(const Offset(100, 100));\n      await tester.pump(const Duration(milliseconds: 100));\n      expect(nTapDown, 1);\n      expect(nTapUp, 1);\n      expect(nLongTapDown, 0);\n      expect(nTapCancel, 0);\n\n      // long tap\n      await tester.longPressAt(const Offset(100, 100));\n      await tester.pump(const Duration(seconds: 1));\n      expect(nTapDown, 2);\n      expect(nTapUp, 2);\n      expect(nLongTapDown, 1);\n      expect(nTapCancel, 0);\n\n      // cancelled tap\n      var gesture = await tester.startGesture(const Offset(100, 100));\n      await gesture.cancel();\n      await tester.pump(const Duration(seconds: 1));\n      expect(nTapDown, 3);\n      expect(nTapUp, 2);\n      expect(nTapCancel, 1);\n\n      // tap cancelled via movement\n      gesture = await tester.startGesture(const Offset(100, 100));\n      await gesture.moveBy(const Offset(20, 20));\n      await tester.pump(const Duration(seconds: 1));\n      expect(nTapDown, 4);\n      expect(nTapUp, 2);\n      expect(nTapCancel, 2);\n    },\n  );\n\n  testWidgets(\n    'TapCallbacks component nested in another TapCallbacks component',\n    (tester) async {\n      var nTapDownChild = 0;\n      var nTapDownParent = 0;\n      var nTapCancelChild = 0;\n      var nTapCancelParent = 0;\n      var nTapUpChild = 0;\n      var nTapUpParent = 0;\n      final game = FlameGame(\n        children: [\n          _TapWithCallbacksComponent(\n            size: Vector2.all(100),\n            position: Vector2.zero(),\n            onTapDown: (e) => nTapDownParent++,\n            onTapUp: (e) => nTapUpParent++,\n            onTapCancel: (e) => nTapCancelParent++,\n            children: [\n              _TapWithCallbacksComponent(\n                size: Vector2.all(50),\n                position: Vector2.all(25),\n                onTapDown: (e) {\n                  nTapDownChild++;\n                  e.continuePropagation = true;\n                },\n                onTapCancel: (e) => nTapCancelChild++,\n                onTapUp: (e) => nTapUpChild++,\n              ),\n            ],\n          ),\n        ],\n      );\n      await tester.pumpWidget(GameWidget(game: game));\n      await tester.pump();\n      await tester.pump();\n      expect(game.children.length, 4);\n      expect(game.children.elementAt(1).children.length, 1);\n\n      await tester.longPressAt(const Offset(50, 50));\n      await tester.pump(const Duration(seconds: 1));\n      expect(nTapDownChild, 1);\n      expect(nTapDownParent, 1);\n      expect(nTapUpChild, 1);\n      expect(nTapUpParent, 1);\n      expect(nTapCancelChild, 0);\n      expect(nTapCancelParent, 0);\n\n      // cancelled tap\n      final gesture = await tester.startGesture(const Offset(50, 50));\n      await gesture.cancel();\n      await tester.pump(const Duration(seconds: 1));\n      expect(nTapDownChild, 2);\n      expect(nTapDownParent, 2);\n      expect(nTapUpChild, 1);\n      expect(nTapUpParent, 1);\n      expect(nTapCancelChild, 1);\n      expect(nTapCancelParent, 1);\n    },\n  );\n\n  testWidgets(\n    'tap events do not propagate down by default',\n    (tester) async {\n      var nTapDownParent = 0;\n      var nTapCancelParent = 0;\n      var nTapUpParent = 0;\n      final game = FlameGame(\n        children: [\n          _TapWithCallbacksComponent(\n            size: Vector2.all(100),\n            position: Vector2.zero(),\n            onTapDown: (e) => nTapDownParent++,\n            onTapUp: (e) => nTapUpParent++,\n            onTapCancel: (e) => nTapCancelParent++,\n            children: [\n              _SimpleTapCallbacksComponent(size: Vector2.all(100)),\n            ],\n          ),\n        ],\n      );\n      await tester.pumpWidget(GameWidget(game: game));\n      await tester.pump();\n      await tester.pump();\n      expect(game.children.length, 4);\n      expect(game.children.elementAt(1).children.length, 1);\n\n      await tester.longPressAt(const Offset(50, 50));\n      await tester.pump(const Duration(seconds: 1));\n      expect(nTapDownParent, 0);\n      expect(nTapUpParent, 0);\n      expect(nTapCancelParent, 0);\n\n      // cancelled tap\n      final gesture = await tester.startGesture(const Offset(50, 50));\n      await gesture.cancel();\n      await tester.pump(const Duration(seconds: 1));\n      expect(nTapDownParent, 0);\n      expect(nTapUpParent, 0);\n      expect(nTapCancelParent, 0);\n    },\n  );\n\n  testWidgets(\n    'local coordinates during tap events',\n    (tester) async {\n      TapDownEvent? tapDownEvent;\n      final game = FlameGame(\n        children: [\n          PositionComponent(\n            size: Vector2.all(400),\n            position: Vector2.all(10),\n            children: [\n              PositionComponent(\n                size: Vector2(300, 200),\n                scale: Vector2(1.5, 2),\n                position: Vector2.all(40),\n                children: [\n                  _TapWithCallbacksComponent(\n                    size: Vector2(100, 50),\n                    position: Vector2(50, 50),\n                    onTapDown: (e) => tapDownEvent = e,\n                  ),\n                ],\n              ),\n            ],\n          ),\n        ],\n      );\n      await tester.pumpWidget(GameWidget(game: game));\n      await tester.pump();\n      await tester.pump();\n      expect(game.children.length, 4);\n      expect(game.children.elementAt(1).children.length, 1);\n\n      await tester.tapAt(const Offset(200, 200));\n      await tester.pump(const Duration(seconds: 1));\n      expect(tapDownEvent, isNotNull);\n      expect(tapDownEvent!.devicePosition, Vector2(200, 200));\n      expect(tapDownEvent!.canvasPosition, Vector2(200, 200));\n      expect(tapDownEvent!.localPosition, Vector2(50, 25));\n      final trace = tapDownEvent!.renderingTrace.reversed.toList();\n      expect(trace[0], Vector2(50, 25));\n      expect(trace[1], Vector2(100, 75));\n      expect(trace[2], Vector2(190, 190));\n      expect(trace[3], Vector2(200, 200));\n    },\n  );\n}\n\nclass _TapWithCallbacksComponent extends PositionComponent with TapCallbacks {\n  _TapWithCallbacksComponent({\n    required Vector2 super.position,\n    required Vector2 super.size,\n    super.children,\n    void Function(TapDownEvent)? onTapDown,\n    void Function(TapDownEvent)? onLongTapDown,\n    void Function(TapUpEvent)? onTapUp,\n    void Function(TapCancelEvent)? onTapCancel,\n  }) : _onTapDown = onTapDown,\n       _onLongTapDown = onLongTapDown,\n       _onTapUp = onTapUp,\n       _onTapCancel = onTapCancel;\n\n  final void Function(TapDownEvent)? _onTapDown;\n  final void Function(TapDownEvent)? _onLongTapDown;\n  final void Function(TapUpEvent)? _onTapUp;\n  final void Function(TapCancelEvent)? _onTapCancel;\n\n  @override\n  void onTapDown(TapDownEvent event) => _onTapDown?.call(event);\n\n  @override\n  void onLongTapDown(TapDownEvent event) => _onLongTapDown?.call(event);\n\n  @override\n  void onTapUp(TapUpEvent event) => _onTapUp?.call(event);\n\n  @override\n  void onTapCancel(TapCancelEvent event) => _onTapCancel?.call(event);\n}\n\nclass _SimpleTapCallbacksComponent extends PositionComponent with TapCallbacks {\n  _SimpleTapCallbacksComponent({super.size});\n}\n\nmixin _TapCounter on TapCallbacks {\n  int tapDownEvent = 0;\n  int tapUpEvent = 0;\n  int longTapDownEvent = 0;\n  int tapCancelEvent = 0;\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    expect(event.raw, isNotNull);\n    event.handled = true;\n    tapDownEvent++;\n  }\n\n  @override\n  void onLongTapDown(TapDownEvent event) {\n    expect(event.raw, isNotNull);\n    event.handled = true;\n    longTapDownEvent++;\n  }\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    expect(event.raw, isNotNull);\n    event.handled = true;\n    tapUpEvent++;\n  }\n\n  @override\n  void onTapCancel(TapCancelEvent event) {\n    event.handled = true;\n    tapCancelEvent++;\n  }\n}\n\nclass _TapCallbacksComponent extends PositionComponent\n    with TapCallbacks, _TapCounter {}\n\nclass _TapCallbacksGame extends FlameGame with TapCallbacks, _TapCounter {}\n"
  },
  {
    "path": "packages/flame/test/events/game_mixins/multi_touch_drag_detector_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:canvas_test/canvas_test.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  final withMultiTouchFragDetector = GameTester(\n    _MultiTouchDragDetectorGame.new,\n  );\n\n  group('MultiTouchDragDetector', () {\n    testWidgets(\n      'Game can have MultiTouchDragDetector',\n      (tester) async {\n        await tester.pumpWidget(\n          GameWidget(\n            game: _MultiTouchDragDetectorGame(),\n          ),\n        );\n        expect(tester.takeException(), null);\n      },\n    );\n\n    withMultiTouchFragDetector.testGameWidget(\n      'update game and render canvas',\n      verify: (game, tester) async {\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.nOnDragStart, 0);\n        game.update(0);\n        expect(game.updated, true);\n        game.render(MockCanvas());\n        expect(game.rendered, true);\n      },\n    );\n\n    withMultiTouchFragDetector.testGameWidget(\n      'drags are delivered',\n      verify: (game, tester) async {\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.nOnDragStart, 0);\n        game.render(MockCanvas());\n        expect(game.rendered, true);\n\n        // regular drag\n        await tester.timedDragFrom(\n          const Offset(50, 50),\n          const Offset(20, 0),\n          const Duration(milliseconds: 100),\n        );\n        expect(game.nOnDragStart, 1);\n        expect(game.nOnDragUpdate, 8);\n        expect(game.nOnDragEnd, 1);\n        expect(game.nOnDragCancel, 0);\n\n        // cancelled drag\n        final gesture = await tester.startGesture(const Offset(50, 50));\n        await gesture.moveBy(const Offset(10, 10));\n        await gesture.cancel();\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.nOnDragStart, 2);\n        expect(game.nOnDragEnd, 1);\n        expect(game.nOnDragCancel, 1);\n      },\n    );\n  });\n}\n\nclass _MultiTouchDragDetectorGame extends Game with MultiTouchDragDetector {\n  bool updated = false;\n  bool rendered = false;\n\n  int nOnDragStart = 0;\n  int nOnDragUpdate = 0;\n  int nOnDragEnd = 0;\n  int nOnDragCancel = 0;\n\n  @override\n  void render(Canvas canvas) => rendered = true;\n\n  @override\n  void update(double dt) => updated = true;\n\n  @override\n  void onDragCancel(int pointerId) {\n    super.onDragCancel(pointerId);\n    nOnDragCancel++;\n  }\n\n  @override\n  void onDragEnd(int pointerId, DragEndInfo info) {\n    super.onDragEnd(pointerId, info);\n    nOnDragEnd++;\n  }\n\n  @override\n  void onDragUpdate(int pointerId, DragUpdateInfo info) {\n    super.onDragUpdate(pointerId, info);\n    nOnDragUpdate++;\n  }\n\n  @override\n  void onDragStart(int pointerId, DragStartInfo info) {\n    super.onDragStart(pointerId, info);\n    nOnDragStart++;\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/events/game_mixins/multi_touch_tap_detector_test.dart",
    "content": "import 'package:canvas_test/canvas_test.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  final withMultiTouchTapDetector = GameTester(\n    _GameWithMultiTouchTapDetector.new,\n  );\n\n  group('MultiTouchTapDetector', () {\n    testWidgets(\n      'Game can have MultiTouchTapDetector',\n      (tester) async {\n        await tester.pumpWidget(\n          GameWidget(\n            game: _GameWithMultiTouchTapDetector(),\n          ),\n        );\n        expect(tester.takeException(), null);\n      },\n    );\n\n    withMultiTouchTapDetector.testGameWidget(\n      'update game and render canvas',\n      verify: (game, tester) async {\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.nOnTapDown, 0);\n        game.update(0);\n        expect(game.updated, true);\n        game.render(MockCanvas());\n        expect(game.rendered, true);\n      },\n    );\n\n    withMultiTouchTapDetector.testGameWidget(\n      'render canvas and taps are delivered',\n      verify: (game, tester) async {\n        await tester.pumpWidget(GameWidget(game: game));\n        await tester.pump();\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.nOnTapDown, 0);\n        game.render(MockCanvas());\n        expect(game.rendered, true);\n\n        // regular tap\n        await tester.tapAt(const Offset(100, 100));\n        await tester.pump(const Duration(milliseconds: 100));\n        expect(game.nOnTapDown, 1);\n        expect(game.nOnTapUp, 1);\n        expect(game.nOnTap, 1);\n        expect(game.nOnLongTapDown, 0);\n        expect(game.nOnTapCancel, 0);\n\n        // long tap\n        await tester.longPressAt(const Offset(100, 100));\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.nOnTapDown, 2);\n        expect(game.nOnTapUp, 2);\n        expect(game.nOnTap, 2);\n        expect(game.nOnLongTapDown, 1);\n        expect(game.nOnTapCancel, 0);\n\n        // cancelled tap\n        var gesture = await tester.startGesture(const Offset(100, 100));\n        await gesture.cancel();\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.nOnTapDown, 3);\n        expect(game.nOnTapUp, 2);\n        expect(game.nOnTap, 2);\n        expect(game.nOnTapCancel, 1);\n\n        // tap cancelled via movement\n        gesture = await tester.startGesture(const Offset(100, 100));\n        await gesture.moveBy(const Offset(20, 20));\n        await tester.pump(const Duration(seconds: 1));\n        expect(game.nOnTapDown, 4);\n        expect(game.nOnTapUp, 2);\n        expect(game.nOnTap, 2);\n        expect(game.nOnLongTapDown, 1);\n        expect(game.nOnTapCancel, 2);\n      },\n    );\n  });\n}\n\nclass _GameWithMultiTouchTapDetector extends Game with MultiTouchTapDetector {\n  bool updated = false;\n  bool rendered = false;\n\n  int nOnTapDown = 0;\n  int nOnLongTapDown = 0;\n  int nOnTapUp = 0;\n  int nOnTap = 0;\n  int nOnTapCancel = 0;\n\n  @override\n  void render(Canvas canvas) => rendered = true;\n\n  @override\n  void update(double dt) => updated = true;\n\n  @override\n  void onTapDown(int pointerId, TapDownInfo info) {\n    super.onTapDown(pointerId, info);\n    nOnTapDown++;\n  }\n\n  @override\n  void onLongTapDown(int pointerId, TapDownInfo info) {\n    super.onLongTapDown(pointerId, info);\n    nOnLongTapDown++;\n  }\n\n  @override\n  void onTapUp(int pointerId, TapUpInfo info) {\n    super.onTapUp(pointerId, info);\n    nOnTapUp++;\n  }\n\n  @override\n  void onTapCancel(int pointerId) {\n    super.onTapCancel(pointerId);\n    nOnTapCancel++;\n  }\n\n  @override\n  void onTap(int pointerId) {\n    super.onTap(pointerId);\n    nOnTap++;\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/events/hardware_keyboard_detector_test.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('HardwareKeyboardDetector', () {\n    TestWidgetsFlutterBinding.ensureInitialized();\n\n    test('zero state', () {\n      final component = _MyKeyboardDetector();\n      expect(component.isAltPressed, false);\n      expect(component.isShiftPressed, false);\n      expect(component.isControlPressed, false);\n      expect(component.isCapsLockOn, false);\n      expect(component.isNumLockOn, false);\n      expect(component.isScrollLockOn, false);\n      expect(component.pauseKeyEvents, true);\n    });\n\n    testWithFlameGame('game detects key presses', (game) async {\n      HardwareKeyboard.instance.clearState();\n      final detector = _MyKeyboardDetector()..addToParent(game);\n      await game.ready();\n\n      await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft);\n      await simulateKeyUpEvent(LogicalKeyboardKey.arrowLeft);\n      await simulateKeyDownEvent(LogicalKeyboardKey.space);\n      await simulateKeyUpEvent(LogicalKeyboardKey.space);\n      expect(detector.events.length, 4);\n      expect(detector.events[0], isA<KeyDownEvent>());\n      expect(detector.events[1], isA<KeyUpEvent>());\n      expect(detector.events[2], isA<KeyDownEvent>());\n      expect(detector.events[3], isA<KeyUpEvent>());\n      expect(detector.events[0].physicalKey, PhysicalKeyboardKey.arrowLeft);\n      expect(detector.events[1].physicalKey, PhysicalKeyboardKey.arrowLeft);\n      expect(detector.events[2].physicalKey, PhysicalKeyboardKey.space);\n      expect(detector.events[3].physicalKey, PhysicalKeyboardKey.space);\n    });\n\n    testWithFlameGame('modifier keys', (game) async {\n      HardwareKeyboard.instance.clearState();\n      await simulateKeyDownEvent(LogicalKeyboardKey.altLeft);\n      await simulateKeyDownEvent(LogicalKeyboardKey.numLock);\n      await simulateKeyUpEvent(LogicalKeyboardKey.numLock);\n      await simulateKeyDownEvent(LogicalKeyboardKey.altRight);\n      await simulateKeyDownEvent(LogicalKeyboardKey.controlRight);\n      await simulateKeyDownEvent(LogicalKeyboardKey.shiftLeft);\n      final detector = _MyKeyboardDetector()..addToParent(game);\n      await game.ready();\n\n      var checkedAll = false;\n      detector.keyEventHandler = (KeyEvent event) {\n        expect(event, isA<KeyDownEvent>());\n        expect(event.physicalKey, PhysicalKeyboardKey.keyZ);\n        expect(event.logicalKey, LogicalKeyboardKey.keyZ);\n        expect(event.character, 'z');\n        expect(detector.isControlPressed, true);\n        expect(detector.isShiftPressed, true);\n        expect(detector.isAltPressed, true);\n        expect(detector.isCapsLockOn, false);\n        expect(detector.isScrollLockOn, false);\n        expect(detector.isNumLockOn, true);\n        checkedAll = true;\n      };\n\n      await simulateKeyDownEvent(LogicalKeyboardKey.keyZ);\n      expect(checkedAll, true);\n\n      await simulateKeyUpEvent(LogicalKeyboardKey.altLeft);\n      await simulateKeyUpEvent(LogicalKeyboardKey.altRight);\n      await simulateKeyUpEvent(LogicalKeyboardKey.controlRight);\n      await simulateKeyUpEvent(LogicalKeyboardKey.shiftLeft);\n      await simulateKeyUpEvent(LogicalKeyboardKey.keyZ);\n    });\n\n    testWithFlameGame('key events paused', (game) async {\n      HardwareKeyboard.instance.clearState();\n      final detector = _MyKeyboardDetector()..addToParent(game);\n      await game.ready();\n\n      detector.pauseKeyEvents = true;\n      await simulateKeyDownEvent(LogicalKeyboardKey.enter);\n      await simulateKeyUpEvent(LogicalKeyboardKey.enter);\n      expect(detector.events, isEmpty);\n\n      detector.pauseKeyEvents = false;\n      await simulateKeyDownEvent(LogicalKeyboardKey.enter);\n      await simulateKeyUpEvent(LogicalKeyboardKey.enter);\n      expect(detector.events.length, 2);\n    });\n\n    testWithFlameGame('pause key events while keys are pressed', (game) async {\n      HardwareKeyboard.instance.clearState();\n      final detector = _MyKeyboardDetector()..addToParent(game);\n      await game.ready();\n\n      await simulateKeyDownEvent(LogicalKeyboardKey.enter);\n      await simulateKeyDownEvent(LogicalKeyboardKey.capsLock);\n      expect(detector.events.length, 2);\n      expect(detector.events.whereType<KeyDownEvent>().length, 2);\n\n      detector.pauseKeyEvents = true;\n      expect(detector.events.length, 4);\n      expect(detector.events.whereType<KeyDownEvent>().length, 2);\n      expect(detector.events.whereType<KeyUpEvent>().length, 2);\n\n      detector.pauseKeyEvents = false;\n      expect(detector.events.length, 6);\n      expect(detector.events.whereType<KeyDownEvent>().length, 4);\n      expect(detector.events.whereType<KeyUpEvent>().length, 2);\n\n      await simulateKeyUpEvent(LogicalKeyboardKey.enter);\n      await simulateKeyUpEvent(LogicalKeyboardKey.capsLock);\n      expect(detector.events.length, 8);\n      expect(detector.events.whereType<KeyDownEvent>().length, 4);\n      expect(detector.events.whereType<KeyUpEvent>().length, 4);\n    });\n\n    testWithFlameGame('stop listening when component unmounts', (game) async {\n      HardwareKeyboard.instance.clearState();\n      final detector = _MyKeyboardDetector()..addToParent(game);\n      await game.ready();\n\n      await simulateKeyDownEvent(LogicalKeyboardKey.keyA);\n      detector.removeFromParent();\n      await game.ready();\n\n      // The detector will send a synthesized \"keyUp\" event when it unmounts.\n      expect(detector.events.length, 2);\n      expect(detector.events.whereType<KeyDownEvent>().length, 1);\n      expect(detector.events.whereType<KeyUpEvent>().length, 1);\n\n      // But new events will not be listened.\n      await simulateKeyUpEvent(LogicalKeyboardKey.keyA);\n      expect(detector.events.length, 2);\n\n      // The fact that Q button is pressed will be delivered once the detector\n      // mounts, whereas the W button was pressed and released while the\n      // detector was unmounted, so it won't be seen.\n      await simulateKeyDownEvent(LogicalKeyboardKey.keyQ);\n      await simulateKeyDownEvent(LogicalKeyboardKey.keyW);\n      await simulateKeyUpEvent(LogicalKeyboardKey.keyW);\n      expect(detector.events.length, 2);\n      game.add(detector);\n      await game.ready();\n      expect(detector.events.length, 3);\n      expect(detector.events.last.physicalKey, PhysicalKeyboardKey.keyQ);\n    });\n  });\n}\n\nclass _MyKeyboardDetector extends HardwareKeyboardDetector {\n  final List<KeyEvent> events = [];\n  void Function(KeyEvent)? keyEventHandler;\n\n  @override\n  void onKeyEvent(KeyEvent event) {\n    events.add(event);\n    keyEventHandler?.call(event);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/events/tap_config_test.dart",
    "content": "import 'package:flame/src/events/tap_config.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('TapConfig', () {\n    test('default longTapDelay is 0.3', () {\n      expect(TapConfig.longTapDelay, 0.3);\n    });\n\n    test('longTapDelay can be set new values', () {\n      TapConfig.longTapDelay = 0.5;\n      expect(TapConfig.longTapDelay, 0.5);\n    });\n\n    test('longTapDelay cannot be set to a value lower than 0.150', () {\n      TapConfig.longTapDelay = 0.1;\n      expect(TapConfig.longTapDelay, 0.15);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/experimental/geometry/shapes/circle_test.dart",
    "content": "import 'dart:math';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Circle', () {\n    test('simple properties', () {\n      final center = Vector2(5, 10);\n      final circle = Circle(center, 2.5);\n      expect(circle.isClosed, true);\n      expect(circle.isConvex, true);\n      expect(circle.center, center);\n      expect(circle.radius, 2.5);\n      expect(circle.perimeter, 2 * pi * 2.5);\n      expect('$circle', 'Circle([5.0, 10.0], 2.5)');\n      // Modifying original `center` vector does not affect the shape\n      center.x += 111;\n      expect(circle.center, closeToVector(Vector2(5, 10)));\n    });\n\n    test('negative radius', () {\n      expect(\n        () => Circle(Vector2.zero(), -1),\n        failsAssert('Radius cannot be negative: -1.0'),\n      );\n    });\n\n    test('zero-radius circle', () {\n      final center = Vector2(3, 5);\n      final circle = Circle(center, 0);\n      expect(circle.radius, 0);\n      expect(circle.center, center);\n      expect(circle.containsPoint(center), true);\n      expect(circle.perimeter, 0);\n      expect(circle.support(Vector2(1, 1)), center);\n      expect(circle.support(Vector2(-2, 10)), center);\n      expect(circle.aabb, closeToAabb(Aabb2.minMax(center, center)));\n    });\n\n    test('aabb', () {\n      final circle = Circle(Vector2(-3, 14), 2.0);\n      expect(\n        circle.aabb,\n        closeToAabb(Aabb2.minMax(Vector2(-5, 12), Vector2(-1, 16))),\n      );\n    });\n\n    testRandom('containsPoint', (Random random) {\n      final center = Vector2(5, 12);\n      const radius = 2.5;\n      final circle = Circle(center, radius);\n      for (var i = 0; i < 100; i++) {\n        final point = (Vector2.random(random) - Vector2.all(0.5)) * 9 + center;\n        final inside = (point - center).length <= radius;\n        expect(circle.containsPoint(point), inside);\n      }\n      expect(circle.containsPoint(Vector2(7.5, 12)), true);\n      expect(circle.containsPoint(Vector2(2.5, 12)), true);\n      expect(circle.containsPoint(Vector2(5, 14.5)), true);\n      expect(circle.containsPoint(Vector2(5, 9.5)), true);\n      expect(circle.containsPoint(Vector2(5 + 1.5, 12 + 2)), true);\n    });\n\n    test('move', () {\n      final circle = Circle(Vector2.zero(), 1);\n      expect(circle.center, closeToVector(Vector2.zero()));\n      expect(circle.radius, 1);\n      expect(\n        circle.aabb,\n        closeToAabb(Aabb2.minMax(Vector2(-1, -1), Vector2(1, 1))),\n      );\n\n      circle.move(Vector2(4, 7));\n      expect(circle.center, closeToVector(Vector2(4, 7)));\n      expect(circle.radius, 1);\n      expect(\n        circle.aabb,\n        closeToAabb(Aabb2.minMax(Vector2(3, 6), Vector2(5, 8))),\n      );\n    });\n\n    test('asPath', () {\n      final circle = Circle(Vector2(30, 40), 100);\n      final path = circle.asPath();\n      final metrics = path.computeMetrics().toList();\n      expect(metrics.length, 1);\n      expect(metrics[0].isClosed, true);\n      expect(metrics[0].length, closeTo(circle.perimeter, 1.5));\n    });\n\n    test('0-radius circle asPath', () {\n      final circle = Circle(Vector2.zero(), 0);\n      final path = circle.asPath();\n      final metrics = path.computeMetrics().toList();\n      expect(metrics.isEmpty, true);\n    });\n\n    test('project', () {\n      final circle = Circle(Vector2.zero(), 100);\n      final transform = Transform2D()..position = Vector2(10, 40);\n      final result = circle.project(transform);\n      expect(result, isA<Circle>());\n      expect(result.center, closeToVector(Vector2(10, 40)));\n      expect((result as Circle).radius, 100);\n\n      transform.scale.setValues(1, 2);\n      expect(\n        () => circle.project(transform),\n        throwsUnimplementedError,\n      );\n    });\n\n    test('project with target', () {\n      final circle = Circle(Vector2.zero(), 100);\n      final target = Circle(Vector2.zero(), 1);\n      final transform = Transform2D()\n        ..position = Vector2(10, 20)\n        ..angle = 1\n        ..scale = Vector2.all(-2);\n      expect(transform.isConformal, true);\n\n      final result = circle.project(transform, target);\n      expect(result, isA<Circle>());\n      expect(result, target);\n      expect(target.radius, 200);\n      expect(target.center, closeToVector(Vector2(10, 20)));\n    });\n\n    test('support', () {\n      final circle = Circle(Vector2(2, 1), 10);\n      expect(circle.support(Vector2(1, 0)), closeToVector(Vector2(12, 1)));\n      expect(circle.support(Vector2(-1, 0)), closeToVector(Vector2(-8, 1)));\n      expect(circle.support(Vector2(0, 3.14)), closeToVector(Vector2(2, 11)));\n      expect(circle.support(Vector2(0, -3)), closeToVector(Vector2(2, -9)));\n      expect(\n        circle.support(Vector2(1, 1)),\n        closeToVector(Vector2(2 + 10 / sqrt(2), 1 + 10 / sqrt(2))),\n      );\n      expect(\n        circle.support(Vector2(3, 4)),\n        closeToVector(Vector2(2 + 6, 1 + 8)),\n      );\n      expect(\n        circle.support(Vector2(-2, -1)),\n        closeToVector(Vector2(2 - 20 / sqrt(5), 1 - 10 / sqrt(5))),\n      );\n    });\n\n    test('nearestPoint', () {\n      final circle = Circle(Vector2(1, 1), 1);\n      const a = 0.7071067811865475; // sqrt(1/2)\n      expect(circle.nearestPoint(Vector2(0, 0)), Vector2.all(1 - a));\n      expect(circle.nearestPoint(Vector2(1, 0)), Vector2(1, 0));\n      expect(circle.nearestPoint(Vector2(2, 0)), Vector2(1 + a, 1 - a));\n      expect(circle.nearestPoint(Vector2(2, 2)), Vector2(1 + a, 1 + a));\n    });\n\n    test('nearestPoint for zero-radius circle', () {\n      final circle = Circle(Vector2(3, 4), 0);\n      expect(circle.nearestPoint(Vector2(3, 4)), Vector2(3, 4));\n      expect(circle.nearestPoint(Vector2(5, 7)), Vector2(3, 4));\n      expect(circle.nearestPoint(Vector2(0, 0)), Vector2(3, 4));\n    });\n\n    test('nearestPoint object ownership', () {\n      final circle = Circle(Vector2(3, 4), 5);\n      final point1 = Vector2(10, 20);\n      final point2 = Vector2(-3, -4);\n      final result1 = circle.nearestPoint(point1);\n      final result2 = circle.nearestPoint(point2);\n\n      // This checks that nearestPoint() does not modify its argument.\n      expect(point1, Vector2(10, 20));\n      expect(point2, Vector2(-3, -4));\n      // at this point result1 can have any value...\n      expect(result1, isNotNull);\n      expect(result2, Vector2(0, 0));\n    });\n\n    test('fromPoints', () {\n      final p1 = Vector2.zero();\n      final p2 = Vector2(0, 1);\n      final p3 = Vector2(1, 0);\n\n      final circle = Circle.fromPoints(p1, p2, p3)!;\n      expect(circle.center, Vector2.all(0.5));\n      expectDouble(circle.radius, 1 / sqrt(2));\n\n      expect(circle.containsPoint(p1), true);\n      expect(circle.containsPoint(p2), true);\n      expect(circle.containsPoint(p3), true);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/experimental/geometry/shapes/polygon_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/experimental.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Polygon', () {\n    test('invalid polygon', () {\n      expect(\n        () => Polygon([]),\n        failsAssert('At least 3 vertices are required'),\n      );\n      expect(\n        () => Polygon([Vector2(1, 5), Vector2.zero()]),\n        failsAssert('At least 3 vertices are required'),\n      );\n    });\n\n    test('simple triangle', () {\n      final polygon = Polygon(\n        [Vector2.zero(), Vector2(0, 60), Vector2(80, 60)],\n      );\n      expect(polygon.vertices.length, 3);\n      expect(polygon.vertices[0], Vector2(0, 0));\n      expect(polygon.vertices[1], Vector2(0, 60));\n      expect(polygon.vertices[2], Vector2(80, 60));\n      expect(polygon.edges.length, 3);\n      expect(polygon.edges[0], Vector2(-80, -60));\n      expect(polygon.edges[1], Vector2(0, 60));\n      expect(polygon.edges[2], Vector2(80, 0));\n      expect(polygon.perimeter, 100 + 60 + 80);\n      expect(polygon.isConvex, true);\n      expect(polygon.isClosed, true);\n      expect(\n        polygon.aabb,\n        closeToAabb(Aabb2.minMax(Vector2.zero(), Vector2(80, 60))),\n      );\n      expect(polygon.center, closeToVector(Vector2(80 / 3, 60 * 2 / 3), 1e-14));\n      expect('$polygon', 'Polygon([[0.0,0.0], [0.0,60.0], [80.0,60.0]])');\n    });\n\n    test('triangle with edges in a wrong order', () {\n      final polygon1 = Polygon(\n        [Vector2(0, 60), Vector2.zero(), Vector2(80, 60)],\n      );\n      final polygon2 = Polygon(\n        [Vector2(80, 60), Vector2.zero(), Vector2(0, 60)],\n      );\n      expect(polygon1.vertices, polygon2.vertices);\n      expect(polygon1.edges, polygon2.edges);\n      expect(polygon1.isConvex, true);\n      expect(polygon2.isConvex, true);\n    });\n\n    test('explicit `convex` flag', () {\n      final polygon = Polygon(\n        [Vector2(0, 60), Vector2.zero(), Vector2(80, 60)],\n        convex: true,\n      );\n      // The polygon is marked as \"convex\", but the vertices are in the wrong\n      // order. As a result, all points will be detected as being \"outside\".\n      expect(polygon.isConvex, true);\n      expect(polygon.vertices[0], Vector2(0, 60));\n      expect(polygon.vertices[1], Vector2(0, 0));\n      expect(polygon.vertices[2], Vector2(80, 60));\n      expect(polygon.containsPoint(Vector2(0, 0)), false);\n      expect(polygon.containsPoint(Vector2(10, 30)), false);\n      expect(polygon.containsPoint(Vector2(-10, 30)), false);\n      expect(polygon.containsPoint(Vector2(100, 30)), false);\n      expect(polygon.containsPoint(Vector2(100, 100)), false);\n    });\n\n    test('asPath', () {\n      final polygon = Polygon(\n        [Vector2.zero(), Vector2(0, 60), Vector2(80, 60)],\n      );\n      final path = polygon.asPath();\n      final metrics = path.computeMetrics().toList();\n      expect(metrics.length, 1);\n      expect(metrics[0].isClosed, true);\n      expect(metrics[0].length, 60 + 80 + 100);\n    });\n\n    test('containsPoint', () {\n      final polygon = Polygon(\n        [Vector2.zero(), Vector2(0, 60), Vector2(80, 60)],\n      );\n      expect(polygon.containsPoint(Vector2(0, 0)), true);\n      expect(polygon.containsPoint(Vector2(1, 0)), false);\n      expect(polygon.containsPoint(Vector2(0, 10)), true);\n      expect(polygon.containsPoint(Vector2(0, 60)), true);\n      expect(polygon.containsPoint(Vector2(20, 30)), true);\n      expect(polygon.containsPoint(Vector2(40, 30)), true);\n      expect(polygon.containsPoint(Vector2(40, 29)), false);\n      expect(polygon.containsPoint(Vector2(41, 30)), false);\n      expect(polygon.containsPoint(Vector2(41, 31)), true);\n      expect(polygon.containsPoint(Vector2(80, 60)), true);\n      expect(polygon.containsPoint(Vector2(70, 55)), true);\n      expect(polygon.containsPoint(Vector2(80, 61)), false);\n    });\n\n    test('move', () {\n      final polygon = Polygon(\n        [Vector2.zero(), Vector2(0, 60), Vector2(80, 60)],\n      );\n      // Force computing (and caching) the aabb and the center\n      expect(polygon.aabb.min, Vector2.zero());\n      expect(polygon.center, closeToVector(Vector2(80 / 3, 40), 1e-14));\n\n      polygon.move(Vector2(5, -10));\n      expect(\n        polygon.vertices,\n        [Vector2(5, -10), Vector2(5, 50), Vector2(85, 50)],\n      );\n      expect(\n        polygon.edges,\n        [Vector2(-80, -60), Vector2(0, 60), Vector2(80, 0)],\n      );\n      expect(\n        polygon.aabb,\n        closeToAabb(Aabb2.minMax(Vector2(5, -10), Vector2(85, 50))),\n      );\n      expect(polygon.center, closeToVector(Vector2(95 / 3, 30), 1e-14));\n    });\n\n    test('support', () {\n      final polygon = Polygon([\n        Vector2(10, 0),\n        Vector2(0, 30),\n        Vector2(30, 70),\n        Vector2(80, 40),\n      ]);\n      expect(polygon.isConvex, true);\n\n      expect(polygon.support(Vector2(1, 0)), Vector2(80, 40));\n      expect(polygon.support(Vector2(-1, 0)), Vector2(0, 30));\n      expect(polygon.support(Vector2(0, 1)), Vector2(30, 70));\n      expect(polygon.support(Vector2(0, -1)), Vector2(10, 0));\n      expect(polygon.support(Vector2(-1, -1)), Vector2(10, 0));\n      expect(polygon.support(Vector2(1, 1)), Vector2(80, 40));\n    });\n\n    test('weird-shape polygon', () {\n      final polygon = Polygon([\n        Vector2(20, 40),\n        Vector2(50, 30),\n        Vector2(70, 60),\n        Vector2(50, 40),\n        Vector2(40, 60),\n        Vector2(90, 80),\n        Vector2(100, 20),\n        Vector2(90, 50),\n        Vector2(60, 10),\n        Vector2(40, 25),\n      ]);\n      expect(polygon.edges.length, 10);\n      expect(polygon.isConvex, false);\n      expect(polygon.vertices[0], Vector2(20, 40));\n      expect(\n        polygon.center.x,\n        (20 + 50 + 70 + 50 + 40 + 90 + 100 + 90 + 60 + 40) / 10,\n      );\n      expect(\n        polygon.center.y,\n        (40 + 30 + 60 + 40 + 60 + 80 + 20 + 50 + 10 + 25) / 10,\n      );\n      expect(polygon.aabb.min, Vector2(20, 10));\n      expect(polygon.aabb.max, Vector2(100, 80));\n\n      // containsPoint\n      expect(polygon.containsPoint(Vector2(40, 25)), true);\n      expect(polygon.containsPoint(Vector2(40, 30)), true);\n      expect(polygon.containsPoint(Vector2(40, 60)), true);\n      expect(polygon.containsPoint(Vector2(60, 20)), true);\n      expect(polygon.containsPoint(Vector2(60, 30)), true);\n      expect(polygon.containsPoint(Vector2(65, 25)), true);\n      expect(polygon.containsPoint(Vector2(60, 40)), true);\n      expect(polygon.containsPoint(Vector2(50, 50)), true);\n      expect(polygon.containsPoint(Vector2(60, 60)), true);\n      expect(polygon.containsPoint(Vector2(80, 40)), true);\n      expect(polygon.containsPoint(Vector2(80, 70)), true);\n      expect(polygon.containsPoint(Vector2(90, 80)), true);\n      expect(polygon.containsPoint(Vector2(90, 50)), true);\n      expect(polygon.containsPoint(Vector2(95, 50)), true);\n      expect(polygon.containsPoint(Vector2(97, 30)), true);\n      expect(polygon.containsPoint(Vector2(40, 40)), false);\n      expect(polygon.containsPoint(Vector2(40, 50)), false);\n      expect(polygon.containsPoint(Vector2(50, 35)), false);\n      expect(polygon.containsPoint(Vector2(60, 49)), false);\n      expect(polygon.containsPoint(Vector2(90, 40)), false);\n      expect(polygon.containsPoint(Vector2(100, 40)), false);\n    });\n\n    test('project', () {\n      final polygon = Polygon([\n        Vector2(0, 20),\n        Vector2(20, 40),\n        Vector2(40, 20),\n        Vector2(20, 0),\n      ]);\n      expect(polygon.isConvex, true);\n      final transform = Transform2D()\n        ..angle = pi / 4\n        ..offset = Vector2(-20, -20);\n      final result = polygon.project(transform);\n      final a = 10 * sqrt(2);\n      expect(result, isA<Polygon>());\n      expect((result as Polygon).edges.length, 4);\n      expect(result.vertices[0], closeToVector(Vector2(-a, -a), 1e-6));\n      expect(result.vertices[1], closeToVector(Vector2(-a, a), 1e-6));\n      expect(result.vertices[2], closeToVector(Vector2(a, a), 1e-6));\n      expect(result.vertices[3], closeToVector(Vector2(a, -a), 1e-6));\n    });\n\n    test('project with target', () {\n      final polygon = Polygon([\n        Vector2(0, 20),\n        Vector2(20, 40),\n        Vector2(40, 20),\n        Vector2(20, 0),\n      ]);\n      final transform = Transform2D()\n        ..position = Vector2(10, 10)\n        ..scale = Vector2(2, 1);\n      final target = Polygon(List.generate(4, (_) => Vector2.zero()));\n      final result = polygon.project(transform, target);\n      expect(result, isA<Polygon>());\n      expect(result, target);\n      expect((result as Polygon).edges.length, 4);\n      expect(result.vertices[0], Vector2(10, 30));\n      expect(result.vertices[1], Vector2(50, 50));\n      expect(result.vertices[2], Vector2(90, 30));\n      expect(result.vertices[3], Vector2(50, 10));\n    });\n\n    test('project with target and reflection transform', () {\n      final polygon = Polygon([\n        Vector2(0, 20),\n        Vector2(20, 40),\n        Vector2(40, 20),\n        Vector2(20, 0),\n      ]);\n      final transform = Transform2D()\n        ..position = Vector2(10, 10)\n        ..scale = Vector2(-2, 1);\n      final target = Polygon(List.generate(4, (_) => Vector2.zero()));\n      final result = polygon.project(transform, target);\n      expect(result, isA<Polygon>());\n      expect(result, target);\n      expect((result as Polygon).edges.length, 4);\n      expect(result.isConvex, true);\n      expect(result.vertices[0], Vector2(-30, 10));\n      expect(result.vertices[1], Vector2(-70, 30));\n      expect(result.vertices[2], Vector2(-30, 50));\n      expect(result.vertices[3], Vector2(10, 30));\n    });\n\n    test('project with wrong-shape target', () {\n      final z = Vector2.zero();\n      final polygon = Polygon([z, z, z, z, z]);\n      final target = Polygon([z, z, z]);\n      final transform = Transform2D()..angle = 1;\n      final result = polygon.project(transform, target);\n      expect(result == target, false);\n      expect(result, isA<Polygon>());\n      expect((result as Polygon).edges.length, 5);\n      expect(target.edges.length, 3);\n      expect(result.vertices, [z, z, z, z, z]);\n      expect(result.edges, [z, z, z, z, z]);\n      expect(result.isConvex, true);\n    });\n\n    test('nearestPoint', () {\n      final polygon = Polygon([\n        Vector2(10, 10),\n        Vector2(20, 30),\n        Vector2(10, 40),\n        Vector2(40, 40),\n        Vector2(50, 0),\n      ]);\n\n      expect(polygon.nearestPoint(Vector2(0, 0)), Vector2(10, 10));\n      expect(\n        polygon.nearestPoint(Vector2(10, 0)),\n        Vector2(12.352941176470589, 9.411764705882353),\n      );\n      expect(polygon.nearestPoint(Vector2(60, 0)), Vector2(50, 0));\n      expect(polygon.nearestPoint(Vector2(10, 30)), Vector2(15, 35));\n      expect(polygon.nearestPoint(Vector2(30, 50)), Vector2(30, 40));\n      expect(polygon.nearestPoint(Vector2(50, 50)), Vector2(40, 40));\n      expect(\n        polygon.nearestPoint(Vector2(50, 20)),\n        Vector2(45.294117647058826, 18.823529411764707),\n      );\n    });\n\n    test('nearestPoint with 0-length edges', () {\n      final polygon = Polygon([\n        Vector2(0, 0),\n        Vector2(10, 10),\n        Vector2(-10, 10),\n        Vector2(0, 0),\n      ]);\n\n      expect(polygon.edges[0].length, 0);\n      expect(polygon.nearestPoint(Vector2(0, -20)), Vector2(0, 0));\n      expect(polygon.nearestPoint(Vector2(5, 20)), Vector2(5, 10));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/experimental/geometry/shapes/rectangle_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/experimental.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Rectangle', () {\n    test('simple rectangle', () {\n      final rectangle = Rectangle.fromLTRB(4, 0, 9, 12);\n      expect(rectangle.left, 4);\n      expect(rectangle.top, 0);\n      expect(rectangle.right, 9);\n      expect(rectangle.bottom, 12);\n      expect(rectangle.width, 5);\n      expect(rectangle.height, 12);\n      expect(rectangle.isConvex, true);\n      expect(rectangle.isClosed, true);\n      expect(rectangle.perimeter, 34);\n      expect(rectangle.area, 60);\n      expect(rectangle.center, closeToVector(Vector2(6.5, 6)));\n      expect('$rectangle', 'Rectangle([4.0, 0.0], [9.0, 12.0])');\n    });\n\n    test('simple rectangle from LTWH', () {\n      final rectangle = Rectangle.fromLTWH(2, 2, 5, 7);\n      expect(rectangle.left, 2);\n      expect(rectangle.top, 2);\n      expect(rectangle.right, 7);\n      expect(rectangle.bottom, 9);\n      expect(rectangle.width, 5);\n      expect(rectangle.height, 7);\n    });\n\n    test('rectangle with inverted left-right edges', () {\n      final rectangle = Rectangle.fromLTRB(3, 4, 0, 10);\n      expect(rectangle.left, 0);\n      expect(rectangle.right, 3);\n      expect(rectangle.top, 4);\n      expect(rectangle.bottom, 10);\n    });\n\n    test('rectangle with inverted top-bottom edges', () {\n      final rectangle = Rectangle.fromLTRB(3, 4, 10, 0);\n      expect(rectangle.left, 3);\n      expect(rectangle.right, 10);\n      expect(rectangle.top, 0);\n      expect(rectangle.bottom, 4);\n    });\n\n    test('.fromPoints', () {\n      final rectangles = [\n        Rectangle.fromPoints(Vector2(2, 8), Vector2(5, 9)),\n        Rectangle.fromPoints(Vector2(5, 9), Vector2(2, 8)),\n        Rectangle.fromPoints(Vector2(5, 8), Vector2(2, 9)),\n        Rectangle.fromPoints(Vector2(2, 9), Vector2(5, 8)),\n      ];\n      for (final rectangle in rectangles) {\n        expect(rectangle.left, 2);\n        expect(rectangle.right, 5);\n        expect(rectangle.top, 8);\n        expect(rectangle.bottom, 9);\n      }\n    });\n\n    test('.fromCenter', () {\n      final rectangle = Rectangle.fromCenter(\n        center: Vector2.zero(),\n        size: Vector2(10.0, 2.0),\n      );\n      expect(rectangle.left, -5);\n      expect(rectangle.top, -1);\n      expect(rectangle.right, 5);\n      expect(rectangle.bottom, 1);\n    });\n\n    test('.fromRect', () {\n      final rectangle = Rectangle.fromRect(const Rect.fromLTWH(5, 10, 3, 2));\n      expect(rectangle.left, 5);\n      expect(rectangle.top, 10);\n      expect(rectangle.right, 5 + 3);\n      expect(rectangle.bottom, 10 + 2);\n    });\n\n    test('fromRect with negative Rect', () {\n      const rect = Rect.fromLTRB(5, 7, 1, 0);\n      final rectangle = Rectangle.fromRect(rect);\n      expect(rectangle.left, 1);\n      expect(rectangle.right, 5);\n      expect(rectangle.top, 0);\n      expect(rectangle.bottom, 7);\n    });\n\n    test('0-size rectangle', () {\n      final rectangle = Rectangle.fromLTRB(0, 0, 0, 0);\n      expect(rectangle.left, 0);\n      expect(rectangle.top, 0);\n      expect(rectangle.right, 0);\n      expect(rectangle.bottom, 0);\n      expect(rectangle.width, 0);\n      expect(rectangle.height, 0);\n      expect(rectangle.perimeter, 0);\n      expect(rectangle.containsPoint(Vector2.zero()), true);\n      expect(rectangle.support(Vector2(2, 9)), Vector2.zero());\n    });\n\n    test('aabb', () {\n      final rectangle = Rectangle.fromLTRB(4, 0, 9, 12);\n      expect(\n        rectangle.aabb,\n        closeToAabb(Aabb2.minMax(Vector2(4, 0), Vector2(9, 12))),\n      );\n    });\n\n    test('asPath', () {\n      final rectangle = Rectangle.fromLTRB(3, 2, 8, 8);\n      final path = rectangle.asPath();\n      final metrics = path.computeMetrics().toList();\n      expect(metrics.length, 1);\n      expect(metrics[0].isClosed, true);\n      expect(metrics[0].length, closeTo(rectangle.perimeter, 0.01));\n    });\n\n    test('containsPoint', () {\n      final rectangle = Rectangle.fromLTRB(4, 2, 6, 5);\n      expect(rectangle.containsPoint(Vector2(3, 2)), false);\n      expect(rectangle.containsPoint(Vector2(4, 2)), true);\n      expect(rectangle.containsPoint(Vector2(5, 2)), true);\n      expect(rectangle.containsPoint(Vector2(6, 2)), true);\n      expect(rectangle.containsPoint(Vector2(7, 2)), false);\n\n      expect(rectangle.containsPoint(Vector2(3, 3)), false);\n      expect(rectangle.containsPoint(Vector2(4, 3)), true);\n      expect(rectangle.containsPoint(Vector2(5, 3)), true);\n      expect(rectangle.containsPoint(Vector2(6, 3)), true);\n      expect(rectangle.containsPoint(Vector2(7, 3)), false);\n\n      expect(rectangle.containsPoint(Vector2(3, 4)), false);\n      expect(rectangle.containsPoint(Vector2(4, 4)), true);\n      expect(rectangle.containsPoint(Vector2(5, 4)), true);\n      expect(rectangle.containsPoint(Vector2(6, 4)), true);\n      expect(rectangle.containsPoint(Vector2(7, 4)), false);\n\n      expect(rectangle.containsPoint(Vector2(3, 5)), false);\n      expect(rectangle.containsPoint(Vector2(4, 5)), true);\n      expect(rectangle.containsPoint(Vector2(5, 5)), true);\n      expect(rectangle.containsPoint(Vector2(6, 5)), true);\n      expect(rectangle.containsPoint(Vector2(7, 5)), false);\n\n      expect(rectangle.containsPoint(Vector2(3, 6)), false);\n      expect(rectangle.containsPoint(Vector2(4, 6)), false);\n      expect(rectangle.containsPoint(Vector2(5, 6)), false);\n      expect(rectangle.containsPoint(Vector2(6, 6)), false);\n      expect(rectangle.containsPoint(Vector2(7, 6)), false);\n    });\n\n    test('move', () {\n      final rectangle = Rectangle.fromLTRB(4, 2, 9, 12);\n      expect(rectangle.aabb.min, closeToVector(Vector2(4, 2)));\n\n      rectangle.move(Vector2(-3, 1));\n      expect(rectangle.left, 4 - 3);\n      expect(rectangle.right, 9 - 3);\n      expect(rectangle.top, 2 + 1);\n      expect(rectangle.bottom, 12 + 1);\n      expect(rectangle.center, closeToVector(Vector2(6.5 - 3, 7 + 1)));\n      expect(\n        rectangle.aabb,\n        closeToAabb(Aabb2.minMax(Vector2(1, 3), Vector2(6, 13))),\n      );\n    });\n\n    test('project', () {\n      final rectangle = Rectangle.fromLTRB(3, 3, 5, 5);\n      final transform = Transform2D()\n        ..position = Vector2(1, 1)\n        ..scale = Vector2(-1, 4);\n      final result = rectangle.project(transform);\n      expect(result, isA<Rectangle>());\n      expect((result as Rectangle).left, -4);\n      expect(result.right, -2);\n      expect(result.top, 13);\n      expect(result.bottom, 21);\n\n      transform.angle = 1;\n      expect(\n        () => rectangle.project(transform),\n        throwsUnimplementedError,\n      );\n    });\n\n    test('project with target', () {\n      final rectangle = Rectangle.fromLTRB(0, 0, 1, 1);\n      final transform = Transform2D()\n        ..position = Vector2(3, 5)\n        ..scale = Vector2(2, 1);\n      expect(transform.isAxisAligned, true);\n\n      final target = Rectangle.fromLTRB(0, 0, 0, 0);\n      expect(target.aabb, closeToAabb(Aabb2()));\n      final result = rectangle.project(transform, target);\n      expect(result, isA<Rectangle>());\n      expect(result, target);\n      expect(target.left, 3);\n      expect(target.right, 5);\n      expect(target.top, 5);\n      expect(target.bottom, 6);\n      expect(\n        target.aabb,\n        closeToAabb(Aabb2.minMax(Vector2(3, 5), Vector2(5, 6))),\n      );\n    });\n\n    test('support', () {\n      final rectangle = Rectangle.fromLTRB(4, 2, 9, 3);\n\n      // For axis-aligned directions, the support points are ambiguous, so test\n      // only their unambiguous coordinates.\n      expect(rectangle.support(Vector2(1, 0)).x, 9);\n      expect(rectangle.support(Vector2(-1, 0)).x, 4);\n      expect(rectangle.support(Vector2(0, 11)).y, 3);\n      expect(rectangle.support(Vector2(0, -1)).y, 2);\n\n      expect(rectangle.support(Vector2(1, 1)), closeToVector(Vector2(9, 3)));\n      expect(rectangle.support(Vector2(-3, 2)), closeToVector(Vector2(4, 3)));\n      expect(\n        rectangle.support(Vector2(-0.13, -2.01)),\n        closeToVector(Vector2(4, 2)),\n      );\n      expect(rectangle.support(Vector2(9, -200)), closeToVector(Vector2(9, 2)));\n    });\n\n    test('nearestPoint', () {\n      final rectangle = Rectangle.fromLTRB(0, 0, 5, 4);\n\n      expect(rectangle.nearestPoint(Vector2(0, 0)), Vector2(0, 0));\n      expect(rectangle.nearestPoint(Vector2(1, -1)), Vector2(1, 0));\n      expect(rectangle.nearestPoint(Vector2(-1, -1)), Vector2(0, 0));\n      expect(rectangle.nearestPoint(Vector2(-3, 2)), Vector2(0, 2));\n      expect(rectangle.nearestPoint(Vector2(-3, 7)), Vector2(0, 4));\n      expect(rectangle.nearestPoint(Vector2(-3, 7)), Vector2(0, 4));\n      expect(rectangle.nearestPoint(Vector2(3, 7)), Vector2(3, 4));\n      expect(rectangle.nearestPoint(Vector2(7, 7)), Vector2(5, 4));\n      expect(rectangle.nearestPoint(Vector2(17, 1)), Vector2(5, 1));\n      expect(rectangle.nearestPoint(Vector2(8, -2)), Vector2(5, 0));\n    });\n  });\n\n  test('edges and vertices', () {\n    final rectangle = Rectangle.fromCenter(\n      center: Vector2.zero(),\n      size: Vector2(10.0, 2.0),\n    );\n    expect(rectangle.topLeft, Vector2(-5, -1));\n    expect(rectangle.topRight, Vector2(5, -1));\n    expect(rectangle.bottomLeft, Vector2(-5, 1));\n    expect(rectangle.bottomRight, Vector2(5, 1));\n\n    expect(rectangle.topEdge.from, Vector2(-5, -1));\n    expect(rectangle.topEdge.to, Vector2(5, -1));\n    expect(rectangle.rightEdge.from, Vector2(5, -1));\n    expect(rectangle.rightEdge.to, Vector2(5, 1));\n    expect(rectangle.bottomEdge.from, Vector2(5, 1));\n    expect(rectangle.bottomEdge.to, Vector2(-5, 1));\n    expect(rectangle.leftEdge.from, Vector2(-5, 1));\n    expect(rectangle.leftEdge.to, Vector2(-5, -1));\n  });\n\n  test('intersections', () {\n    final rectangle = Rectangle.fromCenter(\n      center: Vector2.zero(),\n      size: Vector2(2.0, 2.0),\n    );\n    expect(\n      rectangle.intersections(LineSegment(Vector2.zero(), Vector2(0, -2))),\n      [Vector2(0, -1)],\n    );\n    expect(\n      rectangle.intersections(LineSegment(Vector2.zero(), Vector2(0, 2))),\n      [Vector2(0, 1)],\n    );\n    expect(\n      rectangle.intersections(LineSegment(Vector2(-2, 0), Vector2.zero())),\n      [Vector2(-1, 0)],\n    );\n    expect(\n      rectangle.intersections(LineSegment(Vector2(2, 0), Vector2.zero())),\n      [Vector2(1, 0)],\n    );\n\n    expect(\n      rectangle.intersections(LineSegment(Vector2(2, 2), Vector2(-2, -2))),\n      unorderedMatches([Vector2(1, 1), Vector2(-1, -1)]),\n    );\n    expect(\n      rectangle.intersections(LineSegment(Vector2(-2, 2), Vector2(2, -2))),\n      unorderedMatches([Vector2(-1, 1), Vector2(1, -1)]),\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/experimental/geometry/shapes/rounded_rectangle_test.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/experimental.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('RoundedRectangle', () {\n    test('simple rounded rectangle', () {\n      final rrect = RoundedRectangle.fromLTRBR(4, 0, 9, 12, 2);\n      expect(rrect.left, 4);\n      expect(rrect.top, 0);\n      expect(rrect.right, 9);\n      expect(rrect.bottom, 12);\n      expect(rrect.radius, 2);\n      expect(rrect.width, 5);\n      expect(rrect.height, 12);\n      expect(rrect.isConvex, true);\n      expect(rrect.isClosed, true);\n      expect(rrect.perimeter, closeTo(30.566, 0.001));\n      expect(rrect.center, closeToVector(Vector2(6.5, 6)));\n      expect(\n        rrect.aabb,\n        closeToAabb(Aabb2.minMax(Vector2(4, 0), Vector2(9, 12))),\n      );\n      expect('$rrect', 'RoundedRectangle([4.0, 0.0], [9.0, 12.0], 2.0)');\n    });\n\n    test('negative radius', () {\n      expect(\n        () => RoundedRectangle.fromLTRBR(0, 0, 1, 1, -1),\n        failsAssert('Radius cannot be negative: -1.0'),\n      );\n    });\n\n    test('fromPoints', () {\n      final rrect = RoundedRectangle.fromPoints(\n        Vector2(10, 5),\n        Vector2(50, 20),\n        5,\n      );\n      expect(rrect.left, 10);\n      expect(rrect.top, 5);\n      expect(rrect.right, 50);\n      expect(rrect.bottom, 20);\n      expect(rrect.radius, 5);\n    });\n\n    test('fromRRect', () {\n      final rrect = RoundedRectangle.fromRRect(\n        RRect.fromRectAndRadius(\n          const Rect.fromLTWH(20, 0, 50, 100),\n          const Radius.circular(15),\n        ),\n      );\n      expect(rrect.left, 20);\n      expect(rrect.top, 0);\n      expect(rrect.right, 70);\n      expect(rrect.bottom, 100);\n      expect(rrect.radius, 15);\n    });\n\n    test('from bad RRect', () {\n      final rrect = RRect.fromLTRBAndCorners(\n        0,\n        0,\n        10,\n        10,\n        topLeft: const Radius.circular(1),\n        topRight: const Radius.circular(1),\n        bottomLeft: const Radius.elliptical(1, 2),\n        bottomRight: const Radius.circular(1),\n      );\n      expect(\n        () => RoundedRectangle.fromRRect(rrect),\n        failsAssert('Unequal radii in the $rrect'),\n      );\n    });\n\n    test('inverted order of edges', () {\n      final rrect = RoundedRectangle.fromLTRBR(5, 6, 1, -1, 2);\n      expect(rrect.left, 1);\n      expect(rrect.right, 5);\n      expect(rrect.top, -1);\n      expect(rrect.bottom, 6);\n      expect(rrect.radius, 2);\n    });\n\n    test('radius too large (horizontally)', () {\n      final rrect = RoundedRectangle.fromLTRBR(0, 0, 11, 20, 50);\n      expect(rrect.left, 0);\n      expect(rrect.right, 11);\n      expect(rrect.top, 0);\n      expect(rrect.bottom, 20);\n      expect(rrect.radius, 5.5);\n    });\n\n    test('radius too large (vertically)', () {\n      final rrect = RoundedRectangle.fromLTRBR(0, 0, 101, 20, 50);\n      expect(rrect.left, 0);\n      expect(rrect.right, 101);\n      expect(rrect.top, 0);\n      expect(rrect.bottom, 20);\n      expect(rrect.radius, 10);\n    });\n\n    test('asPath()', () {\n      final rrect = RoundedRectangle.fromLTRBR(0, 0, 200, 150, 15);\n      final path = rrect.asPath();\n      final metrics = path.computeMetrics().toList();\n      expect(metrics.length, 1);\n      expect(metrics.first.length, closeTo(rrect.perimeter, 1.0));\n    });\n\n    test('move', () {\n      final rrect = RoundedRectangle.fromLTRBR(4, 2, 9, 12, 1);\n      expect(rrect.aabb.min, closeToVector(Vector2(4, 2)));\n      expect(rrect.aabb.max, closeToVector(Vector2(9, 12)));\n\n      rrect.move(Vector2(-3, 1));\n      expect(rrect.left, 4 - 3);\n      expect(rrect.right, 9 - 3);\n      expect(rrect.top, 2 + 1);\n      expect(rrect.bottom, 12 + 1);\n      expect(rrect.radius, 1);\n      expect(rrect.center, closeToVector(Vector2(6.5 - 3, 7 + 1)));\n      expect(\n        rrect.aabb,\n        closeToAabb(Aabb2.minMax(Vector2(1, 3), Vector2(6, 13))),\n      );\n    });\n\n    test('containsPoint', () {\n      final rrect = RoundedRectangle.fromLTRBR(0, 0, 20, 30, 5);\n      expect(rrect.containsPoint(Vector2(0, 0)), false);\n      expect(rrect.containsPoint(Vector2(0, 1)), false);\n      expect(rrect.containsPoint(Vector2(0, 5)), true);\n      expect(rrect.containsPoint(Vector2(10, 15)), true);\n      expect(rrect.containsPoint(Vector2(20, 30)), false);\n      expect(rrect.containsPoint(Vector2(14, 0)), true);\n      expect(rrect.containsPoint(Vector2(20, 10)), true);\n      expect(rrect.containsPoint(Vector2(5, 30)), true);\n      // points on the rounded corners\n      expect(rrect.containsPoint(Vector2(2, 1)), true);\n      expect(rrect.containsPoint(Vector2(18, 1)), true);\n      expect(rrect.containsPoint(Vector2(1, 28)), true);\n      expect(rrect.containsPoint(Vector2(19, 28)), true);\n    });\n\n    testRandom('containsPoint random', (Random random) {\n      final shape = RoundedRectangle.fromLTRBR(\n        random.nextDouble() * 100,\n        random.nextDouble() * 100,\n        random.nextDouble() * 100,\n        random.nextDouble() * 100,\n        random.nextDouble() * 5,\n      );\n      final rrect = shape.asRRect();\n      for (var i = 0; i < 100; i++) {\n        final point = Vector2.random(random)..scaled(100);\n        expect(\n          shape.containsPoint(point),\n          rrect.contains(point.toOffset()),\n        );\n      }\n    });\n\n    testRandom('support random', (Random random) {\n      final rrect = RoundedRectangle.fromLTRBR(0, 0, 20, 30, 5);\n      final rect = Rectangle.fromLTRB(5, 5, 15, 25);\n      final circle = Circle(Vector2.zero(), 5);\n      // In this test we use the fact that `rrect` is the Minkowski sum of the\n      // `rect` and the `circle`.\n      for (var i = 0; i < 100; i++) {\n        final direction = Vector2.random(random);\n        final expected = rect.support(direction) + circle.support(direction);\n        expect(\n          rrect.support(direction),\n          closeToVector(expected, 1e-5),\n        );\n      }\n    });\n\n    test('axis-aligned conformal projection', () {\n      final rrect = RoundedRectangle.fromLTRBR(3, 3, 15, 12, 1);\n      final transform = Transform2D()\n        ..position = Vector2(1, 5)\n        ..scale = Vector2(4, 4);\n      final result = rrect.project(transform);\n      expect(result, isA<RoundedRectangle>());\n      expect((result as RoundedRectangle).left, 3 * 4 + 1);\n      expect(result.right, 15 * 4 + 1);\n      expect(result.top, 3 * 4 + 5);\n      expect(result.bottom, 12 * 4 + 5);\n      expect(result.radius, 1 * 4);\n    });\n\n    test('projection with a target', () {\n      final rrect = RoundedRectangle.fromLTRBR(3, 3, 15, 12, 1);\n      final transform = Transform2D()\n        ..position = Vector2(1, 5)\n        ..scale = Vector2(4, 4);\n      final target = RoundedRectangle.fromLTRBR(0, 0, 0, 0, 0);\n      expect(target.aabb, closeToAabb(Aabb2()));\n      final result = rrect.project(transform, target);\n      expect(result, isA<RoundedRectangle>());\n      expect(result, target);\n      expect(target.left, 3 * 4 + 1);\n      expect(target.right, 15 * 4 + 1);\n      expect(target.top, 3 * 4 + 5);\n      expect(target.bottom, 12 * 4 + 5);\n      expect(target.radius, 1 * 4);\n      expect(\n        target.aabb,\n        closeToAabb(Aabb2.minMax(Vector2(13, 17), Vector2(61, 53))),\n      );\n    });\n\n    test('unsupported projection', () {\n      final rrect = RoundedRectangle.fromLTRBR(3, 3, 15, 12, 1);\n      final transform = Transform2D()\n        ..position = Vector2(1, 5)\n        ..scale = Vector2(2, 4);\n      expect(\n        () => rrect.project(transform),\n        throwsUnimplementedError,\n      );\n    });\n\n    test('nearestPoint', () {\n      final rrect = RoundedRectangle.fromLTRBR(0, 0, 50, 30, 10);\n\n      expect(\n        rrect.nearestPoint(Vector2(0, 0)),\n        Vector2(2.9289321881345254, 2.9289321881345254),\n      );\n      expect(\n        rrect.nearestPoint(Vector2(0, -10)),\n        closeToVector(Vector2(5.52786404500042, 1.0557280900008408), 1e-6),\n      );\n      expect(rrect.nearestPoint(Vector2(10, -10)), Vector2(10, 0));\n      expect(rrect.nearestPoint(Vector2(30, -10)), Vector2(30, 0));\n      expect(\n        rrect.nearestPoint(Vector2(55, 5)),\n        Vector2(49.48683298050514, 6.83772233983162),\n      );\n      expect(rrect.nearestPoint(Vector2(60, 15)), Vector2(50, 15));\n      expect(rrect.nearestPoint(Vector2(20, 150)), Vector2(20, 30));\n      expect(rrect.nearestPoint(Vector2(100, 100)), Vector2(46, 28));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/experimental/has_game_reference_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass _GameReferenceGame extends FlameGame {}\n\nvoid main() {\n  group('HasGameReference', () {\n    testWithGame(\n      'component with default HasGameReference',\n      _GameReferenceGame.new,\n      (game) async {\n        final component1 = _Component<FlameGame>();\n        final component2 = _Component<_GameReferenceGame>();\n        game.addAll([component1, component2]);\n        expect(component1.game, game);\n        expect(component2.game, game);\n      },\n    );\n\n    testWithGame<_MyGame>(\n      'component with typed HasGameReference',\n      _MyGame.new,\n      (game) async {\n        final component = _Component<_MyGame>();\n        game.add(component);\n        expect(component.game, game);\n      },\n    );\n\n    testWithFlameGame(\n      'game reference accessed too early',\n      (game) async {\n        final component = _Component();\n        expect(\n          () => component.game,\n          failsAssert(\n            'Could not find Game instance: the component is detached from the '\n            'component tree',\n          ),\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'game reference of wrong type',\n      (game) async {\n        final component = _Component<_MyGame>();\n        game.add(component);\n        expect(\n          () => component.game,\n          failsAssert(\n            'Found game of type FlameGame<World>, while type _MyGame was '\n            'expected',\n          ),\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'game reference can be set explicitly',\n      (game) async {\n        final component = _Component<FlameGame>();\n        component.game = game;\n        expect(component.game, game);\n\n        component.game = null;\n        expect(\n          () => component.game,\n          failsAssert(\n            'Could not find Game instance: the component is detached from the '\n            'component tree',\n          ),\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'game reference propagates quickly',\n      (game) async {\n        final component1 = _Component()..addToParent(game);\n        final component2 = _Component()..addToParent(component1);\n        final component3 = _Component()..addToParent(component2);\n        expect(component3.game, game);\n      },\n    );\n\n    testWithGame<_MyGame>('simple test', _MyGame.new, (game) async {\n      final c = _FooComponent();\n      game.add(c);\n      c.foo();\n      expect(game.calledFoo, true);\n    });\n\n    testWithGame<_MyGame>('gameRef can be mocked', _MyGame.new, (game) async {\n      final component = _BarComponent();\n      await game.ensureAdd(component);\n\n      component.game = _MockFlameGame();\n\n      expect(component.game, isA<_MockFlameGame>());\n    });\n  });\n}\n\nclass _Component<T extends FlameGame> extends Component\n    with HasGameReference<T> {}\n\nclass _MyGame extends FlameGame {\n  bool calledFoo = false;\n  void foo() {\n    calledFoo = true;\n  }\n}\n\nclass _FooComponent extends Component with HasGameReference<_MyGame> {\n  void foo() {\n    game.foo();\n  }\n}\n\nclass _BarComponent extends Component with HasGameReference<_MyGame> {}\n\nclass _MockFlameGame extends Mock implements _MyGame {}\n"
  },
  {
    "path": "packages/flame/test/experimental/has_world_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nvoid main() {\n  group('HasWorldReference', () {\n    testWithGame(\n      'component with default HasWorldReference',\n      () => FlameGame(world: _ReferenceWorld()),\n      (game) async {\n        final component1 = _Component<World>();\n        final component2 = _Component<_ReferenceWorld>();\n        game.world.addAll([component1, component2]);\n        expect(component1.world, game.world);\n        expect(component2.world, game.world);\n      },\n    );\n\n    testWithGame<_MyGame>(\n      'component with typed HasWorldReference',\n      _MyGame.new,\n      (game) async {\n        final component = _Component<_ReferenceWorld>();\n        game.world.ensureAdd(component);\n        expect(component.world, game.world);\n      },\n    );\n\n    testWithFlameGame(\n      'world reference accessed too early',\n      (game) async {\n        final component = _Component();\n        expect(\n          () => component.world,\n          failsAssert('Could not find a World instance of type World'),\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'game reference of wrong type',\n      (game) async {\n        final component = _Component<_ReferenceWorld>();\n        game.world.add(component);\n        expect(\n          () => component.world,\n          failsAssert(\n            'Could not find a World instance of type _ReferenceWorld',\n          ),\n        );\n      },\n    );\n\n    testWithFlameGame(\n      'game reference propagates quickly',\n      (game) async {\n        final component1 = _Component()..addToParent(game.world);\n        final component2 = _Component()..addToParent(component1);\n        final component3 = _Component()..addToParent(component2);\n        expect(component3.world, game.world);\n      },\n    );\n\n    testWithGame<_MyGame>('simple test', _MyGame.new, (game) async {\n      final c = _FooComponent();\n      game.world.add(c);\n      c.foo();\n      expect(c.world.calledFoo, isTrue);\n    });\n\n    testWithGame<_MyGame>('gameRef can be mocked', _MyGame.new, (game) async {\n      final component = _BarComponent();\n      await game.world.ensureAdd(component);\n\n      component.world = _MockWorld();\n\n      expect(component.world, isA<_MockWorld>());\n    });\n  });\n}\n\nclass _ReferenceWorld extends World {\n  bool calledFoo = false;\n  void foo() => calledFoo = true;\n}\n\nclass _Component<T extends World> extends Component with HasWorldReference<T> {}\n\nclass _MyGame extends FlameGame {\n  _MyGame() : super(world: _ReferenceWorld());\n}\n\nclass _FooComponent extends Component with HasWorldReference<_ReferenceWorld> {\n  void foo() {\n    world.foo();\n  }\n}\n\nclass _BarComponent extends Component with HasWorldReference<_ReferenceWorld> {}\n\nclass _MockWorld extends Mock implements _ReferenceWorld {}\n"
  },
  {
    "path": "packages/flame/test/experimental/linear_layout_component_test.dart",
    "content": "import 'package:collection/collection.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:test/test.dart';\nimport 'linear_layout_component_test_helpers.dart';\n\nvoid main() {\n  group('LinearLayoutComponent', () {\n    group('mainAxisAlignment', () {\n      runLinearLayoutComponentTestRegistry(\n        {\n          'mainAxisAlignment = start': (game, direction) async {\n            final circle = CircleComponent(radius: 20);\n            final rectangle = RectangleComponent(size: Vector2(100, 50));\n            final text = TextComponent(text: 'testing');\n            await game.ensureAdd(\n              LinearLayoutComponent.fromDirection(\n                direction,\n                children: [circle, rectangle, text],\n              ),\n            );\n            expect(direction.mainAxisValue(circle.position), 0);\n            expect(\n              direction.mainAxisValue(rectangle.position),\n              direction.mainAxisValue(circle.size),\n            );\n            expect(\n              direction.mainAxisValue(text.position),\n              direction.mainAxisValue(rectangle.position) +\n                  direction.mainAxisValue(rectangle.size),\n            );\n          },\n          'mainAxisAlignment = end': (game, direction) async {\n            final circle = CircleComponent(radius: 20);\n            final rectangle = RectangleComponent(size: Vector2(100, 50));\n            final text = TextComponent(text: 'testing');\n            final layoutComponentSize = Vector2.all(500);\n            await game.ensureAdd(\n              LinearLayoutComponent.fromDirection(\n                direction,\n                children: [circle, rectangle, text],\n                size: layoutComponentSize,\n                mainAxisAlignment: MainAxisAlignment.end,\n              ),\n            );\n            final mainAxisValue = direction.mainAxisValue;\n            expect(\n              mainAxisValue(text.position),\n              mainAxisValue(layoutComponentSize) - mainAxisValue(text.size),\n            );\n            expect(\n              mainAxisValue(rectangle.position),\n              mainAxisValue(text.position) - mainAxisValue(rectangle.size),\n            );\n            expect(\n              mainAxisValue(circle.position),\n              mainAxisValue(rectangle.position) - mainAxisValue(circle.size),\n            );\n          },\n          'mainAxisAlignment = center, gap = 20': (game, direction) async {\n            final circle = CircleComponent(radius: 20);\n            final rectangle = RectangleComponent(size: Vector2(100, 50));\n            final text = TextComponent(text: 'testing');\n            final layoutComponentSize = Vector2.all(500);\n            const gap = 20.0;\n            final layoutComponent = LinearLayoutComponent.fromDirection(\n              direction,\n              children: [circle, rectangle, text],\n              size: layoutComponentSize,\n              mainAxisAlignment: MainAxisAlignment.center,\n              gap: gap,\n            );\n            await game.ensureAdd(layoutComponent);\n            final mainAxisValue = direction.mainAxisValue;\n            final occupiedSpace = [\n              circle.size,\n              rectangle.size,\n              text.size,\n            ].map(mainAxisValue).sum;\n            const gapSpace = gap * 2;\n            final centerOffset =\n                (mainAxisValue(layoutComponentSize) -\n                    occupiedSpace -\n                    gapSpace) /\n                2;\n            expect(mainAxisValue(circle.position), centerOffset);\n            expect(\n              mainAxisValue(rectangle.position),\n              mainAxisValue(circle.position) + mainAxisValue(circle.size) + gap,\n            );\n            expect(\n              mainAxisValue(text.position),\n              mainAxisValue(rectangle.position) +\n                  mainAxisValue(rectangle.size) +\n                  gap,\n            );\n          },\n          'mainAxisAlignment = spaceBetween': (game, direction) async {\n            final circle = CircleComponent(radius: 20);\n            final rectangle = RectangleComponent(size: Vector2(100, 50));\n            final text = TextComponent(text: 'testing');\n            final layoutComponentSize = Vector2.all(500);\n            final layoutComponent = LinearLayoutComponent.fromDirection(\n              direction,\n              children: [circle, rectangle, text],\n              size: layoutComponentSize,\n              mainAxisAlignment: MainAxisAlignment.spaceBetween,\n            );\n            await game.ensureAdd(layoutComponent);\n            final mainAxisValue = direction.mainAxisValue;\n            final occupiedSpace = [\n              circle.size,\n              rectangle.size,\n              text.size,\n            ].map(mainAxisValue).sum;\n            final expectedGap =\n                (mainAxisValue(layoutComponentSize) - occupiedSpace) / 2;\n            expect(layoutComponent.gap, expectedGap);\n            expect(mainAxisValue(circle.position), 0);\n            expect(\n              mainAxisValue(rectangle.position),\n              mainAxisValue(circle.size) + expectedGap,\n            );\n            expect(\n              mainAxisValue(text.position),\n              mainAxisValue(rectangle.position) +\n                  mainAxisValue(rectangle.size) +\n                  expectedGap,\n            );\n          },\n          'mainAxisAlignment = spaceAround': (game, direction) async {\n            final circle = CircleComponent(radius: 20);\n            final rectangle = RectangleComponent(size: Vector2(100, 50));\n            final text = TextComponent(text: 'testing');\n            final layoutComponentSize = Vector2.all(500);\n            final layoutComponent = LinearLayoutComponent.fromDirection(\n              direction,\n              children: [circle, rectangle, text],\n              size: layoutComponentSize,\n              mainAxisAlignment: MainAxisAlignment.spaceAround,\n            );\n            await game.ensureAdd(layoutComponent);\n            final mainAxisValue = direction.mainAxisValue;\n            final occupiedSpace = [\n              circle.size,\n              rectangle.size,\n              text.size,\n            ].map(mainAxisValue).sum;\n            final expectedGap =\n                (mainAxisValue(layoutComponentSize) - occupiedSpace) / 3;\n            expect(layoutComponent.gap, expectedGap);\n            expect(\n              mainAxisValue(circle.position),\n              closeTo(expectedGap / 2, 1e-4),\n            );\n            expect(\n              mainAxisValue(rectangle.position),\n              closeTo(\n                mainAxisValue(circle.position) +\n                    mainAxisValue(circle.size) +\n                    expectedGap,\n                1e-4,\n              ),\n            );\n            expect(\n              mainAxisValue(text.position),\n              closeTo(\n                mainAxisValue(rectangle.position) +\n                    mainAxisValue(rectangle.size) +\n                    expectedGap,\n                1e-4,\n              ),\n            );\n          },\n          'mainAxisAlignment = spaceEvenly': (game, direction) async {\n            final circle = CircleComponent(radius: 20);\n            final rectangle = RectangleComponent(size: Vector2(100, 50));\n            final text = TextComponent(text: 'testing');\n            final layoutComponentSize = Vector2.all(500);\n            final layoutComponent = LinearLayoutComponent.fromDirection(\n              direction,\n              children: [circle, rectangle, text],\n              size: layoutComponentSize,\n              mainAxisAlignment: MainAxisAlignment.spaceEvenly,\n            );\n            await game.ensureAdd(layoutComponent);\n            final mainAxisValue = direction.mainAxisValue;\n            final occupiedSpace = [\n              circle.size,\n              rectangle.size,\n              text.size,\n            ].map(mainAxisValue).sum;\n            final expectedGap =\n                (mainAxisValue(layoutComponentSize) - occupiedSpace) / 4;\n            expect(layoutComponent.gap, expectedGap);\n            expect(mainAxisValue(circle.position), expectedGap);\n            expect(\n              mainAxisValue(rectangle.position),\n              mainAxisValue(circle.position) +\n                  mainAxisValue(circle.size) +\n                  expectedGap,\n            );\n            expect(\n              mainAxisValue(text.position),\n              mainAxisValue(rectangle.position) +\n                  mainAxisValue(rectangle.size) +\n                  expectedGap,\n            );\n          },\n          'set mainAxisAlignment to spaceEvenly then end':\n              (game, direction) async {\n                final circle = CircleComponent(radius: 20);\n                final rectangle = RectangleComponent(size: Vector2(100, 50));\n                final text = TextComponent(text: 'testing');\n                final layoutComponentSize = Vector2.all(500);\n                final layoutComponent = LinearLayoutComponent.fromDirection(\n                  direction,\n                  children: [circle, rectangle, text],\n                  size: layoutComponentSize,\n                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,\n                );\n                await game.ensureAdd(layoutComponent);\n                final mainAxisValue = direction.mainAxisValue;\n                final occupiedSpace = [\n                  circle.size,\n                  rectangle.size,\n                  text.size,\n                ].map(mainAxisValue).sum;\n                final expectedGap =\n                    (mainAxisValue(layoutComponentSize) - occupiedSpace) / 4;\n                expect(layoutComponent.gap, expectedGap);\n                expect(mainAxisValue(circle.position), expectedGap);\n                expect(\n                  mainAxisValue(rectangle.position),\n                  mainAxisValue(circle.position) +\n                      mainAxisValue(circle.size) +\n                      expectedGap,\n                );\n                expect(\n                  mainAxisValue(text.position),\n                  mainAxisValue(rectangle.position) +\n                      mainAxisValue(rectangle.size) +\n                      expectedGap,\n                );\n                layoutComponent.mainAxisAlignment = MainAxisAlignment.end;\n                expect(\n                  mainAxisValue(text.positionOfAnchor(Anchor.bottomRight)),\n                  mainAxisValue(layoutComponentSize),\n                );\n              },\n        },\n      );\n    });\n\n    group('crossAxisAlignment', () {\n      runLinearLayoutComponentTestRegistry({\n        'crossAxisAlignment = start': (game, direction) async {\n          final circle = CircleComponent(radius: 20);\n          final rectangle = RectangleComponent(size: Vector2(100, 50));\n          final text = TextComponent(text: 'testing');\n          final layoutComponentSize = Vector2.all(500);\n          final layoutComponent = LinearLayoutComponent.fromDirection(\n            direction,\n            children: [circle, rectangle, text],\n            size: layoutComponentSize,\n          );\n          await game.ensureAdd(layoutComponent);\n          final crossAxisValue = direction.crossAxisValue;\n          expect(crossAxisValue(circle.position), 0);\n          expect(crossAxisValue(rectangle.position), 0);\n          expect(crossAxisValue(text.position), 0);\n        },\n        'crossAxisAlignment = center': (game, direction) async {\n          final circle = CircleComponent(radius: 20);\n          final rectangle = RectangleComponent(size: Vector2(100, 50));\n          final text = TextComponent(text: 'testing');\n          final layoutComponentSize = Vector2.all(500);\n          final layoutComponent = LinearLayoutComponent.fromDirection(\n            direction,\n            children: [circle, rectangle, text],\n            size: layoutComponentSize,\n            crossAxisAlignment: CrossAxisAlignment.center,\n          );\n          await game.ensureAdd(layoutComponent);\n          final crossAxisValue = direction.crossAxisValue;\n          expect(\n            crossAxisValue(circle.position),\n            (crossAxisValue(layoutComponentSize) -\n                    crossAxisValue(circle.size)) /\n                2,\n          );\n          expect(\n            crossAxisValue(rectangle.position),\n            (crossAxisValue(layoutComponentSize) -\n                    crossAxisValue(rectangle.size)) /\n                2,\n          );\n          expect(\n            crossAxisValue(text.position),\n            (crossAxisValue(layoutComponentSize) - crossAxisValue(text.size)) /\n                2,\n          );\n        },\n        'crossAxisAlignment = end': (game, direction) async {\n          final circle = CircleComponent(radius: 20);\n          final rectangle = RectangleComponent(size: Vector2(100, 50));\n          final text = TextComponent(text: 'testing');\n          final layoutComponentSize = Vector2.all(500);\n          final layoutComponent = LinearLayoutComponent.fromDirection(\n            direction,\n            children: [circle, rectangle, text],\n            size: layoutComponentSize,\n            crossAxisAlignment: CrossAxisAlignment.end,\n          );\n          await game.ensureAdd(layoutComponent);\n          final crossAxisValue = direction.crossAxisValue;\n          expect(\n            crossAxisValue(circle.position),\n            crossAxisValue(layoutComponentSize) - crossAxisValue(circle.size),\n          );\n          expect(\n            crossAxisValue(rectangle.position),\n            crossAxisValue(layoutComponentSize) -\n                crossAxisValue(rectangle.size),\n          );\n          expect(\n            crossAxisValue(text.position),\n            crossAxisValue(layoutComponentSize) - crossAxisValue(text.size),\n          );\n        },\n      });\n    });\n\n    group('size', () {\n      runLinearLayoutComponentTestRegistry({\n        'size=null sets size to intrinsicSize': (game, direction) async {\n          final circle = CircleComponent(radius: 20);\n          final rectangle = RectangleComponent(size: Vector2(100, 50));\n          final text = TextComponent(text: 'testing');\n          final layoutComponentSize = Vector2.all(500);\n          final layoutComponent = LinearLayoutComponent.fromDirection(\n            direction,\n            children: [circle, rectangle, text],\n            size: layoutComponentSize,\n          );\n          await game.ensureAdd(layoutComponent);\n          expect(layoutComponent.size, layoutComponentSize);\n          layoutComponent.setLayoutSize(null, null);\n          expect(layoutComponent.size, layoutComponent.intrinsicSize);\n        },\n        'size=null ignores mainAxisAlignment': (game, direction) async {\n          final circle = CircleComponent(radius: 20);\n          final rectangle = RectangleComponent(size: Vector2(100, 50));\n          final text = TextComponent(text: 'testing');\n          final layoutComponent = LinearLayoutComponent.fromDirection(\n            direction,\n            children: [circle, rectangle, text],\n            mainAxisAlignment: MainAxisAlignment.spaceBetween,\n          );\n          await game.ensureAdd(layoutComponent);\n          expect(layoutComponent.mainAxisAlignment, MainAxisAlignment.start);\n        },\n        'size=null respects supported crossAxisAlignments':\n            (game, direction) async {\n              final circle = CircleComponent(radius: 20);\n              final rectangle = RectangleComponent(size: Vector2(100, 50));\n              final text = TextComponent(text: 'testing');\n              final layoutComponent = LinearLayoutComponent.fromDirection(\n                direction,\n                children: [circle, rectangle, text],\n                crossAxisAlignment: CrossAxisAlignment.center,\n              );\n              await game.ensureAdd(layoutComponent);\n              expect(\n                layoutComponent.crossAxisAlignment,\n                CrossAxisAlignment.center,\n              );\n              layoutComponent.crossAxisAlignment = CrossAxisAlignment.end;\n              expect(\n                layoutComponent.crossAxisAlignment,\n                CrossAxisAlignment.end,\n              );\n              layoutComponent.crossAxisAlignment = CrossAxisAlignment.start;\n              expect(\n                layoutComponent.crossAxisAlignment,\n                CrossAxisAlignment.start,\n              );\n            },\n        'size=null with CrossAxisAlignment.stretch expands children as usual':\n            (game, direction) async {\n              final circle = CircleComponent(radius: 20);\n              final rectangle = RectangleComponent(size: Vector2(100, 50));\n              final text = TextComponent(text: 'testing');\n              final layoutComponent = LinearLayoutComponent.fromDirection(\n                direction,\n                children: [circle, rectangle, text],\n                crossAxisAlignment: CrossAxisAlignment.stretch,\n              );\n              await game.ensureAdd(layoutComponent);\n              final crossAxisLengths = layoutComponent.positionChildren.map(\n                (component) => component.size[direction.crossAxis.axisIndex],\n              );\n              // All the cross axis lengths are the same\n              expect(\n                crossAxisLengths.every(\n                  (length) => length == crossAxisLengths.first,\n                ),\n                true,\n              );\n              expect(\n                crossAxisLengths.first,\n                layoutComponent.size[direction.crossAxis.axisIndex],\n              );\n            },\n      });\n    });\n    group('children', () {\n      runLinearLayoutComponentTestRegistry(\n        {\n          'size responds when children are added and then resized':\n              (game, direction) async {\n                final circle = CircleComponent(radius: 20);\n                final rectangle2 = RectangleComponent(size: Vector2(100, 50));\n                final layoutComponent = LinearLayoutComponent.fromDirection(\n                  direction,\n                );\n                await game.ensureAdd(layoutComponent);\n                expect(layoutComponent.size, Vector2.zero());\n                await layoutComponent.ensureAddAll([\n                  circle,\n                  rectangle2,\n                ]);\n                rectangle2.size = Vector2(200, 70);\n                expect(\n                  layoutComponent.size,\n                  switch (direction) {\n                    Direction.horizontal => Vector2(40 + 200, 70),\n                    Direction.vertical => Vector2(200, 70 + 40),\n                  },\n                );\n              },\n          'ExpandedComponent among children are sized correctly':\n              (game, direction) async {\n                final circle = CircleComponent(radius: 20);\n                final rectangle = RectangleComponent(size: Vector2(100, 50));\n                final expandedComponent = ExpandedComponent(child: rectangle);\n                final layoutComponent = LinearLayoutComponent.fromDirection(\n                  direction,\n                  size: Vector2(200, 100),\n                  children: [circle, expandedComponent],\n                );\n                await game.ensureAdd(layoutComponent);\n                expect(\n                  expandedComponent.size,\n                  switch (direction) {\n                    Direction.horizontal => Vector2(200 - 40, 50),\n                    Direction.vertical => Vector2(100, 100 - 40),\n                  },\n                );\n                expect(expandedComponent.child?.size, expandedComponent.size);\n              },\n        },\n      );\n    });\n\n    group('special behaviors', () {\n      testWithFlameGame(\n        'ColumnComponent with crossAxisAlignment = stretch, will set '\n        'TextBoxComponent child maxWidth',\n        (game) async {\n          final textBoxComponent = TextBoxComponent(\n            text: 'The quick brown fox jumps over the lazy dog.',\n          );\n          final layoutComponentSize = Vector2.all(500);\n          final layoutComponent = ColumnComponent(\n            size: layoutComponentSize,\n            crossAxisAlignment: CrossAxisAlignment.stretch,\n            children: [textBoxComponent],\n          );\n          await game.ensureAdd(layoutComponent);\n          expect(textBoxComponent.boxConfig.maxWidth, layoutComponent.width);\n        },\n      );\n      testWithFlameGame(\n        'setLayoutSize and setLayoutAxisLength each notify once',\n        (game) async {\n          final layoutComponent = ColumnComponent();\n          await game.ensureAdd(layoutComponent);\n          var setLayoutSizeCallCount = 0;\n          var setLayoutAxisLengthCallCount = 0;\n          void firstListener() {\n            setLayoutSizeCallCount += 1;\n          }\n\n          void secondListener() {\n            setLayoutAxisLengthCallCount += 1;\n          }\n\n          layoutComponent.size.addListener(firstListener);\n          layoutComponent.setLayoutSize(100, 100);\n          await game.lifecycleEventsProcessed;\n          layoutComponent.size.removeListener(firstListener);\n          expect(setLayoutSizeCallCount, 1);\n          expect(layoutComponent.size, Vector2(100, 100));\n\n          layoutComponent.size.addListener(secondListener);\n          layoutComponent.setLayoutAxisLength(LayoutAxis.x, 200);\n          await game.lifecycleEventsProcessed;\n          layoutComponent.size.removeListener(secondListener);\n          expect(setLayoutAxisLengthCallCount, 1);\n          expect(layoutComponent.size, Vector2(200, 100));\n        },\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/experimental/linear_layout_component_test_helpers.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:meta/meta.dart';\n\n@isTest\nvoid testLinearLayoutComponent(\n  String testName,\n  LinearLayoutComponent Function() layoutConstructor,\n  Future<void> Function(FlameGame<World>) testBody,\n) {\n  testWithFlameGame(testName, testBody);\n}\n\nFuture<void> runLinearLayoutComponentTestRegistry(\n  Map<String, Future<void> Function(FlameGame<World>, Direction)> testRegistry,\n) async {\n  for (final entry in testRegistry.entries) {\n    final name = entry.key;\n    final testFunction = entry.value;\n    testWithFlameGame('[RowComponent] $name', (game) {\n      return testFunction(game, Direction.horizontal);\n    });\n    testWithFlameGame('[ColumnComponent] $name', (game) {\n      return testFunction(game, Direction.vertical);\n    });\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/experimental/padding_component_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/experimental.dart';\nimport 'package:flame/src/image_composition.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('PaddingComponent', () {\n    testWithFlameGame('properly sizes self and positions child', (game) async {\n      const padding = EdgeInsets.all(16);\n      final circle = CircleComponent(radius: 20);\n      final paddingComponent = PaddingComponent(\n        padding: padding,\n        child: circle,\n      );\n      await game.ensureAdd(paddingComponent);\n      expect(\n        paddingComponent.size,\n        Vector2(\n          circle.size.x + padding.horizontal,\n          circle.size.y + padding.vertical,\n        ),\n      );\n      expect(\n        paddingComponent.child?.topLeftPosition,\n        padding.topLeft.toVector2(),\n      );\n    });\n    testWithFlameGame('properly sizes self and positions after child is set', (\n      game,\n    ) async {\n      const padding = EdgeInsets.all(16);\n      final circle = CircleComponent(radius: 20);\n      final paddingComponent = PaddingComponent(\n        padding: padding,\n      );\n      await game.ensureAdd(paddingComponent);\n      expect(\n        paddingComponent.size,\n        Vector2(\n          padding.horizontal,\n          padding.vertical,\n        ),\n      );\n      paddingComponent.child = circle;\n      await game.ready();\n      expect(\n        paddingComponent.size,\n        Vector2(\n          circle.size.x + padding.horizontal,\n          circle.size.y + padding.vertical,\n        ),\n      );\n      expect(\n        paddingComponent.child?.topLeftPosition,\n        padding.topLeft.toVector2(),\n      );\n    });\n    testWithFlameGame(\n      'properly sizes self and positions after padding is set',\n      (game) async {\n        final circle = CircleComponent(radius: 20);\n        final paddingComponent = PaddingComponent(child: circle);\n        await game.ensureAdd(paddingComponent);\n        expect(\n          paddingComponent.size,\n          circle.size,\n        );\n        const padding = EdgeInsets.symmetric(\n          vertical: 16,\n          horizontal: 24,\n        );\n        paddingComponent.padding = padding;\n        expect(\n          paddingComponent.size,\n          Vector2(\n            circle.size.x + padding.horizontal,\n            circle.size.y + padding.vertical,\n          ),\n        );\n        expect(\n          paddingComponent.child?.topLeftPosition,\n          padding.topLeft.toVector2(),\n        );\n      },\n    );\n    testWithFlameGame(\n      'properly sets child size when inflateChild is true',\n      (game) async {\n        final rectangle = RectangleComponent();\n        const padding = EdgeInsets.symmetric(\n          vertical: 16,\n          horizontal: 24,\n        );\n        final paddingComponent = PaddingComponent(\n          padding: padding,\n          size: Vector2(100, 200),\n          inflateChild: true,\n          child: rectangle,\n        );\n        await game.ensureAdd(paddingComponent);\n        expect(\n          rectangle.size,\n          Vector2(100 - padding.horizontal, 200 - padding.vertical),\n        );\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/extensions/aabb_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('Aabb2Extension', () {\n    // aabb2 is an axis aligned bounding box between a min and a max\n    // see https://api.flutter.dev/flutter/vector_math_64/Aabb2-class.html\n    // The extension is used to convert this bounding box to a rect.\n    test('Default aabb constructor', () {\n      final aab2 = Aabb2();\n      // With this constructor, min and max are set to the origin (0,0)\n      // So the corresponding rect should be (0,0,0,0)\n      final aab2Rect = aab2.toRect();\n      _checkRectValues(\n        aab2Rect,\n        left: 0,\n        top: 0,\n        right: 0,\n        bottom: 0,\n      );\n    });\n\n    testRandom('centerAndHalfExtents constructor', (Random r) {\n      // This constructor is useful in circles (see lib/src/experimental/geometry/shapes/circle.dart)\n      final center = Vector2(r.nextDouble(), r.nextDouble());\n      final halfExtends = Vector2(r.nextDouble(), r.nextDouble());\n\n      final aab2 = Aabb2.centerAndHalfExtents(center, halfExtends);\n      final aab2Rect = aab2.toRect();\n\n      _checkRectValues(\n        aab2Rect,\n        left: aab2.min.x,\n        top: aab2.min.y,\n        right: aab2.max.x,\n        bottom: aab2.max.y,\n      );\n    });\n\n    testRandom('aabb minMax constructor', (Random r) {\n      final min = Vector2(r.nextDouble(), r.nextDouble());\n      final max = Vector2(r.nextDouble(), r.nextDouble());\n\n      final aab2 = Aabb2.minMax(min, max);\n      final aab2Rect = aab2.toRect();\n\n      _checkRectValues(\n        aab2Rect,\n        left: min.x,\n        top: min.y,\n        right: max.x,\n        bottom: max.y,\n      );\n    });\n  });\n}\n\nvoid _checkRectValues(\n  Rect rect, {\n  required double left,\n  required double top,\n  required double right,\n  required double bottom,\n}) {\n  expect(rect.left, left, reason: 'left does not match');\n  expect(rect.top, top, reason: 'top does not match');\n  expect(rect.right, right, reason: 'right does not match');\n  expect(rect.bottom, bottom, reason: 'bottom does not match');\n}\n"
  },
  {
    "path": "packages/flame/test/extensions/canvas_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:canvas_test/canvas_test.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('CanvasExtension', () {\n    test('scaleVector calls scale', () {\n      final canvas = _MocktailCanvas();\n      when(() => canvas.scale(1, 2)).thenReturn(null);\n      canvas.scaleVector(Vector2(1, 2));\n      verify(() => canvas.scale(1, 2)).called(1);\n    });\n\n    test('translateVector calls translate', () {\n      final canvas = _MocktailCanvas();\n      when(() => canvas.translate(1, 2)).thenReturn(null);\n      canvas.translateVector(Vector2(1, 2));\n      verify(() => canvas.translate(1, 2)).called(1);\n    });\n\n    test('renderPoint', () {\n      final canvas = MockCanvas();\n      canvas.renderPoint(Vector2.all(10.0), size: 2);\n      expect(\n        canvas,\n        MockCanvas()..drawRect(const Rect.fromLTWH(9, 9, 2, 2)),\n      );\n    });\n\n    test('renderLine', () {\n      final canvas = MockCanvas();\n      final paint = Paint()..color = const Color(0xFFFF00FF);\n      canvas.renderLine(Vector2.all(1), Vector2(3, 0), paint);\n      expect(\n        canvas,\n        MockCanvas()..drawLine(const Offset(1, 1), const Offset(3, 0), paint),\n      );\n    });\n\n    test('renderAt saves, translates draws and then restores', () {\n      final canvas = _MocktailCanvas();\n      when(canvas.save).thenReturn(null);\n      when(() => canvas.translateVector(Vector2(1, 1))).thenReturn(null);\n      when(canvas.restore).thenReturn(null);\n\n      final drawFunction = _MocktailDrawFunction();\n      when(() => drawFunction.call(canvas)).thenReturn(null);\n      canvas.renderAt(Vector2(1, 1), drawFunction.call);\n      verify(canvas.save).called(1);\n      verify(() => canvas.translateVector(Vector2(1, 1))).called(1);\n      verify(() => drawFunction(canvas)).called(1);\n      verify(canvas.restore).called(1);\n    });\n\n    test('renderRotated saves, translates, rotates, draws, translatesBack'\n        ' and then restores', () {\n      final canvas = _MocktailCanvas();\n      when(canvas.save).thenReturn(null);\n      when(() => canvas.rotate(0.5)).thenReturn(null);\n      when(() => canvas.translateVector(Vector2(1, 1))).thenReturn(null);\n      when(() => canvas.translateVector(Vector2(-1, -1))).thenReturn(null);\n      when(canvas.restore).thenReturn(null);\n\n      final drawFunction = _MocktailDrawFunction();\n      when(() => drawFunction.call(canvas)).thenReturn(null);\n      canvas.renderRotated(0.5, Vector2(1, 1), drawFunction.call);\n      verify(canvas.save).called(1);\n      verify(() => canvas.translateVector(Vector2(1, 1))).called(1);\n      verify(() => canvas.rotate(0.5)).called(1);\n      verify(() => drawFunction(canvas)).called(1);\n      verify(() => canvas.translateVector(Vector2(-1, -1))).called(1);\n      verify(canvas.restore).called(1);\n    });\n  });\n}\n\nclass _MocktailCanvas extends Mock implements Canvas {}\n\nabstract class _DrawerFunction {\n  void call(Canvas _);\n}\n\nclass _MocktailDrawFunction extends Mock implements _DrawerFunction {}\n"
  },
  {
    "path": "packages/flame/test/extensions/color_test.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('ColorExtension', () {\n    test('parse short RGB', () {\n      expect(ColorExtension.fromRGBHexString('#234'), const Color(0xFF223344));\n      expect(ColorExtension.fromRGBHexString('1f0'), const Color(0xFF11FF00));\n      expect(ColorExtension.fromRGBHexString('#ccc'), const Color(0xFFCCCCCC));\n      expect(ColorExtension.fromRGBHexString('b1f'), const Color(0xFFBB11FF));\n    });\n\n    test('Parse RGB fails if format is not correct', () {\n      var colorHex = ''.padRight(1);\n      expect(\n        () => ColorExtension.fromRGBHexString(colorHex),\n        throwsA((Object e) => true),\n      );\n      colorHex = '#'.padRight(1);\n      expect(\n        () => ColorExtension.fromRGBHexString(colorHex),\n        throwsA((Object e) => true),\n      );\n      colorHex = ''.padRight(2);\n      expect(\n        () => ColorExtension.fromRGBHexString(colorHex),\n        throwsA((Object e) => true),\n      );\n      colorHex = '#'.padRight(2);\n      expect(\n        () => ColorExtension.fromRGBHexString(colorHex),\n        throwsA((Object e) => true),\n      );\n      colorHex = ''.padRight(7);\n      expect(\n        () => ColorExtension.fromRGBHexString(colorHex),\n        throwsA((Object e) => true),\n      );\n      colorHex = '#'.padRight(7);\n      expect(\n        () => ColorExtension.fromRGBHexString(colorHex),\n        throwsA((Object e) => true),\n      );\n    });\n\n    test('Parse ARGB fails if format is not correct', () {\n      var colorHex = ''.padRight(1);\n      expect(\n        () => ColorExtension.fromARGBHexString(colorHex),\n        throwsA((Object e) => true),\n      );\n      colorHex = '#'.padRight(1);\n      expect(\n        () => ColorExtension.fromARGBHexString(colorHex),\n        throwsA((Object e) => true),\n      );\n      colorHex = ''.padRight(2);\n      expect(\n        () => ColorExtension.fromARGBHexString(colorHex),\n        throwsA((Object e) => true),\n      );\n      colorHex = '#'.padRight(2);\n      expect(\n        () => ColorExtension.fromARGBHexString(colorHex),\n        throwsA((Object e) => true),\n      );\n      colorHex = ''.padRight(8);\n      expect(\n        () => ColorExtension.fromARGBHexString(colorHex),\n        throwsA((Object e) => true),\n      );\n      colorHex = '#'.padRight(8);\n      expect(\n        () => ColorExtension.fromARGBHexString(colorHex),\n        throwsA((Object e) => true),\n      );\n    });\n\n    test('parse long RGB', () {\n      expect(\n        ColorExtension.fromRGBHexString('#121314'),\n        const Color(0xFF121314),\n      );\n      expect(\n        ColorExtension.fromRGBHexString('100ff0'),\n        const Color(0xFF100FF0),\n      );\n      expect(\n        ColorExtension.fromRGBHexString('#cccccc'),\n        const Color(0xFFCCCCCC),\n      );\n      expect(\n        ColorExtension.fromRGBHexString('decade'),\n        const Color(0xFFDECADE),\n      );\n    });\n\n    test('parse short ARGB', () {\n      expect(\n        ColorExtension.fromARGBHexString('#fccc'),\n        const Color(0xFFCCCCCC),\n      );\n      expect(\n        ColorExtension.fromARGBHexString('dead'),\n        const Color(0xDDEEAADD),\n      );\n      expect(\n        ColorExtension.fromARGBHexString('#8cc0'),\n        const Color(0x88CCCC00),\n      );\n      expect(\n        ColorExtension.fromARGBHexString('0b21'),\n        const Color(0x00BB2211),\n      );\n    });\n\n    test('parse long ARGB', () {\n      expect(\n        ColorExtension.fromARGBHexString('#ffcc1050'),\n        const Color(0xFFCC1050),\n      );\n      expect(\n        ColorExtension.fromARGBHexString('0defaced'),\n        const Color(0x0DEFACED),\n      );\n      expect(\n        ColorExtension.fromARGBHexString('#80cccc00'),\n        const Color(0x80CCCC00),\n      );\n      expect(\n        ColorExtension.fromARGBHexString('01234567'),\n        const Color(0x01234567),\n      );\n    });\n\n    test('random: errors', () {\n      expect(\n        () => ColorExtension.random(base: -1),\n        failsAssert('The base argument should be in the range 0..256'),\n      );\n      expect(\n        () => ColorExtension.random(base: 257),\n        failsAssert('The base argument should be in the range 0..256'),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/extensions/double_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/src/extensions/double.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('DoubleExtension', () {\n    group('toFinite', () {\n      test('Properly converts infinite values to maxFinite', () {\n        const infinity = double.infinity;\n        expect(infinity.toFinite(), double.maxFinite);\n        const negativeInfinity = -double.infinity;\n        expect(negativeInfinity.toFinite(), -double.maxFinite);\n      });\n\n      test('Does not convert already finite value', () {\n        expect(0.0.toFinite(), 0.0);\n        expect(double.maxFinite.toFinite(), double.maxFinite);\n        expect((-double.maxFinite).toFinite(), -double.maxFinite);\n      });\n    });\n\n    group('normalizedAngle', () {\n      test('Does not convert value within [-pi, pi] range', () {\n        expect((pi / 2).toNormalizedAngle(), pi / 2);\n      });\n\n      test('Converts value greater than pi to normalized angle', () {\n        expect((3 * pi / 2).toNormalizedAngle(), -pi / 2);\n      });\n\n      test('Converts value less than -pi to normalized angle', () {\n        expect((-3 * pi / 2).toNormalizedAngle(), pi / 2);\n      });\n\n      test('Converts value equal to 2pi to normalized angle', () {\n        expect((2 * pi).toNormalizedAngle(), 0.0);\n      });\n\n      test('Converts value equal to -2pi to normalized angle', () {\n        expect((-2 * pi).toNormalizedAngle(), 0.0);\n      });\n\n      test('Does not convert value equal to pi', () {\n        expect(pi.toNormalizedAngle(), pi);\n      });\n\n      test('Does not convert value equal to -pi', () {\n        expect((-pi).toNormalizedAngle(), -pi);\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/extensions/image_extension_test.dart",
    "content": "import 'dart:math';\nimport 'dart:typed_data';\nimport 'dart:ui';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nfinal _output = List.filled(8 * 8 * 4, 255);\n\nvoid main() {\n  group('ImageExtension', () {\n    test('fromPixels', () async {\n      final data = Uint8List(8 * 8 * 4);\n      for (var i = 0; i < data.length; i += 4) {\n        data[i] = 255;\n        data[i + 1] = 255;\n        data[i + 2] = 255;\n        data[i + 3] = 255;\n      }\n      final image = await ImageExtension.fromPixels(data, 8, 8);\n      final bytes = await image.toByteData();\n\n      expect(bytes!.buffer.asUint8List(), equals(_output));\n    });\n\n    test('pixelsInUint8', () async {\n      final data = Uint8List(8 * 8 * 4);\n      for (var i = 0; i < data.length; i += 4) {\n        data[i] = 255;\n        data[i + 1] = 255;\n        data[i + 2] = 255;\n        data[i + 3] = 255;\n      }\n      final image = await ImageExtension.fromPixels(data, 8, 8);\n      expect(await image.pixelsInUint8(), equals(_output));\n    });\n\n    testRandom('getBoundingRect', (Random r) async {\n      final width = r.nextInt(4000);\n      final height = r.nextInt(4000);\n      final image = await createTestImage(width: width, height: height);\n      final rect = Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble());\n      expect(image.getBoundingRect(), rect);\n    });\n\n    testRandom('size getter', (Random r) async {\n      final width = r.nextInt(4000);\n      final height = r.nextInt(4000);\n      final image = await createTestImage(width: width, height: height);\n      final size = Vector2(width.toDouble(), height.toDouble());\n      expect(image.size, size);\n    });\n\n    test('darken colors each pixel darker', () async {\n      const transparentColor = Color.fromARGB(0, 255, 0, 255);\n      const originalColor = Color.fromARGB(255, 135, 73, 73);\n      final pixels = Uint8List.fromList(\n        List<int>.generate(\n          100 * 4,\n          (index) => _colorBit(\n            index,\n            index < 200 ? transparentColor : originalColor,\n          ),\n        ),\n      );\n      final image = await ImageExtension.fromPixels(pixels, 10, 10);\n\n      const darkenAmount = 0.5;\n      final actualDarkenedImage = await image.darken(darkenAmount);\n      final actualDarkenedPixels = await actualDarkenedImage.pixelsInUint8();\n\n      final darkenedColor = originalColor.darken(darkenAmount);\n      final expectedDarkenPixels = Uint8List.fromList(\n        List<int>.generate(\n          100 * 4,\n          (index) => _colorBit(\n            index,\n            index < 200 ? transparentColor : darkenedColor,\n          ),\n        ),\n      );\n      expect(actualDarkenedPixels, expectedDarkenPixels);\n    });\n\n    test('brighten colors each pixel brighter', () async {\n      const transparentColor = Color.fromARGB(0, 255, 0, 255);\n      const originalColor = Color.fromARGB(255, 255, 0, 0);\n\n      final pixels = Uint8List.fromList(\n        List<int>.generate(\n          100 * 4,\n          (index) => _colorBit(\n            index,\n            index < 200 ? transparentColor : originalColor,\n          ),\n        ),\n      );\n      final image = await ImageExtension.fromPixels(pixels, 10, 10);\n\n      const brightenAmount = 0.5;\n      final brightenedImage = await image.brighten(brightenAmount);\n      final actualBrightenedPixels = await brightenedImage.pixelsInUint8();\n\n      final brightenColor = originalColor.brighten(brightenAmount);\n      final expectedBrightenPixels = Uint8List.fromList(\n        List<int>.generate(\n          100 * 4,\n          (index) => _colorBit(\n            index,\n            index < 200 ? transparentColor : brightenColor,\n          ),\n        ),\n      );\n      expect(actualBrightenedPixels, expectedBrightenPixels);\n    });\n\n    test('resize resizes the image', () async {\n      final recorder = PictureRecorder();\n      Canvas(recorder).drawRect(\n        const Rect.fromLTWH(0, 0, 100, 100),\n        Paint()..color = Colors.white,\n      );\n      final pic = recorder.endRecording();\n      final image = await pic.toImage(100, 100);\n\n      final resizedImage = await image.resize(Vector2(200, 400));\n      expect(resizedImage.width, equals(200));\n      expect(resizedImage.height, equals(400));\n    });\n  });\n}\n\nint _colorBit(int index, Color color) {\n  return switch (index % 4) {\n    0 => (color.r * 255).round(),\n    1 => (color.g * 255).round(),\n    2 => (color.b * 255).round(),\n    3 => (color.a * 255).round(),\n    _ => throw UnimplementedError(),\n  };\n}\n"
  },
  {
    "path": "packages/flame/test/extensions/list_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('ListExtension', () {\n    test('reverse', () {\n      final list = [1, 3, 3, 7];\n      list.reverse();\n      expect(list, [7, 3, 3, 1]);\n      list.insert(1, 4);\n      list.reverse();\n      expect(list, [1, 3, 3, 4, 7]);\n    });\n\n    test('random', () {\n      final list = [1, 3, 3, 7];\n      final random = Random(0);\n      final element1 = list.random(random);\n      expect(element1, 7);\n      final element2 = list.random(random);\n      expect(element2, 3);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/extensions/matrix4_test.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('Matrix4Extension', () {\n    final matrix4 = Matrix4.fromList([\n      1, 2, 3, 4, // first row\n      5, 6, 7, 8, // second row\n      9, 10, 11, 12, // third row\n      13, 14, 15, 16, // fourth row\n    ]);\n\n    test('test m11', () => expect(matrix4.m11, matrix4.storage[0]));\n    test('test m12', () => expect(matrix4.m12, matrix4.storage[1]));\n    test('test m13', () => expect(matrix4.m13, matrix4.storage[2]));\n    test('test m14', () => expect(matrix4.m14, matrix4.storage[3]));\n    test('test m21', () => expect(matrix4.m21, matrix4.storage[4]));\n    test('test m22', () => expect(matrix4.m22, matrix4.storage[5]));\n    test('test m23', () => expect(matrix4.m23, matrix4.storage[6]));\n    test('test m24', () => expect(matrix4.m24, matrix4.storage[7]));\n    test('test m31', () => expect(matrix4.m31, matrix4.storage[8]));\n    test('test m32', () => expect(matrix4.m32, matrix4.storage[9]));\n    test('test m33', () => expect(matrix4.m33, matrix4.storage[10]));\n    test('test m34', () => expect(matrix4.m34, matrix4.storage[11]));\n    test('test m41', () => expect(matrix4.m41, matrix4.storage[12]));\n    test('test m42', () => expect(matrix4.m42, matrix4.storage[13]));\n    test('test m43', () => expect(matrix4.m43, matrix4.storage[14]));\n    test('test m44', () => expect(matrix4.m44, matrix4.storage[15]));\n\n    group('transformed2', () {\n      test('Without out', () {\n        final matrix4 = Matrix4.translation(Vector3(0, 10, 0));\n        final input = Vector2.all(10);\n        expect(matrix4.transform2(input), closeToVector(Vector2(10, 20)));\n      });\n\n      test('With correct out', () {\n        final matrix4 = Matrix4.translation(Vector3(0, 10, 0));\n        final input = Vector2.all(10);\n        final out = Vector2.zero();\n        final result = matrix4.transformed2(input, out);\n\n        expect(out, closeToVector(input));\n        expect(result, closeToVector(Vector2(10, 20)));\n      });\n\n      test('With explicit null out', () {\n        final matrix4 = Matrix4.translation(Vector3(0, 10, 0));\n        final input = Vector2.all(10);\n        // ignore: avoid_redundant_argument_values\n        final result = matrix4.transformed2(input, null);\n\n        expect(Vector2.copy(input), closeToVector(input));\n        expect(result, closeToVector(Vector2(10, 20)));\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/extensions/offset_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('OffsetExtension', () {\n    testRandom('toVector2 has x to offset.dx and y to offset.dy', (Random r) {\n      final offset = Offset(r.nextDouble(), r.nextDouble());\n      final vector2 = offset.toVector2();\n\n      expect(\n        vector2.x,\n        closeTo(offset.dx, 1e-6),\n        reason: 'x dx does not match',\n      );\n      expect(\n        vector2.y,\n        closeTo(offset.dy, 1e-6),\n        reason: 'y dy does not match',\n      );\n    });\n\n    testRandom('toSize has width to offset.dx and height to offset.dy', (\n      Random r,\n    ) {\n      final offset = Offset(r.nextDouble(), r.nextDouble());\n      final size = offset.toSize();\n\n      expect(size.width, offset.dx, reason: 'width dx does not match');\n      expect(size.height, offset.dy, reason: 'height dy does not match');\n    });\n\n    testRandom('toSize has x to offset.dx and y to offset.dy', (Random r) {\n      final offset = Offset(r.nextDouble(), r.nextDouble());\n      final point = offset.toPoint();\n\n      expect(point.x, offset.dx, reason: 'x dx does not match');\n      expect(point.y, offset.dy, reason: 'y dy does not match');\n    });\n\n    testRandom(\n      'toRect has left: 0, top: 0, width: offset.dx, height: offset.dy',\n      (Random r) {\n        final offset = Offset(r.nextDouble(), r.nextDouble());\n        final rect = offset.toRect();\n\n        expect(rect.left, 0, reason: 'left should be 0 as init');\n        expect(rect.top, 0, reason: 'top should be 0 as init');\n        expect(rect.width, offset.dx, reason: 'width dx does not match');\n        expect(rect.height, offset.dy, reason: 'height dy does not match');\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/extensions/paint_test.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('PaintExtension', () {\n    testRandom('calling darken changes the color of the paint', (Random r) {\n      final baseColor = ColorExtension.random(rng: r);\n      final paint = Paint()..color = baseColor;\n\n      final darkenAmount = r.nextDouble();\n      final darkenBaseColor = baseColor.darken(darkenAmount);\n\n      paint.darken(darkenAmount);\n\n      expectColor(\n        paint.color,\n        darkenBaseColor,\n        reason: \"Paint's color does not match darken color\",\n      );\n    });\n\n    testRandom('calling brighten changes the color of the paint', (Random r) {\n      final baseColor = ColorExtension.random(rng: r);\n      final paint = Paint()..color = baseColor;\n\n      final brightenAmount = r.nextDouble();\n      final brightenBaseColor = baseColor.brighten(brightenAmount);\n\n      paint.brighten(brightenAmount);\n\n      expectColor(\n        paint.color,\n        brightenBaseColor,\n        reason: \"Paint's color does not match brighten color\",\n      );\n    });\n\n    testRandom(\n      'using fromRGBHexString returns a new Paint with the right Color',\n      (Random r) {\n        // As documentation says\n        // valid inputs are : ccc, CCC, #ccc, #CCC, #c1c1c1, #C1C1C1, c1c1c1,\n        // C1C1C1\n        final color = ColorExtension.random();\n        final sixHexColor =\n            (color.r * 255).toInt().toRadixString(16).padLeft(2, '0') +\n            (color.g * 255).toInt().toRadixString(16).padLeft(2, '0') +\n            (color.b * 255).toInt().toRadixString(16).padLeft(2, '0');\n\n        // C1C1C1\n        final sixUpperCaseColor = sixHexColor.toUpperCase();\n        // c1c1c1\n        final sixLowerCaseColor = sixHexColor.toLowerCase();\n        // #C1C1C1\n        final hashtagSixUpperCaseColor = '#$sixUpperCaseColor';\n        // #c1c1c1\n        final hashtagSixLowerCaseColor = '#$sixLowerCaseColor';\n\n        expectColor(\n          PaintExtension.fromRGBHexString(hashtagSixUpperCaseColor).color,\n          color,\n          reason: 'C1C1C1 does not generates the good paint',\n        );\n        expectColor(\n          PaintExtension.fromRGBHexString(sixLowerCaseColor).color,\n          color,\n          reason: 'c1c1c1 does not generates the good paint',\n        );\n        expectColor(\n          PaintExtension.fromRGBHexString(hashtagSixUpperCaseColor).color,\n          color,\n          reason: '#C1C1C1 does not generates the good paint',\n        );\n        expectColor(\n          PaintExtension.fromRGBHexString(hashtagSixLowerCaseColor).color,\n          color,\n          reason: '#c1c1c1 does not generates the good paint',\n        );\n\n        // Let's generate a new color from only 3 digits\n        final threeHexColor =\n            (color.r ~/ 255).toRadixString(16).padLeft(1, '0')[0] +\n            (color.g ~/ 255).toRadixString(16).padLeft(1, '0')[0] +\n            (color.b ~/ 255).toRadixString(16).padLeft(1, '0')[0];\n        final threeDigitsColor = ColorExtension.fromRGBHexString(threeHexColor);\n\n        // CCC\n        final threeUpperCaseColor = threeHexColor.toUpperCase();\n        // ccc\n        final threeLowerCaseColor = threeHexColor.toLowerCase();\n        // #CCC\n        final hashtagThreeUpperCaseColor = '#$threeUpperCaseColor';\n        // #ccc\n        final hashtagThreeLowerCaseColor = '#$threeLowerCaseColor';\n\n        expectColor(\n          PaintExtension.fromRGBHexString(threeUpperCaseColor).color,\n          threeDigitsColor,\n          reason: 'CCC does not generates the good paint',\n        );\n        expectColor(\n          PaintExtension.fromRGBHexString(threeLowerCaseColor).color,\n          threeDigitsColor,\n          reason: 'ccc does not generates the good paint',\n        );\n        expectColor(\n          PaintExtension.fromRGBHexString(hashtagThreeUpperCaseColor).color,\n          threeDigitsColor,\n          reason: '#CCC does not generates the good paint',\n        );\n        expectColor(\n          PaintExtension.fromRGBHexString(hashtagThreeLowerCaseColor).color,\n          threeDigitsColor,\n          reason: '#ccc does not generates the good paint',\n        );\n      },\n    );\n\n    testRandom(\n      'using fromARGBHexString returns a new Paint with the right Color',\n      (Random r) {\n        // used as an example hex color code on the tests below\n        // cSpell:ignore fccc\n\n        // As documentation says\n        // valid inputs are : fccc, FCCC, #fccc, #FCCC, #ffc1c1c1, #FFC1C1C1,\n        // ffc1c1c1, FFC1C1C1\n        var color = ColorExtension.random(rng: r);\n        final sixHexColor =\n            (color.a * 255).toInt().toRadixString(16).padLeft(2, '0') +\n            (color.r * 255).toInt().toRadixString(16).padLeft(2, '0') +\n            (color.g * 255).toInt().toRadixString(16).padLeft(2, '0') +\n            (color.b * 255).toInt().toRadixString(16).padLeft(2, '0');\n\n        // FFC1C1C1\n        final sixUpperCaseColor = sixHexColor.toUpperCase();\n        // ffc1c1c1\n        final sixLowerCaseColor = sixHexColor.toLowerCase();\n        // #FFC1C1C1\n        final hashtagSixUpperCaseColor = '#$sixUpperCaseColor';\n        // #ffc1c1c1\n        final hashtagSixLowerCaseColor = '#$sixLowerCaseColor';\n\n        expectColor(\n          PaintExtension.fromARGBHexString(hashtagSixUpperCaseColor).color,\n          color,\n          reason: 'FFC1C1C1 does not generates the good paint',\n        );\n        expectColor(\n          PaintExtension.fromARGBHexString(sixLowerCaseColor).color,\n          color,\n          reason: 'ffc1c1c1 does not generates the good paint',\n        );\n        expectColor(\n          PaintExtension.fromARGBHexString(hashtagSixUpperCaseColor).color,\n          color,\n          reason: '#FFC1C1C1 does not generates the good paint',\n        );\n        expectColor(\n          PaintExtension.fromARGBHexString(hashtagSixLowerCaseColor).color,\n          color,\n          reason: '#ffc1c1c1 does not generates the good paint',\n        );\n\n        // Let's generate a new color from only 3 digits\n        final threeHexColor =\n            (color.a ~/ 255).toRadixString(16).padLeft(1, '0')[0] +\n            (color.r ~/ 255).toRadixString(16).padLeft(1, '0')[0] +\n            (color.g ~/ 255).toRadixString(16).padLeft(1, '0')[0] +\n            (color.b ~/ 255).toRadixString(16).padLeft(1, '0')[0];\n        color = ColorExtension.fromARGBHexString(threeHexColor);\n\n        // FCCC\n        final threeUpperCaseColor = threeHexColor.toUpperCase();\n        // fccc\n        final threeLowerCaseColor = threeHexColor.toLowerCase();\n        // #FCCC\n        final hashtagThreeUpperCaseColor = '#$threeUpperCaseColor';\n        // #fccc\n        final hashtagThreeLowerCaseColor = '#$threeLowerCaseColor';\n\n        expect(\n          PaintExtension.fromARGBHexString(threeUpperCaseColor).color,\n          color,\n          reason: 'FCCC does not generates the good paint',\n        );\n        expect(\n          PaintExtension.fromARGBHexString(threeLowerCaseColor).color,\n          color,\n          reason: 'fccc does not generates the good paint',\n        );\n        expect(\n          PaintExtension.fromARGBHexString(hashtagThreeUpperCaseColor).color,\n          color,\n          reason: '#FCCC does not generates the good paint',\n        );\n        expect(\n          PaintExtension.fromARGBHexString(hashtagThreeLowerCaseColor).color,\n          color,\n          reason: '#fccc does not generates the good paint',\n        );\n      },\n    );\n\n    testRandom('random returns a new Paint with the right Color', (Random r) {\n      // withAlpha is used by Color.fromRGBO witch takes an argument between 0\n      // and 1\n      final withAlpha = r.nextDouble();\n      final base = r.nextInt(256);\n\n      final paint = PaintExtension.random(\n        withAlpha: withAlpha,\n        base: base,\n        rng: r,\n      );\n      final color = ColorExtension.random(withAlpha: withAlpha, rng: r);\n\n      // As explained in the documentation\n      // object with the set alpha as [withAlpha]\n      expectColorAlpha(\n        paint.color,\n        color,\n        reason: 'alpha does not have the right value',\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/extensions/picture_extension_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nvoid main() {\n  group('PictureExtension', () {\n    test('toImageSafe calls dispose on the Picture', () async {\n      final picture = _MockPicture();\n\n      // Mock the picture.toImage call to return a test image\n      when(() => picture.toImage(1, 1)).thenAnswer((_) => createTestImage());\n\n      await picture.toImageSafe(1, 1);\n\n      // Verify that dispose has been called on picture\n      // (which is the point of toImageSafe)\n      verify(picture.dispose).called(1);\n    });\n  });\n}\n\nclass _MockPicture extends Mock implements Picture {}\n"
  },
  {
    "path": "packages/flame/test/extensions/random_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('RandomExtension', () {\n    test('nextBoolean returns true with odds > 0.5', () {\n      final r = _MockRandom();\n\n      when(r.nextDouble).thenReturn(0.1);\n      expect(r.nextBoolean(odds: 0.6), isTrue);\n\n      when(r.nextDouble).thenReturn(0.7);\n      expect(r.nextBoolean(odds: 0.6), isFalse);\n\n      when(r.nextDouble).thenReturn(0.5);\n      expect(r.nextBoolean(), isFalse);\n\n      expect(() => r.nextBoolean(odds: -0.1), throwsArgumentError);\n      expect(() => r.nextBoolean(odds: 1.1), throwsArgumentError);\n    });\n\n    test('nextDoubleBetween returns a value in the range', () {\n      final r = _MockRandom();\n\n      when(r.nextDouble).thenReturn(0.5);\n      expect(r.nextDoubleBetween(1, 3), 2.0);\n\n      when(r.nextDouble).thenReturn(0.0);\n      expect(r.nextDoubleBetween(1, 3), 1.0);\n\n      when(r.nextDouble).thenReturn(1.0);\n      expect(r.nextDoubleBetween(1, 3), 3.0);\n    });\n\n    test('nextIntBetween returns a value in the range', () {\n      final r = _MockRandom();\n\n      when(() => r.nextInt(any())).thenReturn(1);\n      expect(r.nextIntBetween(1, 3), 2);\n\n      when(() => r.nextInt(any())).thenReturn(0);\n      expect(r.nextIntBetween(0, 1), 0);\n\n      when(() => r.nextInt(any())).thenReturn(2);\n      expect(r.nextIntBetween(1, 4), 3);\n\n      expect(() => r.nextIntBetween(3, 3), throwsArgumentError);\n      expect(() => r.nextIntBetween(3, 2), throwsArgumentError);\n    });\n\n    test('nextColor returns a color with RGB values between 0 and 255', () {\n      final r = _MockRandom();\n\n      var result = 100;\n      when(() => r.nextInt(any())).thenAnswer((_) => result++);\n      expect(r.nextColor(), const Color.fromARGB(255, 100, 101, 102));\n\n      when(() => r.nextInt(any())).thenReturn(0);\n      expect(r.nextColor(), const Color.fromARGB(255, 0, 0, 0));\n\n      when(() => r.nextInt(any())).thenReturn(255);\n      expect(r.nextColor(), const Color.fromARGB(255, 255, 255, 255));\n    });\n  });\n}\n\nclass _MockRandom extends Mock implements Random {}\n"
  },
  {
    "path": "packages/flame/test/extensions/rect_test.dart",
    "content": "import 'dart:math' as math;\nimport 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('RectExtension', () {\n    test('toOffset dx is width and dy is height', () {\n      const rect = Rect.fromLTWH(0, 0, 1, 2);\n      final offset = rect.toOffset();\n      expect(offset.dx, rect.width);\n      expect(offset.dy, rect.height);\n    });\n\n    test('toVector2 x is width and y is height', () {\n      const rect = Rect.fromLTWH(0, 0, 1, 2);\n      final vector = rect.toVector2();\n      expect(vector.x, rect.width);\n      expect(vector.y, rect.height);\n    });\n\n    test('test from ui Rect to math Rectangle', () {\n      const r1 = Rect.fromLTWH(0, 10, 20, 30);\n      final r2 = r1.toMathRectangle();\n      expect(r2.top, r1.top);\n      expect(r2.bottom, r1.bottom);\n      expect(r2.left, r1.left);\n      expect(r2.right, r1.right);\n      expect(r2.width, r1.width);\n      expect(r2.height, r1.height);\n    });\n\n    test('test from ui Rect to Flame Rectangle', () {\n      const r1 = Rect.fromLTWH(0, 10, 20, 30);\n      final r2 = r1.toFlameRectangle();\n      expect(r2.top, r1.top);\n      expect(r2.bottom, r1.bottom);\n      expect(r2.left, r1.left);\n      expect(r2.right, r1.right);\n      expect(r2.width, r1.width);\n      expect(r2.height, r1.height);\n    });\n\n    test('test from math Rectangle to ui Rect', () {\n      const r1 = math.Rectangle(0, 10, 20, 30);\n      final r2 = r1.toRect();\n      expect(r2.top, r1.top);\n      expect(r2.bottom, r1.bottom);\n      expect(r2.left, r1.left);\n      expect(r2.right, r1.right);\n      expect(r2.width, r1.width);\n      expect(r2.height, r1.height);\n    });\n\n    test('test from ui Rect to RectangleComponent', () {\n      const r1 = Rect.fromLTWH(0, 10, 20, 30);\n      final r2 = r1.toRectangleComponent();\n      expect(r2.angle, 0);\n      expect(r2.position.x, r1.left);\n      expect(r2.position.y, r1.top);\n      expect(r2.size.x, r1.width);\n      expect(r2.size.y, r1.height);\n    });\n\n    test('test containsPoint calls contains on Rect', () {\n      final rect = _MockRect();\n      final point = Vector2.zero();\n\n      // mock contains result, but check it is called\n      when(() => rect.contains(point.toOffset())).thenReturn(true);\n      rect.containsPoint(point);\n      verify(() => rect.contains(point.toOffset())).called(1);\n    });\n\n    testRandom('intersectsSegment', (Random r) {\n      const rect = Rect.fromLTWH(0, 0, 1, 1);\n      // create points left, above, right and under\n\n      // y position [0, 1] but left to rect's left\n      final left = Vector2(-0.5, r.nextDouble() * 1);\n      // x position [0, 1] but above rect's top\n      final above = Vector2(r.nextDouble() * 1, -0.5);\n      // y position [0, 1] but right to rect's right\n      final right = Vector2(1.5, r.nextDouble() * 1);\n      // x position [0, 1] but under rect's bottom\n      final under = Vector2(r.nextDouble() * 1, 1.5);\n      expect(rect.intersectsSegment(left, above), true);\n      expect(rect.intersectsSegment(left, under), true);\n      expect(rect.intersectsSegment(right, above), true);\n      expect(rect.intersectsSegment(right, under), true);\n      expect(rect.intersectsSegment(left, right), true);\n      expect(rect.intersectsSegment(under, above), true);\n\n      // above the rect and left to rect's left\n      final aboveLeft = Vector2(-0.5, r.nextDouble() * 1 - 1);\n      // above the rect and left to rect's left\n      final aboveRight = Vector2(1.5, r.nextDouble() * 1 - 1);\n      // under the rect and right to rect's right\n      final underRight = Vector2(1.5, r.nextDouble() * 1 + 1);\n      // under the rect and left to rect's left\n      final underLeft = Vector2(-0.5, r.nextDouble() * 1 + 1);\n      expect(rect.intersectsSegment(aboveLeft, aboveRight), false);\n      expect(rect.intersectsSegment(aboveLeft, underLeft), false);\n      expect(rect.intersectsSegment(underLeft, underRight), false);\n      expect(rect.intersectsSegment(underRight, aboveRight), false);\n\n      // any y position but left to rect's left\n      final nearLeft = Vector2(-0.25, r.nextDouble() * 256);\n      // any x position but above rect's top\n      final nearAbove = Vector2(r.nextDouble() * 256, -0.25);\n      // any y position but right to rect's right\n      final nearRight = Vector2(1.25, r.nextDouble() * 256);\n      // any x position but under rect's bottom\n      final nearUnder = Vector2(r.nextDouble() * 256, 1.25);\n\n      expect(rect.intersectsSegment(left, nearLeft), false);\n      expect(rect.intersectsSegment(above, nearAbove), false);\n      expect(rect.intersectsSegment(right, nearRight), false);\n      expect(rect.intersectsSegment(under, nearUnder), false);\n    });\n\n    testRandom('intersectsLineSegment is the same as intersectsSegment', (\n      Random r,\n    ) {\n      final rect = Rect.fromLTWH(\n        r.nextDouble(),\n        r.nextDouble(),\n        r.nextDouble(),\n        r.nextDouble(),\n      );\n      final a = Vector2(r.nextDouble(), r.nextDouble());\n      final b = Vector2(r.nextDouble(), r.nextDouble());\n      final s = LineSegment(a, b);\n      expect(rect.intersectsLineSegment(s), rect.intersectsSegment(a, b));\n    });\n\n    testRandom(\n      'toVertices returns an array of [topLeft, topRight, bottomRight, '\n      'bottomLeft]',\n      (Random r) {\n        final left = r.nextDouble();\n        final top = r.nextDouble();\n        final right = r.nextDouble();\n        final bottom = r.nextDouble();\n\n        final rect = Rect.fromLTRB(left, top, right, bottom);\n        final vertices = rect.toVertices();\n        expect(vertices.length, 4);\n        expect(\n          vertices[0],\n          Vector2(left, top),\n          reason: 'topLeft value is not right',\n        );\n        expect(\n          vertices[1],\n          Vector2(right, top),\n          reason: 'topRight value is not right',\n        );\n        expect(\n          vertices[2],\n          Vector2(right, bottom),\n          reason: 'bottomRight value is not right',\n        );\n        expect(\n          vertices[3],\n          Vector2(left, bottom),\n          reason: 'bottomLeft value is not right',\n        );\n      },\n    );\n    test('test transform', () {\n      final matrix4 = Matrix4.translation(Vector3(10, 10, 0));\n      const input = Rect.fromLTWH(0, 0, 10, 10);\n      final result = input.transform(matrix4);\n\n      expect(result.topLeft.toVector2(), closeToVector(Vector2(10, 10)));\n      expect(result.bottomRight.toVector2(), closeToVector(Vector2(20, 20)));\n    });\n\n    testRandom('test bounding box', (Random r) {\n      final points = List.generate(\n        r.nextInt(15) + 2,\n        (index) => Vector2(\n          r.nextBool() ? r.nextDouble() : -r.nextDouble(),\n          r.nextBool() ? r.nextDouble() : -r.nextDouble(),\n        ),\n      );\n\n      final boundingBox = RectExtension.getBounds(points);\n      final xList = points.map((e) => e.x);\n      final yList = points.map((e) => e.y);\n      expect(\n        boundingBox.topLeft,\n        Offset(\n          xList.reduce(min),\n          yList.reduce(min),\n        ),\n        reason: 'topLeft offset is not OK',\n      );\n      expect(\n        boundingBox.bottomRight,\n        Offset(\n          xList.reduce(max),\n          yList.reduce(max),\n        ),\n        reason: 'bottomRight offset is not OK',\n      );\n    });\n\n    testRandom('fromCenter position and size is OK', (Random r) {\n      final center = Vector2(r.nextDouble(), r.nextDouble());\n      final width = r.nextDouble();\n      final height = r.nextDouble();\n\n      final rect = RectExtension.fromCenter(\n        center: center,\n        width: width,\n        height: height,\n      );\n\n      expectDouble(rect.top, center.y - height / 2);\n      expectDouble(rect.bottom, center.y + height / 2);\n      expectDouble(rect.left, center.x - width / 2);\n      expectDouble(rect.right, center.x + width / 2);\n    });\n  });\n}\n\nclass _MockRect extends Mock implements Rect {}\n"
  },
  {
    "path": "packages/flame/test/extensions/rectangle_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('RectangleExtension', () {\n    testRandom('Default rectangle constructor', (Random r) {\n      final rectangle = Rectangle(\n        r.nextDouble(),\n        r.nextDouble(),\n        r.nextDouble(),\n        r.nextDouble(),\n      );\n      final rect = rectangle.toRect();\n      _checkRectValues(\n        rect,\n        left: rectangle.left,\n        top: rectangle.top,\n        right: rectangle.right,\n        bottom: rectangle.bottom,\n      );\n    });\n\n    testRandom('fromPoints rectangle constructor', (Random r) {\n      final leftTop = Point(r.nextDouble(), r.nextDouble());\n      final rightBottom = Point(r.nextDouble(), r.nextDouble());\n\n      // T left = min(a.x, b.x);\n      // T width = (max(a.x, b.x) - left) as T;\n      // T top = min(a.y, b.y);\n      // T height = (max(a.y, b.y) - top) as T;\n      final rectangle = Rectangle.fromPoints(leftTop, rightBottom);\n      final rect = rectangle.toRect();\n      _checkRectValues(\n        rect,\n        left: min(leftTop.x, rightBottom.x),\n        top: min(leftTop.y, rightBottom.y),\n        right: max(leftTop.x, rightBottom.x),\n        bottom: max(leftTop.y, rightBottom.y),\n      );\n    });\n  });\n}\n\nvoid _checkRectValues(\n  Rect rect, {\n  // top left angle x\n  required double left,\n  // top left angle y\n  required double top,\n  // bottom right angle's x\n  required double right,\n  // bottom right angle's y\n  required double bottom,\n}) {\n  expect(rect.left, left, reason: 'left does not match');\n  expect(rect.top, top, reason: 'top does not match');\n  expect(rect.right, right, reason: 'right does not match');\n  expect(rect.bottom, bottom, reason: 'bottom does not match');\n}\n"
  },
  {
    "path": "packages/flame/test/extensions/size_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('SizeExtension', () {\n    testRandom('toVector2 has x to size.dx and y to size.dy', (Random r) {\n      final size = Size(r.nextDouble(), r.nextDouble());\n      final vector2 = size.toVector2();\n\n      expect(\n        vector2.x,\n        closeTo(size.width, 1e-6),\n        reason: 'x width does not match',\n      );\n      expect(\n        vector2.y,\n        closeTo(size.height, 1e-6),\n        reason: 'y height does not match',\n      );\n    });\n\n    testRandom('toOffset has dx to size.width and dy to size.dy', (Random r) {\n      final size = Size(r.nextDouble(), r.nextDouble());\n      final offset = size.toOffset();\n\n      expect(offset.dx, size.width, reason: 'dx width does not match');\n      expect(offset.dy, size.height, reason: 'dy height does not match');\n    });\n\n    testRandom('toSize has x to size.dx and y to size.dy', (Random r) {\n      final size = Size(r.nextDouble(), r.nextDouble());\n      final point = size.toPoint();\n\n      expect(point.x, size.width, reason: 'x width does not match');\n      expect(point.y, size.height, reason: 'y height does not match');\n    });\n\n    testRandom(\n      'toRect has left to 0, top to 0, width: size.dx and height: size.dy',\n      (Random r) {\n        final size = Size(r.nextDouble(), r.nextDouble());\n        final rect = size.toRect();\n\n        expect(rect.left, 0, reason: 'left should be 0 as init');\n        expect(rect.top, 0, reason: 'top should be 0 as init');\n        expect(rect.width, size.width, reason: 'width width does not match');\n        expect(\n          rect.height,\n          size.height,\n          reason: 'height height does not match',\n        );\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/extensions/vector2_test.dart",
    "content": "import 'dart:math' as math;\nimport 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('Vector2Extension', () {\n    testRandom('Converting a vector to an offset matches x/dx and y/dy', (\n      Random r,\n    ) {\n      final vector = Vector2(r.nextDouble(), r.nextDouble());\n      final offset = vector.toOffset();\n      expectDouble(offset.dx, vector.x, reason: 'dx and x are not matching');\n      expectDouble(offset.dy, vector.y, reason: 'dy and y are not matching');\n    });\n\n    testRandom('Converting a vector to a Size matches x/width and y/height', (\n      Random r,\n    ) {\n      final vector = Vector2(r.nextDouble(), r.nextDouble());\n      final size = vector.toSize();\n      expectDouble(\n        size.width,\n        vector.x,\n        reason: 'width and x are not matching',\n      );\n      expectDouble(\n        size.height,\n        vector.y,\n        reason: 'height and y are not matching',\n      );\n    });\n\n    testRandom('Converting a vector to a Point matches x/x and y/y', (\n      Random r,\n    ) {\n      final vector = Vector2(r.nextDouble(), r.nextDouble());\n      final point = vector.toPoint();\n      expectDouble(\n        point.x.toDouble(),\n        vector.x,\n        reason: 'x and x are not matching',\n      );\n      expectDouble(\n        point.y.toDouble(),\n        vector.y,\n        reason: 'x and y are not matching',\n      );\n    });\n\n    testRandom('& on two vectors calls toPositionedRect', (Random r) {\n      final v1 = Vector2(r.nextDouble(), r.nextDouble());\n      final v2 = Vector2(r.nextDouble(), r.nextDouble());\n      final actual = v1 & v2;\n      final expected = v1.clone().toPositionedRect(v2.clone());\n      expect(actual.top, expected.top);\n      expect(actual.bottom, expected.bottom);\n      expect(actual.left, expected.left);\n      expect(actual.right, expected.right);\n    });\n\n    testRandom('toPositionedRect creates a rect with correct coordinates', (\n      Random r,\n    ) {\n      final v1 = Vector2(r.nextDouble(), r.nextDouble());\n      final size = Vector2(r.nextDouble(), r.nextDouble());\n      final rect = v1.toPositionedRect(size);\n      expectDouble(rect.left, v1.x, reason: 'rect left should be vector x');\n      expectDouble(rect.top, v1.y, reason: 'rect top should be vector y');\n      expectDouble(rect.width, size.x, reason: 'rect width should be size x');\n      expectDouble(rect.height, size.y, reason: 'rect height should be size y');\n    });\n\n    testRandom('toRect creates a rect with correct coordinates', (Random r) {\n      final v1 = Vector2(r.nextDouble(), r.nextDouble());\n      final rect = v1.toRect();\n      expectDouble(rect.left, 0, reason: 'rect left should be 0');\n      expectDouble(rect.top, 0, reason: 'rect top should be 0');\n      expectDouble(rect.width, v1.x, reason: 'rect width should be vector x');\n      expectDouble(\n        rect.height,\n        v1.y,\n        reason: 'rect height should be vector y',\n      );\n    });\n\n    testRandom('lerp interpolates the vector the right way', (Random r) {\n      final v1 = Vector2(r.nextDouble(), r.nextDouble());\n      final to = Vector2(r.nextDouble(), r.nextDouble());\n      final t = r.nextDouble();\n\n      final v1Clone = v1.clone()..lerp(to, t);\n\n      // setFrom(this + (to - this) * t)\n      expectDouble(v1Clone.x, v1.x + (to.x - v1.x) * t);\n      expectDouble(v1Clone.y, v1.y + (to.y - v1.y) * t);\n    });\n\n    testRandom('isZero returns true with 0 and false otherwise', (Random r) {\n      final vZero = Vector2(0, 0);\n      final vZeroY = Vector2(1, 0);\n      final vZeroX = Vector2(0, 1);\n      final vNotZero = Vector2(r.nextDouble() + 1, r.nextDouble() + 1);\n\n      expect(vZero.isZero(), true, reason: '(0,0) is Zero');\n      expect(vZeroX.isZero(), false, reason: '(1,0) is not zero');\n      expect(vZeroY.isZero(), false, reason: '(0,1) is not zero');\n      expect(vNotZero.isZero(), false, reason: '(x,x) is not zero');\n    });\n\n    testRandom('isIdentity returns true with 1,1 and false otherwise', (\n      Random r,\n    ) {\n      // nextDouble is never 1\n      final vIdentity = Vector2(1, 1);\n      final vIdentityX = Vector2(1, r.nextDouble());\n      final vIdentityY = Vector2(r.nextDouble(), 1);\n      final vNotIdentity = Vector2(r.nextDouble(), r.nextDouble());\n\n      expect(vIdentity.isIdentity(), true, reason: '(1,1) is isIdentity');\n      expect(vIdentityX.isIdentity(), false, reason: '(1,x) is not isIdentity');\n      expect(vIdentityY.isIdentity(), false, reason: '(x,1) is not isIdentity');\n      expect(\n        vNotIdentity.isIdentity(),\n        false,\n        reason: '(x,x) is not isIdentity',\n      );\n    });\n\n    testRandom('taxicabDistanceTo', (Random r) {\n      final origin = Vector2(r.nextDouble(), r.nextDouble());\n      final destination = Vector2(r.nextDouble(), r.nextDouble());\n\n      expect(\n        origin.taxicabDistanceTo(destination),\n        destination.taxicabDistanceTo(origin),\n      );\n      expect(\n        origin.taxicabDistanceTo(destination),\n        (origin.x - destination.x).abs() + (origin.y - destination.y).abs(),\n      );\n    });\n\n    group('Rotation', () {\n      test('test rotate', () {\n        final p = Vector2(1.0, 0.0)..rotate(math.pi / 2);\n        expectDouble(p.x, 0.0);\n        expectDouble(p.y, 1.0);\n      });\n\n      test('rotate - no center defined', () {\n        final position = Vector2(0.0, 1.0);\n        position.rotate(-math.pi / 2);\n        expect(position, closeToVector(Vector2(1.0, 0.0)));\n      });\n\n      test('rotate - no center defined, negative position', () {\n        final position = Vector2(0.0, -1.0);\n        position.rotate(-math.pi / 2);\n        expect(position, closeToVector(Vector2(-1.0, 0.0)));\n      });\n\n      test('rotate - with center defined', () {\n        final position = Vector2(0.0, 1.0);\n        final center = Vector2(1.0, 1.0);\n        position.rotate(-math.pi / 2, center: center);\n        expect(position, closeToVector(Vector2(1.0, 2.0)));\n      });\n\n      test('rotate - with positive direction', () {\n        final position = Vector2(0.0, 1.0);\n        final center = Vector2(1.0, 1.0);\n        position.rotate(math.pi / 2, center: center);\n        expect(position, closeToVector(Vector2(1.0, 0.0)));\n      });\n\n      test('rotate - with a negative y position', () {\n        final position = Vector2(2.0, -3.0);\n        final center = Vector2(1.0, 1.0);\n        position.rotate(math.pi / 2, center: center);\n        expect(position, closeToVector(Vector2(5.0, 2.0)));\n      });\n\n      test('rotate - with a negative x position', () {\n        final position = Vector2(-2.0, 3.0);\n        final center = Vector2(1.0, 1.0);\n        position.rotate(math.pi / 2, center: center);\n        expect(position, closeToVector(Vector2(-1.0, -2.0)));\n      });\n\n      test('rotate - with a negative position', () {\n        final position = Vector2(-2.0, -3.0);\n        final center = Vector2(1.0, 0.0);\n        position.rotate(math.pi / 2, center: center);\n        expect(position, closeToVector(Vector2(4.0, -3.0)));\n      });\n\n      test('rotate - with center as itself', () {\n        final v = Vector2(1, 0);\n        v.rotate(0.01, center: v.clone());\n        expect(v, Vector2(1, 0));\n      });\n    });\n\n    group('Scaling', () {\n      test('scaleTo', () {\n        final p = Vector2(1.0, 0.0)\n          ..rotate(math.pi / 4)\n          ..scaleTo(2.0);\n\n        expectDouble(p.length, 2.0);\n\n        p.rotate(-math.pi / 4);\n        expectDouble(p.length, 2.0);\n        expectDouble(p.x, 2.0);\n        expectDouble(p.y, 0.0);\n      });\n\n      test('scaleTo the zero vector', () {\n        final p = Vector2.zero();\n        expect(p.normalized().length, 0.0);\n      });\n    });\n\n    group('clampLength', () {\n      test('clamp length min', () {\n        final v = Vector2(1, 0)..clampLength(2.0, 3.0);\n        expect(v.length, 2.0);\n      });\n\n      test('clamp length max', () {\n        final v = Vector2(1, 0)..clampLength(0.5, 0.8);\n        expect(v.length, closeTo(0.8, 1e-6));\n      });\n\n      test('clamp negative vector', () {\n        final v = Vector2(-1, -1)..clampLength(0.5, 0.8);\n        expect(v.length, closeTo(0.8, 1e-6));\n      });\n\n      test('no effect on vector in range', () {\n        final v = Vector2(1, 0)..clampLength(0.5, 2.0);\n        expect(v.length, 1.0);\n      });\n    });\n\n    group('clampDouble', () {\n      test('clamp x and y', () {\n        final v = Vector2(10.0, 10.0)..clampDouble(0.0, 5.0, 0.0, 5.0);\n        expect(v, Vector2(5.0, 5.0));\n      });\n\n      test('clamp x min and y max', () {\n        final v = Vector2(-10.0, 20.0)..clampDouble(0.0, 5.0, 0.0, 5.0);\n        expect(v, Vector2(0.0, 5.0));\n      });\n\n      test('no effect when in range', () {\n        final v = Vector2(2.5, 2.5)..clampDouble(0.0, 5.0, 0.0, 5.0);\n        expect(v, Vector2(2.5, 2.5));\n      });\n\n      testRandom('clampDouble clamps x and y', (Random r) {\n        final v = Vector2(r.nextDouble() * 100 - 50, r.nextDouble() * 100 - 50);\n        const minX = -10.0;\n        const maxX = 10.0;\n        const minY = -20.0;\n        const maxY = 20.0;\n        v.clampDouble(minX, maxX, minY, maxY);\n        expect(v.x >= minX && v.x <= maxX, true);\n        expect(v.y >= minY && v.y <= maxY, true);\n      });\n    });\n\n    group('projection', () {\n      test('Project onto longer vector', () {\n        final u = Vector2(5, 2);\n        final v = Vector2(10, 0);\n        final result = u.projection(v);\n        expect(result, Vector2(5, 0));\n      });\n\n      test('Project onto shorter vector', () {\n        final u = Vector2(5, 2);\n        final v = Vector2(2, 0);\n        final result = u.projection(v);\n        expect(result, Vector2(5, 0));\n      });\n\n      test('Project onto vector in other direction', () {\n        final u = Vector2(5, 2);\n        final v = Vector2(-10, 0);\n        final result = u.projection(v);\n        expect(result, Vector2(5, 0));\n      });\n\n      test('Project onto vector with out', () {\n        final out = Vector2.zero();\n        final u = Vector2(5, 2);\n        final v = Vector2(-10, 0);\n        final result = u.projection(v, out: out);\n        expect(result, Vector2(5, 0));\n        expect(out, Vector2(5, 0));\n      });\n\n      test('Project onto vector with out as sane return and argument', () {\n        var out = Vector2.zero();\n        final u = Vector2(5, 2);\n        final v = Vector2(-10, 0);\n        out = u.projection(v, out: out);\n        expect(out, Vector2(5, 0));\n      });\n    });\n\n    group('inversion', () {\n      test('invert', () {\n        final v = Vector2.all(1);\n        v.invert();\n        expect(v, Vector2.all(-1));\n      });\n\n      test('inverted', () {\n        final v = Vector2.all(1);\n        final w = v.inverted();\n        expect(v, Vector2.all(1));\n        expect(w, Vector2.all(-1));\n      });\n    });\n\n    group('inversion', () {\n      test('inverted', () {\n        final v = Vector2.all(1);\n        final w = v.inverted();\n        expect(v, Vector2.all(1));\n        expect(w, Vector2.all(-1));\n      });\n    });\n\n    group('Moving', () {\n      test('moveToTarget - fully horizontal', () {\n        final current = Vector2(10.0, 0.0);\n        final target = Vector2(20.0, 0.0);\n\n        current.moveToTarget(target, 0);\n        expect(current, Vector2(10.0, 0.0));\n\n        current.moveToTarget(target, 1);\n        expect(current, Vector2(11.0, 0.0));\n\n        current.moveToTarget(target, 6);\n        expect(current, Vector2(17.0, 0.0));\n\n        current.moveToTarget(target, 5);\n        expect(current, Vector2(20.0, 0.0));\n      });\n\n      test('moveToTarget - fully vertical', () {\n        final current = Vector2(10.0, 0.0);\n        final target = Vector2(10.0, 100.0);\n\n        current.moveToTarget(target, 0);\n        expect(current, Vector2(10.0, 0.0));\n\n        current.moveToTarget(target, 1);\n        expect(current, Vector2(10.0, 1.0));\n\n        current.moveToTarget(target, 80);\n        expect(current, Vector2(10.0, 81.0));\n\n        current.moveToTarget(target, 19);\n        expect(current, Vector2(10.0, 100.0));\n      });\n\n      test('moveToTarget - arbitrary direction', () {\n        final current = Vector2(2.0, 2.0);\n        final target = Vector2(4.0, 6.0); // direction is 1,2\n\n        current.moveToTarget(target, 0);\n        expect(current, Vector2(2.0, 2.0));\n\n        current.moveToTarget(target, math.sqrt(5));\n        expect(current, Vector2(3.0, 4.0));\n\n        current.moveToTarget(target, math.sqrt(5));\n        expect(current, Vector2(4.0, 6.0));\n      });\n    });\n\n    test('screenAngle', () {\n      // Up\n      final position = Vector2(0.0, -1.0);\n      expectDouble(position.screenAngle(), 0.0);\n      // Down\n      position.setValues(0.0, 1.0);\n      expectDouble(position.screenAngle().abs(), math.pi);\n      // Left\n      position.setValues(-1.0, 0.0);\n      expectDouble(position.screenAngle(), -math.pi / 2);\n      // Right\n      position.setValues(1.0, 0.0);\n      expectDouble(position.screenAngle(), math.pi / 2);\n    });\n\n    testRandom('% created a new vector with modulo a second one', (Random r) {\n      final x = r.nextDouble();\n      final modX = r.nextDouble();\n      final y = r.nextDouble();\n      final modY = r.nextDouble();\n      final actual = Vector2(x, y) % Vector2(modX, modY);\n      expectDouble(actual.x, x % modX);\n      expectDouble(actual.y, y % modY);\n    });\n\n    testRandom('fromInts created a vector with x y', (Random r) {\n      var x = r.nextInt(1 << 31);\n      var y = r.nextInt(1 << 31);\n      var vector = Vector2Extension.fromInts(x, y);\n      expect(\n        vector.x,\n        closeTo(\n          x.toDouble(),\n          nextFloat32(x.toDouble()) - prevFloat32(x.toDouble()),\n        ),\n      );\n      expect(\n        vector.y,\n        closeTo(\n          y.toDouble(),\n          nextFloat32(y.toDouble()) - prevFloat32(y.toDouble()),\n        ),\n      );\n\n      x = -r.nextInt(1 << 31);\n      y = -r.nextInt(1 << 31);\n      vector = Vector2Extension.fromInts(x, y);\n      expect(\n        vector.x,\n        closeTo(\n          x.toDouble(),\n          nextFloat32(x.toDouble()) - prevFloat32(x.toDouble()),\n        ),\n      );\n      expect(\n        vector.y,\n        closeTo(\n          y.toDouble(),\n          nextFloat32(y.toDouble()) - prevFloat32(y.toDouble()),\n        ),\n      );\n    });\n\n    test('identity() creator is identity', () {\n      final identity = Vector2Extension.identity();\n      expect(identity.isIdentity(), true);\n      expect(identity.x, 1);\n      expect(identity.y, 1);\n    });\n\n    // Following are tests of code not implemented by flame\n\n    testRandom('Adding two vectors adds their x and their y', (Random r) {\n      final v1 = Vector2(r.nextDouble(), r.nextDouble());\n      final v2 = Vector2(r.nextDouble(), r.nextDouble());\n      final actual = v1 + v2;\n      expectDouble(\n        actual.x,\n        v1.x + v2.x,\n        reason: \"Vector addition's 'x' is not working\",\n      );\n      expectDouble(\n        actual.y,\n        v1.y + v2.y,\n        reason: \"Vector addition's 'y' is not working\",\n      );\n    });\n\n    testRandom('Cloning a vector gives a vector with the exact same x and y', (\n      Random r,\n    ) {\n      final original = Vector2(r.nextDouble(), r.nextDouble());\n      final clone = original.clone();\n\n      expectDouble(\n        clone.x,\n        original.x,\n        reason: \"Clone's x should be original's x\",\n      );\n      expectDouble(\n        clone.y,\n        original.y,\n        reason: \"Clone's y should be original's y\",\n      );\n    });\n\n    test('test length', () {\n      final p1 = Vector2(3.0, 4.0);\n      expectDouble(p1.length, 5.0);\n\n      final p2 = Vector2(2.0, 0.0);\n      expectDouble(p2.length, 2.0);\n\n      final p3 = Vector2(0.0, 1.5);\n      expectDouble(p3.length, 1.5);\n    });\n\n    test('test distance', () {\n      final p1 = Vector2(10.0, 20.0);\n      final p2 = Vector2(13.0, 24.0);\n      final result = p1.distanceTo(p2);\n      expectDouble(result, 5.0);\n    });\n\n    test('equality', () {\n      final p1 = Vector2.zero();\n      final p2 = Vector2.zero();\n      expect(p1 == p2, true);\n    });\n\n    test('non equality', () {\n      final p1 = Vector2.zero();\n      final p2 = Vector2(1.0, 0.0);\n      expect(p1 == p2, false);\n    });\n\n    test('hashCode', () {\n      final p1 = Vector2(2.0, -1.0);\n      final p2 = Vector2(1.0, 0.0);\n      expect(p1.hashCode == p2.hashCode, false);\n    });\n\n    test('limit', () {\n      final p1 = Vector2(1.0, 0.0);\n      p1.clampScalar(0, 0.75);\n      expect(p1.length, 0.75);\n      expect(p1.x, 0.75);\n      expect(p1.y, 0.0);\n\n      final p2 = Vector2(1.0, 1.0);\n      p2.clampScalar(0, 3.0);\n      expectDouble(p2.length, math.sqrt(2));\n      expect(p2.x, 1.0);\n      expect(p2.y, 1.0);\n      p2.clampScalar(0, 1.0);\n      expectDouble(p2.length, math.sqrt(2));\n      expect(p2.x, p2.y);\n    });\n\n    test('toStringWithMaxPrecision', () {\n      final p1 = Vector2(1.123456789, 2.123456789);\n      expect(p1.toStringWithMaxPrecision(2), 'Vector2(1.12, 2.12)');\n      final p2 = Vector2(1, 2.123456789);\n      expect(p2.toStringWithMaxPrecision(3), 'Vector2(1.0, 2.123)');\n      final p3 = Vector2(-1, -2.123456789);\n      expect(p3.toStringWithMaxPrecision(3), 'Vector2(-1.0, -2.123)');\n    });\n  });\n\n  testRandom('Creating a Vector2 fromRadians points to the correct direction', (\n    Random r,\n  ) {\n    // See more on https://en.wikipedia.org/wiki/Rotation_matrix\n    // TL;DR;\n    // (x', y') = (x cos(p) - y sin(p) , x sin(p) + y cos(p))\n    // Our x is 0 and y -1\n    // (x', y') = (0 - -sin(p), 0 + -cos(p))\n    // (x', y') = (sin(p),-cos(p))\n\n    var angleInRadians = r.nextDouble() * 2 * math.pi;\n    var pointingVector = Vector2Extension.fromRadians(angleInRadians);\n\n    expectDouble(pointingVector.x, math.sin(angleInRadians));\n    // Base Vector2 is (0, -1)\n    expectDouble(pointingVector.y, -math.cos(angleInRadians));\n\n    // Same with negative Radians\n    angleInRadians = -r.nextDouble() * 2 * math.pi;\n    pointingVector = Vector2Extension.fromRadians(angleInRadians);\n    expectDouble(pointingVector.x, math.sin(angleInRadians));\n    expectDouble(pointingVector.y, -math.cos(angleInRadians));\n  });\n\n  testRandom('Creating a Vector2 fromDegrees points to the correct direction', (\n    Random r,\n  ) {\n    // See more on https://en.wikipedia.org/wiki/Rotation_matrix\n    // TL;DR;\n    // (x', y') = (x cos(p) - y sin(p) , x sin(p) + y cos(p))\n    // Our x is 0 and y -1\n    // (x', y') = (0 - -sin(p), 0 + -cos(p))\n    // (x', y') = (sin(p),-cos(p))\n\n    var angleInDegrees = r.nextDouble() * 360;\n    var pointingVector = Vector2Extension.fromDegrees(angleInDegrees);\n\n    var angleInRadians = angleInDegrees * math.pi / 180;\n    expectDouble(pointingVector.x, math.sin(angleInRadians));\n    expectDouble(pointingVector.y, -math.cos(angleInRadians));\n\n    // Same with negative Degrees\n    angleInDegrees = -r.nextDouble() * 360;\n    pointingVector = Vector2Extension.fromDegrees(angleInDegrees);\n\n    angleInRadians = angleInDegrees * math.pi / 180;\n    expectDouble(pointingVector.x, math.sin(angleInRadians));\n    expectDouble(pointingVector.y, -math.cos(angleInRadians));\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/fixtures/fixture_reader.dart",
    "content": "import 'dart:io';\n\nFile fixture(String name) {\n  var dir = Directory.current.path;\n  if (dir.endsWith('/test')) {\n    dir = dir.replaceAll('/test', '');\n  }\n  return File('$dir/test/_resources/$name');\n}\n\nString fixtureForString(String name) =>\n    File('test/fixtures/$name').readAsStringSync();\n"
  },
  {
    "path": "packages/flame/test/game/flame_game_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:collection/collection.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/src/game/game_render_box.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('FlameGame', () {\n    testWithFlameGame(\n      'default viewport does not change size',\n      (game) async {\n        game.onGameResize(Vector2(100.0, 200.0));\n        expect(game.canvasSize, Vector2(100.0, 200.0));\n        expect(game.size, Vector2(100.0, 200.0));\n      },\n    );\n\n    testWithFlameGame('Game in game', (game) async {\n      final innerGame = FlameGame();\n      game.add(innerGame);\n      await game.ready();\n\n      expect(innerGame.canvasSize, closeToVector(Vector2(800, 600)));\n      expect(innerGame.isLoaded, isTrue);\n      expect(innerGame.isMounted, isTrue);\n    });\n\n    group('components', () {\n      testWithFlameGame(\n        'Add component',\n        (game) async {\n          final component = Component();\n          await game.ensureAdd(component);\n\n          expect(component.isMounted, isTrue);\n          expect(game.children.contains(component), isTrue);\n        },\n      );\n\n      testWithFlameGame(\n        'Add component with onLoad function',\n        (game) async {\n          final component = _MyAsyncComponent();\n          await game.ensureAdd(component);\n\n          expect(game.children.contains(component), isTrue);\n          expect(component.gameSize, game.size);\n          expect(component.game, game);\n        },\n      );\n\n      testWithFlameGame(\n        'prepare adds game and calls onGameResize',\n        (game) async {\n          final component = _MyComponent();\n          await game.ensureAdd(component);\n\n          expect(component.gameSize, game.size);\n          expect(component.game, game);\n        },\n      );\n\n      testWithFlameGame(\n        'component can be tapped',\n        (game) async {\n          final component = _MyTappableComponent();\n          await game.ensureAdd(component);\n          final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;\n          tapDispatcher.handleTapDown(\n            1,\n            TapDownDetails(\n              kind: PointerDeviceKind.touch,\n              globalPosition: const Offset(10, 10),\n              localPosition: const Offset(10, 10),\n            ),\n          );\n\n          expect(component.tapped, isTrue);\n        },\n      );\n\n      testWidgets(\n        'component render and update is called',\n        (WidgetTester tester) async {\n          final game = FlameGame();\n          late GameRenderBox renderBox;\n          await tester.pumpWidget(\n            Builder(\n              builder: (BuildContext context) {\n                renderBox = GameRenderBox(\n                  game,\n                  context,\n                  isRepaintBoundary: true,\n                );\n                return GameWidget(game: game);\n              },\n            ),\n          );\n          renderBox.attach(PipelineOwner());\n\n          final component = _MyComponent();\n          await game.add(component);\n          renderBox.gameLoopCallback(1.0);\n\n          expect(component.isUpdateCalled, isTrue);\n          renderBox.paint(\n            PaintingContext(ContainerLayer(), Rect.zero),\n            Offset.zero,\n          );\n          expect(component.isRenderCalled, isTrue);\n          renderBox.detach();\n        },\n      );\n\n      testWithFlameGame(\n        'onRemove is only called once on component',\n        (game) async {\n          final component = _MyComponent();\n          await game.ensureAdd(component);\n          // The component is removed both by removing it on the game instance\n          // and by the function on the component, but the onRemove callback\n          // should only be called once.\n          component.removeFromParent();\n          game.remove(component);\n          // The component is not removed from the component list until an\n          // update has been performed.\n          game.update(0.0);\n\n          expect(component.onRemoveCallCounter, 1);\n        },\n      );\n\n      testWithFlameGame(\n        'removes PositionComponent when removeFromParent is called',\n        (game) async {\n          final world = game.world;\n          final component = PositionComponent();\n          await world.ensureAdd(component);\n          expect(world.children.length, equals(1));\n          component.removeFromParent();\n          game.updateTree(0);\n          expect(world.children.isEmpty, equals(isTrue));\n        },\n      );\n\n      testWidgets(\n        'can add a component to a game without a layout',\n        (WidgetTester tester) async {\n          final game = FlameGame();\n          final world = game.world;\n          final component = Component()..addToParent(world);\n          expect(game.hasLayout, isFalse);\n\n          await tester.pumpWidget(GameWidget(game: game));\n          game.update(0);\n          expect(world.children.length, 1);\n          expect(world.children.first, component);\n        },\n      );\n    });\n\n    testWithGame<FlameGame>(\n      'children in the constructor',\n      () {\n        return FlameGame(\n          world: World(\n            children: [_IndexedComponent(1), _IndexedComponent(2)],\n          ),\n        );\n      },\n      (game) async {\n        final world = game.world;\n        world.add(_IndexedComponent(3));\n        world.add(_IndexedComponent(4));\n        await game.ready();\n\n        expect(world.children.length, 4);\n        expect(\n          world.children\n              .whereType<_IndexedComponent>()\n              .map((c) => c.index)\n              .isSorted((a, b) => a.compareTo(b)),\n          isTrue,\n        );\n      },\n    );\n\n    testWithGame<FlameGame>(\n      'children in the constructor and onLoad',\n      () {\n        return _ConstructorChildrenGame(\n          constructorChildren: [_IndexedComponent(1), _IndexedComponent(2)],\n          onLoadChildren: [_IndexedComponent(3), _IndexedComponent(4)],\n        );\n      },\n      (game) async {\n        game.add(_IndexedComponent(5));\n        game.add(_IndexedComponent(6));\n        await game.ready();\n\n        expect(game.children.whereType<_IndexedComponent>().length, 6);\n        expect(\n          game.children\n              .whereType<_IndexedComponent>()\n              .map((c) => c.index)\n              .isSorted((a, b) => a.compareTo(b)),\n          isTrue,\n        );\n      },\n    );\n\n    group('completers', () {\n      testWidgets(\n        'game calls loaded completer',\n        (WidgetTester tester) async {\n          final game = _CompleterGame();\n\n          await tester.pumpWidget(GameWidget(game: game));\n          expect(game.loadedCompleterCount, 1);\n          expect(game.mountedCompleterCount, 1);\n        },\n      );\n\n      testWithGame(\n        'game calls mount completer',\n        _CompleterGame.new,\n        (game) async {\n          await game.mounted;\n          expect(game.mountedCompleterCount, 1);\n        },\n      );\n\n      testWidgets(\n        'game calls loaded completer',\n        (WidgetTester tester) async {\n          final game = _CompleterGame();\n\n          await tester.pumpWidget(GameWidget(game: game));\n          expect(game.loadedCompleterCount, 1);\n          expect(game.mountedCompleterCount, 1);\n          await tester.pumpWidget(Container());\n          expect(game.removedCompleterCount, 1);\n        },\n      );\n    });\n\n    group('world and camera', () {\n      testWithFlameGame(\n        'game world setter',\n        (game) async {\n          final newWorld = World();\n          game.world = newWorld;\n          expect(game.world, newWorld);\n          expect(game.camera.world, newWorld);\n        },\n      );\n\n      testWithFlameGame(\n        'game camera setter',\n        (game) async {\n          final newCamera = CameraComponent();\n          game.camera = newCamera;\n          expect(game.camera, newCamera);\n          expect(game.world, isNotNull);\n          expect(game.camera.world, game.world);\n        },\n      );\n\n      testWithFlameGame(\n        'game camera setter with another world',\n        (game) async {\n          final camera1 = game.camera;\n          final world1 = game.world;\n          expect(world1, isNotNull);\n          expect(camera1, isNotNull);\n\n          final camera2 = CameraComponent();\n          final world2 = World();\n          camera2.world = world2;\n\n          game.camera = camera2;\n          expect(game.camera, camera2);\n          expect(game.camera.world, world2);\n          expect(game.world, world1);\n\n          game.camera = camera1;\n          expect(game.camera, camera1);\n          expect(game.camera.world, world1);\n          expect(game.world, world1);\n        },\n      );\n    });\n  });\n\n  group('Render box attachment', () {\n    testWidgets('calls on attach', (tester) async {\n      await tester.runAsync(() async {\n        var hasAttached = false;\n        final game = _OnAttachGame(() => hasAttached = true);\n\n        await tester.pumpWidget(GameWidget(game: game));\n        await game.toBeLoaded();\n        await tester.pump();\n\n        expect(hasAttached, isTrue);\n      });\n    });\n  });\n\n  group('pauseWhenBackgrounded:', () {\n    testWidgets(\n      'game resumes when widget is rebuilt',\n      (tester) async {\n        final game = FlameGame();\n\n        await tester.pumpWidget(GameWidget(game: game));\n        expect(game.paused, isFalse);\n        expect(game.isPausedOnBackground, isFalse);\n\n        await tester.pumpWidget(Container());\n        expect(game.paused, isTrue);\n        expect(game.isPausedOnBackground, isTrue);\n\n        await tester.pumpWidget(GameWidget(game: game));\n        expect(game.paused, isFalse, reason: 'Game should resume on remount');\n        expect(\n          game.isPausedOnBackground,\n          isFalse,\n          reason: 'Background pause flag should be cleared on remount resume',\n        );\n      },\n    );\n\n    testWithFlameGame('true', (game) async {\n      game.pauseWhenBackgrounded = true;\n\n      game.lifecycleStateChange(AppLifecycleState.paused);\n      expect(game.paused, isTrue);\n\n      game.lifecycleStateChange(AppLifecycleState.resumed);\n      expect(game.paused, isFalse);\n    });\n\n    testWithFlameGame('false', (game) async {\n      game.pauseWhenBackgrounded = false;\n\n      game.lifecycleStateChange(AppLifecycleState.paused);\n      expect(game.paused, isFalse);\n\n      game.lifecycleStateChange(AppLifecycleState.resumed);\n      expect(game.paused, isFalse);\n    });\n\n    for (final startingLifecycleState in AppLifecycleState.values) {\n      testWidgets(\n        'game is not paused on start when initially $startingLifecycleState',\n        (tester) async {\n          WidgetsBinding.instance.handleAppLifecycleStateChanged(\n            startingLifecycleState,\n          );\n          addTearDown(() {\n            // Don't use [WidgetsBinding.instance.resetLifecycleState()]\n            // because it sets the lifecycle to null which prevents\n            // [game.onLoad] from running in other tests.\n            WidgetsBinding.instance.handleAppLifecycleStateChanged(\n              AppLifecycleState.resumed,\n            );\n          });\n          expect(\n            WidgetsBinding.instance.lifecycleState,\n            startingLifecycleState,\n          );\n\n          final game = FlameGame();\n\n          final gameWidget = GameWidget(game: game);\n          await tester.pumpWidget(gameWidget);\n\n          GameWidgetState.initGameStateListener(game, () {});\n\n          expect(game.paused, isFalse);\n        },\n      );\n    }\n\n    testWidgets(\n      'game is paused when app is backgrounded',\n      (tester) async {\n        final game = FlameGame();\n\n        await tester.pumpWidget(GameWidget(game: game));\n\n        await game.toBeLoaded();\n        await tester.pump();\n\n        expect(game.paused, isFalse);\n        WidgetsBinding.instance.handleAppLifecycleStateChanged(\n          AppLifecycleState.paused,\n        );\n        expect(game.paused, isTrue);\n        WidgetsBinding.instance.handleAppLifecycleStateChanged(\n          AppLifecycleState.resumed,\n        );\n        expect(game.paused, isFalse);\n      },\n    );\n\n    group('dispose', () {\n      testWithFlameGame(\n        'removes all children from the game',\n        (game) async {\n          final component1 = _MyComponent();\n          final component2 = _MyComponent();\n          game.world.addAll([component1, component2]);\n          await game.ready();\n\n          expect(game.world.children.length, 2);\n\n          game.dispose();\n\n          expect(game.children.length, 0);\n        },\n      );\n\n      testWithFlameGame(\n        'calls onRemove on all components in the tree',\n        (game) async {\n          final component = _MyComponent();\n          game.world.add(component);\n          await game.ready();\n\n          expect(component.onRemoveCallCounter, 0);\n\n          game.dispose();\n\n          expect(component.onRemoveCallCounter, 1);\n        },\n      );\n\n      testWithFlameGame(\n        'clears image and asset caches',\n        (game) async {\n          game.dispose();\n\n          expect(game.images.keys, isEmpty);\n          expect(game.assets.cacheCount, 0);\n        },\n      );\n    });\n  });\n}\n\nclass _IndexedComponent extends Component {\n  final int index;\n\n  _IndexedComponent(this.index);\n}\n\nclass _ConstructorChildrenGame extends FlameGame {\n  final Iterable<_IndexedComponent> onLoadChildren;\n\n  _ConstructorChildrenGame({\n    required Iterable<_IndexedComponent> constructorChildren,\n    required this.onLoadChildren,\n  }) : super(children: constructorChildren);\n\n  @override\n  Future<void> onLoad() async {\n    addAll(onLoadChildren);\n  }\n}\n\nclass _MyTappableComponent extends _MyComponent with TapCallbacks {\n  bool tapped = false;\n\n  @override\n  void onTapDown(TapDownEvent info) {\n    info.continuePropagation = true;\n    tapped = true;\n  }\n}\n\nclass _MyComponent extends PositionComponent with HasGameReference {\n  bool isUpdateCalled = false;\n  bool isRenderCalled = false;\n  int onRemoveCallCounter = 0;\n  late Vector2 gameSize;\n\n  @override\n  void update(double dt) {\n    isUpdateCalled = true;\n  }\n\n  @override\n  void render(Canvas canvas) {\n    isRenderCalled = true;\n  }\n\n  @override\n  void onGameResize(Vector2 gameSize) {\n    super.onGameResize(gameSize);\n    this.gameSize = gameSize;\n  }\n\n  @override\n  bool containsLocalPoint(_) => true;\n\n  @override\n  void onRemove() {\n    super.onRemove();\n    ++onRemoveCallCounter;\n  }\n}\n\nclass _MyAsyncComponent extends _MyComponent {\n  @override\n  Future<void> onLoad() {\n    return Future.value();\n  }\n}\n\nclass _OnAttachGame extends FlameGame {\n  final VoidCallback onAttachCallback;\n\n  _OnAttachGame(this.onAttachCallback);\n\n  @override\n  void onAttach() {\n    onAttachCallback();\n  }\n\n  @override\n  Future<void>? onLoad() {\n    return Future.delayed(const Duration(seconds: 1));\n  }\n}\n\nclass _CompleterGame extends FlameGame {\n  int loadedCompleterCount = 0;\n  int mountedCompleterCount = 0;\n  int removedCompleterCount = 0;\n\n  _CompleterGame() {\n    loaded.whenComplete(() => loadedCompleterCount++);\n    mounted.whenComplete(() => mountedCompleterCount++);\n    removed.whenComplete(() => removedCompleterCount++);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/game/game_loop_test.dart",
    "content": "import 'package:flame/src/game/game_loop.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('GameLoop step', () {\n    TestWidgetsFlutterBinding.ensureInitialized();\n\n    test('works when game loop is paused', () {\n      var tickCount = 0;\n      final gameLoop = GameLoop((dt) => ++tickCount);\n\n      gameLoop.step(1);\n      expect(tickCount, 1);\n    });\n\n    test('does not work when game loop is active', () {\n      var tickCount = 0;\n      final gameLoop = GameLoop((dt) => ++tickCount);\n      gameLoop.start();\n\n      gameLoop.step(1);\n      expect(tickCount, 0);\n    });\n\n    test('overrides step time correctly', () {\n      var elapsedTime = 0.0;\n      const frameTime30 = 1 / 30;\n      const frameTime120 = 1 / 120;\n\n      final gameLoop = GameLoop((dt) => elapsedTime += dt);\n\n      gameLoop.step(frameTime30);\n      expectDouble(elapsedTime, frameTime30);\n\n      gameLoop.step(frameTime120);\n      expectDouble(elapsedTime, frameTime30 + frameTime120);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/game/game_render_box_test.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame/src/game/game_render_box.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass _MockFlameGame extends Mock implements FlameGame {}\n\nclass _MockBuildContext extends Mock implements BuildContext {}\n\nfinal _nodesNeedingCompositingBitsUpdate = <RenderObject>[];\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  setUp(_nodesNeedingCompositingBitsUpdate.clear);\n\n  group('GameRenderBox', () {\n    test('game/attach', () {\n      final owner = PipelineOwner();\n      final game = _MockFlameGame();\n      when(() => game.paused).thenReturn(true);\n\n      final BuildContext context = _MockBuildContext();\n      final renderBox = GameRenderBox(game, context, isRepaintBoundary: false);\n      renderBox.attach(owner);\n\n      verify(() => game.attach(owner, renderBox)).called(1);\n      expect(renderBox.gameLoop, isNotNull);\n      verify(() => game.paused).called(1);\n\n      final anotherGame = _MockFlameGame();\n      when(() => anotherGame.paused).thenReturn(true);\n\n      renderBox.game = anotherGame;\n      verify(game.detach).called(1);\n      verify(() => anotherGame.attach(owner, renderBox)).called(1);\n      verify(() => anotherGame.paused).called(1);\n      expect(renderBox.gameLoop, isNotNull);\n    });\n\n    test('buildContext', () {\n      final owner = PipelineOwner();\n      final game = _MockFlameGame();\n      when(() => game.paused).thenReturn(true);\n\n      final BuildContext context = _MockBuildContext();\n      final renderBox = GameRenderBox(game, context, isRepaintBoundary: false);\n      renderBox.attach(owner);\n\n      expect(renderBox.buildContext, context);\n    });\n\n    test('isRepaintBoundary', () {\n      final owner = PipelineOwner();\n\n      final game = _MockFlameGame();\n      when(() => game.paused).thenReturn(true);\n\n      final BuildContext context = _MockBuildContext();\n      final renderBox = GameRenderBox(game, context, isRepaintBoundary: false);\n      renderBox.attach(owner);\n\n      expect(renderBox.isRepaintBoundary, false);\n\n      renderBox.isRepaintBoundary = true;\n\n      expect(renderBox.isRepaintBoundary, true);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/game/game_widget/game_widget_controlled_lifecycle_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _MyGame extends FlameGame {\n  final List<String> events;\n\n  _MyGame(this.events);\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    events.add('onGameResize');\n  }\n\n  @override\n  Future<void>? onLoad() {\n    events.add('onLoad');\n    return null;\n  }\n\n  @override\n  void onMount() {\n    events.add('onMount');\n  }\n\n  @override\n  void onRemove() {\n    super.onRemove();\n    events.add('onRemove');\n  }\n}\n\nclass _TitlePage extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: ElevatedButton(\n        child: const Text('Play'),\n        onPressed: () {\n          Navigator.of(context).pushNamed('/game');\n        },\n      ),\n    );\n  }\n}\n\nclass _GamePage extends StatelessWidget {\n  const _GamePage(this.events);\n\n  final List<String> events;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Stack(\n        children: [\n          Positioned.fill(\n            child: GameWidget.controlled(\n              gameFactory: () => _MyGame(events),\n            ),\n          ),\n          Positioned(\n            top: 0,\n            left: 0,\n            child: ElevatedButton(\n              child: const Text('Back'),\n              onPressed: () {\n                Navigator.of(context).pop();\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _MyApp extends StatelessWidget {\n  final List<String> events;\n\n  const _MyApp(this.events);\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      routes: {\n        '/': (_) => _TitlePage(),\n        '/game': (_) => _GamePage(events),\n      },\n    );\n  }\n}\n\nclass _MyContainer extends StatefulWidget {\n  final List<String> events;\n\n  const _MyContainer(this.events);\n\n  @override\n  State<_MyContainer> createState() => _MyContainerState();\n}\n\nclass _MyContainerState extends State<_MyContainer> {\n  double size = 300;\n\n  late final game = _MyGame(widget.events);\n\n  void causeResize() {\n    setState(() => size = 400);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      width: size,\n      height: size,\n      child: GameWidget(game: game),\n    );\n  }\n}\n\nvoid main() {\n  group('Game Widget - Lifecycle', () {\n    testWidgets('attach upon navigation', (tester) async {\n      final events = <String>[];\n      await tester.pumpWidget(_MyApp(events));\n\n      await tester.tap(find.text('Play'));\n\n      // I am unsure why I need two bumps here, my best theory is\n      // that we need the first one for the navigation animation\n      // and the second one for the page to render\n      await tester.pump();\n      await tester.pump();\n\n      expect(\n        events.contains('onLoad'),\n        true,\n        reason: 'onLoad event was not fired on attach',\n      );\n    });\n\n    testWidgets('detach when navigating out of the page', (tester) async {\n      final events = <String>[];\n      await tester.pumpWidget(_MyApp(events));\n\n      await tester.tap(find.text('Play'));\n\n      await tester.pump();\n      await tester.pump();\n\n      await tester.tap(find.text('Back'));\n\n      // This ensures that Flame is not running anymore after the navigation\n      // happens, if it was, then the pumpAndSettle would break with a timeout\n      await tester.pumpAndSettle();\n\n      expect(\n        events.contains('onLoad'),\n        true,\n        reason: 'onLoad was not called',\n      );\n      expect(\n        events.contains('onRemove'),\n        true,\n        reason: 'onRemove was not called',\n      );\n    });\n\n    testWidgets('on resize, parents are kept', (tester) async {\n      final events = <String>[];\n      await tester.pumpWidget(_MyContainer(events));\n\n      events.clear();\n      final state = tester.state<_MyContainerState>(find.byType(_MyContainer));\n      state.causeResize();\n\n      await tester.pump();\n      expect(events, ['onGameResize']); // no onRemove\n      final game = tester.allWidgets\n          .whereType<GameWidget<_MyGame>>()\n          .first\n          .game;\n      expect(game?.children, everyElement((Component c) => c.parent == game));\n    });\n\n    testWidgets('all events are executed in the correct order', (tester) async {\n      final events = <String>[];\n      await tester.pumpWidget(_MyApp(events));\n\n      await tester.tap(find.text('Play'));\n\n      await tester.pump();\n      await tester.pump();\n\n      await tester.tap(find.text('Back'));\n\n      // This ensures that Flame is not running anymore after the navigation\n      // happens, if it was, then the pumpAndSettle would break with a timeout\n      await tester.pumpAndSettle();\n\n      await tester.tap(find.text('Play'));\n\n      await tester.pump();\n      await tester.pump();\n\n      expect(\n        events,\n        [\n          'onGameResize',\n          'onLoad',\n          'onMount',\n          'onRemove',\n          'onGameResize',\n          'onLoad',\n          'onMount',\n        ],\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/game/game_widget/game_widget_drag_test.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _HorizontalDragGame extends FlameGame with HorizontalDragDetector {\n  bool horizontalDragStarted = false;\n  bool horizontalDragEnded = false;\n\n  @override\n  void onHorizontalDragStart(_) {\n    horizontalDragStarted = true;\n  }\n\n  @override\n  void onHorizontalDragEnd(_) {\n    horizontalDragEnded = true;\n  }\n}\n\nclass _VerticalDragGame extends FlameGame with VerticalDragDetector {\n  bool verticalDragStarted = false;\n  bool verticalDragEnded = false;\n\n  @override\n  void onVerticalDragStart(_) {\n    verticalDragStarted = true;\n  }\n\n  @override\n  void onVerticalDragEnd(_) {\n    verticalDragEnded = true;\n  }\n}\n\nclass _PanGame extends FlameGame with PanDetector {\n  bool panStarted = false;\n  bool panEnded = false;\n\n  @override\n  void onPanStart(_) {\n    panStarted = true;\n  }\n\n  @override\n  void onPanEnd(_) {\n    panEnded = true;\n  }\n}\n\nvoid main() {\n  final horizontalGame = FlameTester(_HorizontalDragGame.new);\n  final verticalGame = FlameTester(_VerticalDragGame.new);\n  final panGame = FlameTester(_PanGame.new);\n\n  group('GameWidget - HorizontalDragDetector', () {\n    horizontalGame.testGameWidget(\n      'register drags',\n      verify: (game, tester) async {\n        await tester.drag(\n          find.byGame<_HorizontalDragGame>(),\n          const Offset(50, 0),\n        );\n\n        expect(game.horizontalDragStarted, isTrue);\n        expect(game.horizontalDragEnded, isTrue);\n      },\n    );\n  });\n\n  group('GameWidget - VerticalDragDetector', () {\n    verticalGame.testGameWidget(\n      'register drags',\n      verify: (game, tester) async {\n        await tester.drag(\n          find.byGame<_VerticalDragGame>(),\n          const Offset(50, 0),\n        );\n\n        expect(game.verticalDragStarted, isTrue);\n        expect(game.verticalDragEnded, isTrue);\n      },\n    );\n  });\n\n  group('GameWidget - PanDetector', () {\n    panGame.testGameWidget(\n      'register drags',\n      verify: (game, tester) async {\n        await tester.drag(\n          find.byGame<_PanGame>(),\n          const Offset(50, 0),\n        );\n\n        expect(game.panStarted, isTrue);\n        expect(game.panEnded, isTrue);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/game/game_widget/game_widget_hot_reload_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _HotReloadGame extends FlameGame {\n  int hotReloadCount = 0;\n\n  @override\n  void onHotReload() {\n    hotReloadCount++;\n    super.onHotReload();\n  }\n}\n\nclass _HotReloadComponent extends Component {\n  int hotReloadCount = 0;\n\n  @override\n  void onHotReload() {\n    hotReloadCount++;\n    super.onHotReload();\n  }\n}\n\nclass _AsyncLoadComponent extends Component {\n  final Completer<void> loadCompleter = Completer<void>();\n  int hotReloadCount = 0;\n\n  @override\n  Future<void> onLoad() => loadCompleter.future;\n\n  @override\n  void onHotReload() {\n    hotReloadCount++;\n    super.onHotReload();\n  }\n}\n\nGameWidgetState<_HotReloadGame> _findGameWidgetState(WidgetTester tester) {\n  final state = tester.allStates\n      .whereType<GameWidgetState<_HotReloadGame>>()\n      .single;\n  return state;\n}\n\nvoid main() {\n  group('Game Widget - Hot Reload', () {\n    testWidgets('reassemble triggers onHotReload on Game', (tester) async {\n      final game = _HotReloadGame();\n      await tester.pumpWidget(\n        Container(\n          width: 300,\n          height: 300,\n          child: GameWidget(game: game),\n        ),\n      );\n      await tester.pump();\n\n      expect(game.hotReloadCount, 0);\n      _findGameWidgetState(tester).reassemble();\n      expect(game.hotReloadCount, 1);\n    });\n\n    testWidgets(\n      'onHotReload propagates to all mounted components',\n      (tester) async {\n        final game = _HotReloadGame();\n        final parent = _HotReloadComponent();\n        final child = _HotReloadComponent();\n        parent.add(child);\n        game.world.add(parent);\n\n        await tester.pumpWidget(\n          Container(\n            width: 300,\n            height: 300,\n            child: GameWidget(game: game),\n          ),\n        );\n        await tester.pump();\n\n        _findGameWidgetState(tester).reassemble();\n\n        expect(game.hotReloadCount, 1);\n        expect(parent.hotReloadCount, 1);\n        expect(child.hotReloadCount, 1);\n      },\n    );\n\n    testWidgets(\n      'onHotReload reaches components in lifecycle queue',\n      (tester) async {\n        final game = _HotReloadGame();\n        await tester.pumpWidget(\n          Container(\n            width: 300,\n            height: 300,\n            child: GameWidget(game: game),\n          ),\n        );\n        await tester.pump();\n\n        final asyncComponent = _AsyncLoadComponent();\n        game.world.add(asyncComponent);\n\n        // Pump once to process the lifecycle queue and start loading\n        await tester.pump();\n        expect(asyncComponent.isLoading, true);\n\n        _findGameWidgetState(tester).reassemble();\n\n        expect(asyncComponent.hotReloadCount, 1);\n      },\n    );\n\n    testWidgets(\n      'onHotReload notifies queued components that are loading',\n      (tester) async {\n        final game = _HotReloadGame();\n        await tester.pumpWidget(\n          Container(\n            width: 300,\n            height: 300,\n            child: GameWidget(game: game),\n          ),\n        );\n        await tester.pump();\n\n        // Add two async components - they'll start loading but stay in\n        // the lifecycle queue since their onLoad hasn't completed\n        final component1 = _AsyncLoadComponent();\n        final component2 = _AsyncLoadComponent();\n        game.world.add(component1);\n        game.world.add(component2);\n\n        await tester.pump();\n        expect(component1.isLoading, true);\n        expect(component2.isLoading, true);\n        expect(component1.isMounted, false);\n        expect(component2.isMounted, false);\n\n        _findGameWidgetState(tester).reassemble();\n\n        // Both queued-but-loading components should receive the notification\n        expect(component1.hotReloadCount, 1);\n        expect(component2.hotReloadCount, 1);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/game/game_widget/game_widget_keyboard_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _KeyboardEventsGame extends FlameGame with KeyboardEvents {\n  final List<String> keysPressed = [];\n\n  _KeyboardEventsGame();\n\n  @override\n  KeyEventResult onKeyEvent(\n    KeyEvent event,\n    Set<LogicalKeyboardKey> keysPressed,\n  ) {\n    this.keysPressed.add(event.character ?? 'none');\n\n    return KeyEventResult.handled;\n  }\n}\n\nclass _KeyboardHandlerComponent extends Component with KeyboardHandler {\n  final List<String> keysPressed = [];\n\n  @override\n  bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {\n    this.keysPressed.add(event.character ?? 'none');\n    return false;\n  }\n}\n\nclass _HasKeyboardHandlerComponentsGame extends FlameGame\n    with HasKeyboardHandlerComponents {\n  _HasKeyboardHandlerComponentsGame();\n\n  late _KeyboardHandlerComponent keyboardHandler;\n\n  @override\n  Future<void> onLoad() async {\n    keyboardHandler = _KeyboardHandlerComponent();\n    add(keyboardHandler);\n  }\n}\n\nclass _GamePage extends StatelessWidget {\n  final Widget child;\n\n  const _GamePage({required this.child});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: Scaffold(\n        body: Stack(\n          children: [\n            Positioned.fill(\n              child: child,\n            ),\n            Positioned(\n              top: 0,\n              right: 0,\n              child: ElevatedButton(\n                child: const Text('Back'),\n                onPressed: () {\n                  Navigator.of(context).pop();\n                },\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nvoid main() {\n  final size = Vector2(1.0, 1.0);\n\n  group('GameWidget', () {\n    testWidgets('adds focus', (tester) async {\n      final focusNode = FocusNode();\n\n      final game = _KeyboardEventsGame();\n\n      await tester.pumpWidget(\n        _GamePage(\n          child: GameWidget(\n            game: game,\n            focusNode: focusNode,\n          ),\n        ),\n      );\n\n      expect(focusNode.hasFocus, true);\n    });\n\n    testWidgets('game with KeyboardEvents receives key events', (tester) async {\n      final game = _KeyboardEventsGame();\n\n      await tester.pumpWidget(\n        _GamePage(\n          child: GameWidget(\n            game: game,\n          ),\n        ),\n      );\n\n      await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);\n      await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB);\n      await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);\n\n      expect(game.keysPressed, ['a', 'b', 'c']);\n    });\n\n    testWidgets(\n      'game with HasKeyboardHandlerComponents receives key events',\n      (tester) async {\n        final game = _HasKeyboardHandlerComponentsGame();\n\n        await tester.pumpWidget(\n          _GamePage(\n            child: GameWidget(\n              game: game,\n            ),\n          ),\n        );\n\n        game.onGameResize(size);\n        game.update(0.1);\n\n        await tester.pump();\n\n        await tester.sendKeyDownEvent(LogicalKeyboardKey.keyZ);\n        await tester.sendKeyDownEvent(LogicalKeyboardKey.keyF);\n        await tester.sendKeyDownEvent(LogicalKeyboardKey.keyI);\n\n        expect(game.keyboardHandler.keysPressed, ['z', 'f', 'i']);\n      },\n    );\n  });\n\n  group('GameWidget.controlled', () {\n    testWidgets('adds focus', (tester) async {\n      final focusNode = FocusNode();\n\n      final game = _KeyboardEventsGame();\n\n      await tester.pumpWidget(\n        _GamePage(\n          child: GameWidget.controlled(\n            gameFactory: () => game,\n            focusNode: focusNode,\n          ),\n        ),\n      );\n\n      expect(focusNode.hasFocus, true);\n    });\n\n    testWidgets('game with KeyboardEvents receives key events', (tester) async {\n      final game = _KeyboardEventsGame();\n\n      await tester.pumpWidget(\n        _GamePage(\n          child: GameWidget.controlled(\n            gameFactory: () => game,\n          ),\n        ),\n      );\n\n      await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);\n      await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB);\n      await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);\n\n      expect(game.keysPressed, ['a', 'b', 'c']);\n    });\n\n    testWidgets(\n      'game with HasKeyboardHandlerComponents receives key events',\n      (tester) async {\n        final game = _HasKeyboardHandlerComponentsGame();\n\n        await tester.pumpWidget(\n          _GamePage(\n            child: GameWidget.controlled(\n              gameFactory: () => game,\n            ),\n          ),\n        );\n\n        game.onGameResize(size);\n        game.update(0.1);\n\n        await tester.pump();\n\n        await tester.sendKeyDownEvent(LogicalKeyboardKey.keyZ);\n        await tester.sendKeyDownEvent(LogicalKeyboardKey.keyF);\n        await tester.sendKeyDownEvent(LogicalKeyboardKey.keyI);\n\n        expect(game.keyboardHandler.keysPressed, ['z', 'f', 'i']);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/game/game_widget/game_widget_lifecycle_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _MyGame extends FlameGame {\n  final List<String> events;\n\n  _MyGame(this.events);\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    events.add('onGameResize');\n  }\n\n  @override\n  Future<void>? onLoad() {\n    events.add('onLoad');\n    return null;\n  }\n\n  @override\n  void onMount() {\n    events.add('onMount');\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    events.add('update');\n  }\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    events.add('render');\n  }\n\n  @override\n  void onRemove() {\n    super.onRemove();\n    events.add('onRemove');\n  }\n\n  @override\n  void onDispose() {\n    super.onDispose();\n    events.add('onDispose');\n  }\n}\n\nclass _TitlePage extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: ElevatedButton(\n        child: const Text('Play'),\n        onPressed: () {\n          Navigator.of(context).pushNamed('/game');\n        },\n      ),\n    );\n  }\n}\n\nclass _GamePage extends StatefulWidget {\n  final _MyGame game;\n\n  const _GamePage(this.game);\n\n  @override\n  State<StatefulWidget> createState() {\n    return _GamePageState();\n  }\n}\n\nclass _GamePageState extends State<_GamePage> {\n  late _MyGame _game;\n\n  @override\n  void initState() {\n    super.initState();\n    _game = widget.game;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Stack(\n        children: [\n          Positioned.fill(\n            child: GameWidget(\n              game: _game,\n            ),\n          ),\n          Positioned(\n            top: 0,\n            left: 0,\n            child: ElevatedButton(\n              child: const Text('Back'),\n              onPressed: () {\n                Navigator.of(context).pop();\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _MyApp extends StatelessWidget {\n  final List<String> events;\n  late final _MyGame game;\n\n  _MyApp(this.events) {\n    game = _MyGame(events);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      routes: {\n        '/': (_) => _TitlePage(),\n        '/game': (_) => _GamePage(game),\n      },\n    );\n  }\n}\n\nclass _MyContainer extends StatefulWidget {\n  final List<String> events;\n\n  const _MyContainer(this.events);\n\n  @override\n  State<_MyContainer> createState() => _MyContainerState();\n}\n\nclass _MyContainerState extends State<_MyContainer> {\n  double size = 300;\n\n  late final game = _MyGame(widget.events);\n\n  void causeResize() {\n    setState(() => size = 400);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      width: size,\n      height: size,\n      child: GameWidget(game: game),\n    );\n  }\n}\n\nvoid main() {\n  group('Game Widget - Lifecycle', () {\n    testWidgets('attach upon navigation', (tester) async {\n      final events = <String>[];\n      await tester.pumpWidget(_MyApp(events));\n\n      await tester.tap(find.text('Play'));\n\n      // I am unsure why I need two bumps here, my best theory is\n      // that we need the first one for the navigation animation\n      // and the second one for the page to render\n      await tester.pump();\n      await tester.pump();\n\n      expect(\n        events.contains('onLoad'),\n        true,\n        reason: 'onLoad event was not fired on attach',\n      );\n    });\n\n    testWidgets('detach when navigating out of the page', (tester) async {\n      final events = <String>[];\n      await tester.pumpWidget(_MyApp(events));\n\n      await tester.tap(find.text('Play'));\n\n      await tester.pump();\n      await tester.pump();\n\n      await tester.tap(find.text('Back'));\n\n      // This ensures that Flame is not running anymore after the navigation\n      // happens, if it was, then the pumpAndSettle would break with a timeout\n      await tester.pumpAndSettle();\n\n      expect(\n        events.contains('onLoad'),\n        true,\n        reason: 'onLoad was not called',\n      );\n      expect(\n        events.contains('onRemove'),\n        true,\n        reason: 'onRemove was not called',\n      );\n      expect(\n        events.contains('onDispose'),\n        true,\n        reason: 'onDispose was not called',\n      );\n    });\n\n    testWidgets('on resize, parents are kept', (tester) async {\n      final events = <String>[];\n      await tester.pumpWidget(_MyContainer(events));\n\n      // This ensures that the game is attached.\n      await tester.pump();\n\n      events.clear();\n      final state = tester.state<_MyContainerState>(find.byType(_MyContainer));\n      state.causeResize();\n\n      await tester.pump();\n      expect(\n        events,\n        [\n          // additional because of the initial pump to ensure attachment\n          'update',\n          'onGameResize',\n          'update',\n          'render',\n        ],\n      ); // no onRemove\n      final game = tester.allWidgets\n          .whereType<GameWidget<_MyGame>>()\n          .first\n          .game;\n      expect(game?.children, everyElement((Component c) => c.parent == game));\n    });\n\n    testWidgets('update is not called when game is paused', (tester) async {\n      final events = <String>[];\n      await tester.pumpWidget(_MyContainer(events));\n\n      events.clear();\n      tester.allWidgets\n          .whereType<GameWidget<_MyGame>>()\n          .first\n          .game\n          ?.pauseEngine();\n      await tester.pump();\n      await tester.pump();\n      expect(events, ['render']);\n    });\n\n    testWidgets('all events are executed in the correct order', (tester) async {\n      final events = <String>[];\n      await tester.pumpWidget(_MyApp(events));\n\n      await tester.tap(find.text('Play'));\n\n      await tester.pump();\n      await tester.pump(const Duration(milliseconds: 16), EnginePhase.paint);\n\n      await tester.tap(find.text('Back'));\n\n      // This ensures that Flame is not running anymore after the navigation\n      // happens, if it was, then the pumpAndSettle would break with a timeout\n      await tester.pumpAndSettle();\n\n      expect(\n        events,\n        [\n          'onGameResize',\n          'onLoad',\n          'onMount',\n          'update',\n          'render',\n          'update',\n          'render',\n          'update',\n          'onRemove',\n          'onDispose',\n        ],\n      );\n\n      await tester.tap(find.text('Play'));\n\n      await tester.pump();\n      await tester.pump(const Duration(milliseconds: 16), EnginePhase.paint);\n\n      expect(events, [\n        'onGameResize',\n        'onLoad',\n        'onMount',\n        'update',\n        'render',\n        'update',\n        'render',\n        'update',\n        'onRemove',\n        'onDispose',\n        'onGameResize',\n        'onMount',\n        'update',\n        'render',\n      ]);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/game/game_widget/game_widget_mouse_cursor_test.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  Finder byMouseCursor(MouseCursor cursor) {\n    return find.byWidgetPredicate(\n      (widget) => widget is MouseRegion && widget.cursor == cursor,\n    );\n  }\n\n  group('GameWidget - MouseCursor', () {\n    testWidgets('renders with the initial cursor', (tester) async {\n      await tester.pumpWidget(\n        MaterialApp(\n          home: GameWidget(\n            game: FlameGame(),\n            mouseCursor: SystemMouseCursors.grab,\n          ),\n        ),\n      );\n\n      expect(\n        byMouseCursor(SystemMouseCursors.grab),\n        findsOneWidget,\n      );\n    });\n\n    testWidgets('can change the cursor', (tester) async {\n      final game = FlameGame();\n\n      await tester.pumpWidget(\n        MaterialApp(\n          home: GameWidget(\n            game: game,\n            mouseCursor: SystemMouseCursors.grab,\n          ),\n        ),\n      );\n      await tester.pump();\n      expect(game.isAttached, true);\n\n      // Making sure this cursor isn't showing yet\n      expect(byMouseCursor(SystemMouseCursors.copy), findsNothing);\n\n      game.mouseCursor = SystemMouseCursors.copy;\n      await tester.pump();\n\n      expect(\n        byMouseCursor(SystemMouseCursors.copy),\n        findsOneWidget,\n      );\n    });\n\n    testWidgets(\n      'can set mouseCursor during onLoad',\n      (tester) async {\n        final game = _GameWithMouseCursorSetDuringOnLoad();\n        await tester.pumpWidget(\n          GameWidget(game: game),\n        );\n        await tester.pump();\n        expect(\n          byMouseCursor(SystemMouseCursors.alias),\n          findsOneWidget,\n        );\n      },\n    );\n  });\n}\n\nclass _GameWithMouseCursorSetDuringOnLoad extends FlameGame {\n  @override\n  Future<void>? onLoad() {\n    mouseCursor = SystemMouseCursors.alias;\n    return null;\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/game/game_widget/game_widget_pause_test.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _Wrapper extends StatefulWidget {\n  const _Wrapper({\n    required this.child,\n    // ignore: unused_element, unused_element_parameter\n    this.small = false,\n  });\n\n  final Widget child;\n  final bool small;\n\n  @override\n  State<_Wrapper> createState() => _WrapperState();\n}\n\nclass _WrapperState extends State<_Wrapper> {\n  late bool _small;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _small = widget.small;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: Scaffold(\n        body: Column(\n          children: [\n            Container(\n              width: _small ? 50 : 100,\n              height: _small ? 50 : 100,\n              child: widget.child,\n            ),\n            ElevatedButton(\n              child: const Text('Toggle'),\n              onPressed: () {\n                setState(() => _small = !_small);\n              },\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _MyGame extends FlameGame {\n  int updateCount = 0;\n  int renderCount = 0;\n  double timePassed = 0;\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    timePassed += dt;\n    updateCount++;\n  }\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    renderCount++;\n  }\n}\n\nvoid main() {\n  FlameTester<_MyGame> myGame({bool paused = false}) {\n    return FlameTester(\n      () => _MyGame()..paused = paused,\n      pumpWidget: (gameWidget, tester) async {\n        await tester.pumpWidget(_Wrapper(child: gameWidget));\n      },\n    );\n  }\n\n  myGame().testGameWidget(\n    'can pause the engine',\n    verify: (game, tester) async {\n      // Run two frames\n      await tester.pump();\n      await tester.pump();\n\n      game.pauseEngine();\n\n      // shouldn't run another frame on the game\n      await tester.pump();\n\n      // Remember that there is one initial update(0) called.\n      expect(game.updateCount, equals(3));\n    },\n  );\n\n  myGame().testGameWidget(\n    'can resume the engine',\n    verify: (game, tester) async {\n      // Run two frames\n      await tester.pump();\n      await tester.pump();\n\n      game.pauseEngine();\n\n      // shouldn't run another frame on the game\n      await tester.pump();\n\n      game.resumeEngine();\n      await tester.pump();\n\n      // Remember that there is one initial update(0) called.\n      expect(game.updateCount, equals(4));\n    },\n  );\n\n  myGame().testGameWidget(\n    \"when paused, don't auto start after a rebuild\",\n    verify: (game, tester) async {\n      // Run two frames\n      await tester.pump();\n      await tester.pump();\n\n      game.pauseEngine();\n\n      await tester.tap(find.text('Toggle'));\n      await tester.pumpAndSettle();\n\n      // Remember that there is one initial update(0) called.\n      expect(game.updateCount, equals(3));\n    },\n  );\n\n  myGame(paused: true).testGameWidget(\n    'can start paused',\n    verify: (game, tester) async {\n      // Run two frames\n      await tester.pump();\n      await tester.pump();\n\n      expect(game.updateCount, equals(0));\n    },\n  );\n\n  myGame(paused: true).testGameWidget(\n    'can start paused and resumed later',\n    verify: (game, tester) async {\n      // Run two frames\n      await tester.pump();\n      await tester.pump();\n\n      game.resumeEngine();\n\n      // Run two frames\n      await tester.pump();\n      await tester.pump();\n\n      expect(game.updateCount, equals(2));\n    },\n  );\n\n  myGame().testGameWidget(\n    'will not add time to dt when paused',\n    verify: (game, tester) async {\n      const frameLength = Duration(seconds: 1);\n      // Run two frames.\n      await tester.pump(frameLength);\n      await tester.pump(frameLength);\n\n      game.pauseEngine();\n\n      // Run two frames when the engine is paused.\n      await tester.pump(frameLength);\n      await tester.pump(frameLength);\n\n      game.resumeEngine();\n      // This time will be thrown away since the ticker hasn't received its\n      // first timestamp yet.\n      await tester.pump(const Duration(seconds: 100));\n      await tester.pump(frameLength);\n\n      // Remember that there is one initial update(0) after mount\n      expect(game.updateCount, equals(5));\n      expect(game.timePassed, equals(3));\n    },\n  );\n}\n"
  },
  {
    "path": "packages/flame/test/game/game_widget/game_widget_tap_passthrough_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _TransparentGame extends FlameGame {\n  @override\n  Color backgroundColor() => const Color(0x00000000);\n}\n\nclass _TrackingGame extends FlameGame {\n  int componentsAtPointCallCount = 0;\n\n  @override\n  Color backgroundColor() => const Color(0x00000000);\n\n  @override\n  Iterable<Component> componentsAtPoint(\n    Vector2 point, [\n    List<Vector2>? nestedPoints,\n  ]) {\n    componentsAtPointCallCount++;\n    return super.componentsAtPoint(point, nestedPoints);\n  }\n}\n\nclass _TappableComponent extends PositionComponent with TapCallbacks {\n  int tapDownCount = 0;\n  int tapUpCount = 0;\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    tapDownCount++;\n  }\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    tapUpCount++;\n  }\n}\n\nvoid main() {\n  group('GameWidget - tap passthrough', () {\n    testWidgets(\n      'taps reach a Flutter button behind a transparent GameWidget',\n      (tester) async {\n        var buttonTapped = false;\n        final game = _TransparentGame();\n\n        await tester.pumpWidget(\n          MaterialApp(\n            home: Scaffold(\n              body: Stack(\n                children: [\n                  Center(\n                    child: ElevatedButton(\n                      onPressed: () => buttonTapped = true,\n                      child: const Text('Tap me'),\n                    ),\n                  ),\n                  Positioned.fill(\n                    child: GameWidget(\n                      game: game,\n                      behavior: HitTestBehavior.deferToChild,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n\n        await tester.tap(find.text('Tap me'));\n        await tester.pump();\n\n        expect(buttonTapped, isTrue);\n      },\n    );\n\n    testWidgets(\n      'taps on a TapCallbacks component are intercepted',\n      (tester) async {\n        var buttonTapped = false;\n        final component = _TappableComponent()\n          ..size = Vector2(800, 600)\n          ..position = Vector2.zero();\n        final game = _TransparentGame()..add(component);\n\n        await tester.pumpWidget(\n          MaterialApp(\n            home: Scaffold(\n              body: Stack(\n                children: [\n                  Center(\n                    child: ElevatedButton(\n                      onPressed: () => buttonTapped = true,\n                      child: const Text('Tap me'),\n                    ),\n                  ),\n                  Positioned.fill(\n                    child: GameWidget(\n                      game: game,\n                      behavior: HitTestBehavior.deferToChild,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n        expect(component.isMounted, isTrue);\n\n        await tester.tapAt(const Offset(400, 300));\n        await tester.pump(const Duration(milliseconds: 100));\n\n        expect(component.tapDownCount, equals(1));\n        expect(component.tapUpCount, equals(1));\n        expect(buttonTapped, isFalse);\n      },\n    );\n\n    testWidgets(\n      'taps on empty game space pass through even when TapCallbacks '\n      'components exist elsewhere',\n      (tester) async {\n        var buttonTapped = false;\n        // Small component in the top-left corner, far from the button\n        final component = _TappableComponent()\n          ..size = Vector2(50, 50)\n          ..position = Vector2.zero();\n        final game = _TransparentGame()..add(component);\n\n        await tester.pumpWidget(\n          MaterialApp(\n            home: Scaffold(\n              body: Stack(\n                children: [\n                  Center(\n                    child: ElevatedButton(\n                      onPressed: () => buttonTapped = true,\n                      child: const Text('Tap me'),\n                    ),\n                  ),\n                  Positioned.fill(\n                    child: GameWidget(\n                      game: game,\n                      behavior: HitTestBehavior.deferToChild,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n        expect(component.isMounted, isTrue);\n\n        // Tap the button area (center of screen) which is NOT over\n        // the small component in the corner\n        await tester.tap(find.text('Tap me'));\n        await tester.pump();\n\n        expect(component.tapDownCount, equals(0));\n        expect(buttonTapped, isTrue);\n      },\n    );\n    testWidgets(\n      'translucent behavior delivers events to both game component and '\n      'widget behind',\n      (tester) async {\n        var pointerDownReceived = false;\n        final component = _TappableComponent()\n          ..size = Vector2(800, 600)\n          ..position = Vector2.zero();\n        final game = _TransparentGame()..add(component);\n\n        await tester.pumpWidget(\n          MaterialApp(\n            home: Scaffold(\n              body: Stack(\n                children: [\n                  Positioned.fill(\n                    child: Listener(\n                      onPointerDown: (_) => pointerDownReceived = true,\n                      child: const ColoredBox(color: Color(0xFF0000FF)),\n                    ),\n                  ),\n                  Positioned.fill(\n                    child: GameWidget(\n                      game: game,\n                      behavior: HitTestBehavior.translucent,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n        expect(component.isMounted, isTrue);\n\n        await tester.tapAt(const Offset(400, 300));\n        await tester.pump(const Duration(milliseconds: 100));\n\n        expect(component.tapDownCount, equals(1));\n        expect(pointerDownReceived, isTrue);\n      },\n    );\n\n    testWidgets(\n      'opaque behavior blocks taps from reaching widgets behind',\n      (tester) async {\n        var buttonTapped = false;\n        final game = _TransparentGame();\n\n        await tester.pumpWidget(\n          MaterialApp(\n            home: Scaffold(\n              body: Stack(\n                children: [\n                  Center(\n                    child: ElevatedButton(\n                      onPressed: () => buttonTapped = true,\n                      child: const Text('Tap me'),\n                    ),\n                  ),\n                  Positioned.fill(\n                    child: GameWidget(game: game),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n\n        await tester.tap(find.text('Tap me'), warnIfMissed: false);\n        await tester.pump();\n\n        expect(buttonTapped, isFalse);\n      },\n    );\n\n    testWidgets(\n      'componentsAtPoint is not called redundantly during hit testing',\n      (tester) async {\n        final game = _TrackingGame();\n        final component = _TappableComponent()\n          ..size = Vector2(800, 600)\n          ..position = Vector2.zero();\n        game.add(component);\n\n        await tester.pumpWidget(\n          MaterialApp(\n            home: GameWidget(game: game),\n          ),\n        );\n        await tester.pump();\n        await tester.pump();\n        expect(component.isMounted, isTrue);\n\n        game.componentsAtPointCallCount = 0;\n\n        await tester.tapAt(const Offset(100, 100));\n        await tester.pump(const Duration(milliseconds: 100));\n\n        // Hit test should reuse its traversal for event delivery,\n        // so we expect only 2 calls (tap down + tap up), not 3.\n        expect(game.componentsAtPointCallCount, equals(2));\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/game/game_widget/game_widget_tap_test.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\n// testing the deprecated TapDetector\n// ignore: deprecated_member_use_from_same_package\nclass _TapGame extends FlameGame with TapDetector {\n  bool tapRegistered = false;\n\n  @override\n  void onTap() {\n    tapRegistered = true;\n  }\n}\n\nclass _DoubleTapGame extends FlameGame with DoubleTapDetector {\n  bool doubleTapRegistered = false;\n  Vector2? doubleTapPosition;\n\n  @override\n  void onDoubleTap() {\n    doubleTapRegistered = true;\n  }\n\n  @override\n  void onDoubleTapDown(TapDownInfo info) {\n    doubleTapPosition = info.eventPosition.global;\n  }\n}\n\nvoid main() {\n  final tapGame = FlameTester(_TapGame.new);\n  final doubleTapGame = FlameTester(_DoubleTapGame.new);\n\n  group('GameWidget - TapDetectors', () {\n    tapGame.testGameWidget(\n      'can receive taps',\n      verify: (game, tester) async {\n        await tester.tapAt(const Offset(10, 10));\n        expect(game.tapRegistered, isTrue);\n      },\n    );\n\n    const tapPosition = Offset(10, 10);\n    doubleTapGame.testGameWidget(\n      'can receive double taps',\n      setUp: (game, tester) async {\n        await tester.tapAt(tapPosition);\n        await Future<void>.delayed(const Duration(milliseconds: 50));\n        await tester.tapAt(tapPosition);\n      },\n      verify: (game, tester) async {\n        expect(game.doubleTapRegistered, isTrue);\n        final tapVector = tapPosition.toVector2();\n        expect(game.doubleTapPosition, closeToVector(tapVector));\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/game/game_widget/game_widget_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _Wrapper extends StatefulWidget {\n  const _Wrapper({\n    required this.child,\n    this.open = false,\n  });\n\n  final Widget child;\n  final bool open;\n\n  @override\n  State<_Wrapper> createState() => _WrapperState();\n}\n\nclass _GameWithKeyboardEvents extends FlameGame with KeyboardEvents {\n  final List<LogicalKeyboardKey> keyEvents = [];\n\n  _GameWithKeyboardEvents();\n\n  @override\n  KeyEventResult onKeyEvent(\n    KeyEvent event,\n    Set<LogicalKeyboardKey> keysPressed,\n  ) {\n    keyEvents.add(event.logicalKey);\n    return KeyEventResult.handled;\n  }\n}\n\nclass _WrapperState extends State<_Wrapper> {\n  late bool _open;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _open = widget.open;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: Scaffold(\n        body: Column(\n          children: [\n            if (_open) Expanded(child: widget.child),\n            ElevatedButton(\n              child: const Text('Toggle'),\n              onPressed: () {\n                setState(() => _open = !_open);\n              },\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _MyGame extends FlameGame {\n  bool onAttachCalled = false;\n  bool onDetachCalled = false;\n\n  @override\n  void onAttach() {\n    super.onAttach();\n\n    onAttachCalled = true;\n  }\n\n  @override\n  void onDetach() {\n    super.onDetach();\n\n    onDetachCalled = true;\n  }\n}\n\nFlameTester<_MyGame> _myGame({required bool open}) {\n  return FlameTester(\n    _MyGame.new,\n    pumpWidget: (gameWidget, tester) async {\n      await tester.pumpWidget(_Wrapper(open: open, child: gameWidget));\n    },\n  );\n}\n\nvoid main() {\n  _myGame(open: false).testGameWidget(\n    'calls onAttach when it enters the tree and onDetach and it leaves',\n    verify: (game, tester) async {\n      expect(game.onAttachCalled, isFalse);\n\n      await tester.tap(find.text('Toggle'));\n      // First will be the build of the wrapper\n      await tester.pump();\n      // Second will be the build of the game widget itself\n      await tester.pump();\n\n      expect(game.onAttachCalled, isTrue);\n      expect(game.onDetachCalled, isFalse);\n\n      await tester.tap(find.text('Toggle'));\n      await tester.pump();\n\n      expect(game.onDetachCalled, isTrue);\n    },\n  );\n\n  _myGame(open: true).testGameWidget(\n    'size is kept on game after a detach',\n    verify: (game, tester) async {\n      expect(game.hasLayout, isTrue);\n\n      await tester.tap(find.text('Toggle'));\n      await tester.pump();\n\n      expect(game.isAttached, isFalse);\n      expect(game.size, isNotNull);\n      expect(game.hasLayout, isTrue);\n    },\n  );\n\n  group('Subscription is valid after game change', () {\n    testWidgets('Uncontrolled to uncontrolled', (tester) async {\n      const key = Key('flame-game');\n      final game1 = FlameGame();\n      await tester.pumpWidget(GameWidget(key: key, game: game1));\n      expect(game1.isMounted, true);\n      expect(game1.gameStateListeners.length, 1);\n\n      final game2 = FlameGame();\n      await tester.pumpWidget(GameWidget(key: key, game: game2));\n      final widget = tester.firstWidget<GameWidget>(\n        find.byWidgetPredicate((widget) => widget is GameWidget),\n      );\n      expect(widget.game, game2);\n      expect(game2.isMounted, true);\n      expect(game2.gameStateListeners.length, 1);\n      expect(game1.gameStateListeners.length, 0);\n    });\n\n    testWidgets('Uncontrolled to controlled', (tester) async {\n      const key = Key('flame-game');\n      final game1 = FlameGame();\n      await tester.pumpWidget(GameWidget(key: key, game: game1));\n      expect(game1.isMounted, true);\n      expect(game1.gameStateListeners.length, 1);\n\n      late final FlameGame game2;\n      await tester.pumpWidget(\n        GameWidget.controlled(\n          key: key,\n          gameFactory: () => game2 = FlameGame(),\n        ),\n      );\n      await tester.pump();\n      final widget = tester.firstWidget<GameWidget>(\n        find.byWidgetPredicate((widget) => widget is GameWidget),\n      );\n\n      expect(widget.game, null);\n\n      expect(game1.gameStateListeners.length, 0);\n      expect(game1.isAttached, false);\n\n      expect(game2.gameStateListeners.length, 1);\n      expect(game2.isAttached, true);\n      expect(game2.isMounted, true);\n    });\n\n    testWidgets('Controlled to uncontrolled', (tester) async {\n      const key = Key('flame-game');\n\n      late final FlameGame game1;\n      await tester.pumpWidget(\n        GameWidget.controlled(\n          key: key,\n          gameFactory: () => game1 = FlameGame(),\n        ),\n      );\n\n      expect(game1.isMounted, true);\n      expect(game1.gameStateListeners.length, 1);\n\n      final game2 = FlameGame();\n      await tester.pumpWidget(GameWidget(key: key, game: game2));\n      await tester.pump();\n      final widget = tester.firstWidget<GameWidget>(\n        find.byWidgetPredicate((widget) => widget is GameWidget),\n      );\n\n      expect(widget.game, game2);\n\n      expect(game1.gameStateListeners.length, 0);\n      expect(game1.isAttached, false);\n\n      expect(game2.gameStateListeners.length, 1);\n      expect(game2.isAttached, true);\n      expect(game2.isMounted, true);\n    });\n\n    testWidgets('Controlled to controlled', (tester) async {\n      const key = Key('flame-game');\n\n      late FlameGame game1;\n      await tester.pumpWidget(\n        GameWidget.controlled(\n          key: key,\n          gameFactory: () => game1 = FlameGame(),\n        ),\n      );\n\n      expect(game1.isMounted, true);\n      expect(game1.gameStateListeners.length, 1);\n\n      FlameGame? game2;\n      await tester.pumpWidget(\n        GameWidget.controlled(\n          key: key,\n          gameFactory: () => game2 = FlameGame(),\n        ),\n      );\n      await tester.pump();\n      final widget = tester.firstWidget<GameWidget>(\n        find.byWidgetPredicate((widget) => widget is GameWidget),\n      );\n\n      expect(widget.game, null);\n\n      expect(game1.gameStateListeners.length, 1);\n      expect(game1.isAttached, true);\n\n      expect(game2, isNull);\n    });\n  });\n\n  group('focus', () {\n    testWidgets('autofocus starts focused', (tester) async {\n      final gameFocusNode = FocusNode();\n\n      await tester.pumpWidget(\n        GameWidget(\n          focusNode: gameFocusNode,\n          game: FlameGame(),\n          // ignore: avoid_redundant_argument_values\n          autofocus: true,\n        ),\n      );\n\n      expect(gameFocusNode.hasFocus, isTrue);\n    });\n\n    testWidgets('autofocus false does not start focused', (tester) async {\n      final gameFocusNode = FocusNode();\n\n      await tester.pumpWidget(\n        GameWidget(\n          focusNode: gameFocusNode,\n          game: FlameGame(),\n          autofocus: false,\n        ),\n      );\n\n      expect(gameFocusNode.hasFocus, isFalse);\n    });\n\n    group('overlay with focus', () {\n      testWidgets('autofocus on overlay', (tester) async {\n        final gameFocusNode = FocusNode();\n        final overlayFocusNode = FocusNode();\n\n        final game = FlameGame();\n\n        await tester.pumpWidget(\n          GameWidget(\n            focusNode: gameFocusNode,\n            game: game,\n            autofocus: false,\n            initialActiveOverlays: const ['some-overlay'],\n            overlayBuilderMap: {\n              'some-overlay': (buildContext, game) {\n                return Focus(\n                  focusNode: overlayFocusNode,\n                  autofocus: true,\n                  child: const SizedBox.shrink(),\n                );\n              },\n            },\n          ),\n        );\n\n        await game.toBeLoaded();\n        await tester.pump();\n\n        expect(gameFocusNode.hasPrimaryFocus, isFalse);\n        expect(gameFocusNode.hasFocus, isTrue);\n        expect(overlayFocusNode.hasPrimaryFocus, isTrue);\n      });\n\n      testWidgets(\n        'focus goes back to game when overlays is removed',\n        (tester) async {\n          final gameFocusNode = FocusNode();\n          final overlayFocusNode = FocusNode();\n\n          final game = FlameGame();\n\n          await tester.pumpWidget(\n            GameWidget(\n              focusNode: gameFocusNode,\n              game: game,\n              initialActiveOverlays: const ['some-overlay'],\n              overlayBuilderMap: {\n                'some-overlay': (buildContext, game) {\n                  return Focus(\n                    focusNode: overlayFocusNode,\n                    child: const SizedBox.shrink(),\n                  );\n                },\n              },\n            ),\n          );\n\n          await game.toBeLoaded();\n          await tester.pump();\n\n          expect(overlayFocusNode.hasFocus, isFalse);\n          expect(gameFocusNode.hasPrimaryFocus, isTrue);\n\n          overlayFocusNode.requestFocus();\n          await tester.pump();\n\n          expect(overlayFocusNode.hasFocus, isTrue);\n          expect(gameFocusNode.hasPrimaryFocus, isFalse);\n\n          game.overlays.remove('some-overlay');\n\n          await tester.pump();\n\n          expect(overlayFocusNode.hasFocus, isFalse);\n          expect(gameFocusNode.hasPrimaryFocus, isTrue);\n        },\n      );\n    });\n  });\n\n  group('keyboard events', () {\n    testWidgets('handles keys when game is KeyboardKeys', (tester) async {\n      final game = _GameWithKeyboardEvents();\n\n      await tester.pumpWidget(\n        GameWidget(\n          game: game,\n        ),\n      );\n\n      await game.toBeLoaded();\n      await tester.pump();\n\n      await simulateKeyDownEvent(LogicalKeyboardKey.keyA);\n      await tester.pump();\n\n      expect(game.keyEvents, [LogicalKeyboardKey.keyA]);\n    });\n\n    testWidgets(\n      'handles keys when focused regardless of being KeyboardKeys',\n      (tester) async {\n        final game = FlameGame();\n\n        await tester.pumpWidget(\n          GameWidget(\n            game: game,\n          ),\n        );\n\n        await game.toBeLoaded();\n        await tester.pump();\n\n        final handled = await simulateKeyDownEvent(LogicalKeyboardKey.keyA);\n\n        expect(handled, isTrue);\n      },\n    );\n\n    testWidgets('overlay handles keys', (tester) async {\n      final overlayKeyEvents = <LogicalKeyboardKey>[];\n      final overlayFocusNode = FocusNode(\n        onKeyEvent: (_, keyEvent) {\n          overlayKeyEvents.add(keyEvent.logicalKey);\n          return KeyEventResult.ignored;\n        },\n      );\n\n      final game = _GameWithKeyboardEvents();\n\n      await tester.pumpWidget(\n        GameWidget(\n          autofocus: false,\n          game: game,\n          initialActiveOverlays: const ['some-overlay'],\n          overlayBuilderMap: {\n            'some-overlay': (buildContext, game) {\n              return Focus(\n                focusNode: overlayFocusNode,\n                autofocus: true,\n                child: const SizedBox.shrink(),\n              );\n            },\n          },\n        ),\n      );\n\n      await game.toBeLoaded();\n      await tester.pump();\n\n      expect(overlayFocusNode.hasPrimaryFocus, isTrue);\n      await simulateKeyDownEvent(LogicalKeyboardKey.keyA);\n      await tester.pump();\n\n      expect(game.keyEvents, <KeyEvent>[]);\n      expect(overlayKeyEvents, [LogicalKeyboardKey.keyA]);\n    });\n  });\n\n  group('buildContext availability', () {\n    testWidgets(\n      'buildContext is available during onLoad',\n      (tester) async {\n        final game = _GameWithBuildContextCheck();\n        await tester.pumpWidget(GameWidget(game: game));\n        await game.toBeLoaded();\n        await tester.pump();\n\n        expect(\n          game.buildContextDuringOnLoad,\n          isNotNull,\n          reason: 'buildContext should be available during onLoad',\n        );\n      },\n    );\n\n    testWidgets(\n      'buildContext is available for child components during onLoad',\n      (tester) async {\n        final game = _GameWithComponentBuildContextCheck();\n        await tester.pumpWidget(GameWidget(game: game));\n        await game.toBeLoaded();\n        await tester.pump();\n\n        expect(\n          game.component.buildContextDuringOnLoad,\n          isNotNull,\n          reason:\n              'buildContext should be available to components during onLoad',\n        );\n      },\n    );\n  });\n}\n\nclass _GameWithBuildContextCheck extends FlameGame {\n  BuildContext? buildContextDuringOnLoad;\n\n  @override\n  Future<void> onLoad() async {\n    buildContextDuringOnLoad = buildContext;\n  }\n}\n\nclass _ComponentWithBuildContextCheck extends Component {\n  BuildContext? buildContextDuringOnLoad;\n\n  @override\n  void onLoad() {\n    buildContextDuringOnLoad = findGame()!.buildContext;\n  }\n}\n\nclass _GameWithComponentBuildContextCheck extends FlameGame {\n  final component = _ComponentWithBuildContextCheck();\n\n  @override\n  Future<void> onLoad() async {\n    add(component);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/game/mixins/has_performance_tracker_test.dart",
    "content": "import 'dart:io';\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  testWidgets(\n    'tracks update and render times.',\n    (widgetTester) async {\n      final game = _GameWithPerformanceTracker(\n        children: [_SlowComponent()],\n      );\n\n      expect(game.updateTime, 0);\n      expect(game.renderTime, 0);\n\n      await widgetTester.pumpFrames(\n        GameWidget(game: game),\n        const Duration(seconds: 1),\n      );\n\n      expect(\n        game.updateTime,\n        greaterThanOrEqualTo(_SlowComponent.duration.inMilliseconds),\n      );\n      expect(\n        game.renderTime,\n        greaterThanOrEqualTo(_SlowComponent.duration.inMilliseconds),\n      );\n    },\n  );\n}\n\nclass _GameWithPerformanceTracker extends FlameGame with HasPerformanceTracker {\n  _GameWithPerformanceTracker({super.children});\n}\n\nclass _SlowComponent extends Component {\n  static const duration = Duration(milliseconds: 8);\n  @override\n  void update(double dt) => sleep(duration);\n  @override\n  void render(Canvas canvas) => sleep(duration);\n}\n"
  },
  {
    "path": "packages/flame/test/game/mixins/keyboard_test.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _ValidGame extends FlameGame with KeyboardEvents {}\n\nclass _InvalidGame extends FlameGame\n    with HasKeyboardHandlerComponents, KeyboardEvents {}\n\nvoid main() {\n  group('Keyboard events', () {\n    test(\n      'game with KeyboardEvents can handle key events',\n      () {\n        final validGame = _ValidGame();\n        const event = KeyDownEvent(\n          physicalKey: PhysicalKeyboardKey.arrowUp,\n          logicalKey: LogicalKeyboardKey.arrowUp,\n          timeStamp: Duration.zero,\n        );\n\n        // Should just work with the default implementation\n        expect(\n          validGame.onKeyEvent(event, {}),\n          KeyEventResult.handled,\n        );\n      },\n    );\n\n    test(\n      'cannot mix KeyboardEvent and HasKeyboardHandlerComponents together',\n      () {\n        final invalidGame = _InvalidGame();\n        const event = KeyDownEvent(\n          physicalKey: PhysicalKeyboardKey.arrowUp,\n          logicalKey: LogicalKeyboardKey.arrowUp,\n          timeStamp: Duration.zero,\n        );\n\n        // Should throw an assertion error\n        expect(\n          () => invalidGame.onKeyEvent(event, {}),\n          throwsA(isA<AssertionError>()),\n        );\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/game/mixins/single_game_instance_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('SingleGameInstance', () {\n    test('game instance becomes statically available', () {\n      final game = _SingletonGame()\n        ..onGameResize(Vector2.all(100))\n        ..onMount();\n      expect(Component.staticGameInstance, game);\n      game.onRemove();\n    });\n\n    test('guard against multiple game instances', () {\n      final game = _SingletonGame()\n        ..onGameResize(Vector2.all(100))\n        ..onMount();\n      expect(\n        FlameGame.new,\n        failsAssert(\n          \"Instance of 'FlameGame<World>' instantiated, while another game \"\n          \"Instance of '_SingletonGame' declares itself to be a singleton\",\n        ),\n      );\n      game.onRemove();\n    });\n\n    testWithGame<_SingletonGame>(\n      'Component starts loading before the parent is mounted',\n      _SingletonGame.new,\n      (game) async {\n        final parent = Component();\n        final child = _DelayedComponent();\n        final future = child.addToParent(parent);\n        expect(parent.isMounted, false);\n        expect(parent.isLoaded, false);\n        expect(child.isMounted, false);\n        expect(child.isLoaded, false); // not yet..\n        await future;\n        expect(parent.isMounted, false);\n        expect(child.isLoaded, true);\n        expect(child.isMounted, false);\n\n        game.add(parent);\n        expect(parent.isLoaded, true);\n        await game.ready();\n        expect(child.isMounted, true);\n      },\n    );\n  });\n}\n\nclass _DelayedComponent extends Component {\n  @override\n  Future<void> onLoad() async {\n    await Future<int?>.delayed(const Duration(milliseconds: 20));\n  }\n}\n\nclass _SingletonGame extends FlameGame with SingleGameInstance {}\n"
  },
  {
    "path": "packages/flame/test/game/notifying_vector2_test.dart",
    "content": "import 'package:flame/src/game/notifying_vector2.dart';\nimport 'package:test/test.dart';\nimport 'package:vector_math/vector_math.dart';\n\nvoid main() {\n  /// This helper function creates a \"normal\" Vector2 copy of [v1],\n  /// then applies [operation] to both vectors, and verifies that the\n  /// end result is the same. It also checks that [v1] produces a\n  /// notification during a modifying operation.\n  void check(NotifyingVector2 v1, void Function(Vector2) operation) {\n    final v2 = v1.clone();\n    expect(v2 is NotifyingVector2, false);\n    var notified = 0;\n    void listener() {\n      notified++;\n    }\n\n    v1.addListener(listener);\n    operation(v1);\n    operation(v2);\n    v1.removeListener(listener);\n    expect(notified, 1);\n    expect(v1, v2);\n  }\n\n  group('NotifyingVector2', () {\n    test('constructors', () {\n      final nv0 = NotifyingVector2.zero();\n      expect(nv0, Vector2.zero());\n      final nv1 = NotifyingVector2(3, 1415);\n      expect(nv1, Vector2(3, 1415));\n      final nv2 = NotifyingVector2.all(111);\n      expect(nv2, Vector2.all(111));\n      final nv3 = NotifyingVector2.copy(Vector2(4, 9));\n      expect(nv3, Vector2(4, 9));\n    });\n\n    test('full setters', () {\n      final nv = NotifyingVector2.zero();\n      check(nv, (v) => v.setValues(3, 2));\n      check(nv, (v) => v.setFrom(Vector2(5, 8)));\n      check(nv, (v) => v.setZero());\n      check(nv, (v) => v.splat(3.2));\n      check(nv, (v) => v.copyFromArray([1, 2, 3, 4, 5]));\n      check(nv, (v) => v.copyFromArray([1, 2, 3, 4, 5], 2));\n      check(nv, (v) => v.xy = Vector2(7, 2));\n      check(nv, (v) => v.yx = Vector2(7, 2));\n      check(nv, (v) => v.rg = Vector2(1, 10));\n      check(nv, (v) => v.gr = Vector2(1, 10));\n      check(nv, (v) => v.st = Vector2(-5, -89));\n      check(nv, (v) => v.ts = Vector2(-5, -89));\n    });\n\n    test('individual field setters', () {\n      final nv = NotifyingVector2.zero();\n      check(nv, (v) => v[0] = 2.5);\n      check(nv, (v) => v[1] = 1.25);\n      check(nv, (v) => v.x = 425);\n      check(nv, (v) => v.y = -1.11e-11);\n      check(nv, (v) => v.r = 101);\n      check(nv, (v) => v.g = 102);\n      check(nv, (v) => v.s = 103);\n      check(nv, (v) => v.t = 104);\n    });\n\n    test('modification methods', () {\n      final nv = NotifyingVector2(23, 3);\n      check(nv, (v) => v.length = 15);\n      check(nv, (v) => v.normalize());\n      check(nv, (v) => v.postmultiply(Matrix2.rotation(1)));\n      check(nv, (v) => v.add(Vector2(0.2, -0.1)));\n      check(nv, (v) => v.addScaled(Vector2(2.05, 1.1), 3));\n      check(nv, (v) => v.sub(Vector2(9.7, 4.62)));\n      check(nv, (v) => v.multiply(Vector2(1.2, -0.62)));\n      check(nv, (v) => v.divide(Vector2(0.69, 1.23)));\n      check(nv, (v) => v.scale(7.802));\n      check(nv, (v) => v.negate());\n      check(nv, (v) => v.absolute());\n      check(nv, (v) => v.clamp(Vector2(-5, -6), Vector2(100, 1e20)));\n      check(nv, (v) => v.clampScalar(-2, 38479.10349));\n      check(nv, (v) => v.floor());\n      nv.scale(1.3891);\n      check(nv, (v) => v.ceil());\n      nv.scale(1.111);\n      check(nv, (v) => v.round());\n      nv.multiply(Vector2(1.23, -4.791));\n      check(nv, (v) => v.roundToZero());\n    });\n\n    test('storage is read-only', () {\n      final nv = NotifyingVector2.zero();\n      expect(nv, Vector2.zero());\n      final storage = nv.storage;\n      // Check that storage is not writable\n      expect(\n        () {\n          storage[0] = 1;\n        },\n        throwsA(isA<UnsupportedError>()),\n      );\n      // Check that the vector wasn't modified, and that storage is readable\n      expect(storage[0], 0);\n      expect(storage[1], 0);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/game/overlays_manager_test.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../_resources/custom_flame_game.dart';\n\nvoid main() {\n  group('OverlaysManager', () {\n    testWidgets(\n      'overlay can be added via initialActiveOverlays',\n      (tester) async {\n        const key1 = ValueKey('one');\n        const key2 = ValueKey('two');\n        await tester.pumpWidget(\n          GameWidget(\n            game: FlameGame(),\n            overlayBuilderMap: {\n              'first!': (_, __) => Container(key: key1),\n              'second': (_, __) => Container(key: key2),\n            },\n            initialActiveOverlays: const ['first!'],\n          ),\n        );\n        await tester.pump();\n\n        expect(find.byKey(key1), findsOneWidget);\n        expect(find.byKey(key2), findsNothing);\n      },\n    );\n\n    testWidgets(\n      'overlay can be added in onLoad',\n      (tester) async {\n        const key1 = ValueKey('one');\n        const key2 = ValueKey('two');\n        await tester.pumpWidget(\n          GameWidget(\n            game: CustomFlameGame(\n              onLoad: (game) async {\n                game.overlays.add('first!');\n              },\n            ),\n            overlayBuilderMap: {\n              'first!': (_, __) => Container(key: key1),\n              'second': (_, __) => Container(key: key2),\n            },\n          ),\n        );\n        await tester.pump();\n\n        expect(find.byKey(key1), findsOneWidget);\n        expect(find.byKey(key2), findsNothing);\n      },\n    );\n\n    testWidgets(\n      'overlay can be added and removed at runtime',\n      (tester) async {\n        const key1 = ValueKey('one');\n        const key2 = ValueKey('two');\n        final game = FlameGame();\n        await tester.pumpWidget(\n          GameWidget(\n            game: game,\n            overlayBuilderMap: {\n              'first!': (_, __) => Container(key: key1),\n              'second': (_, __) => Container(key: key2),\n            },\n          ),\n        );\n        await tester.pump();\n\n        expect(find.byKey(key1), findsNothing);\n        expect(find.byKey(key2), findsNothing);\n\n        game.overlays.add('second');\n        await tester.pump();\n        expect(find.byKey(key1), findsNothing);\n        expect(find.byKey(key2), findsOneWidget);\n\n        game.overlays.remove('second');\n        await tester.pump();\n        expect(find.byKey(key1), findsNothing);\n        expect(find.byKey(key2), findsNothing);\n      },\n    );\n\n    test('can add an overlay', () {\n      final overlays = FlameGame().overlays\n        ..addEntry('test', (ctx, game) => Container());\n      final added = overlays.add('test');\n      expect(added, true);\n      expect(overlays.isActive('test'), true);\n\n      final added2 = overlays.add('test');\n      expect(added2, false);\n      expect(overlays.isActive('test'), true);\n      expect(overlays.activeOverlays.length, 1);\n    });\n\n    test('can toggle an overlay', () {\n      final overlays = FlameGame().overlays\n        ..addEntry('test', (ctx, game) => Container());\n      final added = overlays.toggle('test');\n      expect(added, true);\n      expect(overlays.isActive('test'), true);\n\n      final removed = overlays.toggle('test');\n      expect(removed, true);\n      expect(overlays.isActive('test'), false);\n      expect(overlays.activeOverlays.length, 0);\n    });\n\n    test('can add multiple overlays at once', () {\n      final overlays = FlameGame().overlays\n        ..addEntry('test1', (ctx, game) => Container())\n        ..addEntry('test2', (ctx, game) => Container());\n      overlays.addAll(['test1', 'test2']);\n      expect(overlays.isActive('test1'), true);\n      expect(overlays.isActive('test2'), true);\n      expect(overlays.activeOverlays.length, 2);\n    });\n\n    test('can add multiple overlays with priorities', () {\n      final overlays = FlameGame().overlays\n        ..addEntry('test1', (ctx, game) => Container())\n        ..addEntry('test2', (ctx, game) => Container());\n      overlays.add('test1', priority: 1);\n      overlays.add('test2');\n      expect(overlays.activeOverlays, ['test2', 'test1']);\n      expect(overlays.isActive('test1'), true);\n      expect(overlays.isActive('test2'), true);\n      expect(overlays.activeOverlays.length, 2);\n    });\n\n    test('cannot add an unknown overlay', () {\n      final overlays = FlameGame().overlays;\n      expect(\n        () => overlays.add('wheelbarrow'),\n        failsAssert('Trying to add an unknown overlay \"wheelbarrow\"'),\n      );\n    });\n\n    test('can remove an overlay', () {\n      final overlays = FlameGame().overlays\n        ..addEntry('test', (ctx, game) => Container());\n      overlays.add('test');\n\n      final didRemove = overlays.remove('test');\n      expect(didRemove, true);\n      expect(overlays.isActive('test'), false);\n    });\n\n    test('will not result in removal if there is nothing to remove', () {\n      final overlays = FlameGame().overlays\n        ..addEntry('test', (ctx, game) => Container());\n\n      final didRemove = overlays.remove('test');\n      expect(didRemove, false);\n    });\n\n    test('can remove multiple overlays at once', () {\n      final overlays = FlameGame().overlays\n        ..addEntry('test1', (ctx, game) => Container())\n        ..addEntry('test2', (ctx, game) => Container())\n        ..addEntry('test3', (ctx, game) => Container());\n      overlays.addAll(['test1', 'test2', 'test3']);\n      expect(overlays.activeOverlays.length, 3);\n\n      overlays.removeAll(['test1', 'test2']);\n      expect(overlays.isActive('test1'), false);\n      expect(overlays.isActive('test2'), false);\n      expect(overlays.isActive('test3'), true);\n      expect(overlays.activeOverlays.length, 1);\n    });\n\n    test('clears all overlays', () {\n      final overlays = FlameGame().overlays\n        ..addEntry('test1', (ctx, game) => Container())\n        ..addEntry('test2', (ctx, game) => Container());\n      overlays.add('test1');\n      overlays.add('test2');\n\n      overlays.clear();\n      expect(overlays.isActive('test1'), false);\n      expect(overlays.isActive('test2'), false);\n      expect(overlays.activeOverlays.length, 0);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/game/transform2d_test.dart",
    "content": "import 'dart:math' as math;\n\n// TODO(spydon): Remove this import once Flutter 3.35.0 is the minimum version.\n// ignore: unnecessary_import\nimport 'package:flame/extensions.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame/src/game/transform2d.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n// ignore: unnecessary_import\nimport 'package:vector_math/vector_math.dart';\n\nvoid main() {\n  // Implementation of statistical error propagation based on\n  // Tellinghuisen, J. (2001). Statistical Error Propagation.\n  // https://pubs.acs.org/doi/10.1021/jp003484u\n  // Note: this function is only applicable to the globalToLocal 2D transform\n  // round trip.\n  double transform2dRoundTripUncertainty(\n    Transform2D transform,\n    Vector2 point,\n  ) {\n    final m = transform.transformMatrix.storage;\n    // Calculate the determinant\n    final det = m[0] * m[5] - m[1] * m[4];\n\n    // Base uncertainty\n    const epsilon = 1 / (1 << 23);\n\n    // Calculate indicative condition number\n    final matrixNorm = math.sqrt(\n      m[0] * m[0] + m[1] * m[1] + m[4] * m[4] + m[5] * m[5],\n    );\n\n    double conditionFactor;\n    if (det.abs() > 1e-10) {\n      // Calculate condition number\n      final invMatrixNorm =\n          math.sqrt(m[5] * m[5] + m[1] * m[1] + m[4] * m[4] + m[0] * m[0]) /\n          det.abs();\n      conditionFactor = matrixNorm * invMatrixNorm;\n    } else {\n      // For ~singular matrices, small input change -> large output change\n      conditionFactor = 1e6;\n    }\n\n    // Statistical error propagation\n    // There are 8 variables (point + matrix elements)\n    const double numVariables = 8;\n    // In the round trip there's approx 14? ops\n    const double numOperations = 15;\n\n    // Standard deviation (1-σ)\n    final sigma =\n        epsilon *\n        math.sqrt(numVariables) *\n        math.sqrt(numOperations) *\n        conditionFactor;\n\n    // 99.7 CI + small low-value tolerance\n    final tolerance = 3 * sigma + epsilon;\n\n    // Adjust for relative precision\n    final magnitude = math.max(1.0, math.max(point.x.abs(), point.y.abs()));\n\n    return tolerance * magnitude;\n  }\n\n  group('Transform2D', () {\n    test('basic construction', () {\n      final t = Transform2D()\n        ..position = Vector2(3, 6)\n        ..scale = Vector2(2, 0.5)\n        ..angle = 1\n        ..offset = Vector2(-1, -10);\n      expect(t.position, Vector2(3, 6));\n      expect(t.scale, Vector2(2, 0.5));\n      expect(t.angle, 1);\n      expect(t.offset, Vector2(-1, -10));\n    });\n\n    test('listenable .position', () {\n      final t = Transform2D();\n      var notified = 0;\n      t.position.addListener(() {\n        notified++;\n      });\n\n      final zero = Vector2.zero();\n      expect(t.localToGlobal(zero), zero);\n\n      t.position.setValues(5, 11);\n      expect(t.position, Vector2(5, 11));\n      expect(t.localToGlobal(zero), Vector2(5, 11));\n      expect(notified, 1);\n\n      t.x = 19;\n      expect(t.position, Vector2(19, 11));\n      expect(t.localToGlobal(zero), Vector2(19, 11));\n      expect(notified, 2);\n\n      t.y = 0;\n      expect(t.position, Vector2(19, 0));\n      expect(notified, 3);\n\n      t.position.setFrom(Vector2(7, 2.2));\n      expect(t.x, closeTo(7, toleranceFloat32(7)));\n      expect(t.y, closeTo(2.2, toleranceFloat32(2.2)));\n      expect(notified, 4);\n\n      t.position.setZero();\n      expect(t.position, zero);\n      expect(t.localToGlobal(zero), zero);\n      expect(notified, 5);\n    });\n\n    test('listenable .scale', () {\n      final t = Transform2D();\n      var notified = 0;\n      t.scale.addListener(() {\n        notified++;\n      });\n\n      final one = Vector2.all(1);\n      expect(t.localToGlobal(one), one);\n\n      t.scale.setValues(2, 3);\n      expect(t.scale, Vector2(2, 3));\n      expect(t.localToGlobal(one), Vector2(2, 3));\n      expect(notified, 1);\n\n      t.scale.setFrom(Vector2(-1, 3));\n      expect(t.localToGlobal(one), Vector2(-1, 3));\n      expect(notified, 2);\n\n      t.scale.multiply(Vector2.all(3));\n      expect(t.scale, Vector2(-3, 9));\n      expect(t.localToGlobal(one), Vector2(-3, 9));\n      expect(notified, 3);\n    });\n\n    test('offset', () {\n      final t = Transform2D();\n      t.position = Vector2(5, 5);\n      t.scale = Vector2(2, 10);\n      t.offset = Vector2(1, 0);\n      t.angleDegrees = 90;\n      // offset of 1 in X direction becomes 2 units because of scale,\n      // and changes into Y direction because of rotation:\n      expect(t.localToGlobal(Vector2.zero()), Vector2(5, 7));\n    });\n\n    test('angle', () {\n      final t = Transform2D();\n      t.angle = tau / 6;\n      expect(t.angleDegrees, closeTo(60, 1e-10));\n      t.angleDegrees = 45;\n      expect(t.angle, closeTo(tau / 8, 1e-10));\n      t.angle = 1;\n      expect(t.angle, 1);\n      expect(t.angleDegrees, closeTo(360 / tau, 1e-10));\n    });\n\n    test('.closeTo', () {\n      final t1 = Transform2D();\n      final t2 = Transform2D()..angleDegrees = 359;\n      expect(t1.closeTo(t2, tolerance: 0.02), true);\n      expect(t1.closeTo(t2, tolerance: 0.01), false);\n      t1.angle += 17.2855;\n      t2.angle += 17.2857;\n      expect(t1.closeTo(t2, tolerance: 0.02), true);\n      expect(t1.closeTo(t2, tolerance: 0.01), false);\n    });\n\n    test('.copy', () {\n      final t1 = Transform2D()\n        ..position.setValues(3, 11)\n        ..scale.setValues(2.4, 5.6)\n        ..offset.setValues(-4, 19)\n        ..angle = 4;\n      final t2 = Transform2D.copy(t1);\n      t1.position.setValues(4, 7);\n      t1.scale.setValues(2, 0);\n      t1.offset.setZero();\n      t1.angle = -1;\n      // Now check that t2 didn't change\n      expect(t2.position, Vector2(3, 11));\n      expect(t2.scale, Vector2(2.4, 5.6));\n      expect(t2.offset, Vector2(-4, 19));\n      expect(t2.angle, 4);\n    });\n\n    test('flips', () {\n      final t = Transform2D();\n      final one = Vector2.all(1);\n      expect(t.localToGlobal(one), one);\n      t.flipHorizontally();\n      expect(t.localToGlobal(one), Vector2(-1, 1));\n      t.flipVertically();\n      expect(t.localToGlobal(one), Vector2(-1, -1));\n      t.flipHorizontally();\n      expect(t.localToGlobal(one), Vector2(1, -1));\n      t.flipVertically();\n      expect(t.localToGlobal(one), Vector2(1, 1));\n    });\n\n    testRandom('random', (math.Random rnd) {\n      for (var i = 0; i < 20; i++) {\n        final translation = Vector2(\n          rnd.nextDouble() * 10,\n          rnd.nextDouble() * 10,\n        );\n        final rotation = rnd.nextDouble() * 10;\n        final scale = Vector2(\n          (rnd.nextDouble() - 0.3) * 3,\n          (rnd.nextDouble() - 0.2) * 3,\n        );\n        final offset = Vector2(\n          (rnd.nextDouble() - 0.5) * 10,\n          (rnd.nextDouble() - 0.5) * 10,\n        );\n        final transform2d = Transform2D()\n          ..position = translation\n          ..angle = rotation\n          ..scale = scale\n          ..offset = offset;\n        final matrix4 = Matrix4.identity()\n          ..translateByDouble(translation.x, translation.y, 0.0, 1.0)\n          ..rotateZ(rotation)\n          ..scaleByDouble(scale.x, scale.y, 1.0, 1.0)\n          ..translateByDouble(offset.x, offset.y, 0.0, 1.0);\n\n        for (var k = 0; k < 16; k++) {\n          expect(\n            transform2d.transformMatrix.storage[k],\n            closeTo(\n              matrix4.storage[k],\n              toleranceVector2Float32(translation) +\n                  toleranceFloat32(rotation) +\n                  toleranceVector2Float32(scale) +\n                  toleranceVector2Float32(offset),\n            ),\n          );\n        }\n        // Check round-trip conversion between local and global\n        final point1 = Vector2(\n          (rnd.nextDouble() - 0.5) * 5,\n          (rnd.nextDouble() - 0.5) * 5,\n        );\n        final point2 = transform2d.globalToLocal(\n          transform2d.localToGlobal(point1),\n        );\n\n        final tolerance = transform2dRoundTripUncertainty(transform2d, point1);\n        expect(\n          point1,\n          closeToVector(\n            point2,\n            tolerance,\n          ),\n        );\n      }\n    });\n\n    test('degenerate transform', () {\n      final t = Transform2D();\n      t.scale = Vector2(1, 0);\n      final point = Vector2.all(1);\n      expect(t.localToGlobal(point), Vector2(1, 0));\n      expect(t.globalToLocal(point), Vector2(0, 0));\n\n      t.angleDegrees = 60;\n      expect(t.localToGlobal(point).x, closeTo(1 / 2, toleranceFloat32(1 / 2)));\n      expect(\n        t.localToGlobal(point).y,\n        closeTo(math.sqrt(3) / 2, toleranceFloat32(math.sqrt(3) / 2)),\n      );\n      expect(t.globalToLocal(point), Vector2(0, 0));\n\n      t.scale = Vector2(0, 1);\n      expect(t.globalToLocal(point), Vector2(0, 0));\n    });\n\n    group('transformMatrix setter', () {\n      test('setting transformMatrix updates properties', () {\n        final transform = Transform2D();\n        final matrix = Matrix4.identity()\n          ..translateByDouble(10.0, 20.0, 0.0, 1.0)\n          ..rotateZ(0.5)\n          ..scaleByDouble(2.0, 3.0, 1.0, 1.0);\n\n        transform.transformMatrix = matrix;\n\n        expect(transform.position.x, closeTo(10.0, 1e-7));\n        expect(transform.position.y, closeTo(20.0, 1e-7));\n        expect(transform.angle, closeTo(0.5, 1e-7));\n        expect(transform.scale.x, closeTo(2.0, 1e-7));\n        expect(transform.scale.y, closeTo(3.0, 1e-7));\n        expect(transform.offset.x, 0.0);\n        expect(transform.offset.y, 0.0);\n      });\n\n      test('setting transformMatrix with offset', () {\n        final transform = Transform2D();\n        transform.offset = Vector2(5, 5);\n\n        // Matrix that corresponds to:\n        // translate(10, 20) -> rotateZ(0) -> scale(1, 1) -> translate(5, 5)\n        // which is effectively translate(15, 25)\n        final matrix = Matrix4.identity()\n          ..translateByDouble(15.0, 25.0, 0.0, 1.0);\n\n        transform.transformMatrix = matrix;\n\n        // If we preserve offset(5, 5), then position should be (10, 20)\n        expect(transform.offset, Vector2(5, 5));\n        expect(transform.position.x, closeTo(10.0, 1e-7));\n        expect(transform.position.y, closeTo(20.0, 1e-7));\n        expect(transform.angle, closeTo(0.0, 1e-7));\n        expect(transform.scale.x, closeTo(1.0, 1e-7));\n        expect(transform.scale.y, closeTo(1.0, 1e-7));\n      });\n\n      test('setting transformMatrix triggers notification only once', () {\n        final transform = Transform2D();\n        var notifications = 0;\n        transform.addListener(() => notifications++);\n\n        transform.transformMatrix = Matrix4.identity()\n          ..translateByDouble(10.0, 10.0, 0.0, 1.0);\n\n        expect(notifications, 1);\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/geometry/line_segment_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('LineSegment', () {\n    test('midpoint', () {\n      final lineSegment1 = LineSegment(Vector2.zero(), Vector2.all(2));\n      expect(lineSegment1.midpoint, Vector2.all(1));\n\n      final lineSegment2 = LineSegment(Vector2.all(0), Vector2(0, 2));\n      expect(lineSegment2.midpoint, Vector2(0, 1));\n    });\n\n    test('(to, from) and (direction, length) are equivalent', () {\n      final lineSegment1 = LineSegment(Vector2(1, 1), Vector2(2, 1));\n      expect(lineSegment1.from, Vector2(1, 1));\n      expect(lineSegment1.to, Vector2(2, 1));\n\n      expect(lineSegment1.length, 1);\n      expect(lineSegment1.direction, Vector2(1, 0));\n\n      final lineSegment2 = LineSegment.withLength(\n        start: Vector2(1, 1),\n        direction: Vector2(1, 0),\n        length: 1,\n      );\n      expect(lineSegment2.from, Vector2(1, 1));\n      expect(lineSegment2.to, Vector2(2, 1));\n\n      expect(lineSegment2.length, 1);\n      expect(lineSegment2.direction, Vector2(1, 0));\n    });\n\n    test('translate line segment', () {\n      final line = LineSegment(Vector2.all(1), Vector2.all(2));\n\n      final lineUp = line.translate(Vector2(0, -1));\n      expect(lineUp.from, Vector2(1, 0));\n      expect(lineUp.to, Vector2(2, 1));\n\n      final lineDown = line.translate(Vector2(0, 1));\n      expect(lineDown.from, Vector2(1, 2));\n      expect(lineDown.to, Vector2(2, 3));\n\n      final lineLeft = line.translate(Vector2(-1, 0));\n      expect(lineLeft.from, Vector2(0, 1));\n      expect(lineLeft.to, Vector2(1, 2));\n\n      final lineRight = line.translate(Vector2(1, 0));\n      expect(lineRight.from, Vector2(2, 1));\n      expect(lineRight.to, Vector2(3, 2));\n\n      final lineDiagonal = line.translate(Vector2(1, -1));\n      expect(lineDiagonal.from, Vector2(2, 0));\n      expect(lineDiagonal.to, Vector2(3, 1));\n    });\n\n    test('inflate and deflate', () {\n      final line = LineSegment(Vector2(1, 2), Vector2(3, 2));\n\n      final inflatedLine = line.inflate(1);\n      expect(inflatedLine.from, Vector2(0, 2));\n      expect(inflatedLine.to, Vector2(4, 2));\n\n      final deflatedLine = line.deflate(0.5);\n      expect(deflatedLine.from, Vector2(1.5, 2));\n      expect(deflatedLine.to, Vector2(2.5, 2));\n    });\n\n    test('spread', () {\n      final line = LineSegment(Vector2(0, 0), Vector2(10, 0));\n\n      final points1 = line.spread(4);\n      expect(points1.length, 4);\n      expect(points1[0], Vector2(2, 0));\n      expect(points1[1], Vector2(4, 0));\n      expect(points1[2], Vector2(6, 0));\n      expect(points1[3], Vector2(8, 0));\n\n      final points2 = line.spread(0);\n      expect(points2.length, 0);\n\n      // expect an exception with message for negative amount\n      expect(\n        () => line.spread(-1),\n        throwsA(\n          isA<ArgumentError>().having(\n            (e) => e.message,\n            'message',\n            'Amount must be non-negative',\n          ),\n        ),\n      );\n    });\n\n    test('Slightly angled line segments should intersect', () {\n      // This tests that the epsilon is sufficiently large, see #3587\n      final lineA = LineSegment(Vector2(-27.5, 2.5), Vector2(-22.5, 2.5));\n      final lineB = LineSegment(Vector2(-25, -25), Vector2(-25 + 1 / 18, 25));\n      expect(lineA.intersections(lineB), isNotEmpty);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/geometry/ray2_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('Ray2', () {\n    test('Properly updates direction inverses', () {\n      final direction = Vector2(-10.0, 3).normalized();\n      final ray = Ray2(\n        origin: Vector2.all(2.0),\n        direction: direction,\n      );\n      expect(\n        ray.directionInvX,\n        1 / direction.x,\n        reason: 'The inverse of the x-direction is wrong',\n      );\n      expect(\n        ray.directionInvY,\n        1 / direction.y,\n        reason: 'The inverse of the y-direction is wrong',\n      );\n      final updatedDirection = Vector2(10, 4).normalized();\n      ray.direction = updatedDirection;\n      expect(\n        ray.directionInvX,\n        1 / updatedDirection.x,\n        reason: 'The inverse of the x-direction did not properly update',\n      );\n      expect(\n        ray.directionInvY,\n        1 / updatedDirection.y,\n        reason: 'The inverse of the y-direction did not properly update',\n      );\n    });\n\n    test('Throws assertion if direction is not normalized', () {\n      final direction = Vector2(1.01, 0.0);\n      expect(\n        () {\n          Ray2(\n            origin: Vector2.all(2.0),\n            direction: direction,\n          );\n        },\n        failsAssert('direction must be normalized'),\n      );\n      final ray = Ray2(\n        origin: Vector2.all(2.0),\n        direction: direction.normalized(),\n      );\n      expect(\n        () => ray.direction = direction,\n        failsAssert('direction must be normalized'),\n      );\n    });\n\n    group('intersectsWithAabb2', () {\n      test('Ray from the east', () {\n        final direction = Vector2(1.0, 0.0);\n        final ray = Ray2(\n          origin: Vector2.zero(),\n          direction: direction.normalized(),\n        );\n        expect(\n          ray.intersectsWithAabb2(Aabb2.minMax(Vector2(1, -1), Vector2(2, 1))),\n          isTrue,\n        );\n      });\n\n      test('Ray from the north', () {\n        final direction = Vector2(0.0, 1.0);\n        final ray = Ray2(\n          origin: Vector2.zero(),\n          direction: direction.normalized(),\n        );\n        expect(\n          ray.intersectsWithAabb2(Aabb2.minMax(Vector2(-1, 1), Vector2(1, 2))),\n          isTrue,\n        );\n      });\n\n      test('Ray from the west', () {\n        final direction = Vector2(-1.0, 0.0);\n        final ray = Ray2(\n          origin: Vector2.zero(),\n          direction: direction.normalized(),\n        );\n        expect(\n          ray.intersectsWithAabb2(\n            Aabb2.minMax(Vector2(-2, -1), Vector2(-1, 1)),\n          ),\n          isTrue,\n        );\n      });\n\n      test('Ray from the south', () {\n        final direction = Vector2(0.0, -1.0);\n        final ray = Ray2(\n          origin: Vector2.zero(),\n          direction: direction.normalized(),\n        );\n        expect(\n          ray.intersectsWithAabb2(\n            Aabb2.minMax(Vector2(-1, -2), Vector2(1, -1)),\n          ),\n          isTrue,\n        );\n      });\n\n      test('Ray from the northEast', () {\n        final direction = Vector2(0.5, 0.5);\n        final ray = Ray2(\n          origin: Vector2.zero(),\n          direction: direction.normalized(),\n        );\n        expect(\n          ray.intersectsWithAabb2(Aabb2.minMax(Vector2(1, 1), Vector2(2, 2))),\n          isTrue,\n        );\n      });\n\n      test('Ray from the northWest', () {\n        final direction = Vector2(-0.5, 0.5);\n        final ray = Ray2(\n          origin: Vector2.zero(),\n          direction: direction.normalized(),\n        );\n        expect(\n          ray.intersectsWithAabb2(Aabb2.minMax(Vector2(-2, 1), Vector2(-1, 2))),\n          isTrue,\n        );\n      });\n\n      test('Ray from the southWest', () {\n        final direction = Vector2(-0.5, -0.5);\n        final ray = Ray2(\n          origin: Vector2.zero(),\n          direction: direction.normalized(),\n        );\n        expect(\n          ray.intersectsWithAabb2(\n            Aabb2.minMax(Vector2(-2, -2), Vector2(-1, -1)),\n          ),\n          isTrue,\n        );\n      });\n\n      test('Ray from the southEast', () {\n        final direction = Vector2(0.5, -0.5);\n        final ray = Ray2(\n          origin: Vector2.zero(),\n          direction: direction.normalized(),\n        );\n        expect(\n          ray.intersectsWithAabb2(Aabb2.minMax(Vector2(1, -2), Vector2(2, -1))),\n          isTrue,\n        );\n      });\n\n      test(\n        'Ray that originates from within the box',\n        () {\n          final direction = Vector2(0, 1);\n          const numberOfDirections = 16;\n          for (var i = 0; i < numberOfDirections; i++) {\n            direction.rotate(tau * (i / numberOfDirections));\n            final ray = Ray2(\n              origin: Vector2.all(5),\n              direction: direction.normalized(),\n            );\n            final aabb2 = Aabb2.minMax(Vector2.zero(), Vector2.all(10));\n            expect(\n              ray.intersectsWithAabb2(aabb2),\n              isTrue,\n            );\n          }\n        },\n      );\n\n      test(\n        'Rays that originates from a box edge pointing inwards',\n        () {\n          const epsilon = 0.000001;\n          const numberOfDirections = 100;\n          for (var i = 0; i < numberOfDirections; i++) {\n            final direction = Vector2(0, 1);\n            final angle =\n                (tau / 2 - 2 * epsilon) * (i / numberOfDirections) + epsilon;\n            direction.rotate(angle);\n            final ray = Ray2(\n              origin: Vector2(10, 5),\n              direction: direction.normalized(),\n            );\n            final aabb2 = Aabb2.minMax(Vector2.zero(), Vector2.all(10));\n            expect(\n              ray.intersectsWithAabb2(aabb2),\n              isTrue,\n            );\n          }\n        },\n      );\n\n      test(\n        'Rays that originates from a box edge pointing outwards intersects',\n        () {\n          const epsilon = 0.000001;\n          const numberOfDirections = 100;\n          for (var i = 0; i < numberOfDirections; i++) {\n            final direction = Vector2(0, 1);\n            final angle =\n                (tau / 2 - 2 * epsilon) * (i / numberOfDirections) + epsilon;\n            direction.rotate(-angle);\n            final ray = Ray2(\n              origin: Vector2(10, 5),\n              direction: direction.normalized(),\n            );\n            final aabb2 = Aabb2.minMax(Vector2.zero(), Vector2.all(10));\n            expect(\n              ray.intersectsWithAabb2(aabb2),\n              isTrue,\n            );\n          }\n        },\n      );\n\n      test(\n        'Rays that originates and follows a box edge does intersects',\n        () {\n          final rayVertical = Ray2(\n            origin: Vector2(10, 5),\n            direction: Vector2(0, 1),\n          );\n          final aabb2 = Aabb2.minMax(Vector2.zero(), Vector2.all(10));\n          expect(\n            rayVertical.intersectsWithAabb2(aabb2),\n            isTrue,\n          );\n          final rayHorizontal = Ray2(\n            origin: Vector2(5, 0),\n            direction: Vector2(1, 0),\n          );\n          expect(\n            rayHorizontal.intersectsWithAabb2(aabb2),\n            isTrue,\n          );\n        },\n      );\n\n      test(\n        'Rays that originates in a corner intersects',\n        () {\n          final rayZero = Ray2(\n            origin: Vector2.zero(),\n            direction: Vector2(0, 1),\n          );\n          final aabb2 = Aabb2.minMax(Vector2.zero(), Vector2.all(10));\n          expect(\n            rayZero.intersectsWithAabb2(aabb2),\n            isTrue,\n          );\n          final rayTen = Ray2(\n            origin: Vector2.all(10),\n            direction: Vector2(0, -1),\n          );\n          expect(\n            rayTen.intersectsWithAabb2(aabb2),\n            isTrue,\n          );\n        },\n      );\n\n      test(\n        'Ray in the opposite direction does not intersect',\n        () {\n          final direction = Vector2(1, 0);\n          final ray = Ray2(\n            origin: Vector2(15, 5),\n            direction: direction.normalized(),\n          );\n          final aabb2 = Aabb2.minMax(Vector2.zero(), Vector2.all(10));\n          expect(\n            ray.intersectsWithAabb2(aabb2),\n            isFalse,\n          );\n        },\n      );\n    });\n\n    group('lineSegmentIntersection', () {\n      test(\n        'Correct intersection point length on ray going east',\n        () {\n          final direction = Vector2(1, 0);\n          final ray = Ray2(\n            origin: Vector2(5, 5),\n            direction: direction.normalized(),\n          );\n          final segment = LineSegment(Vector2(10, 0), Vector2.all(10));\n          expect(ray.lineSegmentIntersection(segment), 5);\n        },\n      );\n\n      test(\n        'Correct intersection point length on ray going west',\n        () {\n          final direction = Vector2(-1, 0);\n          final ray = Ray2(\n            origin: Vector2(5, 5),\n            direction: direction.normalized(),\n          );\n          final segment = LineSegment(Vector2(0, 0), Vector2(0, 10));\n          expect(ray.lineSegmentIntersection(segment), 5);\n        },\n      );\n\n      test(\n        'Correct intersection point length on ray going south',\n        () {\n          final direction = Vector2(0, 1);\n          final ray = Ray2(\n            origin: Vector2(5, 5),\n            direction: direction.normalized(),\n          );\n          final segment = LineSegment(Vector2(0, 10), Vector2(10, 10));\n          expect(ray.lineSegmentIntersection(segment), 5);\n        },\n      );\n\n      test(\n        'Correct intersection point length on ray going north',\n        () {\n          final direction = Vector2(0, -1);\n          final ray = Ray2(\n            origin: Vector2(5, 5),\n            direction: direction.normalized(),\n          );\n          final segment = LineSegment(Vector2(0, 0), Vector2(10, 0));\n          expect(ray.lineSegmentIntersection(segment), 5);\n        },\n      );\n\n      test(\n        'Origin as intersection point when ray originates on segment',\n        () {\n          final direction = Vector2(0, -1);\n          final ray = Ray2(\n            origin: Vector2(5, 0),\n            direction: direction.normalized(),\n          );\n          final segment = LineSegment(Vector2(0, 0), Vector2(10, 0));\n          expect(ray.lineSegmentIntersection(segment), 0);\n        },\n      );\n\n      test(\n        'No intersection when ray is parallel and originates on segment',\n        () {\n          final direction = Vector2(1, 0);\n          final ray = Ray2(\n            origin: Vector2(5, 0),\n            direction: direction.normalized(),\n          );\n          final segment = LineSegment(Vector2(0, 0), Vector2(10, 0));\n          expect(ray.lineSegmentIntersection(segment), null);\n        },\n      );\n\n      test(\n        'No intersection point when ray is parallel to the segment',\n        () {\n          final direction = Vector2(1, 0);\n          final ray = Ray2(\n            origin: Vector2(-5, 0),\n            direction: direction.normalized(),\n          );\n          final segment = LineSegment(Vector2(0, 0), Vector2(10, 0));\n          expect(ray.lineSegmentIntersection(segment), null);\n        },\n      );\n\n      test(\n        'No intersection point when ray is parallel without intersection',\n        () {\n          final direction = Vector2(1, 0);\n          final ray = Ray2(\n            origin: Vector2(5, 5),\n            direction: direction.normalized(),\n          );\n          final segment = LineSegment(Vector2(0, 0), Vector2(10, 0));\n          expect(ray.lineSegmentIntersection(segment), null);\n        },\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/gestures/detectors_test.dart",
    "content": "import 'package:flame/events.dart' hide PointerMoveEvent;\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('MultiTouchDragDetector', () {\n    testWidgets(\n      'Game cannot have both MultiTouchDragDetector and PanDetector',\n      (tester) async {\n        await tester.pumpWidget(\n          GameWidget(\n            game: _MultiDragPanGame(),\n          ),\n        );\n        expect(tester.takeException(), isAssertionError);\n      },\n    );\n  });\n\n  group('MultiTouchTapDetector', () {\n    testWidgets(\n      'Game can have both MultiTouchTapDetector and DoubleTapDetector',\n      (tester) async {\n        await tester.pumpWidget(\n          GameWidget(\n            game: _MultiTapDoubleTapGame(),\n          ),\n        );\n        expect(tester.takeException(), null);\n      },\n    );\n  });\n\n  group('TapDetector', () {\n    final tapGame = FlameTester(_TapDetectorGame.new);\n\n    testWithGame<_TapDetectorGame>(\n      'can be Tapped Down',\n      _TapDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleTapDown(TapDownDetails());\n\n        expect(game.hasOnTapDown, isTrue);\n      },\n    );\n\n    testWithGame<_TapDetectorGame>(\n      'can be Tapped Up',\n      _TapDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleTapUp(TapUpDetails(kind: PointerDeviceKind.touch));\n\n        expect(game.hasOnTapUp, isTrue);\n      },\n    );\n\n    tapGame.testGameWidget(\n      'can receive taps',\n      verify: (game, tester) async {\n        await tester.tapAt(const Offset(10, 10));\n        expect(game.tapRegistered, isTrue);\n      },\n    );\n\n    tapGame.testGameWidget(\n      'can receive tapDown',\n      verify: (game, tester) async {\n        await tester.startGesture(const Offset(10, 10));\n        expect(game.hasOnTapDown, isTrue);\n        expect(game.hasOnTapUp, isFalse);\n      },\n    );\n\n    tapGame.testGameWidget(\n      'can receive tapCancel',\n      verify: (game, tester) async {\n        await tester.dragFrom(const Offset(10, 10), const Offset(20, 10));\n        expect(game.hasOnTapDown, isTrue);\n        expect(game.hasOnTapCancel, isTrue);\n      },\n    );\n  });\n\n  group('SecondaryTapDetector', () {\n    final secondaryTapGame = FlameTester(_SecondaryTapDetectorGame.new);\n\n    secondaryTapGame.testGameWidget(\n      'can receive secondary taps',\n      verify: (game, tester) async {\n        await tester.tapAt(const Offset(10, 10), buttons: kSecondaryButton);\n\n        expect(game.hasOnSecondaryTapDown, isTrue);\n        expect(game.hasOnSecondaryTapUp, isTrue);\n      },\n    );\n\n    secondaryTapGame.testGameWidget(\n      'can receive secondaryTapDown',\n      verify: (game, tester) async {\n        await tester.startGesture(\n          const Offset(10, 10),\n          buttons: kSecondaryButton,\n        );\n        expect(game.hasOnSecondaryTapDown, isTrue);\n        expect(game.hasOnSecondaryTapUp, isFalse);\n      },\n    );\n\n    secondaryTapGame.testGameWidget(\n      'can receive secondaryTapCancel',\n      verify: (game, tester) async {\n        await tester.dragFrom(\n          const Offset(10, 10),\n          const Offset(20, 20),\n          buttons: kSecondaryButton,\n        );\n        expect(game.hasOnSecondaryTapDown, isTrue);\n        expect(game.hasOnSecondaryTapCancel, isTrue);\n      },\n    );\n\n    testWithGame<_SecondaryTapDetectorGame>(\n      'can be Secondary Tapped Down',\n      _SecondaryTapDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleSecondaryTapDown(TapDownDetails());\n\n        expect(game.hasOnSecondaryTapDown, isTrue);\n      },\n    );\n\n    testWithGame<_SecondaryTapDetectorGame>(\n      'can be Secondary Tapped Up',\n      _SecondaryTapDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleSecondaryTapUp(TapUpDetails(kind: PointerDeviceKind.touch));\n\n        expect(game.hasOnSecondaryTapUp, isTrue);\n      },\n    );\n\n    testWithGame<_SecondaryTapDetectorGame>(\n      'can be Secondary Tapped Canceled',\n      _SecondaryTapDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleSecondaryTapDown(TapDownDetails());\n        game.onSecondaryTapCancel();\n\n        expect(game.hasOnSecondaryTapCancel, isTrue);\n      },\n    );\n  });\n\n  group('TertiaryTapDetector', () {\n    final tertiaryTapGame = FlameTester(_TertiaryTapDetectorGame.new);\n\n    tertiaryTapGame.testGameWidget(\n      'can receive tertiary taps',\n      verify: (game, tester) async {\n        await tester.tapAt(const Offset(10, 10), buttons: kTertiaryButton);\n\n        expect(game.hasOnTertiaryTapDown, isTrue);\n        expect(game.hasOnTertiaryTapUp, isTrue);\n      },\n    );\n\n    tertiaryTapGame.testGameWidget(\n      'can receive tertiaryTapDown',\n      verify: (game, tester) async {\n        await tester.startGesture(\n          const Offset(10, 10),\n          buttons: kTertiaryButton,\n        );\n        expect(game.hasOnTertiaryTapDown, isTrue);\n        expect(game.hasOnTertiaryTapUp, isFalse);\n      },\n    );\n\n    tertiaryTapGame.testGameWidget(\n      'can receive tertiaryTapCancel',\n      verify: (game, tester) async {\n        await tester.dragFrom(\n          const Offset(10, 10),\n          const Offset(20, 20),\n          buttons: kTertiaryButton,\n        );\n        expect(game.hasOnTertiaryTapDown, isTrue);\n        expect(game.hasOnTertiaryTapCancel, isTrue);\n      },\n    );\n\n    testWithGame<_TertiaryTapDetectorGame>(\n      'can be Secondary Tapped Down',\n      _TertiaryTapDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleTertiaryTapDown(TapDownDetails());\n\n        expect(game.hasOnTertiaryTapDown, isTrue);\n      },\n    );\n\n    testWithGame<_TertiaryTapDetectorGame>(\n      'can be Secondary Tapped Up',\n      _TertiaryTapDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleTertiaryTapUp(TapUpDetails(kind: PointerDeviceKind.touch));\n\n        expect(game.hasOnTertiaryTapUp, isTrue);\n      },\n    );\n  });\n\n  group('DoubleTapDetector', () {\n    final doubleTapGame = FlameTester(_DoubleTapDetectorGame.new);\n\n    doubleTapGame.testGameWidget(\n      'can receive double taps',\n      setUp: (game, tester) async {\n        await tester.tapAt(const Offset(10, 10));\n        await Future<void>.delayed(kDoubleTapMinTime);\n        await tester.tapAt(const Offset(10, 10));\n      },\n      verify: (game, tester) async {\n        expect(game.hasOnDoubleTapDown, isTrue);\n        expect(game.doubleTapRegistered, isTrue);\n      },\n    );\n\n    doubleTapGame.testGameWidget(\n      'can receive double tapCancel',\n      setUp: (game, tester) async {\n        await tester.tapAt(const Offset(10, 10));\n        await Future<void>.delayed(kDoubleTapMinTime);\n        await tester.dragFrom(\n          const Offset(10, 10),\n          const Offset(20, 20),\n        );\n      },\n      verify: (game, tester) async {\n        expect(game.hasOnDoubleTapDown, isTrue);\n        expect(game.hasOnDoubleTapCancel, isTrue);\n      },\n    );\n\n    testWithGame<_DoubleTapDetectorGame>(\n      'can be Double Tapped Down',\n      _DoubleTapDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleDoubleTapDown(TapDownDetails());\n\n        expect(game.hasOnDoubleTapDown, isTrue);\n      },\n    );\n  });\n\n  group('LongPressDetector', () {\n    final longPressGame = FlameTester(_LongPressDetectorGame.new);\n\n    longPressGame.testGameWidget(\n      'can register longPress',\n      verify: (game, tester) async {\n        await tester.longPressAt(const Offset(10, 10));\n\n        expect(game.hasLongPressRegistered, isTrue);\n      },\n    );\n\n    longPressGame.testGameWidget(\n      'can register moving longPress',\n      setUp: (game, tester) async {\n        final gesture = await tester.startGesture(\n          const Offset(10, 10),\n          pointer: 7,\n        );\n\n        await Future<void>.delayed(kLongPressTimeout);\n\n        await gesture.moveTo(const Offset(20, 10));\n\n        await gesture.up();\n      },\n      verify: (game, tester) async {\n        expect(game.hasLongPressStarted, isTrue);\n        expect(game.hasLongPressMoveUpdated, isTrue);\n        expect(game.hasLongPressEnded, isTrue);\n      },\n    );\n\n    longPressGame.testGameWidget(\n      'can register longPressCancel',\n      setUp: (game, tester) async {\n        final gesture = await tester.startGesture(\n          const Offset(10, 10),\n          pointer: 7,\n        );\n\n        await gesture.moveTo(const Offset(20, 10));\n\n        await gesture.up();\n      },\n      verify: (game, tester) async {\n        expect(game.hasLongPressCanceled, isTrue);\n        expect(game.hasLongPressStarted, isFalse);\n      },\n    );\n\n    testWithGame<_LongPressDetectorGame>(\n      'can be Long Pressed Start',\n      _LongPressDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleLongPressStart(const LongPressStartDetails());\n\n        expect(game.hasLongPressStarted, isTrue);\n      },\n    );\n\n    testWithGame<_LongPressDetectorGame>(\n      'can be Long Pressed Move Update',\n      _LongPressDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleLongPressMoveUpdate(const LongPressMoveUpdateDetails());\n\n        expect(game.hasLongPressMoveUpdated, isTrue);\n      },\n    );\n\n    testWithGame<_LongPressDetectorGame>(\n      'can be Long Pressed Move End',\n      _LongPressDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleLongPressEnd(const LongPressEndDetails());\n\n        expect(game.hasLongPressEnded, isTrue);\n      },\n    );\n  });\n\n  group('VerticalDragDetector', () {\n    final verticalDragGame = FlameTester(_VerticalDragDetectorGame.new);\n\n    verticalDragGame.testGameWidget(\n      'can register vertical drag',\n      verify: (game, tester) async {\n        await tester.dragFrom(const Offset(10, 10), const Offset(10, 50));\n\n        expect(game.hasVerticalDragDown, isTrue);\n        expect(game.hasVerticalDragStart, isTrue);\n        expect(game.hasVerticalDragUpdate, isTrue);\n        expect(game.hasVerticalDragEnd, isTrue);\n      },\n    );\n\n    testWithGame<_VerticalDragDetectorGame>(\n      'can be Vertical Dragged Down',\n      _VerticalDragDetectorGame.new,\n      (game) async {\n        await game.ready();\n        game.handleVerticalDragDown(DragDownDetails());\n\n        expect(game.hasVerticalDragDown, isTrue);\n      },\n    );\n\n    testWithGame<_VerticalDragDetectorGame>(\n      'can be Vertical Dragged Start',\n      _VerticalDragDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleVerticalDragStart(DragStartDetails());\n\n        expect(game.hasVerticalDragStart, isTrue);\n      },\n    );\n\n    testWithGame<_VerticalDragDetectorGame>(\n      'can be Vertical Dragged Update',\n      _VerticalDragDetectorGame.new,\n      (game) async {\n        game.handleVerticalDragUpdate(\n          DragUpdateDetails(globalPosition: const Offset(10, 10)),\n        );\n\n        expect(game.hasVerticalDragUpdate, isTrue);\n      },\n    );\n\n    testWithGame<_VerticalDragDetectorGame>(\n      'can be Vertical Dragged End',\n      _VerticalDragDetectorGame.new,\n      (game) async {\n        game.handleVerticalDragEnd(DragEndDetails());\n\n        expect(game.hasVerticalDragEnd, isTrue);\n      },\n    );\n  });\n\n  group('HorizontalDragDetector', () {\n    final horizontalDragGame = FlameTester(_HorizontalDragDetectorGame.new);\n\n    horizontalDragGame.testGameWidget(\n      'can register horizontal drag',\n      verify: (game, tester) async {\n        await tester.dragFrom(const Offset(10, 10), const Offset(50, 10));\n\n        expect(game.hasHorizontalDragDown, isTrue);\n        expect(game.hasHorizontalDragUpdate, isTrue);\n        expect(game.hasHorizontalDragEnd, isTrue);\n      },\n    );\n\n    testWithGame<_HorizontalDragDetectorGame>(\n      'can be horizontal Dragged Down',\n      _HorizontalDragDetectorGame.new,\n      (game) async {\n        await game.ready();\n        game.handleHorizontalDragDown(DragDownDetails());\n\n        expect(game.hasHorizontalDragDown, isTrue);\n      },\n    );\n\n    testWithGame<_HorizontalDragDetectorGame>(\n      'can be horizontal Dragged Start',\n      _HorizontalDragDetectorGame.new,\n      (game) async {\n        await game.ready();\n        game.handleHorizontalDragStart(DragStartDetails());\n\n        expect(game.hasHorizontalDragStart, isTrue);\n      },\n    );\n\n    testWithGame<_HorizontalDragDetectorGame>(\n      'can be horizontal Dragged update',\n      _HorizontalDragDetectorGame.new,\n      (game) async {\n        await game.ready();\n        game.handleHorizontalDragUpdate(\n          DragUpdateDetails(globalPosition: const Offset(10, 10)),\n        );\n\n        expect(game.hasHorizontalDragUpdate, isTrue);\n      },\n    );\n\n    testWithGame<_HorizontalDragDetectorGame>(\n      'can be horizontal Dragged End',\n      _HorizontalDragDetectorGame.new,\n      (game) async {\n        await game.ready();\n        game.handleHorizontalDragEnd(DragEndDetails());\n\n        expect(game.hasHorizontalDragEnd, isTrue);\n      },\n    );\n  });\n\n  group('ForcePressDetector', () {\n    final forcePressGame = FlameTester(_ForcePressDetectorGame.new);\n\n    forcePressGame.testGameWidget(\n      'can register forcePress',\n      verify: (game, tester) async {\n        const forcePressOffset = Offset(10, 10);\n\n        final pointerValue = tester.nextPointer;\n\n        final gesture = await tester.createGesture();\n        await gesture.downWithCustomEvent(\n          forcePressOffset,\n          PointerDownEvent(\n            pointer: pointerValue,\n            position: forcePressOffset,\n            pressure: 0.0,\n            pressureMax: 6.0,\n            pressureMin: 0.0,\n          ),\n        );\n\n        await gesture.updateWithCustomEvent(\n          PointerMoveEvent(\n            pointer: pointerValue,\n            pressure: 0.3,\n            pressureMin: 0,\n          ),\n        );\n\n        expect(game.forcePressStart, equals(0));\n        expect(game.forcePressPeaked, equals(0));\n        expect(game.forcePressUpdate, equals(0));\n        expect(game.forcePressEnded, equals(0));\n\n        await gesture.updateWithCustomEvent(\n          PointerMoveEvent(\n            pointer: pointerValue,\n            pressure: 0.5,\n            pressureMin: 0,\n          ),\n        );\n\n        expect(game.forcePressStart, equals(1));\n        expect(game.forcePressPeaked, equals(0));\n        expect(game.forcePressUpdate, equals(1));\n        expect(game.forcePressEnded, equals(0));\n\n        await gesture.updateWithCustomEvent(\n          PointerMoveEvent(\n            pointer: pointerValue,\n            pressure: 0.9,\n            pressureMin: 0,\n          ),\n        );\n\n        expect(game.forcePressStart, equals(1));\n        expect(game.forcePressPeaked, equals(1));\n        expect(game.forcePressUpdate, equals(2));\n        expect(game.forcePressEnded, equals(0));\n\n        await gesture.up();\n\n        expect(game.forcePressStart, equals(1));\n        expect(game.forcePressPeaked, equals(1));\n        expect(game.forcePressUpdate, equals(2));\n        expect(game.forcePressEnded, equals(1));\n      },\n    );\n\n    testWithGame<_ForcePressDetectorGame>(\n      'can be Force Press started',\n      _ForcePressDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleForcePressStart(\n          ForcePressDetails(\n            globalPosition: const Offset(10, 10),\n            pressure: 0.4,\n          ),\n        );\n\n        expect(game.forcePressStart, equals(1));\n      },\n    );\n\n    testWithGame<_ForcePressDetectorGame>(\n      'can be Force Press Updated',\n      _ForcePressDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleForcePressUpdate(\n          ForcePressDetails(\n            globalPosition: const Offset(10, 10),\n            pressure: 0.7,\n          ),\n        );\n\n        expect(game.forcePressUpdate, equals(1));\n      },\n    );\n\n    testWithGame<_ForcePressDetectorGame>(\n      'can be Force Press peaked',\n      _ForcePressDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleForcePressPeak(\n          ForcePressDetails(\n            globalPosition: const Offset(10, 10),\n            pressure: 0.9,\n          ),\n        );\n\n        expect(game.forcePressPeaked, equals(1));\n      },\n    );\n\n    testWithGame<_ForcePressDetectorGame>(\n      'can be Force Press Ended',\n      _ForcePressDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleForcePressEnd(\n          ForcePressDetails(\n            globalPosition: const Offset(10, 10),\n            pressure: 0.2,\n          ),\n        );\n        expect(game.forcePressEnded, equals(1));\n      },\n    );\n  });\n\n  group('PanDetector', () {\n    final panGame = FlameTester(_PanDetectorGame.new);\n\n    panGame.testGameWidget(\n      'can Register pan',\n      verify: (game, tester) async {\n        await tester.dragFrom(const Offset(10, 10), const Offset(20, 20));\n\n        expect(game.hasPanStart, isTrue);\n        expect(game.hasPanDown, isTrue);\n        expect(game.hasPanUpdate, isTrue);\n        expect(game.hasPanEnd, isTrue);\n      },\n    );\n\n    testWithGame<_PanDetectorGame>(\n      'can receive onPanDown',\n      _PanDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handlePanDown(DragDownDetails());\n        expect(game.hasPanDown, isTrue);\n      },\n    );\n\n    testWithGame<_PanDetectorGame>(\n      'can receive onPanEnd',\n      _PanDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handlePanEnd(DragEndDetails());\n        expect(game.hasPanEnd, isTrue);\n      },\n    );\n\n    testWithGame<_PanDetectorGame>(\n      'can receive onPanStart',\n      _PanDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handlePanStart(DragStartDetails());\n        expect(game.hasPanStart, isTrue);\n      },\n    );\n\n    testWithGame<_PanDetectorGame>(\n      'can receive onPanUpdate',\n      _PanDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handlePanUpdate(\n          DragUpdateDetails(globalPosition: const Offset(10, 10)),\n        );\n        expect(game.hasPanUpdate, isTrue);\n      },\n    );\n  });\n\n  group('ScaleDetector', () {\n    final scaleGame = FlameTester(_ScaleDetectorGame.new);\n\n    scaleGame.testGameWidget(\n      'can register Scale',\n      setUp: (game, tester) async {\n        final gesture1 = await tester.createGesture();\n        final gesture2 = await tester.createGesture();\n\n        await gesture1.down(const Offset(10, 10));\n        await gesture2.down(const Offset(20, 20));\n\n        await gesture1.moveTo(const Offset(15, 10));\n        await gesture2.moveTo(const Offset(15, 20));\n\n        await gesture1.up();\n        await gesture2.up();\n      },\n      verify: (game, tester) async {\n        expect(game.hasOnScaleStart, isTrue);\n        expect(game.hasOnScaleUpdate, isTrue);\n        expect(game.hasOnScaleEnd, isTrue);\n      },\n    );\n\n    testWithGame<_ScaleDetectorGame>(\n      'can receive onScaleStart',\n      _ScaleDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleScaleStart(ScaleStartDetails());\n        expect(game.hasOnScaleStart, isTrue);\n      },\n    );\n\n    testWithGame<_ScaleDetectorGame>(\n      'can receive onScaleUpdate',\n      _ScaleDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleScaleUpdate(ScaleUpdateDetails());\n        expect(game.hasOnScaleUpdate, isTrue);\n      },\n    );\n\n    testWithGame<_ScaleDetectorGame>(\n      'can receive onScaleEnd',\n      _ScaleDetectorGame.new,\n      (game) async {\n        await game.ready();\n\n        game.handleScaleEnd(ScaleEndDetails());\n        expect(game.hasOnScaleEnd, isTrue);\n      },\n    );\n  });\n\n  group('MouseMovementDetector', () {\n    final mouseMoveGame = FlameTester(_MouseMovementDetectorGame.new);\n\n    mouseMoveGame.testGameWidget(\n      'Can register Mouse movements',\n      setUp: (game, tester) async {\n        final gesture = await tester.createGesture(\n          kind: PointerDeviceKind.mouse,\n        );\n        await gesture.addPointer(location: Offset.zero);\n        await gesture.moveTo(const Offset(10, 10));\n      },\n      verify: (game, tester) async {\n        expect(game.hasReceivedMouseMove, isTrue);\n      },\n    );\n  });\n\n  group('ScrollDetector', () {\n    final scrollGame = FlameTester(_ScrollDetectorGame.new);\n\n    scrollGame.testGameWidget(\n      'Can register Scrolling',\n      verify: (game, tester) async {\n        const scrollEventLocation = Offset(0, 300);\n        final testPointer = TestPointer(1, PointerDeviceKind.mouse);\n        testPointer.hover(scrollEventLocation);\n        await tester.sendEventToBinding(\n          testPointer.scroll(const Offset(0.0, -300.0)),\n        );\n\n        expect(game.registeredScrolling, isTrue);\n      },\n    );\n  });\n}\n\n// testing the deprecated TapDetector mixin\n// ignore: deprecated_member_use_from_same_package\nclass _TapDetectorGame extends FlameGame with TapDetector {\n  bool hasOnTapUp = false;\n  bool hasOnTapDown = false;\n  bool hasOnTapCancel = false;\n  bool tapRegistered = false;\n\n  @override\n  void onTap() {\n    tapRegistered = true;\n  }\n\n  @override\n  void onTapUp(TapUpInfo info) {\n    hasOnTapUp = true;\n  }\n\n  @override\n  void onTapDown(TapDownInfo info) {\n    hasOnTapDown = true;\n  }\n\n  @override\n  void onTapCancel() {\n    hasOnTapCancel = true;\n  }\n}\n\nclass _SecondaryTapDetectorGame extends FlameGame with SecondaryTapDetector {\n  bool hasOnSecondaryTapUp = false;\n  bool hasOnSecondaryTapDown = false;\n  bool hasOnSecondaryTapCancel = false;\n\n  @override\n  void onSecondaryTapDown(TapDownInfo info) {\n    hasOnSecondaryTapDown = true;\n  }\n\n  @override\n  void onSecondaryTapUp(TapUpInfo info) {\n    hasOnSecondaryTapUp = true;\n  }\n\n  @override\n  void onSecondaryTapCancel() {\n    hasOnSecondaryTapCancel = true;\n  }\n}\n\nclass _TertiaryTapDetectorGame extends FlameGame with TertiaryTapDetector {\n  bool hasOnTertiaryTapUp = false;\n  bool hasOnTertiaryTapDown = false;\n  bool hasOnTertiaryTapCancel = false;\n\n  @override\n  void onTertiaryTapDown(TapDownInfo info) {\n    hasOnTertiaryTapDown = true;\n  }\n\n  @override\n  void onTertiaryTapUp(TapUpInfo info) {\n    hasOnTertiaryTapUp = true;\n  }\n\n  @override\n  void onTertiaryTapCancel() {\n    hasOnTertiaryTapCancel = true;\n  }\n}\n\nclass _DoubleTapDetectorGame extends FlameGame with DoubleTapDetector {\n  bool doubleTapRegistered = false;\n  bool hasOnDoubleTapDown = false;\n  bool hasOnDoubleTapCancel = false;\n\n  @override\n  void onDoubleTap() {\n    doubleTapRegistered = true;\n  }\n\n  @override\n  void onDoubleTapCancel() {\n    hasOnDoubleTapCancel = true;\n  }\n\n  @override\n  void onDoubleTapDown(TapDownInfo info) {\n    hasOnDoubleTapDown = true;\n  }\n}\n\nclass _LongPressDetectorGame extends FlameGame with LongPressDetector {\n  bool hasLongPressRegistered = false;\n  bool hasLongPressCanceled = false;\n  bool hasLongPressEnded = false;\n  bool hasLongPressMoveUpdated = false;\n  bool hasLongPressStarted = false;\n\n  @override\n  void onLongPress() {\n    hasLongPressRegistered = true;\n  }\n\n  @override\n  void onLongPressCancel() {\n    hasLongPressCanceled = true;\n  }\n\n  @override\n  void onLongPressEnd(LongPressEndInfo info) {\n    hasLongPressEnded = true;\n  }\n\n  @override\n  void onLongPressUp() {}\n\n  @override\n  void onLongPressMoveUpdate(LongPressMoveUpdateInfo info) {\n    hasLongPressMoveUpdated = true;\n  }\n\n  @override\n  void onLongPressStart(LongPressStartInfo info) {\n    hasLongPressStarted = true;\n  }\n}\n\nclass _HorizontalDragDetectorGame extends FlameGame\n    with HorizontalDragDetector {\n  bool hasHorizontalDragDown = false;\n  bool hasHorizontalDragCancel = false;\n  bool hasHorizontalDragEnd = false;\n  bool hasHorizontalDragUpdate = false;\n  bool hasHorizontalDragStart = false;\n\n  @override\n  void onHorizontalDragDown(DragDownInfo info) {\n    hasHorizontalDragDown = true;\n  }\n\n  @override\n  void onHorizontalDragStart(DragStartInfo info) {\n    hasHorizontalDragStart = true;\n  }\n\n  @override\n  void onHorizontalDragUpdate(DragUpdateInfo info) {\n    hasHorizontalDragUpdate = true;\n  }\n\n  @override\n  void onHorizontalDragEnd(DragEndInfo info) {\n    hasHorizontalDragEnd = true;\n  }\n\n  @override\n  void onHorizontalDragCancel() {\n    hasHorizontalDragCancel = true;\n  }\n}\n\nclass _VerticalDragDetectorGame extends FlameGame with VerticalDragDetector {\n  bool hasVerticalDragDown = false;\n  bool hasVerticalDragCancel = false;\n  bool hasVerticalDragEnd = false;\n  bool hasVerticalDragUpdate = false;\n  bool hasVerticalDragStart = false;\n\n  @override\n  void onVerticalDragDown(DragDownInfo info) {\n    hasVerticalDragDown = true;\n  }\n\n  @override\n  void onVerticalDragCancel() {\n    hasVerticalDragCancel = true;\n  }\n\n  @override\n  void onVerticalDragEnd(DragEndInfo info) {\n    hasVerticalDragEnd = true;\n  }\n\n  @override\n  void onVerticalDragUpdate(DragUpdateInfo info) {\n    hasVerticalDragUpdate = true;\n  }\n\n  @override\n  void onVerticalDragStart(DragStartInfo info) {\n    hasVerticalDragStart = true;\n  }\n}\n\nclass _ForcePressDetectorGame extends FlameGame with ForcePressDetector {\n  int forcePressStart = 0;\n  int forcePressPeaked = 0;\n  int forcePressUpdate = 0;\n  int forcePressEnded = 0;\n\n  @override\n  void onForcePressStart(ForcePressInfo info) {\n    forcePressStart++;\n  }\n\n  @override\n  void onForcePressEnd(ForcePressInfo info) {\n    forcePressEnded++;\n  }\n\n  @override\n  void onForcePressUpdate(ForcePressInfo info) {\n    forcePressUpdate++;\n  }\n\n  @override\n  void onForcePressPeak(ForcePressInfo info) {\n    forcePressPeaked++;\n  }\n}\n\nclass _PanDetectorGame extends FlameGame with PanDetector {\n  bool hasPanDown = false;\n  bool hasPanCancel = false;\n  bool hasPanEnd = false;\n  bool hasPanUpdate = false;\n  bool hasPanStart = false;\n\n  @override\n  void onPanDown(DragDownInfo info) {\n    hasPanDown = true;\n  }\n\n  @override\n  void onPanCancel() {\n    hasPanCancel = true;\n  }\n\n  @override\n  void onPanEnd(DragEndInfo info) {\n    hasPanEnd = true;\n  }\n\n  @override\n  void onPanUpdate(DragUpdateInfo info) {\n    hasPanUpdate = true;\n  }\n\n  @override\n  void onPanStart(DragStartInfo info) {\n    hasPanStart = true;\n  }\n}\n\nclass _ScaleDetectorGame extends FlameGame with ScaleDetector {\n  bool hasOnScaleStart = false;\n  bool hasOnScaleUpdate = false;\n  bool hasOnScaleEnd = false;\n\n  @override\n  void onScaleStart(ScaleStartInfo info) {\n    hasOnScaleStart = true;\n  }\n\n  @override\n  void onScaleUpdate(ScaleUpdateInfo info) {\n    hasOnScaleUpdate = true;\n  }\n\n  @override\n  void onScaleEnd(ScaleEndInfo info) {\n    hasOnScaleEnd = true;\n  }\n}\n\nclass _MouseMovementDetectorGame extends FlameGame with MouseMovementDetector {\n  bool hasReceivedMouseMove = false;\n\n  @override\n  void onMouseMove(PointerHoverInfo info) {\n    hasReceivedMouseMove = true;\n  }\n}\n\nclass _ScrollDetectorGame extends FlameGame with ScrollDetector {\n  bool registeredScrolling = false;\n\n  @override\n  void onScroll(PointerScrollInfo info) {\n    registeredScrolling = true;\n  }\n}\n\nclass _MultiDragPanGame extends FlameGame\n    with MultiTouchDragDetector, PanDetector {}\n\nclass _MultiTapDoubleTapGame extends FlameGame\n    with MultiTouchTapDetector, DoubleTapDetector {}\n"
  },
  {
    "path": "packages/flame/test/image_composition_test.dart",
    "content": "import 'package:flame/src/image_composition.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nclass _MockImage extends Mock implements Image {}\n\nvoid main() {\n  group('ImageComposition', () {\n    test('breaks assertion when adding an invalid portion', () {\n      final composition = ImageComposition();\n      final image = _MockImage();\n      when(() => image.width).thenReturn(100);\n      when(() => image.height).thenReturn(100);\n\n      final invalidRects = [\n        const Rect.fromLTWH(-10, 10, 10, 10),\n        const Rect.fromLTWH(10, -10, 10, 10),\n        const Rect.fromLTWH(110, 10, 10, 10),\n        const Rect.fromLTWH(0, 110, 10, 10),\n        const Rect.fromLTWH(0, 0, 110, 110),\n        const Rect.fromLTWH(20, 0, 90, 10),\n        const Rect.fromLTWH(0, 20, 90, 90),\n        const Rect.fromLTWH(0, 0, 190, 90),\n        const Rect.fromLTWH(0, 0, 90, 190),\n      ];\n\n      invalidRects.forEach((rect) {\n        expect(\n          () => composition.add(image, Vector2.zero(), source: rect),\n          failsAssert('Source rect should fit within the image'),\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/layout/align_component_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/layout.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('AlignComponent', () {\n    testWithFlameGame('Valid parent', (game) async {\n      expectLater(\n        () async {\n          final parent = Component();\n          game.add(parent);\n          parent.add(\n            AlignComponent(\n              child: PositionComponent(),\n              alignment: Anchor.center,\n            ),\n          );\n          await game.ready();\n        },\n        failsAssert(\"An AlignComponent's parent must have a size\"),\n      );\n    });\n\n    testGolden(\n      'Align placement: golden',\n      (game, tester) async {\n        final stroke = Paint()\n          ..style = PaintingStyle.stroke\n          ..strokeWidth = 5.0\n          ..color = const Color(0xaaffff00);\n        game.add(\n          AlignComponent(\n            alignment: Anchor.center,\n            child: CircleComponent(radius: 20),\n          ),\n        );\n        for (final alignment in [\n          Anchor.topLeft,\n          Anchor.topRight,\n          Anchor.bottomLeft,\n          Anchor.bottomRight,\n        ]) {\n          game.add(\n            AlignComponent(\n              child: CircleComponent(\n                radius: 60,\n                paint: stroke,\n                anchor: Anchor.center,\n              ),\n              alignment: alignment,\n              keepChildAnchor: true,\n            ),\n          );\n        }\n      },\n      goldenFile: '../_goldens/align_component_1.png',\n      size: Vector2(150, 100),\n    );\n\n    testWithFlameGame(\n      \"Child's alignment remains valid when game resizes\",\n      (game) async {\n        final component = CircleComponent(radius: 20);\n        game.add(\n          AlignComponent(child: component, alignment: Anchor.center),\n        );\n        await game.ready();\n\n        expect(component.anchor, Anchor.center);\n        expect(component.position, Vector2(400, 300));\n        expect(component.size, Vector2.all(40));\n\n        game.onGameResize(Vector2(1000, 2000));\n        expect(component.position, Vector2(500, 1000));\n        expect(component.size, Vector2.all(40));\n      },\n    );\n\n    testWithFlameGame(\n      'Changing alignment value',\n      (game) async {\n        final component = CircleComponent(radius: 20);\n        final alignComponent = AlignComponent(\n          child: component,\n          alignment: Anchor.center,\n        );\n        game.add(alignComponent);\n        await game.ready();\n\n        expect(component.anchor, Anchor.center);\n        expect(component.position, Vector2(400, 300));\n\n        alignComponent.alignment = Anchor.bottomLeft;\n        expect(component.position, Vector2(0, 600));\n        expect(component.anchor, Anchor.bottomLeft);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/math/recycled_queue_test.dart",
    "content": "import 'dart:collection';\nimport 'dart:math';\n\nimport 'package:flame/src/components/core/recycled_queue.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('RecycledQueue', () {\n    test('simple writing/reading', () {\n      final queue = RecycledQueue(_Int.new);\n      expect(queue.isEmpty, true);\n      expect(queue.isNotEmpty, false);\n      expect(queue.length, 0);\n\n      queue.addLast().value = 1;\n      queue.addLast().value = 2;\n      queue.addLast().value = 3;\n      expect(queue.isEmpty, false);\n      expect(queue.isNotEmpty, true);\n      expect(queue.length, 3);\n      expect(queue.first.value, 1);\n      expect(queue.last.value, 3);\n\n      expect(queue.first.value, 1);\n      queue.removeFirst();\n      expect(queue.first.value, 2);\n      queue.removeFirst();\n      expect(queue.first.value, 3);\n      queue.removeFirst();\n      expect(queue.isEmpty, true);\n    });\n\n    test('accessing empty queue', () {\n      final queue = RecycledQueue(_Int.new);\n      expect(queue.isEmpty, true);\n      expect(\n        () => queue.first,\n        failsAssert('Cannot retrieve elements from an empty queue'),\n      );\n      expect(\n        () => queue.last,\n        failsAssert('Cannot retrieve elements from an empty queue'),\n      );\n      expect(\n        queue.removeFirst,\n        failsAssert('Cannot remove elements from an empty queue'),\n      );\n      expect(\n        queue.removeCurrent,\n        failsAssert('Cannot remove current element if not iterating'),\n      );\n    });\n\n    test('queue with no initial capacity', () {\n      final queue = RecycledQueue(_Int.new, initialCapacity: 0);\n      expect(queue.isEmpty, true);\n      queue.addLast().value = 42;\n      expect(queue.isEmpty, false);\n      expect(queue.first.value, 42);\n      expect(queue.last.value, 42);\n    });\n\n    test('queue with overflow', () {\n      final queue = RecycledQueue(_Int.new, initialCapacity: 4);\n      for (final x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) {\n        queue.addLast().value = x;\n      }\n      expect(queue.isEmpty, false);\n      expect(queue.length, 10);\n\n      for (var x = 1; x <= 10; x++) {\n        expect(queue.first, _Int(x));\n        queue.removeFirst();\n      }\n      expect(queue.isEmpty, true);\n\n      queue.addLast().value = -1;\n      expect(queue.first, _Int(-1));\n    });\n\n    test('queue with a hole', () {\n      final queue = RecycledQueue(_Int.new);\n      for (final x in [1, 2, 3, 4, 5, 6]) {\n        queue.addLast().value = x;\n      }\n      queue.removeFirst();\n      queue.removeFirst();\n      expect(queue.first.value, 3);\n\n      for (final x in [-1, -2, -3, -4, -5, -6, -7, -8]) {\n        queue.addLast().value = x;\n      }\n      expect(queue.length, 12);\n      for (final x in [3, 4, 5, 6, -1, -2, -3, -4, -5, -6, -7, -8]) {\n        expect(queue.first.value, x);\n        queue.removeFirst();\n      }\n      expect(queue.isEmpty, true);\n    });\n\n    testRandom('random attack: add/remove elements', (Random random) {\n      final queue0 = Queue<int>();\n      final queue1 = RecycledQueue(_Int.new);\n      var addAction = true;\n      while (true) {\n        expect(queue1.length, queue0.length);\n        final rnd = random.nextDouble();\n        if (rnd < 0.001) {\n          break;\n        }\n        if (rnd < 0.05) {\n          // change action with 5% probability\n          addAction = !addAction;\n        }\n        if (queue0.isEmpty || addAction) {\n          final x = random.nextInt(1024);\n          queue0.addLast(x);\n          queue1.addLast().value = x;\n        } else {\n          final x = queue0.removeFirst();\n          expect(queue1.first, _Int(x));\n          queue1.removeFirst();\n        }\n      }\n    });\n\n    group('iteration', () {\n      test('iterate over an empty queue', () {\n        final queue1 = RecycledQueue(_Int.new, initialCapacity: 0);\n        expect(queue1.toList(), <_Int>[]);\n        expect(queue1.toString(), 'RecycledQueue()');\n        final queue2 = RecycledQueue(_Int.new, initialCapacity: 4);\n        expect(queue2.toList(), <_Int>[]);\n      });\n\n      test('iterate over a single-element queue', () {\n        final queue = RecycledQueue(_Int.new, initialCapacity: 0);\n        queue.addLast().value = 13;\n        expect(queue.toList(), [_Int(13)]);\n        expect(queue.toString(), 'RecycledQueue(<13>)');\n      });\n\n      test('iterate over a simple queue', () {\n        final queue = RecycledQueue(_Int.new, initialCapacity: 0);\n        for (final x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) {\n          queue.addLast().value = x;\n        }\n        expect(queue.toList(), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(_Int.new));\n        expect(\n          queue.toString(),\n          'RecycledQueue(<1>, <2>, <3>, <4>, <5>, <6>, <7>, <8>, <9>, <10>)',\n        );\n      });\n\n      test('iterate over a queue with a gap', () {\n        final queue = RecycledQueue(_Int.new, initialCapacity: 2);\n        for (final x in [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5]) {\n          queue.addLast().value = x;\n        }\n        while (queue.first.value == 0) {\n          queue.removeFirst();\n        }\n        for (final x in [6, 7, 8, 9]) {\n          queue.addLast().value = x;\n        }\n        expect(queue.toList(), [1, 2, 3, 4, 5, 6, 7, 8, 9].map(_Int.new));\n      });\n\n      test('toString() while iterating', () {\n        final queue = RecycledQueue(_Int.new, initialCapacity: 0);\n        for (final x in [1, 2, 3, 4, 5]) {\n          queue.addLast().value = x;\n        }\n        var i = 1;\n        for (final x in queue) {\n          expect(x.value, i);\n          i += 1;\n          expect('$queue', 'RecycledQueue(<1>, <2>, <3>, <4>, <5>)');\n        }\n        expect(i, 6);\n      });\n\n      test('add elements while iterating', () {\n        final queue = RecycledQueue(_Int.new, initialCapacity: 3);\n        queue.addLast().value = 1;\n        queue.addLast().value = 1;\n        queue.removeFirst();\n        var i = 5;\n        for (final _ in queue) {\n          queue.addLast().value = i;\n          i += 1;\n          if (i > 10) {\n            break;\n          }\n        }\n        expect(queue.toList(), [1, 5, 6, 7, 8, 9, 10].map(_Int.new));\n      });\n\n      test('add elements while iterating a wrapped-around queue', () {\n        final queue = RecycledQueue(_Int.new, initialCapacity: 0);\n        queue.addLast().value = 1;\n        queue.addLast().value = 1;\n        queue.removeFirst();\n        queue.addLast().value = 2;\n        var i = 3;\n        for (final _ in queue) {\n          queue.addLast().value = i;\n          i += 1;\n          if (i > 6) {\n            break;\n          }\n        }\n        expect(queue.toList(), [1, 2, 3, 4, 5, 6].map(_Int.new));\n      });\n\n      test('remove elements while iterating', () {\n        final queue = RecycledQueue(_Int.new, initialCapacity: 0);\n        for (final x in [0, 0, 0, 0, 1, 2, 0, 3, 4, 5, 0, 0]) {\n          queue.addLast().value = x;\n        }\n        for (final element in queue) {\n          if (element.value == 0) {\n            queue.removeCurrent();\n          }\n        }\n        expect(queue.toList(), [1, 2, 3, 4, 5].map(_Int.new));\n      });\n\n      test('remove all elements while iterating', () {\n        final queue = RecycledQueue(_Int.new, initialCapacity: 0);\n        for (final x in [1, 2, 0, 3, 4, 5, 0, 0]) {\n          queue.addLast().value = x;\n        }\n        for (final _ in queue) {\n          queue.removeCurrent();\n        }\n        expect(queue.isEmpty, true);\n        expect(queue.isNotEmpty, false);\n        expect(queue.toList(), <_Int>[]);\n      });\n\n      test('remove all elements while iterating wrapped queue', () {\n        final queue = RecycledQueue(_Int.new, initialCapacity: 0);\n        queue.addLast().value = 1;\n        queue.addLast().value = 1;\n        queue.addLast().value = 1;\n        queue.removeFirst();\n        for (final x in [1, 2, 0, 3, 4, 5, 0, 0]) {\n          queue.addLast().value = x;\n        }\n        for (final _ in queue) {\n          queue.removeCurrent();\n        }\n        expect(queue.isEmpty, true);\n        expect(queue.isNotEmpty, false);\n        expect(queue.toList(), <_Int>[]);\n      });\n\n      test('remove almost all elements while iterating', () {\n        final queue = RecycledQueue(_Int.new, initialCapacity: 0);\n        for (final x in [-1, 1, 2, 0, 3, 4, 5, 0, 0]) {\n          queue.addLast().value = x;\n        }\n        for (final x in queue) {\n          if (x.value! >= 0) {\n            queue.removeCurrent();\n          }\n        }\n        expect(queue.toList(), [_Int(-1)]);\n      });\n\n      test('iterate and remove almost all elements from wrapped queue', () {\n        final queue = RecycledQueue(_Int.new, initialCapacity: 0);\n        queue.addLast().value = -2;\n        queue.addLast().value = -2;\n        queue.removeFirst();\n        for (final x in [-1, 1, 2, 0, 3, 4, 5, 0, 0]) {\n          queue.addLast().value = x;\n        }\n        for (final x in queue) {\n          if (x.value! >= 0) {\n            queue.removeCurrent();\n          }\n        }\n        expect(queue.toList(), [_Int(-2), _Int(-1)]);\n        for (final x in queue) {\n          if (x.value! == -1) {\n            queue.removeCurrent();\n          }\n        }\n        expect(queue.toList(), [_Int(-2)]);\n      });\n\n      test('removeFirst() while iterating breaks iteration', () {\n        final queue = RecycledQueue(_Int.new);\n        for (final x in [1, 2, 3, 4, 5]) {\n          queue.addLast().value = x;\n        }\n        for (final x in queue) {\n          expect(x.value, 1);\n          if (x.value == 1) {\n            queue.removeFirst();\n          }\n        }\n      });\n\n      test('add and remove elements while iterating', () {\n        final queue = RecycledQueue(_Int.new, initialCapacity: 0);\n        queue.addLast().value = 1;\n        queue.addLast().value = 2;\n        queue.addLast().value = 1;\n        queue.removeFirst();\n        queue.addLast().value = 1;\n        var i = 3;\n        expect('$queue', 'RecycledQueue(<2>, <1>, <1>)');\n        for (final x in queue) {\n          expect(x, queue.current);\n          if (i <= 10) {\n            queue.addLast().value = i;\n            i += 1;\n          }\n          if (x.value!.isOdd) {\n            queue.removeCurrent();\n          }\n          if (i == 4) {\n            expect(x.value, 2);\n            expect('$queue', 'RecycledQueue(<2>, <1>, <1>, <3>)');\n          }\n          if (i == 5) {\n            expect(x.value, null);\n            expect('$queue', 'RecycledQueue(<2>, <null>, <1>, <3>, <4>)');\n          }\n        }\n        expect(queue.toList(), [2, 4, 6, 8, 10].map(_Int.new));\n        expect(queue.toString(), 'RecycledQueue(<2>, <4>, <6>, <8>, <10>)');\n      });\n\n      testRandom('random attack: add/remove while iterating', (Random random) {\n        final queue0 = Queue<int>();\n        final queue1 = RecycledQueue(_Int.new);\n        var nextNumberToAdd = 1;\n        while (true) {\n          if (random.nextDouble() < 0.01) {\n            break;\n          }\n          if (queue0.isEmpty && queue1.isEmpty) {\n            for (var i = 0; i < 10; i++) {\n              queue0.addLast(nextNumberToAdd);\n              queue1.addLast().value = nextNumberToAdd;\n              nextNumberToAdd += 1;\n            }\n          }\n          var it0 = queue0.iterator;\n          for (final item1 in queue1) {\n            expect(it0.moveNext(), true);\n            final item0 = it0.current;\n            expect(item0, item1.value);\n            final rnd = random.nextDouble();\n            if (rnd < 0.2) {\n              queue0.addLast(nextNumberToAdd);\n              queue1.addLast().value = nextNumberToAdd;\n              nextNumberToAdd += 1;\n              it0 = queue0.iterator;\n              expect(it0.moveNext(), true);\n              while (it0.current != item0) {\n                it0.moveNext();\n              }\n            } else if (rnd < 0.4) {\n              final index = queue0.toList().indexOf(item0);\n              queue0.remove(item0);\n              queue1.removeCurrent();\n              it0 = queue0.skip(index).iterator;\n            }\n          }\n          expect(it0.moveNext(), false);\n        }\n      });\n    });\n  });\n}\n\nclass _Int implements Disposable {\n  _Int([this.value]);\n\n  int? value;\n\n  @override\n  void dispose() => value = null;\n\n  @override\n  String toString() => '<$value>';\n\n  // ignore_for_file: hash_and_equals\n  // ignore_for_file: avoid_equals_and_hash_code_on_mutable_classes\n  @override\n  bool operator ==(Object other) => other is _Int && other.value == value;\n}\n"
  },
  {
    "path": "packages/flame/test/math/solve_cubic_test.dart",
    "content": "import 'package:flame/math.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('solveCubic', () {\n    const repeatCount = 3;\n\n    testRandom(\n      'solve equation with 3 roots',\n      (rnd) {\n        final x1 = rnd.nextDouble() * 5 - 1;\n        final x2 = rnd.nextDouble() * 0.3 - 0.2;\n        final x3 = rnd.nextDouble() * 2 - 1;\n        // a(x - x1)(x - x2)(x - x3) == 0\n        final a = rnd.nextDouble() + 1e-6;\n        final b = -(x1 + x2 + x3) * a;\n        final c = (x1 * x2 + x2 * x3 + x3 * x1) * a;\n        final d = -x1 * x2 * x3 * a;\n        final solutions = solveCubic(a, b, c, d);\n        if (((x1 - x2) * (x2 - x3) * (x3 - x1)).abs() > 1e-5) {\n          _check(solutions, [x1, x2, x3]);\n        }\n      },\n      repeatCount: repeatCount,\n    );\n\n    testRandom(\n      'solve equation with 2 roots',\n      (rnd) {\n        final x1 = rnd.nextDouble() * 5 - 1;\n        final x2 = rnd.nextDouble() * 0.3 - 0.2;\n        // a(x - x1)(x - x2)² == 0\n        final a = rnd.nextDouble();\n        final b = -(x1 + x2 + x2) * a;\n        final c = (2 * x1 * x2 + x2 * x2) * a;\n        final d = -x1 * x2 * x2 * a;\n        final solutions = solveCubic(a, b, c, d);\n        if (solutions.length == 1) {\n          _check(solutions, [x1]);\n        } else {\n          _check(solutions, [x1, x2, x2]);\n        }\n      },\n      repeatCount: repeatCount,\n    );\n\n    test('solve equation with 1 triple root', () {\n      _check(solveCubic(1, -3, 3, -1), [1, 1, 1]);\n      _check(solveCubic(10, 30, 30, 10), [-1, -1, -1]);\n      const x = 2.78;\n      _check(solveCubic(1, -3 * x, 3 * x * x, -x * x * x), [x, x, x]);\n    });\n\n    testRandom(\n      'solve equation with 1 real root',\n      (rnd) {\n        final x1 = rnd.nextDouble() * 5 - 1;\n        final x2 = rnd.nextDouble() * 0.3 - 0.2;\n        // a(x - x1)((x - x2)² + 0.5) == 0\n        final a = rnd.nextDouble();\n        final b = -(x1 + 2 * x2) * a;\n        final c = (2 * x1 * x2 + x2 * x2 + 0.5) * a;\n        final d = -x1 * (x2 * x2 + 0.5) * a;\n        final solutions = solveCubic(a, b, c, d);\n        _check(solutions, [x1]);\n      },\n      repeatCount: repeatCount,\n    );\n\n    test('solve degenerate equation', () {\n      _check(solveCubic(0, 1, 2, 1), [-1, -1]);\n      _check(solveCubic(0, 0, 1, 3), [-3]);\n    });\n\n    test('solve depressed equation', () {\n      _check(solveCubic(1, 0, -7, 6), [1, 2, -3]);\n      _check(solveCubic(0.1, 0, -0.7, -0.6), [-1, -2, 3]);\n    });\n\n    testRandom(\n      'solve random equation',\n      (rnd) {\n        final a = rnd.nextDouble();\n        final b = rnd.nextDouble() * 2;\n        final c = rnd.nextDouble() * 4;\n        final d = rnd.nextDouble() * 6;\n        final solutions = solveCubic(a, b, c, d);\n        for (final x in solutions) {\n          expect(a * x * x * x + b * x * x + c * x + d, closeTo(0, 1e-6));\n        }\n      },\n      repeatCount: repeatCount,\n    );\n  });\n}\n\nvoid _check(List<double> list1, List<double> list2) {\n  expect(\n    list1.length,\n    equals(list2.length),\n    reason: 'solutions are: $list1 vs $list2',\n  );\n  list1.sort();\n  list2.sort();\n  for (var i = 0; i < list1.length; i++) {\n    expect(list1[i], closeTo(list2[i], 1e-6));\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/math/solve_quadratic_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/math.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('solveQuadratic', () {\n    const repeatCount = 3;\n\n    testRandom(\n      'solve equation with 2 roots',\n      (Random rnd) {\n        final x1 = rnd.nextDouble() * 5 - 1;\n        final x2 = rnd.nextDouble() * 0.3 - 0.2;\n        final a = rnd.nextDouble();\n        final b = (-x1 - x2) * a;\n        final c = x1 * x2 * a;\n        final solutions = solveQuadratic(a, b, c)..sort();\n        expect(solutions.length, 2);\n        expect(solutions[0], closeTo(min(x1, x2), 1e-10));\n        expect(solutions[1], closeTo(max(x1, x2), 1e-10));\n      },\n      repeatCount: repeatCount,\n    );\n\n    testRandom(\n      'solve equation with 1 double root',\n      (Random rnd) {\n        final x1 = rnd.nextDouble();\n        final b = -2 * x1;\n        final c = x1 * x1;\n        final solutions = solveQuadratic(1, b, c);\n        expect(solutions.length, 2);\n        expect(solutions[0], closeTo(x1, 1e-10));\n        expect(solutions[1], closeTo(x1, 1e-10));\n      },\n      repeatCount: repeatCount,\n    );\n\n    testRandom(\n      'solve equation with no roots',\n      (Random rnd) {\n        final x1 = rnd.nextDouble();\n        final b = -2 * x1;\n        final c = x1 * x1 + rnd.nextDouble().abs() * 0.1;\n        final solutions = solveQuadratic(1, b, c);\n        expect(solutions.length, 0);\n      },\n      repeatCount: repeatCount,\n    );\n\n    testRandom(\n      'solve random equation',\n      (Random rnd) {\n        final a = rnd.nextDouble();\n        final b = rnd.nextDouble() * 2;\n        final c = rnd.nextDouble() * 4;\n        final solutions = solveQuadratic(a, b, c);\n        for (final x in solutions) {\n          expect(a * x * x + b * x + c, closeTo(0, 1e-15 / a));\n        }\n      },\n      repeatCount: repeatCount,\n    );\n\n    test('solve degenerate equations', () {\n      expect(solveQuadratic(0, 1, 5), [-5]);\n      expect(solveQuadratic(0, -2, 5), [2.5]);\n      expect(solveQuadratic(1e-314, -2, 5), [0, double.infinity]);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/nine_tile_box_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '_resources/load_image.dart';\n\nvoid main() {\n  group('NineTileBox', () {\n    testGolden(\n      'Render with default grid',\n      (game, tester) async {\n        game.add(_MyComponent1());\n      },\n      size: Vector2(300, 200),\n      goldenFile: '_goldens/nine_tile_box_test_1.png',\n    );\n\n    testGolden(\n      'Render with specified grid',\n      (game, tester) async {\n        game.add(_MyComponent2());\n      },\n      size: Vector2(300, 200),\n      goldenFile: '_goldens/nine_tile_box_test_2.png',\n    );\n\n    test('default tile sizes calculated correctly', () async {\n      final sprite = Sprite(await loadImage('speech-bubble-1.png'));\n      final nineTileBox = NineTileBox(sprite);\n\n      expect(nineTileBox.tileSize, equals(30));\n      expect(nineTileBox.destTileSize, equals(30));\n    });\n\n    test('tile sizes set correctly', () async {\n      final sprite = Sprite(await loadImage('speech-bubble-1.png'));\n      final nineTileBox = NineTileBox(sprite, tileSize: 20, destTileSize: 25);\n\n      expect(nineTileBox.tileSize, equals(20));\n      expect(nineTileBox.destTileSize, equals(25));\n    });\n\n    test('grid sizes set correctly', () async {\n      final sprite = Sprite(await loadImage('speech-bubble-2.png'));\n      final nineTileBox = NineTileBox.withGrid(\n        sprite,\n        leftWidth: 31,\n        rightWidth: 5,\n        topHeight: 5,\n        bottomHeight: 21,\n      );\n\n      expect(nineTileBox.center.left, equals(31.0));\n      expect(nineTileBox.center.right, equals(34.0));\n      expect(nineTileBox.center.top, equals(5.0));\n      expect(nineTileBox.center.bottom, equals(18.0));\n    });\n  });\n}\n\nclass _MyComponent1 extends PositionComponent {\n  _MyComponent1() : super(size: Vector2(300, 200));\n  late final Sprite sprite;\n  late final NineTileBox nineTileBox;\n  final bgPaint = Paint()..color = const Color.fromARGB(255, 57, 113, 158);\n\n  @override\n  Future<void> onLoad() async {\n    sprite = Sprite(await loadImage('speech-bubble-1.png'));\n    nineTileBox = NineTileBox(sprite);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(\n      size.toRect(),\n      bgPaint,\n    );\n    nineTileBox.draw(canvas, Vector2(25, 25), Vector2(250, 150));\n  }\n}\n\nclass _MyComponent2 extends PositionComponent {\n  _MyComponent2() : super(size: Vector2(300, 200));\n  late final Sprite sprite;\n  late final NineTileBox nineTileBox;\n  final bgPaint = Paint()..color = const Color.fromARGB(255, 57, 113, 158);\n\n  @override\n  Future<void> onLoad() async {\n    sprite = Sprite(await loadImage('speech-bubble-2.png'));\n    nineTileBox = NineTileBox.withGrid(\n      sprite,\n      leftWidth: 31,\n      rightWidth: 5,\n      topHeight: 5,\n      bottomHeight: 21,\n    );\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(\n      size.toRect(),\n      bgPaint,\n    );\n    nineTileBox.draw(canvas, Vector2(25, 25), Vector2(250, 150));\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/particles/circle_particle_test.dart",
    "content": "import 'package:canvas_test/canvas_test.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/particles.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass _MockParticle extends Mock implements CircleParticle {}\n\nvoid main() {\n  group('CircleParticle', () {\n    test('Should render this Particle to given Canvas', () {\n      final particle = _MockParticle();\n\n      final canvas = MockCanvas();\n      ParticleSystemComponent(particle: particle).render(canvas);\n\n      verify(() => particle.render(canvas)).called(1);\n    });\n\n    testWithFlameGame(\n      'Consider composing this with other Particle to achieve needed effects',\n      (game) async {\n        final childParticle = CircleParticle(\n          paint: Paint()..color = Colors.red,\n          lifespan: 2,\n        );\n\n        final component = ParticleSystemComponent(\n          particle: childParticle,\n        );\n\n        game.add(component);\n        await game.ready();\n        game.update(1);\n\n        expect(childParticle.progress, 0.5);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/particles/component_particle_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/particles.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('ComponentParticle', () {\n    test(\n      'CircleComponent renders without error when used outside of the '\n      'component lifecycle',\n      () {\n        final particle = ComponentParticle(\n          component: CircleComponent(radius: 2),\n          lifespan: 1,\n        );\n\n        expect(\n          () => particle.render(Canvas(PictureRecorder())),\n          returnsNormally,\n        );\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/particles/composed_particle_test.dart",
    "content": "import 'package:flame/particles.dart';\nimport 'package:flame/src/components/particle_system_component.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('ComposedParticle', () {\n    testWithFlameGame('particles with parent lifespan applied to children', (\n      game,\n    ) async {\n      final childParticle1 = CircleParticle(\n        paint: Paint()..color = Colors.red,\n        lifespan: 1,\n      );\n      final childParticle2 = CircleParticle(\n        paint: Paint()..color = Colors.red,\n        lifespan: 3,\n      );\n\n      final particle = ComposedParticle(\n        children: [\n          childParticle1,\n          childParticle2,\n        ],\n        lifespan: 2,\n      );\n\n      final component = ParticleSystemComponent(\n        particle: particle,\n      );\n\n      game.add(component);\n      await game.ready();\n      game.update(1);\n\n      expect(particle.progress, 0.5);\n      expect(childParticle1.progress, 0.5);\n      expect(childParticle2.progress, 0.5);\n      expect(particle.children.length, 2);\n\n      game.update(1);\n\n      expect(particle.progress, 1);\n      expect(childParticle1.progress, 1);\n      expect(childParticle2.progress, 1);\n      expect(particle.children.length, 2);\n    });\n\n    testWithFlameGame('particles without parent lifespan applied to children', (\n      game,\n    ) async {\n      final childParticle1 = CircleParticle(\n        paint: Paint()..color = Colors.red,\n        lifespan: 1,\n      );\n      final childParticle2 = CircleParticle(\n        paint: Paint()..color = Colors.red,\n        lifespan: 4,\n      );\n\n      final particle = ComposedParticle(\n        children: [\n          childParticle1,\n          childParticle2,\n        ],\n        lifespan: 2,\n        applyLifespanToChildren: false,\n      );\n\n      final component = ParticleSystemComponent(\n        particle: particle,\n      );\n\n      game.add(component);\n      await game.ready();\n      game.update(1);\n\n      expect(particle.progress, 0.5);\n      expect(childParticle1.progress, 1);\n      expect(childParticle2.progress, 0.25);\n      expect(particle.children.length, 2);\n\n      game.update(1);\n\n      expect(particle.progress, 1);\n      expect(childParticle1.progress, 1);\n      expect(childParticle2.progress, 0.5);\n      expect(particle.children.length, 1);\n    });\n\n    testWithFlameGame(\n      'generate particles without parent lifespan applied to children',\n      (game) async {\n        const particlesCount = 15;\n        final component = ParticleSystemComponent(\n          particle: Particle.generate(\n            count: particlesCount,\n            generator: (i) {\n              return CircleParticle(\n                paint: Paint()..color = Colors.red,\n                lifespan: 5,\n              );\n            },\n            applyLifespanToChildren: false,\n            lifespan: 10,\n          ),\n        );\n\n        game.add(component);\n        await game.ready();\n        game.update(1);\n\n        expect(component.particle!.progress, 0.1);\n        final children1 = (component.particle! as ComposedParticle).children;\n        expect(children1.length, particlesCount);\n        for (final child in children1) {\n          expect(child.progress, 0.2);\n        }\n\n        game.update(1);\n\n        expect(component.particle!.progress, 0.2);\n        final children2 = (component.particle! as ComposedParticle).children;\n        expect(children2.length, particlesCount);\n        for (final child in children2) {\n          expect(child.progress, 0.4);\n        }\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/particles/computed_particle_test.dart",
    "content": "import 'package:flame/particles.dart';\nimport 'package:flame/src/components/particle_system_component.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('ComputedParticle', () {\n    testWithFlameGame(\n      'Particle container which delegates rendering particle on each frame',\n      (game) async {\n        final cellSize = game.size / 5.0;\n        final halfCellSize = cellSize / 2;\n\n        final particle = ComputedParticle(\n          renderer: (canvas, particle) {\n            canvas.drawCircle(\n              Offset.zero,\n              particle.progress * halfCellSize.x,\n              Paint()\n                ..color = Color.lerp(\n                  Colors.red,\n                  Colors.blue,\n                  particle.progress,\n                )!,\n            );\n          },\n          lifespan: 2,\n        );\n\n        final component = ParticleSystemComponent(\n          particle: particle,\n        );\n\n        game.add(component);\n        await game.ready();\n        game.update(1);\n        expect(particle.progress, 0.5);\n\n        game.update(1);\n        expect(particle.progress, 1);\n      },\n    );\n\n    testWithFlameGame('Particle to use custom tweening', (game) async {\n      final cellSize = game.size / 5.0;\n      final halfCellSize = cellSize / 2;\n      final steppedTween = StepTween(begin: 0, end: 5);\n\n      final particle = ComputedParticle(\n        lifespan: 2,\n        renderer: (canvas, particle) {\n          const steps = 5;\n          final steppedProgress =\n              steppedTween.transform(particle.progress) / steps;\n\n          canvas.drawCircle(\n            Offset.zero,\n            (1 - steppedProgress) * halfCellSize.x,\n            Paint()\n              ..color = Color.lerp(\n                Colors.red,\n                Colors.blue,\n                steppedProgress,\n              )!,\n          );\n        },\n      );\n\n      final component = ParticleSystemComponent(\n        particle: particle,\n      );\n\n      game.add(component);\n      await game.ready();\n      game.update(1);\n      expect(particle.progress, 0.5);\n\n      game.update(1);\n      expect(particle.progress, 1);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/particles/curved_particle_test.dart",
    "content": "import 'package:flame/src/components/particle_system_component.dart';\nimport 'package:flame/src/particles/curved_particle.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('CurvedParticle', () {\n    test('A Particle which applies certain Curve', () {\n      final particle = CurvedParticle();\n\n      expect(particle.curve, Curves.linear);\n    });\n\n    test('A Particle which applies certain Curve', () {\n      final particle = CurvedParticle(curve: Curves.decelerate);\n\n      expect(particle.curve, Curves.decelerate);\n    });\n\n    testWithFlameGame(\n      'A Particle which applies certain Curve for easing or other purposes'\n      ' to its progress getter.',\n      (game) async {\n        final particle = CurvedParticle(lifespan: 2);\n        final component = ParticleSystemComponent(\n          particle: particle,\n        );\n\n        game.add(component);\n        await game.ready();\n        game.update(1);\n\n        expect(particle.curve, Curves.linear);\n        expect(particle.progress, 0.5);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/particles/moving_particle_test.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/components/particle_system_component.dart';\nimport 'package:flame/src/particles/circle_particle.dart';\nimport 'package:flame/src/particles/moving_particle.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('MovingParticle', () {\n    testWithFlameGame(\n      'Particle which is moving from one predefined position to another one',\n      (game) async {\n        final childParticle = CircleParticle(\n          paint: Paint()..color = Colors.red,\n          lifespan: 2,\n        );\n\n        final particle = MovingParticle(\n          from: Vector2(-20, -20),\n          to: Vector2(20, 20),\n          child: childParticle,\n        );\n\n        final component = ParticleSystemComponent(\n          particle: particle,\n        );\n\n        game.add(component);\n        await game.ready();\n        game.update(1);\n        expect(particle.progress, 1.0);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/particles/scaled_particle_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/src/components/particle_system_component.dart';\nimport 'package:flame/src/particles/computed_particle.dart';\nimport 'package:flame/src/particles/rotating_particle.dart';\nimport 'package:flame/src/particles/scaled_particle.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('ScaledParticle', () {\n    testWithFlameGame(\n      'A particle which rotates its child over the lifespan between two '\n      'given bounds in radians',\n      (game) async {\n        final paint = Paint()..color = Colors.red;\n        final rect = ComputedParticle(\n          renderer: (canvas, _) => canvas.drawRect(\n            Rect.fromCenter(center: Offset.zero, width: 10, height: 10),\n            paint,\n          ),\n        );\n\n        final particle = ScaledParticle(\n          lifespan: 2,\n          child: rect.rotating(to: pi / 2),\n        );\n\n        final component = ParticleSystemComponent(\n          particle: particle,\n        );\n\n        game.add(component);\n        await game.ready();\n        game.update(1);\n\n        expect(particle.scale, 1.0);\n        expect(particle.child, isInstanceOf<RotatingParticle>());\n        expect(particle.child.progress, 0.5);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/particles/scaling_particle_test.dart",
    "content": "import 'package:flame/src/components/particle_system_component.dart';\nimport 'package:flame/src/particles/computed_particle.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('ScalingParticle', () {\n    testWithFlameGame('A particle which scale its child over the lifespan '\n        'between 1 and a provided scale', (game) async {\n      final paint = Paint()..color = Colors.red;\n      final rect = ComputedParticle(\n        lifespan: 2,\n        renderer: (canvas, _) => canvas.drawRect(\n          Rect.fromCenter(center: Offset.zero, width: 10, height: 10),\n          paint,\n        ),\n      );\n\n      final particle = rect.scaling(to: 0.5, curve: Curves.easeIn);\n\n      final component = ParticleSystemComponent(\n        particle: particle,\n      );\n\n      game.add(component);\n      await game.ready();\n      game.update(1);\n\n      expect(particle.scale, 0.841796875);\n      expect(particle.curve, Curves.easeIn);\n      expect(particle.progress, 0.31640625);\n      expect(particle.child, isInstanceOf<ComputedParticle>());\n      expect(particle.child.progress, 0.5);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/particles/sprite_particle_test.dart",
    "content": "import 'package:canvas_test/canvas_test.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/src/components/particle_system_component.dart';\nimport 'package:flame/src/particles/sprite_particle.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass _MockParticle extends Mock implements SpriteParticle {}\n\nFuture<void> main() async {\n  // Generate an image\n  final image = await generateImage();\n\n  group('SpriteParticle', () {\n    test('Should render this Particle to given Canvas', () {\n      final particle = _MockParticle();\n\n      final canvas = MockCanvas();\n      ParticleSystemComponent(particle: particle).render(canvas);\n\n      verify(() => particle.render(canvas)).called(1);\n    });\n\n    final sprite = Sprite(image);\n    testWithFlameGame(\n      'SpriteParticle allows easily embed Flames Sprite into the effect',\n      (game) async {\n        final particle = SpriteParticle(\n          sprite: sprite,\n          size: Vector2(50, 50),\n          lifespan: 2,\n        );\n\n        final component = ParticleSystemComponent(\n          particle: particle,\n        );\n\n        game.add(component);\n        await game.ready();\n        game.update(1);\n        expect(particle.progress, 0.5);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/post_process/post_process_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:canvas_test/canvas_test.dart';\nimport 'package:flame/post_process.dart';\nimport 'package:test/test.dart';\nimport 'package:vector_math/vector_math.dart';\n\nclass _CallbackPostProcess extends PostProcess {\n  _CallbackPostProcess(this.callback);\n\n  final void Function() callback;\n\n  @override\n  void postProcess(Vector2 size, Canvas canvas) {\n    renderSubtree(canvas);\n    callback();\n  }\n}\n\nvoid main() {\n  group('$PostProcessSequentialGroup', () {\n    test('render in canvas in sequence', () {\n      final calls = <String>[];\n      final postProcess1 = _CallbackPostProcess(() => calls.add('1'));\n      final postProcess2 = _CallbackPostProcess(() => calls.add('2'));\n      final postProcess3 = _CallbackPostProcess(() => calls.add('3'));\n      final postProcess = PostProcessSequentialGroup(\n        postProcesses: [\n          postProcess1,\n          postProcess2,\n          postProcess3,\n        ],\n      );\n\n      postProcess.render(\n        MockCanvas(),\n        Vector2(0, 0),\n        (canvas) {\n          calls.add('render');\n        },\n        (postProcess) {},\n      );\n\n      expect(calls, [\n        'render',\n        '1',\n        '2',\n        '3',\n      ]);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/raster_sprite_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/cache.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '_resources/load_image.dart';\n\nvoid main() {\n  group('Raster Sprite', () {\n    group('rasterize sprite', () {\n      testGolden(\n        'still renders correctly',\n        (game, tester) async {\n          game.add(_MyRasterComponent()..position = Vector2.all(25));\n        },\n        goldenFile: '_goldens/sprite_test_1.png',\n      );\n    });\n\n    test('adds the image to the cache', () async {\n      final images = Images();\n      expect(images.keys, isEmpty);\n\n      final image = await loadImage('flame.png');\n      images.add('flame.png', image);\n\n      final baseSprite = await Sprite.load(\n        'flame.png',\n        images: images,\n      );\n      await baseSprite.rasterize(images: images);\n\n      expect(images.keys, isNotEmpty);\n\n      // A second rasterization with the same image should not add a new entry\n      final secondBaseSprite = await Sprite.load(\n        'flame.png',\n        images: images,\n      );\n      await secondBaseSprite.rasterize(images: images);\n      expect(images.keys, hasLength(2));\n    });\n\n    group('when using a custom cache key', () {\n      test('adds the image to the cache with the custom key', () async {\n        final images = Images();\n        expect(images.keys, isEmpty);\n\n        final image = await loadImage('flame.png');\n        images.add('flame.png', image);\n\n        final baseSprite = await Sprite.load(\n          'flame.png',\n          images: images,\n        );\n        const customKey = 'custom_key';\n        await baseSprite.rasterize(cacheKey: customKey, images: images);\n\n        expect(images.keys, contains(customKey));\n\n        // A second rasterization with the same image and custom key should not\n        // add a new entry\n        await baseSprite.rasterize(cacheKey: customKey, images: images);\n        expect(images.keys, hasLength(2));\n      });\n    });\n  });\n}\n\nclass _MyRasterComponent extends PositionComponent {\n  _MyRasterComponent() : super(size: Vector2(200, 400));\n  late final Sprite sprite;\n\n  @override\n  Future<void> onLoad() async {\n    final baseSprite = Sprite(await loadImage('flame.png'));\n    sprite = await baseSprite.rasterize();\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(\n      size.toRect(),\n      Paint()\n        ..color = const Color(0xffffffff)\n        ..style = PaintingStyle.stroke\n        ..strokeWidth = 2,\n    );\n    // Expected: sprite is rendered in the center of the rect\n    sprite.render(canvas, position: size / 2, anchor: Anchor.center);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/rendering/hue_decorator_test.dart",
    "content": "import 'dart:math' as math;\nimport 'dart:ui';\nimport 'package:flame/rendering.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('HueDecorator', () {\n    test('can be instantiated', () {\n      final decorator = HueDecorator();\n      expect(decorator.hue, 0.0);\n    });\n\n    test('hue property updates correctly', () {\n      final decorator = HueDecorator();\n      decorator.hue = math.pi;\n      expect(decorator.hue, math.pi);\n    });\n\n    test('apply with hue 0 does not use saveLayer', () {\n      final decorator = HueDecorator();\n      var drawCalled = false;\n      final canvas = _MockCanvas();\n\n      decorator.apply((c) => drawCalled = true, canvas);\n\n      expect(drawCalled, isTrue);\n      expect(canvas.saveLayerCalled, isFalse);\n    });\n\n    test('apply with non-zero hue uses saveLayer', () {\n      final decorator = HueDecorator(hue: math.pi / 2);\n      var drawCalled = false;\n      final canvas = _MockCanvas();\n\n      decorator.apply((c) => drawCalled = true, canvas);\n\n      expect(drawCalled, isTrue);\n      expect(canvas.saveLayerCalled, isTrue);\n    });\n  });\n}\n\nclass _MockCanvas extends Fake implements Canvas {\n  bool saveLayerCalled = false;\n  bool restoreCalled = false;\n\n  @override\n  void saveLayer(Rect? bounds, Paint paint) {\n    saveLayerCalled = true;\n  }\n\n  @override\n  void restore() {\n    restoreCalled = true;\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/rendering/paint_decorator_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/rendering.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../_resources/load_image.dart';\n\nvoid main() {\n  group('PaintDecorator', () {\n    testGolden(\n      'blur effect',\n      (game, tester) async {\n        final image = await loadImage('flame.png');\n        game.addAll([\n          SpriteComponent(sprite: Sprite(image)),\n          _DecoratedSprite(\n            sprite: Sprite(image),\n            decorator: PaintDecorator.blur(0, 10),\n            position: Vector2(150, 0),\n          ),\n        ]);\n      },\n      size: Vector2(300, 220),\n      goldenFile: '../_goldens/paint_decorator_blur.png',\n    );\n\n    testGolden(\n      'grayscale effect',\n      (game, tester) async {\n        final image = await loadImage('flame.png');\n        game.addAll([\n          SpriteComponent(sprite: Sprite(image)),\n          _DecoratedSprite(\n            sprite: Sprite(image),\n            decorator: PaintDecorator.grayscale(),\n            position: Vector2(150, 0),\n          ),\n          _DecoratedSprite(\n            sprite: Sprite(image),\n            decorator: PaintDecorator.grayscale(opacity: 0.5),\n            position: Vector2(300, 0),\n          ),\n          _DecoratedSprite(\n            sprite: Sprite(image),\n            decorator: PaintDecorator.grayscale(opacity: 0.25),\n            position: Vector2(450, 0),\n          ),\n        ]);\n      },\n      size: Vector2(600, 220),\n      goldenFile: '../_goldens/paint_decorator_grayscale.png',\n    );\n\n    testGolden(\n      'tint effect',\n      (game, tester) async {\n        final image = await loadImage('zz_guitar.png');\n        game.addAll([\n          SpriteComponent(sprite: Sprite(image)),\n          _DecoratedSprite(\n            sprite: Sprite(image),\n            decorator: PaintDecorator.tint(const Color(0x8800FF00)),\n            position: Vector2(100, 0),\n          ),\n          _DecoratedSprite(\n            sprite: Sprite(image),\n            decorator: PaintDecorator.tint(const Color(0x880000FF)),\n            position: Vector2(200, 0),\n          ),\n          _DecoratedSprite(\n            sprite: Sprite(image),\n            decorator: PaintDecorator.tint(const Color(0xAAFFFFFF)),\n            position: Vector2(300, 0),\n          ),\n        ]);\n      },\n      size: Vector2(400, 300),\n      goldenFile: '../_goldens/paint_decorator_tinted.png',\n    );\n\n    testGolden(\n      'grayscale/tinted with blur',\n      (game, tester) async {\n        final image = await loadImage('zz_guitar.png');\n        const color = Color(0x88EBFF7F);\n        game.addAll([\n          SpriteComponent(sprite: Sprite(image)),\n          _DecoratedSprite(\n            sprite: Sprite(image),\n            decorator: PaintDecorator.grayscale()..addBlur(3),\n            position: Vector2(100, 0),\n          ),\n          _DecoratedSprite(\n            sprite: Sprite(image),\n            decorator: PaintDecorator.tint(color)..addBlur(3),\n            position: Vector2(200, 0),\n          ),\n        ]);\n      },\n      size: Vector2(300, 300),\n      goldenFile: '../_goldens/paint_decorator_with_blur.png',\n    );\n  });\n}\n\nclass _DecoratedSprite extends SpriteComponent {\n  _DecoratedSprite({\n    required Decorator decorator,\n    super.sprite,\n    super.position,\n  }) {\n    this.decorator.addLast(decorator);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/rendering/rotate3d_decorator_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/rendering.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Rotate3DDecorator', () {\n    testGolden(\n      'Rotation around X axis',\n      (game, tester) async {\n        for (var angle = 0.0; angle <= 1.5; angle += 0.5) {\n          game.add(\n            _DecoratedRectangle(\n              position: Vector2(20, 30),\n              size: Vector2(60, 100),\n              paint: Paint()..color = const Color(0x9dde0445),\n              decorator: Rotate3DDecorator(\n                center: Vector2(30, 50),\n                angleX: angle,\n                perspective: 0.005,\n              ),\n            ),\n          );\n        }\n      },\n      size: Vector2(100, 160),\n      goldenFile: '../_goldens/rotate3d_decorator_1.png',\n    );\n\n    testGolden(\n      'Rotation around Y axis',\n      (game, tester) async {\n        for (var angle = 0.0; angle <= 1.5; angle += 0.5) {\n          game.add(\n            _DecoratedRectangle(\n              position: Vector2(20, 30),\n              size: Vector2(60, 100),\n              paint: Paint()..color = const Color(0x9dde0445),\n              decorator: Rotate3DDecorator(\n                center: Vector2(30, 50),\n                angleY: angle,\n                perspective: 0.005,\n              ),\n            ),\n          );\n        }\n      },\n      size: Vector2(100, 160),\n      goldenFile: '../_goldens/rotate3d_decorator_2.png',\n    );\n\n    testGolden(\n      'Rotation around all axes',\n      (game, tester) async {\n        game.add(\n          _DecoratedRectangle(\n            position: Vector2(20, 30),\n            size: Vector2(60, 100),\n            paint: Paint()..color = const Color(0xff199f2b),\n            decorator: Rotate3DDecorator(\n              center: Vector2(30, 50),\n              angleX: 0.7,\n              angleY: 1.0,\n              angleZ: 0.5,\n              perspective: 0.005,\n            ),\n          ),\n        );\n      },\n      size: Vector2(100, 160),\n      goldenFile: '../_goldens/rotate3d_decorator_3.png',\n    );\n\n    test('isFlipped', () {\n      final decorator = Rotate3DDecorator();\n      expect(decorator.isFlipped, false);\n      decorator.angleZ = 2.0;\n      expect(decorator.isFlipped, false);\n      decorator.angleX = 2.0;\n      expect(decorator.isFlipped, true);\n      decorator.angleY = 2.0;\n      expect(decorator.isFlipped, false);\n      decorator.angleY = -0.5;\n      expect(decorator.isFlipped, true);\n      decorator.angleY = -1.5;\n      expect(decorator.isFlipped, true);\n      decorator.angleY = -1.6;\n      expect(decorator.isFlipped, false);\n    });\n  });\n}\n\nclass _DecoratedRectangle extends RectangleComponent {\n  _DecoratedRectangle({\n    super.position,\n    super.size,\n    super.paint,\n    Decorator? decorator,\n  }) {\n    this.decorator.addLast(decorator);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/rendering/shadow3d_decorator_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame/rendering.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Shadow3DDecorator', () {\n    test('shadow default properties', () {\n      final decorator = Shadow3DDecorator();\n      expect(decorator.base, Vector2(0, 0));\n      expect(decorator.ascent, 0.0);\n      expect(decorator.angle, -1.4);\n      expect(decorator.xShift, 100.0);\n      expect(decorator.yScale, 1.0);\n      expect(decorator.blur, 0.0);\n      expect(decorator.opacity, 0.6);\n      expect(decorator.baseColor, BasicPalette.black.color);\n    });\n\n    testGolden(\n      'shadow behind object',\n      (game, tester) async {\n        game.addAll([\n          _Background(const Color(0xffc9c9c9)),\n          _DecoratedRectangle(\n            position: Vector2(20, 30),\n            size: Vector2(60, 100),\n            paint: Paint()..color = const Color(0xcc199f2b),\n            decorator: Shadow3DDecorator(\n              base: Vector2(30, 100),\n              xShift: 200,\n              yScale: 2,\n            ),\n          ),\n        ]);\n      },\n      size: Vector2(120, 150),\n      goldenFile: '../_goldens/shadow3d_decorator_1.png',\n    );\n\n    testGolden(\n      'shadow in front object',\n      (game, tester) async {\n        game.addAll([\n          _Background(const Color(0xffc9c9c9)),\n          _DecoratedRectangle(\n            position: Vector2(60, 20),\n            size: Vector2(60, 100),\n            paint: Paint()..color = const Color(0xcc199f2b),\n            decorator: Shadow3DDecorator(\n              base: Vector2(30, 100),\n              angle: 1.7,\n              xShift: 200,\n              yScale: 2,\n              opacity: 0.5,\n              blur: 2.0,\n            ),\n          ),\n        ]);\n      },\n      size: Vector2(140, 180),\n      goldenFile: '../_goldens/shadow3d_decorator_2.png',\n    );\n\n    testGolden(\n      'dynamically change shadow properties',\n      (game, tester) async {\n        game.addAll([\n          _Background(const Color(0xffc9c9c9)),\n          _DecoratedRectangle(\n            position: Vector2(60, 20),\n            size: Vector2(60, 100),\n            paint: Paint()..color = const Color(0xcc199f2b),\n            decorator: Shadow3DDecorator()\n              ..base = Vector2(30, 100)\n              ..ascent = 0\n              ..angle = 1.8\n              ..xShift = 250.0\n              ..yScale = 1.5\n              ..opacity = 0.4\n              ..blur = 1.0\n              ..baseColor = BasicPalette.red.color,\n          ),\n        ]);\n      },\n      size: Vector2(140, 180),\n      goldenFile: '../_goldens/shadow3d_decorator_3.png',\n    );\n  });\n}\n\nclass _DecoratedRectangle extends RectangleComponent {\n  _DecoratedRectangle({\n    super.position,\n    super.size,\n    super.paint,\n    Decorator? decorator,\n  }) {\n    this.decorator.addLast(decorator);\n  }\n}\n\nclass _Background extends Component {\n  _Background(this.color);\n  final Color color;\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawColor(color, BlendMode.src);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/sprite_animation_test.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/src/sprite_animation.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('SpriteAnimation', () {\n    test('Throw assertion error on empty list of frames', () {\n      expect(\n        () => SpriteAnimation.spriteList([], stepTime: 1),\n        failsAssert('There must be at least one animation frame'),\n      );\n    });\n\n    test('Throw assertion error on non-positive step time', () {\n      final sprite = _MockSprite();\n      expect(\n        () => SpriteAnimation.spriteList([sprite], stepTime: 0),\n        failsAssert('All frames must have positive durations'),\n      );\n      expect(\n        () => SpriteAnimation.variableSpriteList(\n          [sprite, sprite, sprite],\n          stepTimes: [1, -1, 0],\n        ),\n        failsAssert('All frames must have positive durations'),\n      );\n    });\n\n    test('Throw assertion error when setting non-positive step time', () {\n      final sprite = _MockSprite();\n      final animation = SpriteAnimation.spriteList([\n        sprite,\n        sprite,\n        sprite,\n      ], stepTime: 1);\n      expect(\n        () => animation.stepTime = 0,\n        failsAssert('Step time must be positive'),\n      );\n      expect(\n        () => animation.variableStepTimes = [3, 2, 0],\n        failsAssert('All step times must be positive'),\n      );\n    });\n  });\n\n  group('SpriteAnimationData', () {\n    test(\n      'throws assertion error when amountPerRow is greater than amount',\n      () {\n        expect(\n          () => SpriteAnimationData.variable(\n            amount: 5,\n            stepTimes: List.filled(5, 0.1),\n            textureSize: Vector2(50, 50),\n            amountPerRow: 10,\n          ),\n          failsAssert(),\n        );\n      },\n    );\n\n    test('creates a new SpriteAnimationData using the range constructor', () {\n      final animationData = SpriteAnimationData.range(\n        start: 6,\n        end: 11,\n        amount: 18,\n        stepTimes: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6],\n        amountPerRow: 6,\n        textureSize: Vector2(1, 1),\n      );\n      expect(animationData.frames.length, 6);\n      expect(\n        animationData.frames.map((f) => f.stepTime),\n        [0.1, 0.2, 0.3, 0.4, 0.5, 0.6],\n      );\n      expect(animationData.frames.map((f) => f.srcPosition), [\n        Vector2(0, 1),\n        Vector2(1, 1),\n        Vector2(2, 1),\n        Vector2(3, 1),\n        Vector2(4, 1),\n        Vector2(5, 1),\n      ]);\n    });\n  });\n}\n\nclass _MockSprite extends Mock implements Sprite {}\n"
  },
  {
    "path": "packages/flame/test/sprite_animation_ticker_test.dart",
    "content": "import 'package:flame/sprite.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nvoid main() {\n  group('SpriteAnimationTicker', () {\n    test('onStart called for single-frame animation', () {\n      var counter = 0;\n      final sprite = _MockSprite();\n      final animationTicker = SpriteAnimation.spriteList(\n        [sprite],\n        stepTime: 1,\n        loop: false,\n      ).createTicker()..onStart = () => counter++;\n\n      expect(counter, 0);\n      animationTicker.update(0.5);\n      expect(counter, 1);\n      animationTicker.update(1);\n      expect(counter, 1);\n    });\n\n    test('onComplete called for single-frame animation', () {\n      var counter = 0;\n      final sprite = _MockSprite();\n      final animationTicker = SpriteAnimation.spriteList(\n        [sprite],\n        stepTime: 1,\n        loop: false,\n      ).createTicker()..onComplete = () => counter++;\n      expect(counter, 0);\n      animationTicker.update(0.5);\n      expect(counter, 0);\n      animationTicker.update(0.5);\n      expect(counter, 1);\n      animationTicker.update(1);\n      expect(counter, 1);\n    });\n\n    test(\n      'verify call is being made at first of frame for multi-frame animation',\n      () {\n        var timePassed = 0.0;\n        const dt = 0.03;\n        var timesCalled = 0;\n        final sprite = _MockSprite();\n        final spriteList = [sprite, sprite, sprite];\n        final animationTicker = SpriteAnimation.spriteList(\n          spriteList,\n          stepTime: 1,\n          loop: false,\n        ).createTicker();\n        animationTicker.onFrame = (index) {\n          expect(timePassed, closeTo(index * 1.0, dt));\n          timesCalled++;\n        };\n        while (timePassed <= spriteList.length) {\n          timePassed += dt;\n          animationTicker.update(dt);\n        }\n        expect(timesCalled, spriteList.length);\n      },\n    );\n\n    test('test sequence of event lifecycle for an animation', () {\n      var animationStarted = false;\n      var animationRunning = false;\n      var animationComplete = false;\n      final sprite = _MockSprite();\n      final animationTicker = SpriteAnimation.spriteList(\n        [sprite],\n        stepTime: 1,\n        loop: false,\n      ).createTicker();\n      animationTicker.onStart = () {\n        expect(animationStarted, false);\n        expect(animationRunning, false);\n        expect(animationComplete, false);\n        animationStarted = true;\n        animationRunning = true;\n      };\n      animationTicker.onFrame = (index) {\n        expect(animationStarted, true);\n        expect(animationRunning, true);\n        expect(animationComplete, false);\n      };\n      animationTicker.onComplete = () {\n        expect(animationStarted, true);\n        expect(animationRunning, true);\n        expect(animationComplete, false);\n        animationComplete = true;\n      };\n      animationTicker.update(1);\n      expect(animationComplete, true);\n    });\n\n    test('completed completes', () {\n      final sprite = _MockSprite();\n      final animationTicker = SpriteAnimation.spriteList(\n        [sprite],\n        stepTime: 1,\n        loop: false,\n      ).createTicker();\n\n      expectLater(animationTicker.completed, completes);\n\n      animationTicker.update(1);\n    });\n\n    test('completed completes when the animation has already completed', () {\n      final sprite = _MockSprite();\n      final animationTicker = SpriteAnimation.spriteList(\n        [sprite],\n        stepTime: 1,\n        loop: false,\n      ).createTicker();\n\n      animationTicker.update(1);\n      expectLater(animationTicker.completed, completes);\n    });\n\n    test(\n      \"completed doesn't complete when the animation is yet to complete\",\n      () {\n        final sprite = _MockSprite();\n        final animationTicker = SpriteAnimation.spriteList(\n          [sprite],\n          stepTime: 1,\n          loop: false,\n        ).createTicker();\n\n        expectLater(animationTicker.completed, doesNotComplete);\n      },\n    );\n\n    test(\"completed doesn't complete when animation is looping\", () {\n      final sprite = _MockSprite();\n      final animationTicker = SpriteAnimation.spriteList([\n        sprite,\n      ], stepTime: 1).createTicker();\n\n      expectLater(animationTicker.completed, doesNotComplete);\n    });\n\n    test(\n      \"completed doesn't complete when animation is looping and on last frame\",\n      () {\n        final sprite = _MockSprite();\n        final animationTicker = SpriteAnimation.spriteList([\n          sprite,\n        ], stepTime: 1).createTicker();\n\n        animationTicker.update(1);\n        expectLater(animationTicker.completed, doesNotComplete);\n      },\n    );\n\n    test(\"completed doesn't complete after the animation is reset\", () {\n      final sprite = _MockSprite();\n      final animationTicker = SpriteAnimation.spriteList(\n        [sprite],\n        stepTime: 1,\n        loop: false,\n      ).createTicker();\n\n      animationTicker.completed;\n      animationTicker.update(1);\n      expect(animationTicker.completeCompleter!.isCompleted, true);\n\n      animationTicker.reset();\n      animationTicker.completed;\n      expect(animationTicker.completeCompleter!.isCompleted, false);\n    });\n\n    test('paused pauses ticket', () {\n      final sprite = _MockSprite();\n      final animationTicker = SpriteAnimation.spriteList(\n        [sprite, sprite],\n        stepTime: 1,\n        loop: false,\n      ).createTicker();\n\n      expect(animationTicker.isPaused, false);\n      expect(animationTicker.currentIndex, 0);\n      animationTicker.update(1);\n      expect(animationTicker.currentIndex, 1);\n      animationTicker.paused = true;\n      expect(animationTicker.isPaused, true);\n      animationTicker.update(1);\n      expect(animationTicker.currentIndex, 1);\n      animationTicker.reset();\n      expect(animationTicker.currentIndex, 0);\n      expect(animationTicker.isPaused, false);\n    });\n  });\n}\n\nclass _MockSprite extends Mock implements Sprite {}\n"
  },
  {
    "path": "packages/flame/test/sprite_batch_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/sprite.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart' hide Image;\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nimport '_resources/load_image.dart';\n\nclass _MockImage extends Mock implements Image {}\n\nvoid main() {\n  group('SpriteBatch', () {\n    test('can add to the batch', () {\n      final image = _MockImage();\n      final spriteBatch = SpriteBatch(image);\n      final index = spriteBatch.add(source: Rect.zero);\n\n      expect(spriteBatch.getBatchItem(index), isNotNull);\n    });\n\n    test('can replace the color of a batch', () {\n      final image = _MockImage();\n      final spriteBatch = SpriteBatch(image);\n      spriteBatch.add(source: Rect.zero, color: Colors.blue);\n\n      spriteBatch.replace(0, color: Colors.red);\n\n      final batchItem = spriteBatch.getBatchItem(0);\n\n      /// Use .closeTo() to avoid floating point rounding errors.\n      expect(batchItem.paint.color.a, closeTo(Colors.red.a, 0.001));\n      expect(batchItem.paint.color.r, closeTo(Colors.red.r, 0.001));\n      expect(batchItem.paint.color.g, closeTo(Colors.red.g, 0.001));\n      expect(batchItem.paint.color.b, closeTo(Colors.red.b, 0.001));\n    });\n\n    test('can replace the source of a batch', () {\n      final image = _MockImage();\n      final spriteBatch = SpriteBatch(image);\n      spriteBatch.add(source: Rect.zero);\n\n      spriteBatch.replace(0, source: const Rect.fromLTWH(1, 1, 1, 1));\n      final batchItem = spriteBatch.getBatchItem(0);\n\n      expect(batchItem.source, const Rect.fromLTWH(1, 1, 1, 1));\n    });\n\n    test('can replace the transform of a batch', () {\n      final image = _MockImage();\n      final spriteBatch = SpriteBatch(image);\n      spriteBatch.add(source: Rect.zero);\n\n      spriteBatch.replace(0, transform: RSTransform(1, 1, 1, 1));\n      final batchItem = spriteBatch.getBatchItem(0);\n\n      expect(\n        batchItem.transform,\n        isA<RSTransform>()\n            .having((t) => t.scos, 'scos', 1)\n            .having((t) => t.ssin, 'ssin', 1)\n            .having((t) => t.tx, 'tx', 1)\n            .having((t) => t.ty, 'ty', 1),\n      );\n    });\n\n    const margin = 2.0;\n    const tileSize = 6.0;\n\n    testGolden(\n      'can render a batch with blend mode',\n      (game, tester) async {\n        final spriteSheet = await loadImage('alphabet.png');\n        final spriteBatch = SpriteBatch(spriteSheet);\n\n        const source = Rect.fromLTWH(3 * tileSize, 0, tileSize, tileSize);\n\n        spriteBatch.add(\n          source: source,\n          color: Colors.redAccent,\n          offset: Vector2.all(margin),\n        );\n\n        spriteBatch.add(\n          source: source,\n          offset: Vector2(2 * margin + tileSize, margin),\n        );\n\n        game.add(\n          SpriteBatchComponent(\n            spriteBatch: spriteBatch,\n            blendMode: BlendMode.srcOver,\n          ),\n        );\n      },\n      size: Vector2(3 * margin + 2 * tileSize, 2 * margin + tileSize),\n      backgroundColor: const Color(0xFFFFFFFF),\n      goldenFile: '_goldens/sprite_batch_test_1.png',\n    );\n\n    testGolden(\n      'can render a batch without blend mode',\n      (game, tester) async {\n        final spriteSheet = await loadImage('alphabet.png');\n        final spriteBatch = SpriteBatch(spriteSheet);\n\n        const source = Rect.fromLTWH(3 * tileSize, 0, tileSize, tileSize);\n\n        spriteBatch.add(\n          source: source,\n          offset: Vector2.all(margin),\n        );\n\n        spriteBatch.add(\n          source: source,\n          offset: Vector2(2 * margin + tileSize, margin),\n        );\n\n        game.add(\n          SpriteBatchComponent(\n            spriteBatch: spriteBatch,\n          ),\n        );\n      },\n      size: Vector2(3 * margin + 2 * tileSize, 2 * margin + tileSize),\n      backgroundColor: const Color(0xFFFFFFFF),\n      goldenFile: '_goldens/sprite_batch_test_2.png',\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/sprite_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '_resources/load_image.dart';\n\nvoid main() {\n  group('Sprite', () {\n    testGolden(\n      'Render with anchor',\n      (game, tester) async {\n        game.add(_MyComponent()..position = Vector2.all(25));\n      },\n      goldenFile: '_goldens/sprite_test_1.png',\n    );\n  });\n}\n\nclass _MyComponent extends PositionComponent {\n  _MyComponent() : super(size: Vector2(200, 400));\n  late final Sprite sprite;\n\n  @override\n  Future<void> onLoad() async {\n    sprite = Sprite(await loadImage('flame.png'));\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(\n      size.toRect(),\n      Paint()\n        ..color = const Color(0xffffffff)\n        ..style = PaintingStyle.stroke\n        ..strokeWidth = 2,\n    );\n    // Expected: sprite is rendered in the center of the rect\n    sprite.render(canvas, position: size / 2, anchor: Anchor.center);\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/spritesheet_test.dart",
    "content": "import 'package:flame/sprite.dart';\nimport 'package:flame/src/extensions/image.dart';\nimport 'package:flame/src/extensions/vector2.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass _MockImage extends Mock implements Image {}\n\nvoid main() {\n  group('SpriteSheet', () {\n    late final Image image = _MockImage();\n    when(() => image.width).thenReturn(100);\n    when(() => image.height).thenReturn(100);\n\n    test('calculates all field from SpriteSheet', () {\n      final spriteSheet = SpriteSheet(\n        image: image,\n        srcSize: Vector2(1, 2),\n      );\n\n      expect(spriteSheet.rows, 50);\n      expect(spriteSheet.columns, 100);\n    });\n\n    test('calculates columns and rows with margin and spacing', () {\n      final spriteSheet = SpriteSheet(\n        image: image,\n        srcSize: Vector2(1, 2),\n        margin: 3,\n        spacing: 2,\n      );\n\n      expect(spriteSheet.rows, 24);\n      expect(spriteSheet.columns, 32);\n    });\n\n    test('calculates srcSize with margin and spacing', () {\n      final spriteSheet = SpriteSheet.fromColumnsAndRows(\n        image: image,\n        columns: 32,\n        rows: 24,\n        margin: 3,\n        spacing: 2,\n      );\n\n      expect(spriteSheet.srcSize.x, 1);\n      expect(spriteSheet.srcSize.y, 2);\n    });\n\n    test('assign the correct time in sprite', () {\n      final spriteSheet = SpriteSheet(\n        image: image,\n        srcSize: Vector2(50, 50),\n      );\n\n      final animationTicker = spriteSheet\n          .createAnimationWithVariableStepTimes(\n            row: 1,\n            stepTimes: [2.0, 3.0],\n          )\n          .createTicker();\n\n      expect(animationTicker.totalDuration(), 5.0);\n    });\n\n    test(\n      'throws error when stepTimes.length != spriteSheet.length',\n      () {\n        final spriteSheet = SpriteSheet(\n          image: image,\n          srcSize: Vector2(50, 50),\n        );\n\n        expect(\n          () => spriteSheet.createAnimationWithVariableStepTimes(\n            row: 1,\n            stepTimes: [2.0],\n          ),\n          failsAssert('Lengths of stepTimes and sprites lists must be equal'),\n        );\n      },\n    );\n\n    test('return sprite based on row and column', () {\n      final spriteSheet = SpriteSheet(\n        image: image,\n        srcSize: Vector2(50, 50),\n      );\n\n      expect(\n        spriteSheet.getSprite(1, 1),\n        isA<Sprite>().having(\n          (sprite) => sprite.srcPosition,\n          'srcPosition',\n          equals(Vector2(50, 50)),\n        ),\n      );\n    });\n\n    test('return sprite based on id', () {\n      final spriteSheet = SpriteSheet(\n        image: image,\n        srcSize: Vector2(50, 50),\n      );\n\n      expect(\n        spriteSheet.getSpriteById(3),\n        isA<Sprite>().having(\n          (sprite) => sprite.srcPosition,\n          'srcPosition',\n          equals(Vector2(50, 50)),\n        ),\n      );\n    });\n\n    test('create sprite animation frame data based on row and column', () {\n      final spriteSheet = SpriteSheet(\n        image: image,\n        srcSize: Vector2(50, 50),\n      );\n\n      expect(\n        spriteSheet.createFrameData(1, 1, stepTime: 0.1),\n        isA<SpriteAnimationFrameData>().having(\n          (frame) => frame.srcPosition,\n          'srcPosition',\n          equals(Vector2(50, 50)),\n        ),\n      );\n    });\n\n    test('create sprite animation frame data based on id', () {\n      final spriteSheet = SpriteSheet(\n        image: image,\n        srcSize: Vector2(50, 50),\n      );\n\n      expect(\n        spriteSheet.createFrameDataFromId(3, stepTime: 0.1),\n        isA<SpriteAnimationFrameData>().having(\n          (frame) => frame.srcPosition,\n          'srcPosition',\n          equals(Vector2(50, 50)),\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/text/common/line_metrics_test.dart",
    "content": "import 'package:flame/src/text/common/line_metrics.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('LineMetrics', () {\n    test('default LineMetrics box', () {\n      final box = LineMetrics(left: 10, baseline: 20);\n      expect(box.left, 10);\n      expect(box.right, 10);\n      expect(box.width, 0);\n      expect(box.baseline, 20);\n      expect(box.top, 20);\n      expect(box.bottom, 20);\n      expect(box.ascent, 0);\n      expect(box.descent, 0);\n      expect(box.height, 0);\n    });\n\n    test('with height only', () {\n      final box = LineMetrics(baseline: 15, height: 10);\n      expect(box.baseline, 15);\n      expect(box.height, 10);\n      expect(box.top, 15 - 10);\n      expect(box.bottom, 15);\n      expect(box.ascent, 10);\n      expect(box.descent, 0);\n    });\n\n    test('with height and ascent', () {\n      final box = LineMetrics(baseline: 15, height: 10, ascent: 8);\n      expect(box.baseline, 15);\n      expect(box.height, 10);\n      expect(box.top, 15 - 8);\n      expect(box.bottom, 15 - 8 + 10);\n      expect(box.ascent, 8);\n      expect(box.descent, 10 - 8);\n    });\n\n    test('with height and descent', () {\n      final box = LineMetrics(baseline: 15, height: 10, descent: 3);\n      expect(box.baseline, 15);\n      expect(box.height, 10);\n      expect(box.top, 15 + 3 - 10);\n      expect(box.bottom, 15 + 3);\n      expect(box.ascent, 10 - 3);\n      expect(box.descent, 3);\n    });\n\n    test('with ascent and descent', () {\n      final box = LineMetrics(baseline: 15, ascent: 10, descent: 3);\n      expect(box.baseline, 15);\n      expect(box.height, 10 + 3);\n      expect(box.top, 15 - 10);\n      expect(box.bottom, 15 + 3);\n      expect(box.ascent, 10);\n      expect(box.descent, 3);\n    });\n\n    test('translate', () {\n      final box = LineMetrics(width: 40, descent: 2, ascent: 8);\n      expect(box.left, 0);\n      expect(box.baseline, 0);\n      box.translate(5, 11);\n      expect(box.left, 5);\n      expect(box.baseline, 11);\n      expect(box.width, 40);\n      expect(box.height, 10);\n      expect(box.ascent, 8);\n      box.translate(-1, -1);\n      expect(box.left, 4);\n      expect(box.baseline, 10);\n    });\n\n    test('moveToOrigin', () {\n      final box = LineMetrics(left: 33, baseline: 78, ascent: 8, descent: 2);\n      box.moveToOrigin();\n      expect(box.left, 0);\n      expect(box.baseline, 0);\n      expect(box.top, -8);\n      expect(box.bottom, 2);\n    });\n\n    test('setLeftEdge', () {\n      final box = LineMetrics(left: 33, baseline: 78, width: 101);\n      expect(box.right, 33 + 101);\n      box.setLeftEdge(49);\n      expect(box.right, 33 + 101);\n      expect(box.left, 49);\n    });\n\n    test('append', () {\n      final box1 = LineMetrics(left: 4, width: 10, baseline: 17, height: 6);\n      final box2 = LineMetrics(\n        left: 18,\n        width: 40,\n        baseline: 17,\n        ascent: 8,\n        descent: 2,\n      );\n      box1.append(box2);\n      expect(box1.left, 4);\n      expect(box1.right, 18 + 40);\n      expect(box1.top, 17 - 8);\n      expect(box1.bottom, 17 + 2);\n\n      expect(\n        () => box1.append(LineMetrics()),\n        failsAssert('Baselines do not match: 17.0 vs 0.0'),\n      );\n    });\n\n    test('toString', () {\n      final box = LineMetrics(left: 33, baseline: 78, width: 101, height: 11);\n      expect(\n        box.toString(),\n        'LineMetrics(left: 33.0, baseline: 78.0, width: 101.0, ascent: 11.0, '\n        'descent: 0.0)',\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/text/common/sprite_font_test.dart",
    "content": "import 'package:flame/text.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('SpriteFont', () {\n    test('glyph parsing 1', () async {\n      final spriteFont = SpriteFont(\n        source: await generateImage(),\n        size: 1,\n        ascent: 1,\n        glyphs: [\n          Glyph('a', left: 1, top: 0),\n          Glyph('b', left: 2, top: 0),\n          Glyph('c', left: 3, top: 0),\n          Glyph('d', left: 4, top: 0),\n          Glyph('e', left: 5, top: 0),\n          Glyph('f', left: 6, top: 0),\n          Glyph('ff', left: 7, top: 0),\n        ],\n      );\n      void check(String text, List<double> codes) {\n        expect(spriteFont.textToGlyphs(text).map((g) => g.srcLeft), codes);\n      }\n\n      // cSpell:ignore effef, feadafac\n      check('a', [1]);\n      check('abcd', [1, 2, 3, 4]);\n      check('effef', [5, 7, 5, 6]);\n      check('fffff', [7, 7, 6]);\n      check('feadafac', [6, 5, 1, 4, 1, 6, 1, 3]);\n    });\n\n    test('glyph parsing 2', () async {\n      final spriteFont = SpriteFont(\n        source: await generateImage(),\n        size: 1,\n        ascent: 1,\n        glyphs: [\n          Glyph('flame', left: 1, top: 0),\n          Glyph('a', left: 2, top: 0),\n          Glyph('m', left: 3, top: 0),\n          Glyph('f', left: 4, top: 0),\n          Glyph('lame', left: 5, top: 0),\n          Glyph('am', left: 6, top: 0),\n          Glyph('r', left: 7, top: 0),\n          Glyph('s', left: 8, top: 0),\n          Glyph('l', left: 9, top: 0),\n          Glyph('i', left: 10, top: 0),\n          Glyph('n', left: 11, top: 0),\n        ],\n      );\n      void check(String text, List<double> codes) {\n        expect(spriteFont.textToGlyphs(text).map((g) => g.srcLeft), codes);\n      }\n\n      check('a', [2]);\n      check('flame', [1]);\n      check('lame', [5]);\n      check('flames', [1, 8]);\n      check('flamers', [1, 7, 8]);\n      check('inflame', [10, 11, 1]);\n      check('aflame', [2, 1]);\n      check('flams', [4, 9, 6, 8]);\n\n      expect(\n        () => spriteFont.textToGlyphs('mom'),\n        throwsA(\n          isA<ArgumentError>().having(\n            (e) => e.message,\n            'message',\n            'No glyph data for character \"o\"',\n          ),\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/text/sprite_font_renderer_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:canvas_test/canvas_test.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/text.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nimport '../_resources/load_image.dart';\n\nvoid main() {\n  group('SpriteFontRenderer', () {\n    test('creating SpriteFontRenderer', () async {\n      final renderer = await _createRenderer();\n      expect(renderer.font.source, isA<Image>());\n      expect(renderer.font.size, 6);\n      expect(renderer.scale, 1.0);\n      expect(renderer.letterSpacing, 0);\n\n      expect(\n        () => renderer.render(MockCanvas(), 'Ї', Vector2.zero()),\n        throwsA(\n          isArgumentError.having(\n            (e) => e.message,\n            'message',\n            'No glyph data for character \"Ї\"',\n          ),\n        ),\n      );\n    });\n\n    testGolden(\n      'text rendering at different scales',\n      (game, tester) async {\n        game.addAll([\n          RectangleComponent(size: Vector2(800, 600)),\n          TextBoxComponent(\n            text: _textSample,\n            textRenderer: await _createRenderer(letterSpacing: 1),\n            boxConfig: const TextBoxConfig(maxWidth: 800),\n          ),\n          TextBoxComponent(\n            text: _textSample,\n            textRenderer: await _createRenderer(scale: 2),\n            boxConfig: const TextBoxConfig(maxWidth: 800),\n            position: Vector2(0, 100),\n          ),\n          TextComponent(\n            text: 'FLAME',\n            textRenderer: (await _createRenderer(scale: 25))\n              ..paint.color = const Color(0x44000000),\n            position: Vector2(400, 500),\n            anchor: Anchor.center,\n          ),\n        ]);\n      },\n      goldenFile: '../_goldens/sprite_font_renderer_1.png',\n    );\n\n    testGolden(\n      'Render text with ligatures',\n      (game, tester) async {\n        final font = SpriteFont(\n          source: await loadImage('sprite_font.png'),\n          size: 18,\n          ascent: 14,\n          glyphs: [\n            Glyph('a', left: 5, top: 1, width: 10),\n            Glyph('b', left: 26, top: 1, width: 9),\n            Glyph('c', left: 46, top: 1, width: 9),\n            Glyph('d', left: 66, top: 1, width: 9),\n            Glyph('e', left: 86, top: 1, width: 9),\n            Glyph('f', left: 51, top: 21, width: 6),\n            Glyph('ff', left: 66, top: 21, width: 10),\n            Glyph('g', left: 86, top: 21, width: 9),\n            Glyph('Flame', left: 3, top: 21, width: 41),\n            Glyph(\n              ' ',\n              left: 0,\n              top: 0,\n              width: 9,\n              srcTop: 0,\n              srcBottom: 0,\n              srcLeft: 0,\n              srcRight: 0,\n            ),\n          ],\n        );\n        // cSpell:ignore caffefe, badface\n        game.addAll([\n          RectangleComponent(\n            size: Vector2(200, 200),\n            paint: Paint()..color = const Color(0xffd1dae1),\n          ),\n          TextComponent(\n            text: 'facade',\n            textRenderer: SpriteFontRenderer.fromFont(font),\n            position: Vector2(10, 10),\n          ),\n          TextComponent(\n            text: 'caffefe',\n            textRenderer: SpriteFontRenderer.fromFont(font, scale: 2),\n            position: Vector2(10, 30),\n          ),\n          TextComponent(\n            text: 'Flame bag',\n            textRenderer: SpriteFontRenderer.fromFont(font, scale: 2),\n            position: Vector2(10, 70),\n          ),\n          TextComponent(\n            text: 'faded cabbage gaffe',\n            textRenderer: SpriteFontRenderer.fromFont(font, letterSpacing: 1),\n            position: Vector2(10, 110),\n          ),\n        ]);\n      },\n      goldenFile: '../_goldens/sprite_font_renderer_2.png',\n      size: Vector2(200, 140),\n    );\n\n    testGolden(\n      'Render text with large src rectangles',\n      (game, tester) async {\n        final font = SpriteFont(\n          source: await loadImage('sprite_font.png'),\n          size: 18,\n          ascent: 14,\n          glyphs: [\n            Glyph(\n              'a',\n              left: 5,\n              top: 1,\n              width: 10,\n              srcLeft: 0,\n              srcTop: 0,\n              srcRight: 20,\n              srcBottom: 20,\n            ),\n            Glyph(\n              'b',\n              left: 26,\n              top: 1,\n              width: 9,\n              srcLeft: 20,\n              srcTop: 0,\n              srcBottom: 20,\n              srcRight: 40,\n            ),\n            Glyph(\n              'c',\n              left: 46,\n              top: 1,\n              width: 9,\n              srcLeft: 40,\n              srcTop: 0,\n              srcBottom: 20,\n              srcRight: 60,\n            ),\n            Glyph(\n              'd',\n              left: 66,\n              top: 1,\n              width: 9,\n              srcLeft: 60,\n              srcTop: 0,\n              srcBottom: 20,\n              srcRight: 80,\n            ),\n            Glyph(\n              'e',\n              left: 86,\n              top: 1,\n              width: 9,\n              srcLeft: 80,\n              srcTop: 0,\n              srcBottom: 20,\n              srcRight: 100,\n            ),\n            Glyph(\n              'f',\n              left: 51,\n              top: 21,\n              width: 6,\n              srcLeft: 46,\n              srcTop: 20,\n              srcBottom: 40,\n              srcRight: 61,\n            ),\n            Glyph(\n              'g',\n              left: 86,\n              top: 21,\n              width: 9,\n              srcLeft: 80,\n              srcTop: 20,\n              srcBottom: 40,\n              srcRight: 100,\n            ),\n          ],\n        );\n        game.addAll([\n          RectangleComponent(\n            size: Vector2(200, 200),\n            paint: Paint()..color = const Color(0xffd8e1c4),\n          ),\n          TextComponent(\n            text: 'badface',\n            textRenderer: SpriteFontRenderer.fromFont(font),\n            position: Vector2(10, 10),\n          ),\n          TextComponent(\n            text: 'badface',\n            textRenderer: SpriteFontRenderer.fromFont(font, letterSpacing: 11),\n            position: Vector2(10, 40),\n          ),\n        ]);\n      },\n      goldenFile: '../_goldens/sprite_font_renderer_3.png',\n      size: Vector2(200, 70),\n    );\n\n    testGolden(\n      'Render colored text',\n      (game, tester) async {\n        const lines = [\n          'ABCDEFGHIJKLMNOPQRSTUVWXYZ',\n          'abcdefghijklmnopqrstuvwxyz',\n        ];\n        final font = SpriteFont(\n          source: await loadImage('alphabet.png'),\n          size: 6,\n          ascent: 6,\n          glyphs: [\n            for (var j = 0; j < lines.length; j++)\n              for (var i = 0; i < lines[j].length; i++)\n                Glyph(lines[j][i], left: i * 6, top: 1 + j * 6),\n          ],\n        );\n        const colors = [\n          Color(0xffff0000),\n          Color(0x66ff0000),\n          Color(0x33ff0000),\n          Color(0x11ff0000),\n          Color(0xffffffff),\n          Color(0xff8c27c9),\n        ];\n        game.addAll([\n          RectangleComponent(\n            size: Vector2(200, 200),\n            paint: Paint()..color = const Color(0xffcfc6e5),\n          ),\n          for (var i = 0; i < colors.length; i++)\n            TextComponent(\n              text: 'pandemonium',\n              textRenderer: SpriteFontRenderer.fromFont(\n                font,\n                scale: 2,\n                color: colors[i],\n              ),\n              position: Vector2(10, 10 + i * 20),\n            ),\n        ]);\n      },\n      goldenFile: '../_goldens/sprite_font_renderer_4.png',\n      size: Vector2(200, 130),\n    );\n  });\n}\n\nconst _textSample =\n    'We hold these truths to be self-evident, that all men are '\n    'created equal, that they are endowed by their Creator with certain '\n    'unalienable Rights, that among these are Life, Liberty and the pursuit of '\n    'Happiness. — That to secure these rights, Governments are instituted '\n    'among Men, deriving their just powers from the consent of the governed, '\n    '— That whenever any Form of Government becomes destructive of these ends, '\n    'it is the Right of the People to alter or to abolish it, and to institute '\n    'new Government, laying its foundation on such principles and organizing '\n    'its powers in such form, as to them shall seem most likely to effect '\n    'their Safety and Happiness. Prudence, indeed, will dictate that '\n    'Governments long established should not be changed for light and '\n    'transient causes; and accordingly all experience hath shewn, that mankind '\n    'are more disposed to suffer, while evils are sufferable, than to right '\n    'themselves by abolishing the forms to which they are accustomed. But when '\n    'a long train of abuses and usurpations, pursuing invariably the same '\n    'Object evinces a design to reduce them under absolute Despotism, it is '\n    'their right, it is their duty, to throw off such Government, and to '\n    'provide new Guards for their future security.';\n\nFuture<SpriteFontRenderer> _createRenderer({\n  double scale = 1,\n  double letterSpacing = 0,\n}) async {\n  const lines = [\n    'ABCDEFGHIJKLMNOPQRSTUVWXYZ',\n    'abcdefghijklmnopqrstuvwxyz',\n    r'0123456789.,:;—_!?@$%+-=/*',\n    '#^&()[]{}<>|\\\\\\'\"`~←→↑↓ ',\n  ];\n  return SpriteFontRenderer.fromFont(\n    SpriteFont(\n      source: await loadImage('alphabet.png'),\n      size: 6,\n      ascent: 6,\n      glyphs: [\n        for (var j = 0; j < lines.length; j++)\n          for (var i = 0; i < lines[j].length; i++)\n            Glyph(lines[j][i], left: i * 6, top: 1 + j * 6),\n      ],\n    ),\n    scale: scale,\n    letterSpacing: letterSpacing,\n  );\n}\n"
  },
  {
    "path": "packages/flame/test/text/text_element_test.dart",
    "content": "import 'package:flame/text.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('text elements', () {\n    test('bounding box for empty group', () {\n      final emptyGroup = GroupElement(width: 0, height: 0, children: []);\n      expect(emptyGroup.boundingBox, Rect.zero);\n    });\n\n    test('bounding box for inline elements', () {\n      final document = DocumentRoot([\n        ParagraphNode.group([\n          PlainTextNode('Hello'),\n        ]),\n      ]);\n\n      final element1 = document.format(\n        DocumentStyle(\n          paragraph: const BlockStyle(\n            margin: EdgeInsets.zero,\n            padding: EdgeInsets.zero,\n          ),\n        ),\n        width: 80,\n        height: 16,\n      );\n      const expected = Rect.fromLTWH(0, 0, 80, 16);\n\n      expect(element1.boundingBox, expected);\n      final element2 = element1.children.single as GroupElement;\n      expect(element2.boundingBox, expected);\n      final element3 = element2.children.single as InlineTextElement;\n      expect(element3.boundingBox, expected);\n    });\n\n    test('bounding box is composed', () {\n      final document = DocumentRoot([\n        ParagraphNode.group([\n          PlainTextNode('Hello, '),\n          PlainTextNode('there'),\n        ]),\n        ParagraphNode.group([\n          ItalicTextNode.simple('General '),\n          BoldTextNode.simple('Kenobi'),\n        ]),\n      ]);\n\n      final element1 = document.format(\n        DocumentStyle(\n          paragraph: const BlockStyle(\n            margin: EdgeInsets.zero,\n            padding: EdgeInsets.zero,\n          ),\n        ),\n        width: 600,\n        height: 400,\n      );\n      expect(element1.boundingBox, const Rect.fromLTWH(0, 0, 224, 32));\n      final element2 = element1.children[0] as GroupElement;\n      expect(element2.boundingBox, const Rect.fromLTWH(0, 0, 192, 16));\n      final element3 = element1.children[1] as GroupElement;\n      expect(element3.boundingBox, const Rect.fromLTWH(0, 16, 224, 16));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/text/text_layouting_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/text.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:test/test.dart';\n\nfinal _size = Vector2(100, 100);\n\nvoid main() {\n  group('text layouting', () {\n    testGolden(\n      'Text is properly laid out across multiple lines',\n      (game, tester) async {\n        game.addAll([\n          RectangleComponent(\n            size: _size,\n            paint: Paint()..color = const Color(0xffcfc6e5),\n          ),\n          TextElementComponent.fromDocument(\n            document: DocumentRoot([\n              ParagraphNode.group(\n                ['012345', '67 89'].map(PlainTextNode.new).toList(),\n              ),\n            ]),\n            style: DocumentStyle(\n              text: InlineTextStyle(\n                // using DebugTextRenderer, this will make each char 10px wide\n                fontSize: 10,\n              ),\n            ),\n            size: _size,\n          ),\n        ]);\n      },\n      goldenFile: '../_goldens/text_layouting_1.png',\n      size: _size,\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/text/text_paint_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/text.dart';\nimport 'package:flutter/rendering.dart' as flutter;\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('TextPaint', () {\n    test('copyWith returns a new instance with the new values', () {\n      const style = flutter.TextStyle(fontSize: 12, fontFamily: 'Times');\n      final tp = TextPaint(\n        style: style,\n      ).copyWith((t) => t.copyWith(fontFamily: 'Helvetica'));\n      expect(tp.style.fontSize, 12);\n      expect(tp.style.fontFamily, 'Helvetica');\n    });\n\n    test(\n      'can convert back and forth between TextPaint and InlineTextStyle',\n      () {\n        const flutterStyle = flutter.TextStyle(\n          fontSize: 12,\n          fontFamily: 'Times',\n          fontStyle: flutter.FontStyle.italic,\n          fontWeight: flutter.FontWeight.bold,\n          color: Color(0xFF00FF00),\n          letterSpacing: 1.5,\n          wordSpacing: 2.5,\n          height: 3.5,\n          leadingDistribution: TextLeadingDistribution.even,\n          shadows: [\n            Shadow(\n              color: Color(0xFFFF0000),\n              offset: Offset(1, 1),\n              blurRadius: 1,\n            ),\n          ],\n          fontFeatures: [\n            flutter.FontFeature.alternativeFractions(),\n          ],\n          fontVariations: [\n            flutter.FontVariation.slant(0.3),\n          ],\n          decoration: TextDecoration.lineThrough,\n          decorationColor: Color(0xFF0000FF),\n          decorationStyle: TextDecorationStyle.dashed,\n          decorationThickness: 1.5,\n          backgroundColor: Color(0xFFFF00FF),\n        );\n        final textPaint = TextPaint(style: flutterStyle);\n\n        final inlineTextStyle = textPaint.asInlineTextStyle();\n        expect(inlineTextStyle.fontSize, 12);\n        expect(inlineTextStyle.fontFamily, 'Times');\n        expect(inlineTextStyle.fontStyle, flutter.FontStyle.italic);\n        expect(inlineTextStyle.fontWeight, flutter.FontWeight.bold);\n        expect(inlineTextStyle.color, const Color(0xFF00FF00));\n        expect(inlineTextStyle.letterSpacing, 1.5);\n        expect(inlineTextStyle.wordSpacing, 2.5);\n        expect(inlineTextStyle.height, 3.5);\n        expect(\n          inlineTextStyle.leadingDistribution,\n          TextLeadingDistribution.even,\n        );\n        expect(inlineTextStyle.shadows, [\n          const Shadow(\n            color: Color(0xFFFF0000),\n            offset: Offset(1, 1),\n            blurRadius: 1,\n          ),\n        ]);\n        expect(inlineTextStyle.fontFeatures, [\n          const flutter.FontFeature.alternativeFractions(),\n        ]);\n        expect(inlineTextStyle.fontVariations, [\n          const FontVariation.slant(0.3),\n        ]);\n        expect(inlineTextStyle.decoration, TextDecoration.lineThrough);\n        expect(inlineTextStyle.decorationColor, const Color(0xFF0000FF));\n        expect(inlineTextStyle.decorationStyle, TextDecorationStyle.dashed);\n        expect(inlineTextStyle.decorationThickness, 1.5);\n        expect(inlineTextStyle.background!.color, const Color(0xFFFF00FF));\n\n        final newTextPaint = inlineTextStyle.asTextRenderer();\n        expect(newTextPaint.style.fontSize, 12);\n        expect(newTextPaint.style.fontFamily, 'Times');\n        expect(newTextPaint.style.fontStyle, flutter.FontStyle.italic);\n        expect(newTextPaint.style.fontWeight, flutter.FontWeight.bold);\n        expect(newTextPaint.style.color, const Color(0xFF00FF00));\n        expect(newTextPaint.style.letterSpacing, 1.5);\n        expect(newTextPaint.style.wordSpacing, 2.5);\n        expect(newTextPaint.style.height, 3.5);\n        expect(\n          newTextPaint.style.leadingDistribution,\n          TextLeadingDistribution.even,\n        );\n        expect(newTextPaint.style.shadows, [\n          const Shadow(\n            color: Color(0xFFFF0000),\n            offset: Offset(1, 1),\n            blurRadius: 1,\n          ),\n        ]);\n        expect(newTextPaint.style.fontFeatures, [\n          const flutter.FontFeature.alternativeFractions(),\n        ]);\n        expect(newTextPaint.style.fontVariations, [\n          const FontVariation.slant(0.3),\n        ]);\n        expect(newTextPaint.style.decoration, TextDecoration.lineThrough);\n        expect(newTextPaint.style.decorationColor, const Color(0xFF0000FF));\n        expect(newTextPaint.style.decorationStyle, TextDecorationStyle.dashed);\n        expect(newTextPaint.style.decorationThickness, 1.5);\n        expect(newTextPaint.style.background!.color, const Color(0xFFFF00FF));\n      },\n    );\n  });\n\n  test(\n    'TextPaint and InlineTextStyle can receive Paint instead of Color',\n    () {\n      final flutterStyle = flutter.TextStyle(\n        fontSize: 12,\n        fontFamily: 'Times',\n        foreground: Paint()..color = const Color(0xFF0000FF),\n        background: Paint()..color = const Color(0xFFFF00FF),\n      );\n      final textPaint = TextPaint(style: flutterStyle);\n\n      final inlineTextStyle = textPaint.asInlineTextStyle();\n      expect(inlineTextStyle.fontSize, 12);\n      expect(inlineTextStyle.fontFamily, 'Times');\n      expect(inlineTextStyle.foreground!.color, const Color(0xFF0000FF));\n      expect(inlineTextStyle.background!.color, const Color(0xFFFF00FF));\n\n      final newTextPaint = inlineTextStyle.asTextRenderer();\n      expect(newTextPaint.style.fontSize, 12);\n      expect(newTextPaint.style.fontFamily, 'Times');\n      expect(newTextPaint.style.foreground!.color, const Color(0xFF0000FF));\n      expect(newTextPaint.style.background!.color, const Color(0xFFFF00FF));\n    },\n  );\n}\n"
  },
  {
    "path": "packages/flame/test/text/text_renderer_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/text.dart';\nimport 'package:flutter/material.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('TextRenderer', () {\n    test('createDefault', () {\n      final tp = TextRendererFactory.createDefault<TextPaint>();\n      expect(tp, isNotNull);\n      expect(tp, isA<TextPaint>());\n\n      final tr = TextRendererFactory.createDefault();\n      expect(tr, isNotNull);\n      expect(tr, isA<TextRenderer>());\n    });\n\n    test('change parameters of text component', () {\n      final tc = TextComponent<TextPaint>(text: 'foo');\n      tc.textRenderer = tc.textRenderer.copyWith(\n        (c) => c.copyWith(fontSize: 200),\n      );\n      expect(tc.textRenderer.style.fontSize, 200);\n    });\n\n    test('custom renderer', () {\n      TextRendererFactory.defaultRegistry[_CustomTextRenderer] =\n          _CustomTextRenderer.new;\n      final tc = TextComponent<_CustomTextRenderer>(text: 'foo');\n      expect(tc.textRenderer, isA<_CustomTextRenderer>());\n    });\n  });\n}\n\nclass _CustomTextRenderer extends TextRenderer {\n  @override\n  InlineTextElement format(String text) {\n    return _CustomTextElement();\n  }\n}\n\nclass _CustomTextElement extends InlineTextElement {\n  @override\n  LineMetrics get metrics => LineMetrics();\n\n  @override\n  void draw(Canvas canvas) {}\n\n  @override\n  void translate(double dx, double dy) {}\n}\n"
  },
  {
    "path": "packages/flame/test/text/text_style_test.dart",
    "content": "import 'package:flame/text.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('text styles', () {\n    test('document style defaults are applied', () {\n      final style = DocumentStyle();\n      expect(style.text.fontSize, 16);\n      expect(style.boldText.fontWeight, FontWeight.bold);\n      expect(style.italicText.fontStyle, FontStyle.italic);\n      expect(style.paragraph.margin, const EdgeInsets.all(6));\n      expect(style.paragraph.padding, EdgeInsets.zero);\n    });\n\n    test('document style parameters are respected', () {\n      final style = DocumentStyle(\n        text: InlineTextStyle(\n          fontSize: 8,\n        ),\n        boldText: InlineTextStyle(\n          fontWeight: FontWeight.w900,\n        ),\n        italicText: InlineTextStyle(\n          fontStyle: FontStyle.normal,\n        ),\n        paragraph: const BlockStyle(\n          margin: EdgeInsets.zero,\n          padding: EdgeInsets.zero,\n        ),\n      );\n      expect(style.text.fontSize, 8);\n      expect(style.boldText.fontWeight, FontWeight.w900);\n      expect(style.italicText.fontStyle, FontStyle.normal);\n      expect(style.paragraph.margin, EdgeInsets.zero);\n      expect(style.paragraph.padding, EdgeInsets.zero);\n    });\n\n    test('document style can be copied', () {\n      final style = DocumentStyle(\n        text: InlineTextStyle(\n          fontSize: 8,\n        ),\n        boldText: InlineTextStyle(\n          fontWeight: FontWeight.w900,\n        ),\n        paragraph: const BlockStyle(\n          margin: EdgeInsets.zero,\n          padding: EdgeInsets.zero,\n        ),\n        background: BackgroundStyle(\n          color: const Color(0xFFFF00FF),\n        ),\n      );\n      final newStyle = style.copyWith(\n        DocumentStyle(\n          text: InlineTextStyle(\n            fontSize: 10,\n          ),\n          boldText: InlineTextStyle(\n            fontWeight: FontWeight.w900,\n          ),\n          italicText: InlineTextStyle(\n            fontStyle: FontStyle.italic,\n          ),\n          paragraph: const BlockStyle(\n            margin: EdgeInsets.all(10),\n            padding: EdgeInsets.zero,\n          ),\n        ),\n      );\n      expect(newStyle.text.fontSize, 10);\n      expect(newStyle.boldText.fontWeight, FontWeight.w900);\n      expect(newStyle.italicText.fontStyle, FontStyle.italic);\n      expect(newStyle.paragraph.margin, const EdgeInsets.all(10));\n      expect(newStyle.paragraph.padding, EdgeInsets.zero);\n      expect(\n        newStyle.background!.backgroundPaint!.color,\n        const Color(0xFFFF00FF),\n      );\n    });\n\n    test('styles are cascading', () {\n      final style = DocumentStyle(\n        width: 600.0,\n        height: 400.0,\n        text: InlineTextStyle(\n          fontFamily: 'Helvetica',\n          fontSize: 8,\n        ),\n        boldText: InlineTextStyle(\n          fontFamily: 'Arial',\n          fontSize: 10,\n          fontWeight: FontWeight.w900,\n        ),\n        italicText: InlineTextStyle(\n          fontFamily: 'Arial',\n          fontSize: 6,\n          fontStyle: FontStyle.italic,\n        ),\n        paragraph: BlockStyle(\n          text: InlineTextStyle(\n            fontSize: 12,\n          ),\n        ),\n      );\n      final document = DocumentRoot([\n        ParagraphNode.group([\n          PlainTextNode('This is '),\n          BoldTextNode.simple('my'),\n          PlainTextNode(' town -  '),\n          ItalicTextNode.simple('The Sheriff'),\n        ]),\n      ]);\n\n      final element = document.format(style);\n      final groupElement = element.children.first as GroupElement;\n      final groupTextElement = groupElement.children.first as GroupTextElement;\n      final styles = groupTextElement.children.map((e) {\n        return (e as TextPainterTextElement).textPainter.text!.style!;\n      }).toList();\n\n      expect(styles[0].fontSize, 12);\n      expect(styles[0].fontWeight, isNull);\n      expect(styles[0].fontStyle, isNull);\n      expect(styles[0].fontFamily, 'Helvetica');\n\n      expect(styles[1].fontSize, 10);\n      expect(styles[1].fontWeight, FontWeight.w900);\n      expect(styles[1].fontStyle, isNull);\n      expect(styles[1].fontFamily, 'Arial');\n\n      expect(styles[2].fontSize, 12);\n      expect(styles[2].fontWeight, isNull);\n      expect(styles[2].fontStyle, isNull);\n      expect(styles[2].fontFamily, 'Helvetica');\n\n      expect(styles[3].fontSize, 6);\n      expect(styles[3].fontWeight, isNull);\n      expect(styles[3].fontStyle, FontStyle.italic);\n      expect(styles[3].fontFamily, 'Arial');\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/timer_test.dart",
    "content": "import 'package:flame/timer.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('Timer', () {\n    test('can be started and stopped, discarding progress', () {\n      final timer = Timer(1.0, autoStart: false);\n      expect(timer.isRunning(), false);\n      timer.start();\n      expect(timer.isRunning(), true);\n      timer.update(0.5);\n      timer.stop();\n      expect(timer.isRunning(), false);\n      expect(timer.current, 0.0);\n    });\n\n    test('can be paused and resumed, retaining progress', () {\n      final timer = Timer(1.0, autoStart: false);\n      expect(timer.isRunning(), false);\n      timer.start();\n      expect(timer.isRunning(), true);\n      timer.update(0.5);\n      timer.pause();\n      expect(timer.isRunning(), false);\n      timer.resume();\n      expect(timer.isRunning(), true);\n      expect(timer.current, 0.5);\n    });\n\n    test('tracks current delta time', () {\n      final timer = Timer(1.0);\n      timer.update(0.5);\n      expect(timer.current, 0.5);\n      timer.update(0.2);\n      expect(timer.current, 0.7);\n    });\n\n    test('tracks progress percent capped at 1.0', () {\n      final timer = Timer(2.0);\n      timer.update(0.5);\n      expect(timer.progress, 0.25);\n      timer.update(0.5);\n      expect(timer.progress, 0.5);\n      timer.update(9999);\n      expect(timer.progress, 1.0);\n    });\n\n    test('onTick fires once if non-repeating', () {\n      var onTickCount = 0;\n      final timer = Timer(1.0, onTick: () => onTickCount++);\n      timer.update(0.9);\n      expect(onTickCount, 0);\n      timer.update(0.2);\n      expect(onTickCount, 1);\n      timer.update(1.0);\n      expect(onTickCount, 1);\n    });\n\n    test('finishes when complete if non-repeating', () {\n      final timer = Timer(1.0);\n      expect(timer.finished, false);\n      timer.update(1.1);\n      expect(timer.finished, true);\n    });\n\n    test('onTick fires repeatedly if repeating', () {\n      var onTickCount = 0;\n      final timer = Timer(1.0, repeat: true, onTick: () => onTickCount++);\n      timer.update(0.9);\n      expect(onTickCount, 0);\n      timer.update(0.2);\n      expect(onTickCount, 1);\n      timer.update(1.0);\n      expect(onTickCount, 2);\n    });\n\n    test('does not finish past limit if repeating', () {\n      final timer = Timer(1.0, repeat: true);\n      expect(timer.finished, false);\n      timer.update(1.1);\n      expect(timer.finished, false);\n    });\n\n    test('when tickCount is provided, tick only the provided amount', () {\n      var count = 0;\n      final timer = Timer(\n        1,\n        repeat: true,\n        tickCount: 2,\n        onTick: () {\n          count++;\n        },\n      );\n      timer.update(1.1);\n      timer.update(1.1);\n      timer.update(1.1);\n\n      expect(count, equals(2));\n      expect(timer.finished, isTrue);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/widgets/components_notifier_builder_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/widgets.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _Enemy extends PositionComponent with Notifier {}\n\nvoid main() {\n  group('ComponentsNotifierBuilder', () {\n    testWidgets('renders the initial value', (tester) async {\n      final notifier = ComponentsNotifier<_Enemy>([_Enemy()]);\n      await tester.pumpWidget(\n        MaterialApp(\n          home: Scaffold(\n            body: ComponentsNotifierBuilder(\n              notifier: notifier,\n              builder: (context, notifier) {\n                return Text(\n                  'Enemies: ${notifier.components.length}',\n                );\n              },\n            ),\n          ),\n        ),\n      );\n\n      expect(find.text('Enemies: 1'), findsOneWidget);\n    });\n\n    testWidgets('rebuilds when an enemy is added', (tester) async {\n      final notifier = ComponentsNotifier<_Enemy>([_Enemy()]);\n      await tester.pumpWidget(\n        MaterialApp(\n          home: Scaffold(\n            body: ComponentsNotifierBuilder(\n              notifier: notifier,\n              builder: (context, notifier) {\n                return Text(\n                  'Enemies: ${notifier.components.length}',\n                );\n              },\n            ),\n          ),\n        ),\n      );\n\n      notifier.add(_Enemy());\n      await tester.pump();\n\n      expect(find.text('Enemies: 2'), findsOneWidget);\n    });\n\n    testWidgets('rebuilds when an enemy is added', (tester) async {\n      final enemy = _Enemy();\n      final notifier = ComponentsNotifier<_Enemy>([enemy]);\n      await tester.pumpWidget(\n        MaterialApp(\n          home: Scaffold(\n            body: ComponentsNotifierBuilder(\n              notifier: notifier,\n              builder: (context, notifier) {\n                return Text(\n                  'Enemies: ${notifier.components.length}',\n                );\n              },\n            ),\n          ),\n        ),\n      );\n\n      notifier.remove(enemy);\n      await tester.pump();\n\n      expect(find.text('Enemies: 0'), findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/widgets/loading_widget.dart",
    "content": "import 'package:flutter/widgets.dart';\n\nclass LoadingWidget extends StatelessWidget {\n  const LoadingWidget({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return const SizedBox();\n  }\n}\n"
  },
  {
    "path": "packages/flame/test/widgets/nine_tile_box_widget_test.dart",
    "content": "import 'dart:ui' as ui;\n\nimport 'package:flame/flame.dart';\nimport 'package:flame/widgets.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'loading_widget.dart';\n\nFuture<void> main() async {\n  final image = await generateImage();\n\n  group('NineTileBoxWidget', () {\n    testWidgets('has no FutureBuilder when passed an animation', (\n      tester,\n    ) async {\n      await tester.pumpWidget(\n        NineTileBoxWidget(\n          image: image,\n          tileSize: 10,\n          destTileSize: 10,\n        ),\n      );\n\n      final futureBuilderFinder = find.byType(FutureBuilder);\n      final nineTileBoxWidgetFinder = find.byType(NineTileBoxWidget);\n\n      expect(futureBuilderFinder, findsNothing);\n      expect(nineTileBoxWidgetFinder, findsOneWidget);\n    });\n\n    testWidgets(\n      'has FutureBuilder and LoadingWidget when passed an asset path',\n      (tester) async {\n        const imagePath = 'test_path';\n        Flame.images.add(imagePath, image);\n\n        await tester.pumpWidget(\n          NineTileBoxWidget.asset(\n            path: imagePath,\n            tileSize: 10,\n            destTileSize: 10,\n            loadingBuilder: (_) => const LoadingWidget(),\n          ),\n        );\n\n        final futureBuilderFinder = find.byType(FutureBuilder<ui.Image>);\n        final nineTileBoxWidgetFinder = find.byType(InternalNineTileBox);\n        final loadingWidgetFinder = find.byType(LoadingWidget);\n\n        expect(futureBuilderFinder, findsOneWidget);\n        expect(loadingWidgetFinder, findsOneWidget);\n        expect(nineTileBoxWidgetFinder, findsNothing);\n\n        /// loading to be removed\n        await tester.pump();\n\n        expect(futureBuilderFinder, findsOneWidget);\n        expect(loadingWidgetFinder, findsNothing);\n        expect(nineTileBoxWidgetFinder, findsOneWidget);\n      },\n    );\n\n    group('when the nine tile box changes', () {\n      testWidgets('updates the widget', (tester) async {\n        const imagePath = 'test_path_2';\n        const imagePath2 = 'test_path_3';\n\n        final image = await generateImage(100, 100);\n        final image2 = await generateImage(100, 102);\n\n        Flame.images.add(imagePath, image);\n        Flame.images.add(imagePath2, image2);\n\n        var flag = false;\n        await tester.pumpWidget(\n          StatefulBuilder(\n            builder: (context, setState) {\n              return MaterialApp(\n                home: Scaffold(\n                  body: SizedBox(\n                    height: 200,\n                    width: 200,\n                    child: Wrap(\n                      children: [\n                        ElevatedButton(\n                          onPressed: () {\n                            setState(() {\n                              flag = !flag;\n                            });\n                          },\n                          child: const Text('Change sprite'),\n                        ),\n                        NineTileBoxWidget.asset(\n                          path: flag ? imagePath2 : imagePath,\n                          tileSize: 10,\n                          destTileSize: 10,\n                          loadingBuilder: (_) => const LoadingWidget(),\n                        ),\n                      ],\n                    ),\n                  ),\n                ),\n              );\n            },\n          ),\n        );\n\n        await tester.pumpAndSettle();\n\n        var internalWidget = tester.widget<InternalNineTileBox>(\n          find.byType(InternalNineTileBox),\n        );\n\n        expect(internalWidget.image, image);\n\n        await tester.tap(find.byType(ElevatedButton));\n        await tester.pumpAndSettle();\n\n        internalWidget = tester.widget<InternalNineTileBox>(\n          find.byType(InternalNineTileBox),\n        );\n\n        expect(internalWidget.image, image2);\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/widgets/sprite_animation_widget_test.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/src/sprite_animation_ticker.dart';\nimport 'package:flame/widgets.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'loading_widget.dart';\n\nFuture<void> main() async {\n  final image = await generateImage();\n\n  group('SpriteAnimationWidget', () {\n    testWidgets('has no FutureBuilder when passed an animation', (\n      tester,\n    ) async {\n      final sprite1 = Sprite(image);\n      final sprite2 = Sprite(image);\n      final spriteAnimation = SpriteAnimation.spriteList(\n        [sprite1, sprite2],\n        stepTime: 0.1,\n      );\n\n      await tester.pumpWidget(\n        SpriteAnimationWidget(\n          animation: spriteAnimation,\n          animationTicker: spriteAnimation.createTicker(),\n        ),\n      );\n\n      final futureBuilderFinder = find.byType(FutureBuilder);\n      final spriteAnimationWidgetFinder = find.byType(SpriteAnimationWidget);\n\n      expect(futureBuilderFinder, findsNothing);\n      expect(spriteAnimationWidgetFinder, findsOneWidget);\n    });\n\n    testWidgets(\n      'has FutureBuilder and LoadingWidget when passed an asset path',\n      (tester) async {\n        const imagePath = 'test_path';\n        Flame.images.add(imagePath, image);\n        final spriteAnimationData = SpriteAnimationData.sequenced(\n          amount: 2,\n          stepTime: 1,\n          textureSize: Vector2(10, 10),\n        );\n\n        await tester.pumpWidget(\n          SpriteAnimationWidget.asset(\n            path: imagePath,\n            data: spriteAnimationData,\n            loadingBuilder: (_) => const LoadingWidget(),\n          ),\n        );\n\n        final futureBuilderFinder = find.byType(FutureBuilder<SpriteAnimation>);\n        final spriteAnimationWidgetFinder = find.byType(\n          InternalSpriteAnimationWidget,\n        );\n        final loadingWidgetFinder = find.byType(LoadingWidget);\n\n        expect(futureBuilderFinder, findsOneWidget);\n        expect(loadingWidgetFinder, findsOneWidget);\n        expect(spriteAnimationWidgetFinder, findsNothing);\n\n        /// loading to be removed\n        await tester.pump();\n\n        expect(futureBuilderFinder, findsOneWidget);\n        expect(loadingWidgetFinder, findsNothing);\n        expect(spriteAnimationWidgetFinder, findsOneWidget);\n      },\n    );\n\n    testWidgets(\n      'can safely change animation parameter',\n      (tester) async {\n        const executionCount = 10;\n        final frames = List.generate(5, (_) => Sprite(image));\n        final animation1 = SpriteAnimation.spriteList(frames, stepTime: 0.1);\n        final animation2 = SpriteAnimation.spriteList(frames, stepTime: 0.2);\n        final animationTicker1 = SpriteAnimationTicker(animation1);\n        final animationTicker2 = SpriteAnimationTicker(animation2);\n\n        var animation1Started = false;\n        var animation2Started = false;\n\n        animationTicker1.onStart = () => animation1Started = true;\n        animationTicker2.onStart = () => animation2Started = true;\n\n        for (var index = 0; index < executionCount; index++) {\n          animation1Started = false;\n          animation2Started = false;\n\n          await tester.pumpWidget(\n            SpriteAnimationWidget(\n              animation: animation1,\n              animationTicker: animationTicker1,\n            ),\n          );\n          await tester.pump();\n\n          expect(animationTicker1.onComplete, isNotNull);\n          expect(animationTicker2.onComplete, isNull);\n\n          await tester.pump();\n\n          expect(animation1Started, true);\n\n          // This will call didUpdateWidget lifecycle\n          await tester.pumpWidget(\n            SpriteAnimationWidget(\n              animation: animation2,\n              animationTicker: animationTicker2,\n            ),\n          );\n\n          await tester.pump();\n\n          expect(animationTicker1.onComplete, isNull);\n          expect(animationTicker2.onComplete, isNotNull);\n\n          await tester.pump();\n          expect(animation2Started, true);\n        }\n      },\n    );\n\n    testWidgets(\n      'does not resets the ticker when has a new animation',\n      (tester) async {\n        final frames = List.generate(5, (_) => Sprite(image));\n        final animation = SpriteAnimation.spriteList(\n          frames,\n          stepTime: 0.1,\n          loop: false,\n        );\n        final animationTicker = SpriteAnimationTicker(animation);\n\n        animationTicker.setToLast();\n\n        await tester.pumpWidget(\n          MaterialApp(\n            home: StatefulBuilder(\n              builder: (context, setState) {\n                return Column(\n                  children: [\n                    SpriteAnimationWidget(\n                      animation: animation,\n                      animationTicker: animationTicker,\n                    ),\n                    ElevatedButton(\n                      onPressed: () {\n                        setState(() {});\n                      },\n                      child: const Text('Change animations'),\n                    ),\n                  ],\n                );\n              },\n            ),\n          ),\n        );\n        animationTicker.setToLast();\n\n        await tester.tap(find.byType(ElevatedButton));\n        await tester.pump();\n\n        expect(animationTicker.isLastFrame, isTrue);\n      },\n    );\n\n    testWidgets(\n      'onComplete callback is called when the animation is finished',\n      (tester) async {\n        const imagePath = 'test_image_path';\n        Flame.images.add(imagePath, image);\n        final spriteAnimationData = SpriteAnimationData.sequenced(\n          amount: 1,\n          stepTime: 0.1,\n          textureSize: Vector2(16, 16),\n          loop: false,\n        );\n\n        var onCompleteCalled = false;\n\n        await tester.pumpWidget(\n          SpriteAnimationWidget.asset(\n            path: imagePath,\n            data: spriteAnimationData,\n            onComplete: () => onCompleteCalled = true,\n          ),\n        );\n\n        await tester.pumpAndSettle();\n\n        expect(onCompleteCalled, isTrue);\n      },\n    );\n\n    group('when the image changes', () {\n      testWidgets('updates the widget', (tester) async {\n        const imagePath = 'test_path_2';\n        const imagePath2 = 'test_path_3';\n\n        final image = await generateImage(100, 100);\n        final image2 = await generateImage(100, 102);\n\n        Flame.images.add(imagePath, image);\n        Flame.images.add(imagePath2, image2);\n\n        final spriteAnimationData = SpriteAnimationData.sequenced(\n          amount: 1,\n          stepTime: 0.1,\n          textureSize: Vector2(16, 16),\n          loop: false,\n        );\n\n        var flag = false;\n        await tester.pumpWidget(\n          StatefulBuilder(\n            builder: (context, setState) {\n              return MaterialApp(\n                home: Scaffold(\n                  body: SizedBox(\n                    height: 200,\n                    width: 200,\n                    child: Wrap(\n                      children: [\n                        ElevatedButton(\n                          onPressed: () {\n                            setState(() {\n                              flag = !flag;\n                            });\n                          },\n                          child: const Text('Change sprite'),\n                        ),\n                        SpriteAnimationWidget.asset(\n                          path: flag ? imagePath2 : imagePath,\n                          data: spriteAnimationData,\n                        ),\n                      ],\n                    ),\n                  ),\n                ),\n              );\n            },\n          ),\n        );\n\n        await tester.pump();\n        await tester.pump();\n        await tester.pump();\n        await tester.pump();\n\n        var internalWidget = tester.widget<InternalSpriteAnimationWidget>(\n          find.byType(InternalSpriteAnimationWidget),\n        );\n\n        expect(internalWidget.animation.frames.first.sprite.image, image);\n\n        await tester.tap(find.byType(ElevatedButton));\n        await tester.pump();\n        await tester.pump();\n        await tester.pump();\n        await tester.pump();\n\n        internalWidget = tester.widget<InternalSpriteAnimationWidget>(\n          find.byType(InternalSpriteAnimationWidget),\n        );\n\n        expect(internalWidget.animation.frames.first.sprite.image, image2);\n      });\n    });\n\n    group('when the sprite data changes', () {\n      group('when the frame length changes', () {\n        testWidgets('updates the widget', (tester) async {\n          const imagePath = 'test_path_2';\n\n          final image = await generateImage(100, 100);\n\n          Flame.images.add(imagePath, image);\n\n          final spriteAnimationData = SpriteAnimationData.sequenced(\n            amount: 1,\n            stepTime: 0.1,\n            textureSize: Vector2(16, 16),\n            loop: false,\n          );\n\n          final spriteAnimationData2 = SpriteAnimationData.sequenced(\n            amount: 2,\n            stepTime: 0.1,\n            textureSize: Vector2(16, 16),\n            loop: false,\n          );\n\n          var flag = false;\n          await tester.pumpWidget(\n            StatefulBuilder(\n              builder: (context, setState) {\n                return MaterialApp(\n                  home: Scaffold(\n                    body: SizedBox(\n                      height: 200,\n                      width: 200,\n                      child: Wrap(\n                        children: [\n                          ElevatedButton(\n                            onPressed: () {\n                              setState(() {\n                                flag = !flag;\n                              });\n                            },\n                            child: const Text('Change sprite'),\n                          ),\n                          SpriteAnimationWidget.asset(\n                            path: imagePath,\n                            data: flag\n                                ? spriteAnimationData2\n                                : spriteAnimationData,\n                          ),\n                        ],\n                      ),\n                    ),\n                  ),\n                );\n              },\n            ),\n          );\n\n          await tester.pump();\n          await tester.pump();\n\n          var internalWidget = tester.widget<InternalSpriteAnimationWidget>(\n            find.byType(InternalSpriteAnimationWidget),\n          );\n\n          expect(internalWidget.animation.frames, hasLength(1));\n\n          await tester.tap(find.byType(ElevatedButton));\n          await tester.pump();\n          await tester.pump();\n          await tester.pump();\n          await tester.pump();\n\n          internalWidget = tester.widget<InternalSpriteAnimationWidget>(\n            find.byType(InternalSpriteAnimationWidget),\n          );\n\n          expect(internalWidget.animation.frames, hasLength(2));\n        });\n      });\n\n      group('when a single frame  changes', () {\n        testWidgets('updates the widget', (tester) async {\n          const imagePath = 'test_path_2';\n\n          final image = await generateImage(100, 100);\n\n          Flame.images.add(imagePath, image);\n\n          final spriteAnimationData = SpriteAnimationData.sequenced(\n            amount: 1,\n            stepTime: 0.1,\n            textureSize: Vector2(16, 16),\n            loop: false,\n          );\n\n          final spriteAnimationData2 = SpriteAnimationData.sequenced(\n            amount: 1,\n            stepTime: 0.1,\n            textureSize: Vector2(12, 12),\n            loop: false,\n          );\n\n          var flag = false;\n          await tester.pumpWidget(\n            StatefulBuilder(\n              builder: (context, setState) {\n                return MaterialApp(\n                  home: Scaffold(\n                    body: SizedBox(\n                      height: 200,\n                      width: 200,\n                      child: Wrap(\n                        children: [\n                          ElevatedButton(\n                            onPressed: () {\n                              setState(() {\n                                flag = !flag;\n                              });\n                            },\n                            child: const Text('Change sprite'),\n                          ),\n                          SpriteAnimationWidget.asset(\n                            path: imagePath,\n                            data: flag\n                                ? spriteAnimationData2\n                                : spriteAnimationData,\n                          ),\n                        ],\n                      ),\n                    ),\n                  ),\n                );\n              },\n            ),\n          );\n\n          await tester.pump();\n          await tester.pump();\n          await tester.pump();\n          await tester.pump();\n\n          var internalWidget = tester.widget<InternalSpriteAnimationWidget>(\n            find.byType(InternalSpriteAnimationWidget),\n          );\n\n          expect(\n            internalWidget.animation.frames.first.sprite.srcSize,\n            Vector2.all(16),\n          );\n\n          await tester.tap(find.byType(ElevatedButton));\n          await tester.pump();\n          await tester.pump();\n          await tester.pump();\n          await tester.pump();\n\n          internalWidget = tester.widget<InternalSpriteAnimationWidget>(\n            find.byType(InternalSpriteAnimationWidget),\n          );\n\n          expect(\n            internalWidget.animation.frames.first.sprite.srcSize,\n            Vector2.all(12),\n          );\n        });\n      });\n\n      group('when looping changes', () {\n        testWidgets('updates the widget', (tester) async {\n          const imagePath = 'test_path_2';\n\n          final image = await generateImage(100, 100);\n\n          Flame.images.add(imagePath, image);\n\n          final spriteAnimationData = SpriteAnimationData.sequenced(\n            amount: 1,\n            stepTime: 0.1,\n            textureSize: Vector2(16, 16),\n            loop: false,\n          );\n\n          final spriteAnimationData2 = SpriteAnimationData.sequenced(\n            amount: 1,\n            stepTime: 0.1,\n            textureSize: Vector2(16, 16),\n          );\n\n          var flag = false;\n          await tester.pumpWidget(\n            StatefulBuilder(\n              builder: (context, setState) {\n                return MaterialApp(\n                  home: Scaffold(\n                    body: SizedBox(\n                      height: 200,\n                      width: 200,\n                      child: Wrap(\n                        children: [\n                          ElevatedButton(\n                            onPressed: () {\n                              setState(() {\n                                flag = !flag;\n                              });\n                            },\n                            child: const Text('Change sprite'),\n                          ),\n                          SpriteAnimationWidget.asset(\n                            path: imagePath,\n                            data: flag\n                                ? spriteAnimationData2\n                                : spriteAnimationData,\n                          ),\n                        ],\n                      ),\n                    ),\n                  ),\n                );\n              },\n            ),\n          );\n\n          await tester.pump();\n          await tester.pump();\n          await tester.pump();\n          await tester.pump();\n\n          var internalWidget = tester.widget<InternalSpriteAnimationWidget>(\n            find.byType(InternalSpriteAnimationWidget),\n          );\n\n          expect(\n            internalWidget.animation.loop,\n            isFalse,\n          );\n\n          await tester.tap(find.byType(ElevatedButton));\n          await tester.pump();\n          await tester.pump();\n          await tester.pump();\n          await tester.pump();\n\n          internalWidget = tester.widget<InternalSpriteAnimationWidget>(\n            find.byType(InternalSpriteAnimationWidget),\n          );\n\n          expect(\n            internalWidget.animation.loop,\n            isTrue,\n          );\n        });\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/widgets/sprite_button_test.dart",
    "content": "import 'package:flame/cache.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/widgets.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nimport 'loading_widget.dart';\n\nclass _MockImages extends Mock implements Images {}\n\nFuture<void> main() async {\n  TestWidgetsFlutterBinding.ensureInitialized();\n  final image = await generateImage();\n\n  group('SpriteButton', () {\n    testWidgets('has no FutureBuilder when passed an animation', (\n      tester,\n    ) async {\n      final sprite1 = Sprite(image);\n      final sprite2 = Sprite(image);\n\n      await tester.pumpWidget(\n        SpriteButton(\n          sprite: sprite1,\n          pressedSprite: sprite2,\n          onPressed: () {},\n          width: 100,\n          height: 100,\n          label: const SizedBox(),\n        ),\n      );\n\n      final futureBuilderFinder = find.byType(FutureBuilder);\n      final nineTileBoxWidgetFinder = find.byType(SpriteButton);\n\n      expect(futureBuilderFinder, findsNothing);\n      expect(nineTileBoxWidgetFinder, findsOneWidget);\n    });\n\n    testWidgets(\n      'has FutureBuilder and LoadingWidget when passed an asset path',\n      (tester) async {\n        const imagePath1 = 'test_path_1';\n        const imagePath2 = 'test_path_2';\n\n        final mockImageCache = _MockImages();\n\n        when(() => mockImageCache.load(any())).thenAnswer(\n          (_) => generateImage(),\n        );\n\n        when(() => mockImageCache.containsKey(any())).thenAnswer((_) => false);\n\n        await tester.pumpWidget(\n          SpriteButton.asset(\n            path: imagePath1,\n            pressedPath: imagePath2,\n            onPressed: () {},\n            width: 100,\n            height: 100,\n            label: const SizedBox(),\n            loadingBuilder: (_) => const LoadingWidget(),\n            images: mockImageCache,\n          ),\n        );\n\n        final futureBuilderFinder = find.byType(FutureBuilder<List<Sprite>>);\n        final internalButtonFinder = find.byType(InternalSpriteButton);\n        final loadingWidgetFinder = find.byType(LoadingWidget);\n\n        expect(futureBuilderFinder, findsOneWidget);\n        expect(loadingWidgetFinder, findsOneWidget);\n        expect(internalButtonFinder, findsNothing);\n\n        /// loading to be removed\n        await tester.pump();\n\n        expect(futureBuilderFinder, findsOneWidget);\n        expect(loadingWidgetFinder, findsNothing);\n        expect(internalButtonFinder, findsOneWidget);\n      },\n    );\n\n    testWidgets(\n      'has no FutureBuilder or LoadingWidget when passed already '\n      'loaded asset paths',\n      (tester) async {\n        const imagePath1 = 'test_path_1';\n        const imagePath2 = 'test_path_2';\n        Flame.images.add(imagePath1, image);\n        Flame.images.add(imagePath2, image);\n\n        await tester.pumpWidget(\n          SpriteButton.asset(\n            path: imagePath1,\n            pressedPath: imagePath2,\n            onPressed: () {},\n            width: 100,\n            height: 100,\n            label: const SizedBox(),\n            loadingBuilder: (_) => const LoadingWidget(),\n          ),\n        );\n\n        final futureBuilderFinder = find.byType(FutureBuilder<List<Sprite>>);\n        final internalButtonFinder = find.byType(InternalSpriteButton);\n        final loadingWidgetFinder = find.byType(LoadingWidget);\n\n        expect(futureBuilderFinder, findsNothing);\n        expect(loadingWidgetFinder, findsNothing);\n        expect(internalButtonFinder, findsOneWidget);\n\n        /// loading to be removed\n        await tester.pump();\n\n        expect(futureBuilderFinder, findsNothing);\n        expect(loadingWidgetFinder, findsNothing);\n        expect(internalButtonFinder, findsOneWidget);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame/test/widgets/sprite_widget_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/widgets.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'loading_widget.dart';\n\nFuture<void> main() async {\n  final image = await generateImage();\n\n  group('SpriteWidget', () {\n    testWidgets('has no FutureBuilder when passed an sprite', (tester) async {\n      final sprite = Sprite(image);\n\n      await tester.pumpWidget(SpriteWidget(sprite: sprite));\n\n      final futureBuilderFinder = find.byType(FutureBuilder);\n      final spriteWidgetFinder = find.byType(SpriteWidget);\n\n      expect(futureBuilderFinder, findsNothing);\n      expect(spriteWidgetFinder, findsOneWidget);\n    });\n\n    testWidgets(\n      'has FutureBuilder and LoadingWidget when passed an asset path',\n      (tester) async {\n        const imagePath = 'test_path';\n        Flame.images.add(imagePath, image);\n\n        await tester.pumpWidget(\n          SpriteWidget.asset(\n            path: imagePath,\n            loadingBuilder: (_) => const LoadingWidget(),\n          ),\n        );\n\n        final futureBuilderFinder = find.byType(FutureBuilder<Sprite>);\n        final spriteWidgetFinder = find.byType(InternalSpriteWidget);\n        final loadingWidgetFinder = find.byType(LoadingWidget);\n\n        expect(futureBuilderFinder, findsOneWidget);\n        expect(loadingWidgetFinder, findsOneWidget);\n        expect(spriteWidgetFinder, findsNothing);\n\n        /// loading to be removed\n        await tester.pump();\n\n        expect(futureBuilderFinder, findsOneWidget);\n        expect(loadingWidgetFinder, findsNothing);\n        expect(spriteWidgetFinder, findsOneWidget);\n      },\n    );\n\n    group('when the sprite changes', () {\n      testWidgets('updates the sprite widget', (tester) async {\n        const imagePath = 'test_path_2';\n        Flame.images.add(imagePath, await generateImage(100, 100));\n\n        var flag = false;\n        await tester.pumpWidget(\n          StatefulBuilder(\n            builder: (context, setState) {\n              return MaterialApp(\n                home: Scaffold(\n                  body: SizedBox(\n                    height: 200,\n                    width: 200,\n                    child: Wrap(\n                      children: [\n                        ElevatedButton(\n                          onPressed: () {\n                            setState(() {\n                              flag = !flag;\n                            });\n                          },\n                          child: const Text('Change sprite'),\n                        ),\n                        SpriteWidget.asset(\n                          path: imagePath,\n                          srcPosition: flag ? Vector2(10, 10) : Vector2(0, 0),\n                          loadingBuilder: (_) => const LoadingWidget(),\n                        ),\n                      ],\n                    ),\n                  ),\n                ),\n              );\n            },\n          ),\n        );\n\n        await tester.pumpAndSettle();\n\n        var internalSpriteWidgetFinder = tester.widget<InternalSpriteWidget>(\n          find.byType(InternalSpriteWidget),\n        );\n\n        expect(internalSpriteWidgetFinder.sprite.srcPosition, Vector2(0, 0));\n\n        await tester.tap(find.byType(ElevatedButton));\n        await tester.pumpAndSettle();\n\n        internalSpriteWidgetFinder = tester.widget<InternalSpriteWidget>(\n          find.byType(InternalSpriteWidget),\n        );\n\n        expect(internalSpriteWidgetFinder.sprite.srcPosition, Vector2(10, 10));\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_3d/CHANGELOG.md",
    "content": "## 0.1.1+7\n\n - **FIX**(flame_3d): Use float for numLights uniform to fix GLES crash ([#3810](https://github.com/flame-engine/flame/issues/3810)). ([f241ebd6](https://github.com/flame-engine/flame/commit/f241ebd6d73caa43387972cffeb609af2909ac2f))\n - **FIX**(flame_3d): Fix alpha blend factors for transparent background compositing ([#3812](https://github.com/flame-engine/flame/issues/3812)). ([8d7212a1](https://github.com/flame-engine/flame/commit/8d7212a1c79d561330131ea794a0ed318c3945f8))\n - **FIX**(flame_3d): Remove duplicate baseColor multiply in ambient calculation ([#3814](https://github.com/flame-engine/flame/issues/3814)). ([080bde7f](https://github.com/flame-engine/flame/commit/080bde7f4c14c9f4f91e09d9bf6e737f75449351))\n - **FIX**: Use scaledRadius for CircleHitbox collision detection ([#3808](https://github.com/flame-engine/flame/issues/3808)). ([3498c1e5](https://github.com/flame-engine/flame/commit/3498c1e565985ce7d12212e6a625f538da98c362))\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 0.1.1+6\n\n - Update a dependency to the latest release.\n\n## 0.1.1+5\n\n - Update a dependency to the latest release.\n\n## 0.1.1+4\n\n - Update a dependency to the latest release.\n\n## 0.1.1+3\n\n - Update a dependency to the latest release.\n\n## 0.1.1+2\n\n - **DOCS**: Update README.md of flame_3d with newer instructions ([#3711](https://github.com/flame-engine/flame/issues/3711)). ([ad7dc059](https://github.com/flame-engine/flame/commit/ad7dc059f2f97e7bc78a74bdff149b26adb22fbc))\n\n## 0.1.1+1\n\n - **FIX**: When parsing OBJs with no normals, fallback to normal generation ([#3708](https://github.com/flame-engine/flame/issues/3708)). ([a59ccaa3](https://github.com/flame-engine/flame/commit/a59ccaa341a562c25ad54f4af567b160084e06d8))\n - **FIX**: Update compiled shaderbundle for flame_3d and add color example ([#3704](https://github.com/flame-engine/flame/issues/3704)). ([a1070d97](https://github.com/flame-engine/flame/commit/a1070d975fd3af52d44f3febb13dd97afd4990a6))\n\n## 0.1.1\n\n - **FIX**: Cleanup and make OBJ parser more resilient ([#3702](https://github.com/flame-engine/flame/issues/3702)). ([b96421d5](https://github.com/flame-engine/flame/commit/b96421d5035393d764a9ec34aeba8db380222f45))\n - **FIX**: Fix color to byte conversion for shaders ([#3700](https://github.com/flame-engine/flame/issues/3700)). ([2426c06b](https://github.com/flame-engine/flame/commit/2426c06b799797720c9df8fbd73e337422654d00))\n - **FIX**: Add fallback default material to avoid crashes ([#3701](https://github.com/flame-engine/flame/issues/3701)). ([8e6b04e3](https://github.com/flame-engine/flame/commit/8e6b04e38855b6fae6761a9f8d20c82c6bff6d76))\n - **FIX**: Expose Line3D on components.dart ([#3698](https://github.com/flame-engine/flame/issues/3698)). ([d58d7b54](https://github.com/flame-engine/flame/commit/d58d7b5482f646e4536160449f4a17317b8aff2b))\n - **FEAT**: Introduce cone mesh ([#3699](https://github.com/flame-engine/flame/issues/3699)). ([874a97fe](https://github.com/flame-engine/flame/commit/874a97fe421dadbed9f3825c8562f8bd7e6c95c9))\n - **DOCS**: Update instructions on how to use flame_3d ([#3696](https://github.com/flame-engine/flame/issues/3696)). ([11cc2a8f](https://github.com/flame-engine/flame/commit/11cc2a8fae0f1c17106f91ef1ac319e6f8e39036))\n\n## 0.1.0\n\n - **REFACTOR**: Add collections library to flame_3d ([#3680](https://github.com/flame-engine/flame/issues/3680)). ([89e5e58e](https://github.com/flame-engine/flame/commit/89e5e58efb580ec267a0dca78a3a0f320203d4ee))\n - **FIX**: Update flame_3d to support both old and newer Flutter APIs ([#3663](https://github.com/flame-engine/flame/issues/3663)). ([d9f1fe7f](https://github.com/flame-engine/flame/commit/d9f1fe7f9abd8f0307ecc22ff24d3b492e9ca332))\n - **FEAT**: Add ability to run on flame_3d example ([#3679](https://github.com/flame-engine/flame/issues/3679)). ([801692bf](https://github.com/flame-engine/flame/commit/801692bfa1226e01f1540166a8140ab42e36ed87))\n - **FEAT**: Add support for model parsing and rendering in flame_3d, including skeletal animations ([#3675](https://github.com/flame-engine/flame/issues/3675)). ([cc58aef5](https://github.com/flame-engine/flame/commit/cc58aef5b53f208fb1cbb116bfb9f9af9a351e8e))\n - **FEAT**: Add setup command to flame_3d example ([#3671](https://github.com/flame-engine/flame/issues/3671)). ([2f5ba87b](https://github.com/flame-engine/flame/commit/2f5ba87be8068b12c2604b79f7db9c1f3307a4b6))\n - **FEAT**: Organize components and add destroy command to flame_3d example ([#3665](https://github.com/flame-engine/flame/issues/3665)). ([d5915752](https://github.com/flame-engine/flame/commit/d591575263d8b13aee862efe05842001aa60f89d))\n - **FEAT**: Add keybind to jump on flame_3d example ([#3664](https://github.com/flame-engine/flame/issues/3664)). ([7be0bccd](https://github.com/flame-engine/flame/commit/7be0bccda0040bfc3734f90b6bca7d5e99455bee))\n\n## 0.1.0-dev.14\n\n - Update a dependency to the latest release.\n\n## 0.1.0-dev.13\n\n - Update a dependency to the latest release.\n\n## 0.1.0-dev.12\n\n - **FIX**: Only expose `ReadOnlyOrderedset` from `component.children` ([#3606](https://github.com/flame-engine/flame/issues/3606)). ([79351d19](https://github.com/flame-engine/flame/commit/79351d195ea968b8016129e79a489ef113a0fdf3))\n\n## 0.1.0-dev.11\n\n - **FEAT**: Add flame_console to flame_3d example to enable more complex setups ([#3580](https://github.com/flame-engine/flame/issues/3580)). ([15a6f8b0](https://github.com/flame-engine/flame/commit/15a6f8b001deb714134976d7cbb5ef0a6ec31c86))\n - **DOCS**: Update flame_3d example to fix movement and camera, organize files ([#3573](https://github.com/flame-engine/flame/issues/3573)). ([54ca8433](https://github.com/flame-engine/flame/commit/54ca8433f20b451eb1a2c3c5f5a47a3430e71a6e))\n\n## 0.1.0-dev.10\n\n - **FIX**: Update ordered_set, remove ComponentSet ([#3554](https://github.com/flame-engine/flame/issues/3554)). ([728e13f8](https://github.com/flame-engine/flame/commit/728e13f855d988e8f8e24976557b213b98221603))\n\n## 0.1.0-dev.9\n\n - **FEAT**: Bump to new lint package ([#3545](https://github.com/flame-engine/flame/issues/3545)). ([bf6ee518](https://github.com/flame-engine/flame/commit/bf6ee51897591b7ad6e5f9da2193b1eeeaf026f4))\n\n## 0.1.0-dev.8\n\n - Update a dependency to the latest release.\n\n## 0.1.0-dev.7\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 0.1.0-dev.6\n\n - **FEAT**: Add helper methods to create matrix4 with sensible defaults ([#3486](https://github.com/flame-engine/flame/issues/3486)). ([9345c870](https://github.com/flame-engine/flame/commit/9345c870d4de57d8d3a4d07a014d18cb71c01618))\n - **FEAT**: Add setFromMatrix4 to Transform3D ([#3484](https://github.com/flame-engine/flame/issues/3484)). ([1dbb4433](https://github.com/flame-engine/flame/commit/1dbb4433ca9e06b93a49b430fe9c084885551ff2))\n - **FEAT**: Add Line3D mesh component [flame_3d] ([#3412](https://github.com/flame-engine/flame/issues/3412)). ([c332c965](https://github.com/flame-engine/flame/commit/c332c9651ad8b930281c1d0bc13b89754fd0b2c1))\n\n## 0.1.0-dev.5\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FEAT**: Make resource creation be on demand to enable testing (#3411).\n\n## 0.1.0-dev.4\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n - **FIX**: Use saner default value for camera's target ([#3238](https://github.com/flame-engine/flame/issues/3238)). ([78522c62](https://github.com/flame-engine/flame/commit/78522c624d846c827a1c0d7377837e04a30ba4e7))\n - **FIX**: Use saner default value for camera's target ([#3238](https://github.com/flame-engine/flame/issues/3238)). ([99ca355b](https://github.com/flame-engine/flame/commit/99ca355b3d4a3e9c7677454dc9bc2c874ee2e4a9))\n - **FIX**: Fix albedo texture binding on shader ([#3400](https://github.com/flame-engine/flame/issues/3400)). ([c380c91f](https://github.com/flame-engine/flame/commit/c380c91f7a7e22ab71c3617c0386159861a64abc))\n - **FIX**: Make build_shaders script compatible with other platforms [flame_3d] ([#3395](https://github.com/flame-engine/flame/issues/3395)). ([38331309](https://github.com/flame-engine/flame/commit/38331309ca5ee609e85422ee9d740569a35e5e9e))\n - **FIX**: Make `flame_3d` work with latest stable ([#3387](https://github.com/flame-engine/flame/issues/3387)). ([f940d3f9](https://github.com/flame-engine/flame/commit/f940d3f9420d3ce001f47a9155c582b8b4cd1dcb))\n - **FIX**: MeshComponent.bind should bind to the provided device ([#3278](https://github.com/flame-engine/flame/issues/3278)). ([d06b4ce6](https://github.com/flame-engine/flame/commit/d06b4ce6935987705447772f912719c405d39c9e))\n - **FIX**: Improve behavior of Quaternion.slerp function [flame_3d] ([#3306](https://github.com/flame-engine/flame/issues/3306)). ([8872a45a](https://github.com/flame-engine/flame/commit/8872a45a1b49d7ba7688d62ca25b2a8d31c495cb))\n - **FIX**: Improve quaternion slerp logic to avoid NaN edge cases ([#3303](https://github.com/flame-engine/flame/issues/3303)). ([712b03fb](https://github.com/flame-engine/flame/commit/712b03fbe176cf238a3780aee641254436520366))\n - **FIX**: Make the `build_shader` script work for windows ([#3402](https://github.com/flame-engine/flame/issues/3402)). ([ce7822a0](https://github.com/flame-engine/flame/commit/ce7822a0d0c5ac1c766635a5cf2c242d5e5f98ec))\n - **FIX**: MeshComponent.bind should bind to the provided device ([#3278](https://github.com/flame-engine/flame/issues/3278)). ([3ae3ef54](https://github.com/flame-engine/flame/commit/3ae3ef5476fa5f9ead7069efeee35cc31c0e9dd2))\n - **FIX**: Fix typo on loadTexture ([#3253](https://github.com/flame-engine/flame/issues/3253)). ([3a20a8cd](https://github.com/flame-engine/flame/commit/3a20a8cd61543aad21c1015de5c31ec1cbe71aed))\n - **FIX**: Improve behavior of Quaternion.slerp function [flame_3d] ([#3306](https://github.com/flame-engine/flame/issues/3306)). ([b9d6a0f1](https://github.com/flame-engine/flame/commit/b9d6a0f1d34e009cd91ae9d2ab0eed09b546d110))\n - **FIX**: Improve quaternion slerp logic to avoid NaN edge cases ([#3303](https://github.com/flame-engine/flame/issues/3303)). ([565b68b9](https://github.com/flame-engine/flame/commit/565b68b9da52d44281e93f9ae8617f9dbe9551f3))\n - **FIX**: Fix typo on loadTexture ([#3253](https://github.com/flame-engine/flame/issues/3253)). ([3d1f1437](https://github.com/flame-engine/flame/commit/3d1f143700506467798e030450227d8029b74ef2))\n - **FIX**: Add missing export for CylinderMesh [flame_3d] ([#3256](https://github.com/flame-engine/flame/issues/3256)). ([d8fb65c1](https://github.com/flame-engine/flame/commit/d8fb65c158a3cce442972fb24f55fb522729085f))\n - **FIX**: Add missing export for CylinderMesh [flame_3d] ([#3256](https://github.com/flame-engine/flame/issues/3256)). ([d517c169](https://github.com/flame-engine/flame/commit/d517c169ed9b4d4457df6ac1ae363277577597fa))\n - **FEAT**: More Lights! [flame_3d] ([#3250](https://github.com/flame-engine/flame/issues/3250)). ([5c508e81](https://github.com/flame-engine/flame/commit/5c508e81bddfb17857355da80e57f1ac77ab368d))\n - **FEAT**(flame_3d): Add helpful extension functions to Vector ([#3141](https://github.com/flame-engine/flame/issues/3141)). ([92195989](https://github.com/flame-engine/flame/commit/9219598904131d8fceba8d1ad980bea2805e3515))\n - **FEAT**: Add CylinderMesh [flame_3d] ([#3239](https://github.com/flame-engine/flame/issues/3239)). ([d5de1733](https://github.com/flame-engine/flame/commit/d5de1733c64c4fbaeee83089a3b0f9a3fbb3355d))\n - **FEAT**: Refactor shader uniform binding to support shader arrays [flame_3d] ([#3282](https://github.com/flame-engine/flame/issues/3282)). ([edae1662](https://github.com/flame-engine/flame/commit/edae166252a519d38496bb6bf7c84fe9f401b8d4))\n - **FEAT**(flame_3d): Make shader api more useful ([#3085](https://github.com/flame-engine/flame/issues/3085)). ([fe2e4f20](https://github.com/flame-engine/flame/commit/fe2e4f20195b453268b34e589616343fdce6201a))\n - **FEAT**(flame_3d): Make shader api more useful ([#3085](https://github.com/flame-engine/flame/issues/3085)). ([4042d300](https://github.com/flame-engine/flame/commit/4042d3002ce619bf6b8597aa7dc17a803bc57c69))\n - **FEAT**: Add more useful extensions to VectorN and Quaternion [flame_3d] ([#3296](https://github.com/flame-engine/flame/issues/3296)). ([f5f03e5e](https://github.com/flame-engine/flame/commit/f5f03e5ed7095ede713727d6bbab39db0505e7bf))\n - **FEAT**: Expose vector classes on core file [flame_3d] ([#3211](https://github.com/flame-engine/flame/issues/3211)). ([8f403ac2](https://github.com/flame-engine/flame/commit/8f403ac23ae7cdf5343652c30f9c0ee71d627b0a))\n - **FEAT**(flame_3d): Add helpful extension functions to Vector ([#3141](https://github.com/flame-engine/flame/issues/3141)). ([39e15fb3](https://github.com/flame-engine/flame/commit/39e15fb30256cbfaa86f4d7b8e3453c52942d1a5))\n - **FEAT**: Add CylinderMesh [flame_3d] ([#3239](https://github.com/flame-engine/flame/issues/3239)). ([01872fb6](https://github.com/flame-engine/flame/commit/01872fb6e45e10dc380fee7a176a8b37eeaef880))\n - **FEAT**(flame_3d): initial implementation of 3D support ([#3012](https://github.com/flame-engine/flame/issues/3012)). ([acb8e6fc](https://github.com/flame-engine/flame/commit/acb8e6fc07592a7df041512ed9e033b33eda8799))\n - **FEAT**: Support skeletal animation basics [flame_3d]  ([#3291](https://github.com/flame-engine/flame/issues/3291)). ([9c0d1500](https://github.com/flame-engine/flame/commit/9c0d15006047597097dc0e054c1e03a04491cff9))\n - **FEAT**: Add normals to surfaces when not specified ([#3257](https://github.com/flame-engine/flame/issues/3257)). ([1dd21d7d](https://github.com/flame-engine/flame/commit/1dd21d7d6d4f58c9ac54a38363ee2fd0978a9d0c))\n - **FEAT**(flame_3d): initial implementation of 3D support ([#3012](https://github.com/flame-engine/flame/issues/3012)). ([0242e1dd](https://github.com/flame-engine/flame/commit/0242e1dd12a9b50a411d895b662f9df33536f6d9))\n - **FEAT**: Support skeletal animation basics [flame_3d]  ([#3291](https://github.com/flame-engine/flame/issues/3291)). ([12927e41](https://github.com/flame-engine/flame/commit/12927e4100a7b4b46e4218db6792d25be1623f88))\n - **FEAT**: Add more useful extensions to VectorN and Quaternion [flame_3d] ([#3296](https://github.com/flame-engine/flame/issues/3296)). ([9cb95279](https://github.com/flame-engine/flame/commit/9cb9527909a4faa38609d25ebd7463f1e2e1a1ab))\n - **FEAT**: Make it easier work with the Mesh class [flame_3d] ([#3212](https://github.com/flame-engine/flame/issues/3212)). ([ebf2ee62](https://github.com/flame-engine/flame/commit/ebf2ee62e535fd1d0f499112b314e1d88e59bbc1))\n - **FEAT**: More Lights! [flame_3d] ([#3250](https://github.com/flame-engine/flame/issues/3250)). ([1780630e](https://github.com/flame-engine/flame/commit/1780630e7fcb386a331ba1219c15cb1ae8b139e6))\n - **FEAT**: Add normals to surfaces when not specified ([#3257](https://github.com/flame-engine/flame/issues/3257)). ([844c1d72](https://github.com/flame-engine/flame/commit/844c1d726e04e9c3c5739214720cf26fc62d3f9f))\n - **FEAT**(flame_3d): Fix minor nits on flame_3d ([#3140](https://github.com/flame-engine/flame/issues/3140)). ([11cdfb5e](https://github.com/flame-engine/flame/commit/11cdfb5ebeb62dd1aec2d51fd7fadfbfb17c6da5))\n - **FEAT**: Expose vector classes on core file [flame_3d] ([#3211](https://github.com/flame-engine/flame/issues/3211)). ([c3e68dff](https://github.com/flame-engine/flame/commit/c3e68dffd2e53a8dc8d4d3804c47e956dfc0ebb4))\n - **FEAT**(flame_3d): Fix minor nits on flame_3d ([#3140](https://github.com/flame-engine/flame/issues/3140)). ([b537d20a](https://github.com/flame-engine/flame/commit/b537d20ab65ce0312e9c05ba9156d794234d93e0))\n - **FEAT**: Make it easier work with the Mesh class [flame_3d] ([#3212](https://github.com/flame-engine/flame/issues/3212)). ([7f80b530](https://github.com/flame-engine/flame/commit/7f80b53078c037f81d386a44fa9b749cf7835ffa))\n - **DOCS**: Update docs and comments (flame_3d) ([#3057](https://github.com/flame-engine/flame/issues/3057)). ([14047879](https://github.com/flame-engine/flame/commit/14047879a13e1f13e51ce3411feb7c7962d6d7ee))\n - **DOCS**: Update docs and comments (flame_3d) ([#3057](https://github.com/flame-engine/flame/issues/3057)). ([b5fd457a](https://github.com/flame-engine/flame/commit/b5fd457a6b6bcad73006fc77a538c7e8521178a5))\n - **DOCS**: Update README.md docs to reflect current state of affairs ([#3305](https://github.com/flame-engine/flame/issues/3305)). ([ac3f48ff](https://github.com/flame-engine/flame/commit/ac3f48ffa6527c595035e8a9cc24307343dacb31))\n - **DOCS**: Update wording on README to match newer instructions [flame_3d] ([#3399](https://github.com/flame-engine/flame/issues/3399)). ([9c416cbf](https://github.com/flame-engine/flame/commit/9c416cbfcb4106ab7b1ad14324ca9cb89593b80d))\n - **DOCS**: Update README.md docs to reflect current state of affairs ([#3305](https://github.com/flame-engine/flame/issues/3305)). ([be72daee](https://github.com/flame-engine/flame/commit/be72daee6b92bcef2af3af78c1f64abe94c49d18))\n - **BREAKING** **FEAT**: Allow for custom shaders and materials ([#3384](https://github.com/flame-engine/flame/issues/3384)). ([ae731814](https://github.com/flame-engine/flame/commit/ae73181466c7e32b0e5e9e814f5170310c20f263))\n - **BREAKING** **FEAT**: Refactor the `CameraComponent3D` ([#3394](https://github.com/flame-engine/flame/issues/3394)). ([4a61718d](https://github.com/flame-engine/flame/commit/4a61718d3b8d45d18a74f662f1bf1eb8e6069983))\n\n## 0.1.0-dev.3\n\n - **FIX**: Improve behavior of Quaternion.slerp function [flame_3d] ([#3306](https://github.com/flame-engine/flame/issues/3306)). ([b9d6a0f1](https://github.com/flame-engine/flame/commit/b9d6a0f1d34e009cd91ae9d2ab0eed09b546d110))\n - **DOCS**: Update README.md docs to reflect current state of affairs ([#3305](https://github.com/flame-engine/flame/issues/3305)). ([be72daee](https://github.com/flame-engine/flame/commit/be72daee6b92bcef2af3af78c1f64abe94c49d18))\n\n## 0.1.0-dev.2\n\n - **FIX**: Improve quaternion slerp logic to avoid NaN edge cases ([#3303](https://github.com/flame-engine/flame/issues/3303)). ([565b68b9](https://github.com/flame-engine/flame/commit/565b68b9da52d44281e93f9ae8617f9dbe9551f3))\n - **FIX**: MeshComponent.bind should bind to the provided device ([#3278](https://github.com/flame-engine/flame/issues/3278)). ([3ae3ef54](https://github.com/flame-engine/flame/commit/3ae3ef5476fa5f9ead7069efeee35cc31c0e9dd2))\n - **FIX**: Add missing export for CylinderMesh [flame_3d] ([#3256](https://github.com/flame-engine/flame/issues/3256)). ([d517c169](https://github.com/flame-engine/flame/commit/d517c169ed9b4d4457df6ac1ae363277577597fa))\n - **FIX**: Fix typo on loadTexture ([#3253](https://github.com/flame-engine/flame/issues/3253)). ([3a20a8cd](https://github.com/flame-engine/flame/commit/3a20a8cd61543aad21c1015de5c31ec1cbe71aed))\n - **FIX**: Use saner default value for camera's target ([#3238](https://github.com/flame-engine/flame/issues/3238)). ([78522c62](https://github.com/flame-engine/flame/commit/78522c624d846c827a1c0d7377837e04a30ba4e7))\n - **FIX**: Revert \"feat(flame_3d): initial implementation of 3D support\" ([#3060](https://github.com/flame-engine/flame/issues/3060)). ([741d9384](https://github.com/flame-engine/flame/commit/741d9384dbfea7bb692f181a7689a7b10a947ef0))\n - **FEAT**: Support skeletal animation basics [flame_3d]  ([#3291](https://github.com/flame-engine/flame/issues/3291)). ([12927e41](https://github.com/flame-engine/flame/commit/12927e4100a7b4b46e4218db6792d25be1623f88))\n - **FEAT**: Add more useful extensions to VectorN and Quaternion [flame_3d] ([#3296](https://github.com/flame-engine/flame/issues/3296)). ([9cb95279](https://github.com/flame-engine/flame/commit/9cb9527909a4faa38609d25ebd7463f1e2e1a1ab))\n - **FEAT**: More Lights! [flame_3d] ([#3250](https://github.com/flame-engine/flame/issues/3250)). ([1780630e](https://github.com/flame-engine/flame/commit/1780630e7fcb386a331ba1219c15cb1ae8b139e6))\n - **FEAT**: Add normals to surfaces when not specified ([#3257](https://github.com/flame-engine/flame/issues/3257)). ([844c1d72](https://github.com/flame-engine/flame/commit/844c1d726e04e9c3c5739214720cf26fc62d3f9f))\n - **FEAT**: Add CylinderMesh [flame_3d] ([#3239](https://github.com/flame-engine/flame/issues/3239)). ([01872fb6](https://github.com/flame-engine/flame/commit/01872fb6e45e10dc380fee7a176a8b37eeaef880))\n - **FEAT**(flame_3d): Make shader api more useful ([#3085](https://github.com/flame-engine/flame/issues/3085)). ([fe2e4f20](https://github.com/flame-engine/flame/commit/fe2e4f20195b453268b34e589616343fdce6201a))\n - **FEAT**: Make it easier work with the Mesh class [flame_3d] ([#3212](https://github.com/flame-engine/flame/issues/3212)). ([ebf2ee62](https://github.com/flame-engine/flame/commit/ebf2ee62e535fd1d0f499112b314e1d88e59bbc1))\n - **FEAT**: Expose vector classes on core file [flame_3d] ([#3211](https://github.com/flame-engine/flame/issues/3211)). ([c3e68dff](https://github.com/flame-engine/flame/commit/c3e68dffd2e53a8dc8d4d3804c47e956dfc0ebb4))\n - **FEAT**(flame_3d): Add helpful extension functions to Vector ([#3141](https://github.com/flame-engine/flame/issues/3141)). ([92195989](https://github.com/flame-engine/flame/commit/9219598904131d8fceba8d1ad980bea2805e3515))\n - **FEAT**(flame_3d): Fix minor nits on flame_3d ([#3140](https://github.com/flame-engine/flame/issues/3140)). ([11cdfb5e](https://github.com/flame-engine/flame/commit/11cdfb5ebeb62dd1aec2d51fd7fadfbfb17c6da5))\n - **FEAT**(flame_3d): initial implementation of 3D support ([#3012](https://github.com/flame-engine/flame/issues/3012)). ([0242e1dd](https://github.com/flame-engine/flame/commit/0242e1dd12a9b50a411d895b662f9df33536f6d9))\n - **FEAT**(flame_3d): initial implementation of 3D support ([#3012](https://github.com/flame-engine/flame/issues/3012)). ([e434bafb](https://github.com/flame-engine/flame/commit/e434bafb15fc486c51b43aaa9d9190b8b7e783cb))\n - **DOCS**: Update docs and comments (flame_3d) ([#3057](https://github.com/flame-engine/flame/issues/3057)). ([14047879](https://github.com/flame-engine/flame/commit/14047879a13e1f13e51ce3411feb7c7962d6d7ee))\n\n## 0.1.0-dev.1\n\n- Initial experimental release of `flame_3d`.\n"
  },
  {
    "path": "packages/flame_3d/CONTRIBUTING.md",
    "content": "# Contribution Guidelines\n\nRead the main [Flame Contribution Guidelines](https://github.com/flame-engine/flame/blob/main/CONTRIBUTING.md)\nfirst and then come back to this one.\n\n\n## How To Contribute\n\n\n### Environment Setup\n\nFirst follow the steps described in the main [Flame Contribution Guidelines](https://github.com/flame-engine/flame/blob/main/CONTRIBUTING.md#environment-setup)\n\nAfter you have followed those steps you have to setup Flutter to use the specific build that this\npackage is built against:\n\n```sh\ncd $(dirname $(which flutter)) \\\n  && git fetch \\\n  && git checkout bcdd1b2c481bca0647beff690238efaae68ca5ac -q \\\n  && echo \"Engine commit: $(cat internal/engine.version)\" \\\n  && cd - >/dev/null\n```\n\nThis will check out the GIT repo of your Flutter installation to the specific commit that we require\nand also gets us the commit SHA of the Flutter Engine that you need to use in setting up the\nFlutter GPU. For that you can follow the steps described in the\n[Flutter Wiki](https://github.com/flutter/flutter/wiki/Flutter-GPU#try-out-flutter-gpu).\n\nOnce you have cloned the Flutter engine you can add the `flutter_gpu` as an override dependency\nto the `pubspec_overrides.yaml` file in the `flame_3d` directory and it's example:\n\n```yaml\ndependency_overrides:\n  ... # Melos related overrides\n  flutter_gpu:\n    path: <path_to_the_cloned_flutter_engine_directory>/lib/gpu\n```\n\nAfter all of that you should run `flutter pub get` one more time to ensure all dependencies are\nset up correctly.\n\n\n### Shader changes\n\nIf you have added/changed/removed any of the shaders in the `shaders` directory make sure to run the\nbuild script for shaders:\n\n```sh\ndart bin/build_shaders.dart\n```\n\nThis is currently a manual process until Flutter provides bundling support.\n"
  },
  {
    "path": "packages/flame_3d/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_3d/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nAdds 3D support for <a href=\"https://github.com/flame-engine/flame\">Flame</a> using the <a href=\"https://github.com/flutter/flutter/wiki/Flutter-GPU\">Flutter GPU</a>.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_3d\" ><img src=\"https://img.shields.io/pub/v/flame_3d.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n# flame_3d\n\nThis package provides an experimental implementation of 3D support for Flame. The main focus is to\nexplore the potential capabilities of 3D for Flame while providing a familiar API to existing Flame\ndevelopers.\n\nSupported platforms:\n\n| Platform | Supported |\n| -------- | --------- |\n| Android  | ✅        |\n| iOS      | ✅        |\n| macOS    | ✅        |\n| Windows  | ❌        |\n| Linux    | ❌        |\n| Web      | ❌        |\n\n\n## Prologue\n\n**STOP**, we know you are hyped up and want to start coding some funky 3D stuff but we first have to\nset your expectations and clarify some things. So turn down your music, put away the coffee and make\nsome tea instead because you have to do some reading first!\n\nThis package provides 3D support for Flame but it depends on the still experimental\n[Flutter GPU](https://github.com/flutter/flutter/wiki/Flutter-GPU), which in turn depends on\nImpeller.\n\nTherefore, this package is also experimental; you can check our\n[Roadmap](https://github.com/flame-engine/flame/blob/main/packages/flame_3d/ROADMAP.md)\nfor more details on our plans and what is currently supported.\n\nThis package does not guarantee that it will follow correct [semver](https://semver.org/) versioning\nrules, nor does it assure that its APIs wont break. Be ready to constantly have to refactor your\ncode if you are planning on using this package, and potentially to have to contribute with\nimprovements and fixes. Please do not use this for production environments.\n\nDocumentation and tests might be lacking for quite a while because of the potential constant changes\nof the API. Where possible, we will try to provide in-code documentation and code examples to help\ndevelopers but our main goal for now is to enable the usage of 3D rendering within a Flame\necosystem.\n\n\n## Prerequisites\n\nIn order to use flame_3d, you will need to ensure a few things. Firstly, the only platforms that we\nhave explicitly tested so far for support were Android, iOS, and macOS.\n\nThen, you need to enable Impeller, if not already enabled by default. For example, for macOS, add\nthe following to the generated `macos/runner/Info.plist` directory:\n\n```xml\n<dict>\n  ...\n  <key>FLTEnableImpeller</key>\n  <true/>\n  <key>FLTEnableFlutterGPU</key>\n  <true/>\n</dict>\n```\n\nAlternatively, you can run Flutter with this flag instead:\n\n```bash\nflutter run --enable-flutter-gpu\n```\n\nNow everything is set up you can start doing some 3D magic! You can check out the\n[example](https://github.com/flame-engine/flame/tree/main/packages/flame_3d/example) to see how you\ncan set up a simple 3D environment using Flame.\n\nAlso check our more advanced examples, [Collect the Donut](https://github.com/luanpotter/collect_the_donut)\nand [Defend the Donut](https://github.com/flame-engine/defend_the_donut).\n\n\n## Building shaders\n\nIf you are using the `SpatialMaterial` provided by `flame_3d`, you do not need to worry about shaders.\n\nThat being said, you can write your own shaders and use them on custom materials.\nCurrently, Flutter does not do the bundling of shaders for us so this package provides a simple\nDart script. Create your fragment and vertex shader in a `shaders` directory,\nmake sure the file names are identical. Like so:\n\n- `my_custom_shader`.frag\n- `my_custom_shader`.vert\n\nYou can then run `dart pub run flame_3d:build_shaders` to bundle the shaders. They will\nautomatically be placed in `assets/shaders`.\n\nYou can check out the\n[default shaders](https://github.com/flame-engine/flame/tree/main/packages/flame_3d/shaders) if you\nwant to have some examples.\n\n\n## Contributing\n\nHave you found a bug or have a suggestion of how to enhance the 3D APIs? Open an issue and we will\ntake a look at it as soon as possible.\n\nDo you want to contribute with a PR? PRs are always welcome, just make sure to create it from the\ncorrect branch (main) and follow the [checklist](.github/pull_request_template.md) which will\nappear when you open the PR.\n\nFor bigger changes, or if in doubt, make sure to talk about your contribution to the team. Either\nvia an issue, GitHub discussion, or reach out to the team using the\n[Discord server](https://discord.gg/pxrBmy4).\n"
  },
  {
    "path": "packages/flame_3d/ROADMAP.md",
    "content": "# Roadmap\n\nIn the interest of transparency, we provide a high-level detail of the roadmap for adding 3D\nsupport to Flame. We hope this roadmap will help others in making plans and priorities based on the\nwork we are doing and potentially contribute back to the project itself.\n\nThe goal of the package can be split up into two sections, the primary goal is to provide an API for\nFlame developers so they can create 3D environments without having to learn new Flame concepts. This\nmeans the package will tie into the existing [FCS](https://docs.flame-engine.org/latest/flame/components.html#component)\nand provide the tools needed, like a [`CameraComponent`](https://docs.flame-engine.org/latest/flame/camera_component.html),\n`World` and similar components.\n\nIn a perfect world this API does not depend or even know about the Flutter GPU, which brings us\nto our secondary goal: to abstract the Flutter GPU into an API that is user-friendly for 3D\ndevelopment. That includes simplifying things like creating render targets, setting up the color\nand depth textures and configuring depth stencils. But it also includes higher level APIs like\ngeometric shapes, texture/material rendering and creating Meshes that can use those shapes and\nmaterials.\n\n\n## Goals\n\n\n### Abstracting the Flutter GPU into a user-friendly API for 3D\n\n- [x] Abstract the GPU setup into a class that represents the graphics device\n  - [ ] Setup binding logic for meshes, geometry and materials.\n- [ ] Provide a `Mesh` API\n  - [x] Provide `Surface`s that can hold geometric shapes.\n  - [x] Provide a `Material` API\n    - [x] Define a `Texture` API to be used with the `Material` API\n      - [x] Support images as textures\n      - [x] Support single color textures\n      - [x] Support generated textures\n    - [x] Provide a standard `Material`\n  - [ ] Support custom shaders\n    - [ ] Add a more dev friendly way to set uniforms\n  - [x] Support multiple `Material`s by defining surfaces on a mesh.\n\n\n### Providing a familiar API for developers to use 3D with Flame\n\n- [x] Use the existing `CameraComponent` API for 3D rendering\n  - [x] Provide a custom `World`\n  - [x] Support existing and custom viewports\n  - [ ] Support existing and custom viewfinders\n- [x] Create a new core component for 3D rendering (`Component3D`)\n  - [x] Implement a `Transform3D` for 3D transformations\n    - [x] Implement a notifying `Vector3` and `Quaternion` for 3D positioning and rotation\n  - [ ] Add support for gesture event callbacks\n- [x] Create a component that can show meshes (`MeshComponent`)\n  - [x] Ensure materials can be set outside of construction (in the `onLoad` for instance)\n"
  },
  {
    "path": "packages/flame_3d/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_3d/bin/build_shaders.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\n/// Bundle a shader ('name'.frag & 'name'.vert) into a single shader bundle and\n/// store it in the assets directory.\n///\n/// This script is just a temporary way to bundle shaders. In the long run\n/// Flutter might support auto-bundling themselves but until then we have to\n/// do it manually.\n///\n/// Note: this script should be run from the root of the package:\n/// packages/flame_3d\nvoid main(List<String> arguments) async {\n  final root = Directory.current;\n  final assets = Directory.fromUri(root.uri.resolve('assets/shaders'));\n  final shaders = Directory.fromUri(root.uri.resolve('shaders'));\n\n  await compute(assets, shaders);\n  if (arguments.contains('watch')) {\n    stdout.writeln('Running in watch mode');\n    shaders.watch(recursive: true).listen((event) {\n      compute(assets, shaders);\n    });\n  }\n}\n\nFuture<void> compute(Directory assets, Directory shaders) async {\n  // Delete all the bundled shaders so we can replace them with new ones.\n  if (assets.existsSync()) {\n    assets.deleteSync(recursive: true);\n  }\n  // Create if not exists.\n  assets.createSync(recursive: true);\n\n  if (!shaders.existsSync()) {\n    return stderr.writeln('Missing shader directory');\n  }\n\n  // Get a list of unique shader names. Each shader should have a .frag and\n  // .vert with the same basename to be considered a bundle.\n  final uniqueShaders = shaders\n      .listSync()\n      .whereType<File>()\n      .map((f) => f.path.split(Platform.pathSeparator).last.split('.').first)\n      .toSet();\n\n  for (final name in uniqueShaders) {\n    final bundle = {\n      'TextureFragment': {\n        'type': 'fragment',\n        'file': '${shaders.path}${Platform.pathSeparator}$name.frag',\n      },\n      'TextureVertex': {\n        'type': 'vertex',\n        'file': '${shaders.path}${Platform.pathSeparator}$name.vert',\n      },\n    };\n\n    stdout.writeln('Computing shader \"$name\"');\n    final impellerC = await findImpellerC();\n    final result = await Process.run(impellerC.toFilePath(), [\n      '--sl=${assets.path}${Platform.pathSeparator}$name.shaderbundle',\n      '--shader-bundle=${jsonEncode(bundle)}',\n    ]);\n\n    if (result.exitCode != 0) {\n      return stderr.writeln(result.stderr);\n    }\n  }\n}\n\n// Copied from https://github.com/bdero/flutter_gpu_shaders/blob/master/lib/environment.dart#L53\nconst _macosHostArtifacts = 'darwin-x64';\nconst _linuxHostArtifacts = 'linux-x64';\nconst _windowsHostArtifacts = 'windows-x64';\n\nconst _impellercLocations = [\n  '$_macosHostArtifacts/impellerc',\n  '$_linuxHostArtifacts/impellerc',\n  '$_windowsHostArtifacts/impellerc.exe',\n];\n\n/// Locate the engine artifacts cache directory in the Flutter SDK.\nUri findEngineArtifactsDir({String? dartPath}) {\n  // Could be:\n  //   `/path/to/flutter/bin/cache/dart-sdk/bin/dart`\n  //   `/path/to/flutter/bin/cache/artifacts/engine/darwin-x64/flutter_tester`\n  //   `/path/to/.user/shared/caches/94cf8c8fad31206e440611e309757a5a9b3be712/dart-sdk/bin/dart`\n  final dartExec = Uri.file(dartPath ?? Platform.resolvedExecutable);\n\n  Uri? cacheDir;\n  // Search backwards through the segment list until finding `bin` and `cache`\n  // in sequence.\n  for (var i = dartExec.pathSegments.length - 1; i >= 0; i--) {\n    if (dartExec.pathSegments[i] == 'dart-sdk' ||\n        dartExec.pathSegments[i] == 'artifacts') {\n      // Note: The final empty string denotes that this is a directory path.\n      cacheDir = dartExec.replace(\n        pathSegments: dartExec.pathSegments.sublist(0, i) + [''],\n      );\n      break;\n    }\n  }\n  if (cacheDir == null) {\n    throw Exception(\n      'Unable to find Flutter SDK cache directory! '\n      'Dart executable: `${dartExec.toFilePath()}`',\n    );\n  }\n  // We should now have a path of `/path/to/flutter/bin/cache/`.\n\n  final engineArtifactsDir = cacheDir.resolve(\n    './artifacts/engine/',\n  ); // Note: The final slash is important.\n\n  return engineArtifactsDir;\n}\n\n/// Locate the ImpellerC offline shader compiler in the engine artifacts cache\n/// directory.\nFuture<Uri> findImpellerC() async {\n  /////////////////////////////////////////////////////////////////////////////\n  /// 1. If the `IMPELLERC` environment variable is set, use it.\n  ///\n\n  // ignore: do_not_use_environment\n  const impellercEnvVar = String.fromEnvironment('IMPELLERC');\n  if (impellercEnvVar != '') {\n    if (!doesFileExist(impellercEnvVar)) {\n      throw Exception(\n        'IMPELLERC environment variable is set, '\n        \"but it doesn't point to a valid file!\",\n      );\n    }\n    return Uri.file(impellercEnvVar);\n  }\n\n  /////////////////////////////////////////////////////////////////////////////\n  /// 3. Search for the `impellerc` binary within the host-specific artifacts.\n  ///\n\n  final engineArtifactsDir = findEngineArtifactsDir();\n\n  // No need to get fancy. Just search all the possible directories rather than\n  // picking the correct one for the specific host type.\n  Uri? found;\n  final tried = <Uri>[];\n  for (final variant in _impellercLocations) {\n    final impellercPath = engineArtifactsDir.resolve(variant);\n    if (doesFileExist(impellercPath.toFilePath())) {\n      found = impellercPath;\n      break;\n    }\n    tried.add(impellercPath);\n  }\n  if (found == null) {\n    throw Exception(\n      'Unable to find impellerc! Tried the following locations: $tried',\n    );\n  }\n\n  return found;\n}\n\nbool doesFileExist(String path) {\n  return File(path).existsSync();\n}\n"
  },
  {
    "path": "packages/flame_3d/example/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.build/\n.buildlog/\n.history\n.svn/\n.swiftpm/\nmigrate_working_dir/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.pub-cache/\n.pub/\n/build/\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Android Studio will place build artifacts here\n/android/app/debug\n/android/app/profile\n/android/app/release\n"
  },
  {
    "path": "packages/flame_3d/example/README.md",
    "content": "# flame_3d example\n\nAn example for using the `flame_3d` package.\n"
  },
  {
    "path": "packages/flame_3d/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_3d/example/lib/commands/commands.dart",
    "content": "import 'package:flame_3d_example/commands/destroy_command.dart';\nimport 'package:flame_3d_example/commands/reset_command.dart';\nimport 'package:flame_3d_example/commands/setup_command.dart';\n\nconst customCommandProviders = [\n  ResetCommand.new,\n  DestroyCommand.new,\n  SetupCommand.new,\n];\n"
  },
  {
    "path": "packages/flame_3d/example/lib/commands/destroy_command.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_3d/camera.dart';\nimport 'package:flame_3d/components.dart';\nimport 'package:flame_3d_example/components/crate.dart';\nimport 'package:flame_3d_example/components/player.dart';\nimport 'package:flame_3d_example/components/rendered_point_light.dart';\nimport 'package:flame_3d_example/components/room_bounds.dart';\nimport 'package:flame_3d_example/example_game_3d.dart';\nimport 'package:flame_console/flame_console.dart';\n\nclass DestroyCommand extends FlameConsoleCommand<ExampleGame3D> {\n  @override\n  String get name => 'destroy';\n\n  @override\n  String get description => 'Destroys objects in the world';\n\n  @override\n  (String?, String) execute(ExampleGame3D game, ArgResults args) {\n    var count = 0;\n    for (final arg in args.arguments) {\n      switch (arg) {\n        case '@all':\n          count += destroyMatching(game, (e) => true);\n        case '@crate':\n          count += destroyMatching(game, (e) => e is Crate);\n        case '@light':\n          count += destroyMatching(game, (e) => e is RenderedPointLight);\n        case '@mesh':\n          count += destroyMatching(game, (e) => e is MeshComponent);\n        default:\n          return ('Invalid argument: $arg', '');\n      }\n    }\n    return (null, '$count objects were destroyed.');\n  }\n\n  static bool _ignoredComponents(Component component) {\n    return switch (component) {\n      Player() => true,\n      CameraComponent3D() => true,\n      RoomBounds() => true,\n      _ => false,\n    };\n  }\n\n  static int destroyMatching(\n    ExampleGame3D game,\n    bool Function(Component) predicate,\n  ) {\n    final toDestroy = game.world.children\n        .where((element) => !_ignoredComponents(element))\n        .where(predicate)\n        .toList();\n    for (final obj in toDestroy) {\n      game.world.remove(obj);\n    }\n    return toDestroy.length;\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/example/lib/commands/reset_command.dart",
    "content": "import 'package:flame_3d_example/example_game_3d.dart';\nimport 'package:flame_console/flame_console.dart';\n\nclass ResetCommand extends FlameConsoleCommand<ExampleGame3D> {\n  @override\n  String get name => 'reset';\n\n  @override\n  String get description => 'Resets the camera and player';\n\n  @override\n  (String?, String) execute(ExampleGame3D game, ArgResults args) {\n    game.camera.reset();\n    game.player.reset();\n    return (null, 'Camera was reset.');\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/example/lib/commands/setup_command.dart",
    "content": "import 'package:flame_3d_example/commands/destroy_command.dart';\nimport 'package:flame_3d_example/example_game_3d.dart';\nimport 'package:flame_3d_example/scenarios/game_scenario.dart';\nimport 'package:flame_console/flame_console.dart';\n\nclass SetupCommand extends FlameConsoleCommand<ExampleGame3D> {\n  @override\n  String get name => 'setup';\n\n  @override\n  String get description => 'Sets up the game world';\n\n  @override\n  (String?, String) execute(ExampleGame3D game, ArgResults args) {\n    final scenarioName = args.arguments.firstOrNull;\n    if (scenarioName == null) {\n      return ('No scenario name provided.', '');\n    }\n\n    final scenario = GameScenario.scenarios[scenarioName];\n    if (scenario == null) {\n      return ('Unknown scenario: $scenarioName', '');\n    }\n\n    DestroyCommand.destroyMatching(game, (_) => true);\n    scenario.setup(game);\n    return (null, 'Scenario $scenarioName has been set up.');\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/example/lib/components/crate.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/flame.dart';\nimport 'package:flame_3d/components.dart';\nimport 'package:flame_3d/game.dart';\nimport 'package:flame_3d/resources.dart';\n\nclass Crate extends MeshComponent {\n  Crate({\n    required Vector3 size,\n    super.position,\n  }) : super(mesh: CuboidMesh(size: size));\n\n  @override\n  FutureOr<void> onLoad() async {\n    final crateTexture = await Flame.images.loadTexture('crate.jpg');\n    mesh.updateSurfaces((surfaces) {\n      surfaces[0].material = SpatialMaterial(\n        albedoTexture: crateTexture,\n      );\n    });\n  }\n\n  double direction = 0.1;\n\n  @override\n  void update(double dt) {\n    if (scale.x >= 1.19 || scale.x <= 0.99) {\n      direction *= -1;\n    }\n    scale.add(Vector3.all(direction * dt));\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/example/lib/components/player.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart' show HasGameReference, KeyboardHandler;\nimport 'package:flame/geometry.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame_3d/components.dart';\nimport 'package:flame_3d/game.dart';\nimport 'package:flame_3d/resources.dart';\nimport 'package:flame_3d_example/example_game_3d.dart';\nimport 'package:flame_3d_example/keyboard_utils.dart';\nimport 'package:flutter/services.dart';\n\nclass Player extends MeshComponent\n    with HasGameReference<ExampleGame3D>, KeyboardHandler {\n  final Vector2 _input = Vector2.zero();\n\n  bool isRunning = false;\n  double speedY = 0.0;\n\n  double _lookAngle = 0.0;\n  double get lookAngle => _lookAngle;\n  set lookAngle(double value) {\n    _lookAngle = value % tau;\n    transform.rotation.setAxisAngle(_up, value);\n  }\n\n  Vector3 get lookAt => Vector3(sin(_lookAngle), 0.0, cos(_lookAngle));\n\n  Player({required Vector3 position})\n    : super(\n        position: position,\n        mesh: CuboidMesh(\n          size: Vector3(1, 2, 1),\n          material: SpatialMaterial(\n            albedoTexture: ColorTexture(BasicPalette.yellow.color),\n          ),\n        ),\n      );\n\n  @override\n  bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {\n    final shiftKeys = {\n      LogicalKeyboardKey.shift,\n      LogicalKeyboardKey.shiftLeft,\n      LogicalKeyboardKey.shiftRight,\n    };\n    isRunning = shiftKeys.any(keysPressed.contains);\n\n    final isDown = event is KeyDownEvent || event is KeyRepeatEvent;\n    if (isDown && event.logicalKey == LogicalKeyboardKey.space) {\n      jump();\n      return false;\n    }\n\n    return readArrowLikeKeysIntoVector2(event, keysPressed, _input);\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    _handleMovement(dt);\n  }\n\n  void reset() {\n    position.setFrom(Vector3(0, 1, 0));\n    lookAngle = 0.0;\n    _input.setZero();\n  }\n\n  void jump() {\n    if (position.y == _floorHeight) {\n      speedY = _jumpSpeed;\n    }\n  }\n\n  void _handleMovement(double dt) {\n    lookAngle += -_input.x * _rotationSpeed * dt;\n\n    final runningModifier = isRunning ? 2.5 : 1.0;\n    final movement = lookAt.scaled(\n      -_input.y * runningModifier * _walkingSpeed * dt,\n    );\n    position.add(movement);\n\n    if (speedY != 0 || position.y > _floorHeight) {\n      position.y += speedY * dt + 0.5 * _accY * dt * dt;\n      speedY += _accY * dt;\n      if (position.y < _floorHeight) {\n        position.y = _floorHeight;\n        speedY = 0;\n      }\n    } else {\n      position.y = _floorHeight;\n      speedY = 0;\n    }\n  }\n\n  static const double _rotationSpeed = 3.0;\n  static const double _walkingSpeed = 1.85;\n  static const double _floorHeight = 1.0;\n  static const double _jumpSpeed = 5.0;\n  static const double _accY = -9.81;\n  static final Vector3 _up = Vector3(0, 1, 0);\n}\n"
  },
  {
    "path": "packages/flame_3d/example/lib/components/rendered_point_light.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame_3d/components.dart';\nimport 'package:flame_3d/game.dart';\nimport 'package:flame_3d/resources.dart';\n\nclass RenderedPointLight extends Component with HasGameReference<FlameGame3D> {\n  final Vector3 position;\n  final Color color;\n\n  RenderedPointLight({\n    required this.position,\n    required this.color,\n  });\n\n  late LightComponent _light;\n\n  @override\n  FutureOr<void> onLoad() async {\n    // TODO(luan): support lights being added nested in the component tree.\n    game.world.add(\n      _light = LightComponent.point(\n        position: position,\n        color: color,\n      ),\n    );\n    addAll([\n      MeshComponent(\n        mesh: SphereMesh(\n          radius: 0.05,\n          material: SpatialMaterial(\n            albedoTexture: ColorTexture(\n              color,\n            ),\n          ),\n        ),\n        position: position,\n      ),\n    ]);\n  }\n\n  @override\n  void onRemove() {\n    if (_light.isMounted) {\n      game.world.remove(_light);\n    }\n    super.onRemove();\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/example/lib/components/room_bounds.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame_3d/components.dart';\nimport 'package:flame_3d/resources.dart';\n\nclass RoomBounds extends Component {\n  @override\n  FutureOr<void> onLoad() {\n    addAll([\n      // Floor\n      MeshComponent(\n        mesh: PlaneMesh(\n          size: Vector2(32, 32),\n          material: SpatialMaterial(\n            albedoTexture: ColorTexture(\n              BasicPalette.gray.color,\n            ),\n          ),\n        ),\n      ),\n\n      // Front wall\n      MeshComponent(\n        position: Vector3(16.5, 2.5, 0),\n        mesh: CuboidMesh(\n          size: Vector3(1, 5, 32),\n          material: SpatialMaterial(\n            albedoTexture: ColorTexture(\n              BasicPalette.yellow.color,\n            ),\n          ),\n        ),\n      ),\n\n      // Left wall\n      MeshComponent(\n        position: Vector3(0, 2.5, 16.5),\n        mesh: CuboidMesh(\n          size: Vector3(32, 5, 1),\n          material: SpatialMaterial(\n            albedoTexture: ColorTexture(\n              BasicPalette.blue.color,\n            ),\n          ),\n        ),\n      ),\n\n      // Right wall\n      MeshComponent(\n        position: Vector3(0, 2.5, -16.5),\n        mesh: CuboidMesh(\n          size: Vector3(32, 5, 1),\n          material: SpatialMaterial(\n            albedoTexture: ColorTexture(\n              BasicPalette.lime.color,\n            ),\n          ),\n          useFaceNormals: false,\n        ),\n      ),\n    ]);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/example/lib/components/rotating_light.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\n\nimport 'package:flame_3d/components.dart';\nimport 'package:flame_3d/game.dart';\n\nclass RotatingLight extends LightComponent {\n  RotatingLight()\n    : super.point(\n        position: Vector3.zero(),\n        color: const Color(0xFF00FF00),\n        intensity: 20.0,\n      );\n\n  @override\n  void update(double dt) {\n    const radius = 15;\n    final angle = DateTime.now().millisecondsSinceEpoch / 4000;\n    final x = cos(angle) * radius;\n    final z = sin(angle) * radius;\n    position.setValues(x, 10, z);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/example/lib/components/simple_hud.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/text.dart';\nimport 'package:flame_3d/camera.dart';\nimport 'package:flame_3d_example/example_game_3d.dart';\n\nconst _width = 1.2;\nconst _color = Color(0xFFFFFFFF);\n\nfinal _style = TextStyle(\n  color: const Color(0xFF000000),\n  shadows: [\n    for (var x = 1; x < _width + 5; x++)\n      for (var y = 1; y < _width + 5; y++) ...[\n        Shadow(offset: Offset(-_width / x, -_width / y), color: _color),\n        Shadow(offset: Offset(-_width / x, _width / y), color: _color),\n        Shadow(offset: Offset(_width / x, -_width / y), color: _color),\n        Shadow(offset: Offset(_width / x, _width / y), color: _color),\n      ],\n  ],\n);\n\nclass SimpleHud extends Component with HasGameReference<ExampleGame3D> {\n  SimpleHud() : super(children: [FpsComponent()]);\n\n  String get fps =>\n      children.query<FpsComponent>().firstOrNull?.fps.toStringAsFixed(2) ?? '0';\n\n  final _textLeft = TextPaint(style: _style);\n\n  final _textCenter = TextPaint(style: _style.copyWith(fontSize: 20));\n\n  final _textRight = TextPaint(style: _style, textDirection: TextDirection.rtl);\n\n  @override\n  void render(Canvas canvas) {\n    final CameraComponent3D(:position, :target, :up) = game.camera;\n\n    _textLeft.render(\n      canvas,\n      '''\nCamera controls:\n- Move using W, A, S, D, Space, Left-Ctrl\n- Look around with arrow keys or mouse\n- Change camera mode with 1, 2, 3 or 4\n- Change camera projection with P\n- Zoom in and out with scroll\n''',\n      Vector2.all(8),\n    );\n\n    _textCenter.render(\n      canvas,\n      'Welcome to the 3D world',\n      Vector2(game.size.x / 2, game.size.y - 8),\n      anchor: Anchor.bottomCenter,\n    );\n\n    _textRight.render(\n      canvas,\n      '''\nFPS: $fps\nProjection: ${game.camera.projection.name}\nCulled: ${game.world.culled}\n\nPosition: ${position.x.toStringAsFixed(2)}, ${position.y.toStringAsFixed(2)}, ${position.z.toStringAsFixed(2)}\nTarget: ${target.x.toStringAsFixed(2)}, ${target.y.toStringAsFixed(2)}, ${target.z.toStringAsFixed(2)}\nUp: ${up.x.toStringAsFixed(2)}, ${up.y.toStringAsFixed(2)}, ${up.z.toStringAsFixed(2)}\n''',\n      Vector2(game.size.x - 8, 8),\n      anchor: Anchor.topRight,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/example/lib/example_camera_3d.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart' as v64 show Vector2;\nimport 'package:flame_3d/camera.dart';\nimport 'package:flame_3d_example/components/player.dart';\nimport 'package:flame_3d_example/components/simple_hud.dart';\nimport 'package:flame_3d_example/example_game_3d.dart';\n\nclass ExampleCamera3D extends CameraComponent3D\n    with HasGameReference<ExampleGame3D> {\n  CameraMode _mode = CameraMode.player;\n  double distance = 5.0;\n  Vector2 delta = Vector2.zero();\n\n  ExampleCamera3D()\n    : super(\n        position: Vector3(0, 2, 4),\n        projection: CameraProjection.perspective,\n        viewport: FixedResolutionViewport(\n          resolution: v64.Vector2(800, 600),\n        ),\n        hudComponents: [SimpleHud()],\n      );\n\n  CameraMode get mode => _mode;\n\n  set mode(CameraMode mode) {\n    _mode = mode;\n    reset();\n  }\n\n  void reset() {\n    if (_mode == CameraMode.drag) {\n      position.setFrom(Vector3(0, 2, 4));\n      target.setFrom(Vector3(-1, 0, 0));\n    } else {\n      position = player.position + _computePositionOffset(player.rotation);\n      target = player.position + player.lookAt;\n    }\n    delta.setZero();\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    if (_mode == CameraMode.drag) {\n      _dragCameraUpdate(dt);\n    } else if (_mode == CameraMode.player) {\n      _playerCameraUpdate(dt);\n    }\n  }\n\n  void _dragCameraUpdate(double dt) {\n    rotate(delta.x * dt, delta.y * dt);\n    target.setFrom(position + _computePositionOffset(rotation));\n  }\n\n  Player get player => game.player;\n\n  void _playerCameraUpdate(double dt) {\n    final targetOffset =\n        player.position + _computePositionOffset(player.rotation);\n    final targetLookAt = player.position + player.lookAt;\n\n    position += (targetOffset - position) * _cameraLinearSpeed * dt;\n    target += (targetLookAt - target) * _cameraRotationSpeed * dt;\n  }\n\n  Vector3 _computePositionOffset(Quaternion rotation) {\n    final forward = Vector3(0, -1, 1)..applyQuaternion(rotation);\n    return forward.normalized() * -distance;\n  }\n\n  static const double _cameraRotationSpeed = 6.0;\n  static const double _cameraLinearSpeed = 12.0;\n}\n\nenum CameraMode {\n  drag,\n  player,\n}\n"
  },
  {
    "path": "packages/flame_3d/example/lib/example_game_3d.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/events.dart';\nimport 'package:flame_3d/camera.dart';\nimport 'package:flame_3d/components.dart';\nimport 'package:flame_3d/game.dart';\nimport 'package:flame_3d_example/components/player.dart';\nimport 'package:flame_3d_example/components/room_bounds.dart';\nimport 'package:flame_3d_example/example_camera_3d.dart';\nimport 'package:flame_3d_example/scenarios/game_scenario.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter/widgets.dart';\n\nclass ExampleGame3D extends FlameGame3D<World3D, ExampleCamera3D>\n    with DragCallbacks, ScrollDetector, HasKeyboardHandlerComponents {\n  late final Player player;\n\n  ExampleGame3D()\n    : super(\n        world: World3D(clearColor: const Color(0xFFFFFFFF)),\n        camera: ExampleCamera3D(),\n      );\n\n  @override\n  KeyEventResult onKeyEvent(\n    KeyEvent event,\n    Set<LogicalKeyboardKey> keysPressed,\n  ) {\n    if (overlays.isActive('console')) {\n      if (event is KeyDownEvent &&\n          event.logicalKey == LogicalKeyboardKey.backquote) {\n        overlays.remove('console');\n      }\n    } else {\n      if (event is KeyDownEvent) {\n        if (event.logicalKey == LogicalKeyboardKey.backquote) {\n          overlays.add('console');\n          return KeyEventResult.handled;\n        } else if (event.logicalKey == LogicalKeyboardKey.keyR) {\n          camera.reset();\n          player.reset();\n          return KeyEventResult.handled;\n        } else if (event.logicalKey == LogicalKeyboardKey.keyM) {\n          camera.mode = camera.mode == CameraMode.drag\n              ? CameraMode.player\n              : CameraMode.drag;\n          return KeyEventResult.handled;\n        }\n      }\n    }\n\n    return super.onKeyEvent(event, keysPressed);\n  }\n\n  @override\n  FutureOr<void> onLoad() async {\n    await GameScenario.loadAll();\n\n    world.addAll([\n      RoomBounds(),\n      LightComponent.ambient(\n        intensity: 1.0,\n      ),\n      player = Player(\n        position: Vector3(0, 1, 0),\n      ),\n    ]);\n\n    GameScenario.defaultSetup(this);\n  }\n\n  @override\n  void onScroll(PointerScrollInfo info) {\n    const scrollSensitivity = 0.01;\n    final delta = info.scrollDelta.global.y.clamp(-10, 10) * scrollSensitivity;\n\n    camera.distance += delta;\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    if (camera.mode == CameraMode.drag) {\n      camera.delta.setValues(event.deviceDelta.x, event.deviceDelta.y);\n    }\n    super.onDragUpdate(event);\n  }\n\n  @override\n  void onDragEnd(DragEndEvent event) {\n    camera.delta.setZero();\n    super.onDragEnd(event);\n  }\n\n  @override\n  void onDragCancel(DragCancelEvent event) {\n    camera.delta.setZero();\n    super.onDragCancel(event);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/example/lib/keyboard_utils.dart",
    "content": "import 'package:flame_3d/core.dart';\nimport 'package:flutter/services.dart';\n\n// TODO(luan): add this as part of the input API\nbool readArrowLikeKeysIntoVector2(\n  KeyEvent event,\n  Set<LogicalKeyboardKey> keysPressed,\n  Vector2 vector, {\n  LogicalKeyboardKey up = LogicalKeyboardKey.keyW,\n  LogicalKeyboardKey down = LogicalKeyboardKey.keyS,\n  LogicalKeyboardKey left = LogicalKeyboardKey.keyA,\n  LogicalKeyboardKey right = LogicalKeyboardKey.keyD,\n}) {\n  final isDown = event is KeyDownEvent || event is KeyRepeatEvent;\n  if (event.logicalKey == up) {\n    if (isDown) {\n      vector.y = -1;\n    } else if (keysPressed.contains(down)) {\n      vector.y = 1;\n    } else {\n      vector.y = 0;\n    }\n    return false;\n  } else if (event.logicalKey == down) {\n    if (isDown) {\n      vector.y = 1;\n    } else if (keysPressed.contains(up)) {\n      vector.y = -1;\n    } else {\n      vector.y = 0;\n    }\n    return false;\n  } else if (event.logicalKey == left) {\n    if (isDown) {\n      vector.x = -1;\n    } else if (keysPressed.contains(right)) {\n      vector.x = 1;\n    } else {\n      vector.x = 0;\n    }\n    return false;\n  } else if (event.logicalKey == right) {\n    if (isDown) {\n      vector.x = 1;\n    } else if (keysPressed.contains(left)) {\n      vector.x = -1;\n    } else {\n      vector.x = 0;\n    }\n    return false;\n  }\n  return true;\n}\n"
  },
  {
    "path": "packages/flame_3d/example/lib/main.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame_3d_example/commands/commands.dart';\nimport 'package:flame_3d_example/example_game_3d.dart';\nimport 'package:flame_console/flame_console.dart';\nimport 'package:flutter/widgets.dart';\n\nvoid main() {\n  runApp(\n    GameWidget.controlled(\n      gameFactory: ExampleGame3D.new,\n      overlayBuilderMap: {\n        'console': (BuildContext context, ExampleGame3D game) {\n          return FlameConsoleView(\n            game: game,\n            customCommands: customCommandProviders.map((it) => it()).toList(),\n            onClose: () {\n              game.overlays.remove('console');\n            },\n          );\n        },\n      },\n    ),\n  );\n}\n"
  },
  {
    "path": "packages/flame_3d/example/lib/scenarios/boxes_scenario.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame_3d/components.dart';\nimport 'package:flame_3d/resources.dart';\nimport 'package:flame_3d_example/components/crate.dart';\nimport 'package:flame_3d_example/components/rendered_point_light.dart';\nimport 'package:flame_3d_example/components/rotating_light.dart';\nimport 'package:flame_3d_example/example_game_3d.dart';\nimport 'package:flame_3d_example/scenarios/game_scenario.dart';\n\nclass BoxesScenario implements GameScenario {\n  @override\n  Future<void> onLoad() async {}\n\n  @override\n  void setup(ExampleGame3D game) {\n    game.world.addAll([\n      RotatingLight(),\n\n      RenderedPointLight(\n        position: Vector3(0, 0.1, 0),\n        color: const Color(0xFFFF00FF),\n      ),\n      RenderedPointLight(\n        position: Vector3(-2, 3, 2),\n        color: const Color(0xFFFF2255),\n      ),\n\n      // Floating crate\n      Crate(\n        position: Vector3(0, 5, 0),\n        size: Vector3.all(1),\n      ),\n\n      // Floating sphere\n      MeshComponent(\n        position: Vector3(5, 5, 5),\n        mesh: SphereMesh(\n          radius: 1,\n          material: SpatialMaterial(\n            albedoTexture: ColorTexture(\n              BasicPalette.green.color,\n            ),\n          ),\n        ),\n      ),\n    ]);\n\n    final rnd = Random();\n    for (var i = 0; i < 20; i++) {\n      final height = rnd.nextDoubleBetween(1, 12);\n\n      game.world.add(\n        MeshComponent(\n          position: Vector3(\n            rnd.nextDoubleBetween(-15, 15),\n            height / 2,\n            rnd.nextDoubleBetween(-15, 15),\n          ),\n          mesh: CuboidMesh(\n            size: Vector3(1, height, 1),\n            material: SpatialMaterial(\n              albedoTexture: ColorTexture(\n                Color.fromRGBO(\n                  rnd.nextIntBetween(20, 255),\n                  rnd.nextIntBetween(10, 55),\n                  30,\n                  1,\n                ),\n              ),\n            ),\n          ),\n        ),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/example/lib/scenarios/colors_scenario.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame_3d/components.dart';\nimport 'package:flame_3d/resources.dart';\nimport 'package:flame_3d_example/components/rendered_point_light.dart';\nimport 'package:flame_3d_example/example_game_3d.dart';\nimport 'package:flame_3d_example/scenarios/game_scenario.dart';\n\nclass ColorsScenario implements GameScenario {\n  @override\n  Future<void> onLoad() async {}\n\n  @override\n  void setup(ExampleGame3D game) {\n    game.world.addAll([\n      RenderedPointLight(\n        position: Vector3(0, 0.2, 0),\n        color: const Color(0xFFFF00FF),\n      ),\n\n      MeshComponent(\n        position: Vector3(5, 2, 5),\n        mesh: SphereMesh(\n          radius: 1,\n          material: SpatialMaterial(\n            albedoColor: BasicPalette.red.color,\n          ),\n        ),\n      ),\n\n      MeshComponent(\n        position: Vector3(-5, 2, -5),\n        mesh: ConeMesh(\n          radius: 1,\n          height: 2,\n          material: SpatialMaterial(\n            albedoColor: BasicPalette.green.color,\n          ),\n        ),\n      ),\n\n      MeshComponent(\n        position: Vector3(5, 0, -5),\n        mesh: CuboidMesh(\n          size: Vector3(1, 2, 1),\n          material: SpatialMaterial(\n            albedoColor: BasicPalette.blue.color,\n          ),\n        ),\n      ),\n    ]);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/example/lib/scenarios/game_scenario.dart",
    "content": "import 'package:flame_3d_example/example_game_3d.dart';\nimport 'package:flame_3d_example/scenarios/boxes_scenario.dart';\nimport 'package:flame_3d_example/scenarios/colors_scenario.dart';\nimport 'package:flame_3d_example/scenarios/models_scenario.dart';\n\nabstract class GameScenario {\n  Future<void> onLoad();\n\n  void setup(ExampleGame3D game);\n\n  static const String defaultScenario = 'boxes';\n\n  static final Map<String, GameScenario> scenarios = {\n    'boxes': BoxesScenario(),\n    'colors': ColorsScenario(),\n    'models': ModelsScenario(),\n  };\n\n  static void defaultSetup(ExampleGame3D game) {\n    scenarios[defaultScenario]?.setup(game);\n  }\n\n  static Future<void> loadAll() async {\n    await Future.wait([\n      for (final scenario in scenarios.values) scenario.onLoad(),\n    ]);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/example/lib/scenarios/models_scenario.dart",
    "content": "import 'package:flame_3d/core.dart';\nimport 'package:flame_3d/model.dart';\nimport 'package:flame_3d/parser.dart';\nimport 'package:flame_3d_example/example_game_3d.dart';\nimport 'package:flame_3d_example/scenarios/game_scenario.dart';\n\nclass ModelsScenario implements GameScenario {\n  late final Model model;\n\n  @override\n  Future<void> onLoad() async {\n    // source: https://kaylousberg.itch.io/kaykit-skeletons\n    model = await ModelParser.parse('objects/skeleton.glb');\n  }\n\n  @override\n  void setup(ExampleGame3D game) {\n    final skeleton = ModelComponent(\n      model: model,\n      position: Vector3(0, 0, 0),\n      scale: Vector3.all(1.0),\n    )..playAnimationByName('Cheer');\n    game.world.addAll([\n      skeleton,\n    ]);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/example/pubspec.yaml",
    "content": "name: flame_3d_example\nresolution: workspace\ndescription: An example for flame_3d. The example shows how to set up 3D support in a flame game.\nversion: 0.0.1+1\npublish_to: none\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_3d: ^0.1.1+7\n  flame_console: ^0.1.2+17\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n\nflutter:\n  uses-material-design: true\n\n  assets:\n    - assets/images/\n    - assets/objects/\n"
  },
  {
    "path": "packages/flame_3d/lib/camera.dart",
    "content": "export 'package:flame/camera.dart';\n\nexport 'package:vector_math/vector_math.dart' show Frustum;\n\nexport 'src/camera/camera_component_3d.dart';\nexport 'src/camera/first_person_camera.dart';\nexport 'src/camera/third_person_camera.dart';\nexport 'src/camera/world_3d.dart';\n"
  },
  {
    "path": "packages/flame_3d/lib/components.dart",
    "content": "export 'src/components/component_3d.dart';\nexport 'src/components/light_component.dart';\nexport 'src/components/line_3d.dart';\nexport 'src/components/mesh_component.dart';\nexport 'src/components/object_3d.dart';\n"
  },
  {
    "path": "packages/flame_3d/lib/core.dart",
    "content": "export 'package:vector_math/vector_math.dart'\n    show\n        Vector2,\n        Vector3,\n        Vector4,\n        Matrix2,\n        Matrix3,\n        Matrix4,\n        Quaternion,\n        Aabb3;\n\nexport 'extensions.dart';\n"
  },
  {
    "path": "packages/flame_3d/lib/extensions.dart",
    "content": "export 'src/extensions/aabb3.dart';\nexport 'src/extensions/color.dart';\nexport 'src/extensions/matrix4.dart';\nexport 'src/extensions/quaternion.dart';\nexport 'src/extensions/vector2.dart';\nexport 'src/extensions/vector3.dart';\nexport 'src/extensions/vector4.dart';\n"
  },
  {
    "path": "packages/flame_3d/lib/game.dart",
    "content": "export 'core.dart';\nexport 'src/game/flame_game_3d.dart';\nexport 'src/game/notifying_quaternion.dart';\nexport 'src/game/notifying_vector3.dart';\nexport 'src/game/transform_3d.dart';\n"
  },
  {
    "path": "packages/flame_3d/lib/graphics.dart",
    "content": "export 'src/graphics/graphics_device.dart';\n"
  },
  {
    "path": "packages/flame_3d/lib/model.dart",
    "content": "export 'src/model/animation_state.dart';\nexport 'src/model/model.dart';\nexport 'src/model/model_animation.dart';\nexport 'src/model/model_component.dart';\nexport 'src/model/model_node.dart';\n"
  },
  {
    "path": "packages/flame_3d/lib/parser.dart",
    "content": "export 'src/parser/model_parser.dart';\n"
  },
  {
    "path": "packages/flame_3d/lib/resources.dart",
    "content": "import 'package:flame/cache.dart';\nimport 'package:flame_3d/resources.dart';\n\nexport 'src/resources/light.dart';\nexport 'src/resources/material.dart';\nexport 'src/resources/mesh.dart';\nexport 'src/resources/resource.dart';\nexport 'src/resources/shader.dart';\nexport 'src/resources/texture.dart';\n\nextension TextureCache on Images {\n  Future<Texture> loadTexture(String path) {\n    return load(path).then(ImageTexture.create);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/camera/camera_component_3d.dart",
    "content": "import 'package:flame_3d/camera.dart';\nimport 'package:flame_3d/game.dart';\n\nenum CameraProjection { perspective, orthographic }\n\n/// {@template camera_component_3d}\n/// [CameraComponent3D] is a component through which a [World3D] is observed.\n/// {@endtemplate}\nclass CameraComponent3D extends CameraComponent {\n  /// {@macro camera_component_3d}\n  CameraComponent3D({\n    this.fovY = 60,\n    Vector3? position,\n    Quaternion? rotation,\n    Vector3? target,\n    Vector3? up,\n    this.projection = CameraProjection.perspective,\n    World3D? super.world,\n    super.viewport,\n    super.viewfinder,\n    super.backdrop,\n    super.hudComponents,\n  }) : position = position?.clone() ?? Vector3.zero(),\n       rotation = rotation ?? Quaternion.identity(),\n       target = target?.clone() ?? Vector3(0, 0, -1),\n       _up = up?.clone() ?? Vector3(0, 1, 0);\n\n  @override\n  World3D? get world => super.world as World3D?;\n\n  @override\n  set world(covariant World3D? world) => super.world = world;\n\n  /// The [fovY] is the field of view in Y (degrees) when the [projection] is\n  /// [CameraProjection.perspective] otherwise it is used as the near plane when\n  /// the [projection] is [CameraProjection.orthographic].\n  double fovY;\n\n  /// The position of the camera in 3D space.\n  ///\n  /// Often also referred to as the \"eye\".\n  Vector3 position;\n\n  /// The target in 3D space that the camera is looking at.\n  Vector3 target;\n\n  /// The forward direction relative to the camera.\n  Vector3 get forward => target - position;\n\n  /// The right direction relative to the camera.\n  Vector3 get right => forward.cross(up);\n\n  /// The up direction relative to the camera.\n  Vector3 get up => _up.normalized();\n  set up(Vector3 up) => _up.setFrom(up);\n  final Vector3 _up;\n\n  /// The rotation of the camera.\n  Quaternion rotation;\n\n  /// The current camera projection.\n  CameraProjection projection;\n\n  /// The view matrix of the camera, this is without any projection applied on\n  /// it.\n  Matrix4 get viewMatrix => _viewMatrix..setAsViewMatrix(position, target, up);\n  final Matrix4 _viewMatrix = Matrix4.zero();\n\n  /// The projection matrix of the camera.\n  Matrix4 get projectionMatrix => switch (projection) {\n    CameraProjection.perspective =>\n      _projectionMatrix..setAsPerspective(\n        fovY,\n        viewport.virtualSize.x / viewport.virtualSize.y,\n        distanceNear,\n        distanceFar,\n      ),\n    CameraProjection.orthographic =>\n      _projectionMatrix..setAsOrthographic(\n        fovY,\n        viewport.virtualSize.x / viewport.virtualSize.y,\n        distanceNear,\n        distanceFar,\n      ),\n  };\n  final Matrix4 _projectionMatrix = Matrix4.zero();\n\n  /// The view projection matrix used for rendering.\n  Matrix4 get viewProjectionMatrix => _viewProjectionMatrix\n    ..setFrom(projectionMatrix)\n    ..multiply(viewMatrix);\n  final Matrix4 _viewProjectionMatrix = Matrix4.zero();\n\n  /// The frustum of the [viewProjectionMatrix].\n  Frustum get frustum => _frustum..setFromMatrix(viewProjectionMatrix);\n  final Frustum _frustum = Frustum();\n\n  /// Rotates the camera's yaw and pitch.\n  ///\n  /// Both [yawDelta] and [pitchDelta] are in radians.\n  void rotate(double yawDelta, double pitchDelta) {\n    // Create quaternions for both yaw and pitch rotations.\n    final yawRotation = Quaternion.axisAngle(Vector3(0, 1, 0), yawDelta);\n    final pitchRotation = Quaternion.axisAngle(Vector3(1, 0, 0), pitchDelta);\n\n    // Multiply the yaw with the current and pitch rotation to get the new\n    // camera rotation.\n    rotation = (yawRotation * rotation * pitchRotation)..normalize();\n  }\n\n  /// Resets the camera's rotation to its default state, making it look forward.\n  void resetRotation() => rotation = Quaternion.identity();\n\n  static CameraComponent3D? get currentCamera =>\n      CameraComponent.currentCamera as CameraComponent3D?;\n\n  static const distanceNear = 0.01;\n  static const distanceFar = 1000.0;\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/camera/first_person_camera.dart",
    "content": "import 'package:flame_3d/camera.dart';\nimport 'package:flame_3d/core.dart';\nimport 'package:meta/meta.dart';\n\nclass FirstPersonCamera extends CameraComponent3D {\n  FirstPersonCamera({\n    required this.following,\n    super.fovY,\n    super.position,\n    super.rotation,\n    super.up,\n    super.projection,\n    super.world,\n    super.viewport,\n    super.viewfinder,\n    super.backdrop,\n    super.hudComponents,\n  });\n\n  /// The point the camera should follow.\n  Vector3 following;\n\n  @override\n  @mustCallSuper\n  void update(double dt) {\n    // Always set the camera's position to the following point.\n    position.setFrom(following);\n\n    // Compute the desired target to look at.\n    target.setFrom(position + _getForwardDirection());\n  }\n\n  Vector3 _getForwardDirection() {\n    final forward = Vector3(0, 0, -1)..applyQuaternion(rotation);\n    return forward.normalized();\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/camera/third_person_camera.dart",
    "content": "import 'package:flame_3d/camera.dart';\nimport 'package:flame_3d/core.dart';\nimport 'package:meta/meta.dart';\n\nclass ThirdPersonCamera extends CameraComponent3D {\n  ThirdPersonCamera({\n    required this.following,\n    double distance = 5.0,\n    this.followDamping = 1.0,\n    super.fovY,\n    super.position,\n    super.rotation,\n    super.up,\n    super.projection,\n    super.world,\n    super.viewport,\n    super.viewfinder,\n    super.backdrop,\n    super.hudComponents,\n  }) : _distance = distance;\n\n  /// The point the camera should follow.\n  Vector3 following;\n\n  /// The distance the camera should maintain from the `following` point.\n  double get distance => _distance;\n  set distance(double value) => _distance = value.clamp(0.1, double.infinity);\n  double _distance;\n\n  /// Damping factor for smoothing out rotation and position changes.\n  ///\n  /// If the value is `1`, no damping is applied.\n  double followDamping;\n\n  @override\n  @mustCallSuper\n  void update(double dt) {\n    // Compute the desired position based on the rotation and distance.\n    final desiredPosition = following + _getRotatedOffset();\n\n    // Smoothly interpolate the camera's position toward the desired position.\n    position = position + (desiredPosition - position) * (followDamping * dt);\n\n    // Always look at the following point.\n    target.setFrom(following);\n  }\n\n  Vector3 _getRotatedOffset() {\n    final forward = Vector3(0, 0, -1)..applyQuaternion(rotation);\n    return forward.normalized() * -distance;\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/camera/world_3d.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart' as flame;\nimport 'package:flame_3d/camera.dart';\nimport 'package:flame_3d/components.dart';\nimport 'package:flame_3d/graphics.dart';\nimport 'package:flame_3d/resources.dart';\nimport 'package:flutter/widgets.dart' show MediaQuery;\nimport 'package:meta/meta.dart';\n\n/// {@template world_3d}\n/// The root component for all 3D world elements.\n///\n/// The primary feature of this component is that it allows [Component3D]s to\n/// render directly to a [GraphicsDevice] instead of the regular rendering.\n/// {@endtemplate}\nclass World3D extends flame.World with flame.HasGameReference {\n  /// {@macro world_3d}\n  World3D({\n    super.children,\n    super.priority,\n    Color clearColor = const Color(0x00000000),\n  }) : device = GraphicsDevice(clearValue: clearColor) {\n    children.register<LightComponent>();\n  }\n\n  /// The graphical device attached to this world.\n  @internal\n  final GraphicsDevice device;\n\n  Iterable<Light> get lights =>\n      children.query<LightComponent>().map((component) => component.light);\n\n  final _paint = Paint();\n\n  @internal\n  @override\n  void renderFromCamera(Canvas canvas) {\n    final camera = CameraComponent3D.currentCamera!;\n    final viewport = camera.viewport;\n\n    final devicePixelRatio = MediaQuery.of(game.buildContext!).devicePixelRatio;\n    final size = Size(\n      viewport.virtualSize.x * devicePixelRatio,\n      viewport.virtualSize.y * devicePixelRatio,\n    );\n\n    device\n      // Set the view matrix\n      ..view.setFrom(camera.viewMatrix)\n      // Set the projection matrix\n      ..projection.setFrom(camera.projectionMatrix)\n      ..begin(size);\n\n    culled = 0;\n\n    _prepareDevice();\n    // ignore: invalid_use_of_internal_member\n    super.renderFromCamera(canvas);\n\n    final image = device.end();\n    canvas.drawImageRect(\n      image,\n      Offset.zero & size,\n      (-viewport.virtualSize / 2).toOffset() &\n          Size(viewport.virtualSize.x, viewport.virtualSize.y),\n      _paint,\n    );\n    image.dispose();\n  }\n\n  // TODO(luan): consider making this a fixed-size array later\n  void _prepareDevice() {\n    device.lightingInfo.lights = lights;\n  }\n\n  // TODO(wolfenrain): this is only here for testing purposes\n  int culled = 0;\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/components/component_3d.dart",
    "content": "import 'package:flame/components.dart' show Component, HasWorldReference;\nimport 'package:flame_3d/camera.dart';\nimport 'package:flame_3d/components.dart';\nimport 'package:flame_3d/game.dart';\n\n/// {@template component_3d}\n/// [Component3D] is a base class for any concept that lives in 3D space.\n///\n/// It is a [Component] implementation that represents a 3D object that can be\n/// freely moved around in 3D space, rotated, and scaled.\n///\n/// The main property of this class is the [transform] (which combines\n/// the [position], [rotation], and [scale]). Thus, the [Component3D] can be\n/// seen as an object in 3D space.\n///\n/// It is typically not used directly, but rather use one of the following\n/// implementations:\n/// - [Object3D] for a 3D object that can be bound and rendered by the GPU\n/// - [LightComponent] for a light source that affects how objects are rendered\n///\n/// If you want to have a pure group for several components, you have two\n/// options:\n/// - Use an [Object3D], the group itself will have some superfluous render\n/// logic but should not affect your children.\n/// - Extend the abstract class [Component3D] yourself.\n///\n/// The base [Component3D] class can also be used as a container\n/// for several other components. In this case, changing the position,\n/// rotating or scaling the [Component3D] will affect the whole\n/// group as if it was a single entity.\n/// {@endtemplate}\nabstract class Component3D extends Component with HasWorldReference<World3D> {\n  final Transform3D transform;\n\n  /// {@macro component_3d}\n  Component3D({\n    Vector3? position,\n    Vector3? scale,\n    Quaternion? rotation,\n    List<Component3D> children = const [],\n  }) : transform = Transform3D()\n         ..position = position ?? Vector3.zero()\n         ..rotation = rotation ?? Quaternion.euler(0, 0, 0)\n         ..scale = scale ?? Vector3.all(1),\n       super(children: children);\n\n  /// The total transformation matrix for the component. This matrix combines\n  /// translation, rotation and scale transforms into a single entity. The\n  /// matrix is cached and gets recalculated only as necessary.\n  Matrix4 get transformMatrix => transform.transformMatrix;\n\n  /// The position of this component's anchor on the screen.\n  NotifyingVector3 get position => transform.position;\n  set position(Vector3 position) => transform.position = position;\n\n  /// X position of this component's anchor on the screen.\n  double get x => transform.x;\n  set x(double x) => transform.x = x;\n\n  /// Y position of this component's anchor on the screen.\n  double get y => transform.y;\n  set y(double y) => transform.y = y;\n\n  /// Z position of this component's anchor on the screen.\n  double get z => transform.z;\n  set z(double z) => transform.z = z;\n\n  /// The rotation of this component.\n  NotifyingQuaternion get rotation => transform.rotation;\n  set rotation(NotifyingQuaternion rotation) => transform.rotation = rotation;\n\n  /// The scale factor of this component. The scale can be different along\n  /// the X, Y and Z dimensions. A scale greater than 1 makes the component\n  /// bigger along that axis, and less than 1 smaller. The scale can also be\n  /// negative, which results in a mirror reflection along the corresponding\n  /// axis.\n  NotifyingVector3 get scale => transform.scale;\n  set scale(Vector3 scale) => transform.scale = scale;\n\n  /// Measure the distance (in parent's coordinate space) between this\n  /// component's anchor and the [other] component's anchor.\n  double distance(Component3D other) => position.distanceTo(other.position);\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/components/light_component.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame_3d/camera.dart';\nimport 'package:flame_3d/components.dart';\nimport 'package:flame_3d/game.dart';\nimport 'package:flame_3d/resources.dart';\n\n/// A [Component3D] that represents a light source in the 3D world.\nclass LightComponent extends Component3D {\n  LightComponent({\n    required this.source,\n    super.position,\n  });\n\n  LightComponent.point({\n    Vector3? position,\n    Color color = const Color(0xFFFFFFFF),\n    double intensity = 1.0,\n  }) : this(\n         source: PointLight(\n           color: color,\n           intensity: intensity,\n         ),\n         position: position,\n       );\n\n  LightComponent.ambient({\n    Color color = const Color(0xFFFFFFFF),\n    double intensity = 0.2,\n  }) : this(\n         source: AmbientLight(\n           color: color,\n           intensity: intensity,\n         ),\n       );\n\n  final LightSource source;\n\n  late final Light _light = Light(\n    transform: transform,\n    source: source,\n  );\n\n  Light get light => _light;\n\n  @override\n  void onMount() {\n    super.onMount();\n    assert(\n      parent is World3D,\n      'Lights must be added to the root of the World3D',\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/components/line_3d.dart",
    "content": "import 'dart:math' as math;\nimport 'dart:ui';\n\nimport 'package:flame_3d/components.dart';\nimport 'package:flame_3d/game.dart';\nimport 'package:flame_3d/resources.dart';\n\nclass Line3D extends MeshComponent {\n  Line3D._({\n    required double radius,\n    required double height,\n    required Color color,\n  }) : super(\n         mesh: CylinderMesh(\n           radius: radius,\n           height: height,\n           material: SpatialMaterial()..albedoColor = color,\n         ),\n       );\n\n  factory Line3D.generate({\n    required Vector3 start,\n    required Vector3 end,\n    required Color color,\n    double radius = 0.01,\n  }) {\n    final line = Line3D._(\n      radius: radius,\n      height: start.distanceTo(end),\n      color: color,\n    );\n    line.transform.setFrom(_calculateTransform(start, end));\n    return line;\n  }\n\n  static Transform3D _calculateTransform(\n    Vector3 start,\n    Vector3 end,\n  ) {\n    final direction = end - start;\n    final length = direction.length;\n\n    final bottomCenter = Vector3(0, -length / 2, 0);\n    final topCenter = Vector3(0, length / 2, 0);\n\n    final translation = start - bottomCenter;\n\n    final rotation = _calculateRotationMatrix(\n      bottomCenter: translation + bottomCenter,\n      topCenter: translation + topCenter,\n      target: end,\n    );\n\n    final translationMatrix = Matrix4.translation(translation);\n    final rotationMatrix = _rotateAroundPoint(rotation, start);\n    final transform = rotationMatrix.multiplied(translationMatrix);\n\n    return Transform3D.fromMatrix4(transform);\n  }\n\n  static Matrix4 _calculateRotationMatrix({\n    required Vector3 bottomCenter,\n    required Vector3 topCenter,\n    required Vector3 target,\n  }) {\n    final origin = (topCenter - bottomCenter).normalized();\n    final dest = (target - bottomCenter).normalized();\n\n    final normal = origin.cross(dest);\n    if (normal.length == 0) {\n      return Matrix4.identity();\n    }\n\n    final dotProduct = origin.dot(dest);\n    final angle = math.acos(dotProduct);\n\n    return Matrix4.identity()..rotate(normal, angle);\n  }\n\n  static Matrix4 _rotateAroundPoint(Matrix4 rotation, Vector3 point) {\n    return Matrix4.translation(\n      point,\n    ).multiplied(rotation).multiplied(Matrix4.translation(-point));\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/components/mesh_component.dart",
    "content": "import 'package:flame_3d/components.dart';\nimport 'package:flame_3d/game.dart';\nimport 'package:flame_3d/resources.dart';\nimport 'package:flame_3d/src/camera/camera_component_3d.dart';\nimport 'package:flame_3d/src/graphics/graphics_device.dart';\n\n/// {@template mesh_component}\n/// An [Object3D] that renders a [Mesh] at the [position] with the [rotation]\n/// and [scale] applied.\n///\n/// This is a commonly used subclass of [Object3D].\n/// {@endtemplate}\nclass MeshComponent extends Object3D {\n  /// {@macro mesh_component}\n  MeshComponent({\n    required Mesh mesh,\n    super.position,\n    super.scale,\n    super.rotation,\n  }) : _mesh = mesh;\n\n  /// The mesh resource.\n  Mesh get mesh => _mesh;\n  final Mesh _mesh;\n\n  Aabb3 get aabb => _aabb\n    ..setFrom(mesh.aabb)\n    ..transform(transformMatrix);\n  final Aabb3 _aabb = Aabb3();\n\n  @override\n  void bind(GraphicsDevice device) {\n    device\n      ..model.setFrom(transformMatrix)\n      ..bindMesh(mesh);\n  }\n\n  @override\n  bool shouldCull(CameraComponent3D camera) {\n    return camera.frustum.intersectsWithAabb3(aabb);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/components/object_3d.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/game.dart' show FlameGame;\nimport 'package:flame_3d/camera.dart';\nimport 'package:flame_3d/components.dart';\nimport 'package:flame_3d/graphics.dart';\nimport 'package:flame_3d/resources.dart';\n\n/// {@template object_3d}\n/// [Object3D]s are the basic building blocks for a 3D [FlameGame].\n///\n/// It is an object that is positioned in 3D space and can be bind to be\n/// rendered by a [GraphicsDevice].\n///\n/// However, it has no visual representation of its own (except in\n/// debug mode). It is common, therefore, to derive from this class\n/// and implement a specific rendering logic.\n///\n/// See the [MeshComponent] for an [Object3D] that has a visual representation\n/// using a [Mesh].\n/// {@endtemplate}\nabstract class Object3D extends Component3D {\n  /// {@macro object_3d}\n  Object3D({\n    super.position,\n    super.scale,\n    super.rotation,\n  });\n\n  @override\n  void renderTree(Canvas canvas) {\n    super.renderTree(canvas);\n    final camera = CameraComponent3D.currentCamera;\n    assert(\n      camera != null,\n      '''Component is either not part of a World3D or the render is being called outside of the camera rendering''',\n    );\n    if (!shouldCull(camera!)) {\n      world.culled++;\n      return;\n    }\n\n    // We set the priority to the distance between the camera and the object.\n    // This ensures that our rendering is done in a specific order allowing for\n    // alpha blending.\n    //\n    // Note(wolfenrain): we should optimize this in the long run it currently\n    // sucks.\n    priority = -(CameraComponent3D.currentCamera!.position - position).length\n        .abs()\n        .toInt();\n\n    bind(world.device);\n  }\n\n  void bind(GraphicsDevice device);\n\n  bool shouldCull(CameraComponent3D camera) {\n    return camera.frustum.containsVector3(position);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/extensions/aabb3.dart",
    "content": "import 'package:flame_3d/game.dart';\n\nextension Aabb3Extension on Aabb3 {\n  /// Set the min and max from the [other].\n  void setFrom(Aabb3 other) {\n    min.setFrom(other.min);\n    max.setFrom(other.max);\n  }\n\n  /// Set the min and max to zero.\n  void setZero() {\n    min.setZero();\n    max.setZero();\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/extensions/color.dart",
    "content": "import 'dart:typed_data';\nimport 'dart:ui';\n\nextension ColorExtension on Color {\n  /// Returns a Float32List that represents the color as a vector.\n  Float32List get storage => Float32List.fromList([\n    r,\n    g,\n    b,\n    a,\n  ]);\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/extensions/matrix4.dart",
    "content": "import 'package:flame_3d/game.dart';\n\nexport 'package:vector_math/vector_math.dart'\n    show\n        degrees2Radians,\n        setViewMatrix,\n        setPerspectiveMatrix,\n        setOrthographicMatrix;\n\nextension Matrix4Extension on Matrix4 {\n  /// Set the matrix to be a view matrix.\n  void setAsViewMatrix(Vector3 position, Vector3 target, Vector3 up) {\n    setViewMatrix(this, position, target, up);\n  }\n\n  /// Set the matrix to use a projection view.\n  void setAsPerspective(\n    double fovY,\n    double aspectRatio,\n    double zNear,\n    double zFar,\n  ) {\n    final fovYRadians = fovY * degrees2Radians;\n    setPerspectiveMatrix(this, fovYRadians, aspectRatio, zNear, zFar);\n  }\n\n  /// Set the matrix to use a orthographic view.\n  void setAsOrthographic(\n    double nearPlaneWidth,\n    double aspectRatio,\n    double zNear,\n    double zFar,\n  ) {\n    final top = nearPlaneWidth / 2.0;\n    final right = top * aspectRatio;\n    setOrthographicMatrix(this, -right, right, -top, top, zNear, zFar);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/extensions/quaternion.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame_3d/core.dart';\n\nextension QuaternionExtension on Quaternion {\n  List<double> get storage => [x, y, z, w];\n\n  double dot(Quaternion other) {\n    return x * other.x + y * other.y + z * other.z + w * other.w;\n  }\n\n  Quaternion operator /(double s) {\n    return Quaternion(x / s, y / s, z / s, w / s);\n  }\n\n  Quaternion lerp(Quaternion other, double t) {\n    return QuaternionUtils.lerp(this, other, t);\n  }\n\n  Quaternion slerp(Quaternion other, double t, {double epsilon = 10e-6}) {\n    return QuaternionUtils.slerp(this, other, t, epsilon: epsilon);\n  }\n}\n\nfinal class QuaternionUtils {\n  QuaternionUtils._();\n\n  static Quaternion slerp(\n    Quaternion q0,\n    Quaternion q1,\n    double t, {\n    double epsilon = 10e-6,\n  }) {\n    final result = _slerp(q0, q1, t, epsilon: epsilon);\n    if (result.storage.any((e) => e.isNaN || e.isInfinite)) {\n      throw Exception(\n        'Quaternion slerp result is invalid: slerp($q0, $q1) = $result',\n      );\n    }\n    return result;\n  }\n\n  /// Some background on the correct shortest-path implementation can be found\n  /// here:\n  /// https://blog.magnum.graphics/backstage/the-unnecessarily-short-ways-to-do-a-quaternion-slerp/\n  static Quaternion _slerp(\n    Quaternion q0,\n    Quaternion q1,\n    double t, {\n    double epsilon = 10e-6,\n  }) {\n    if (isEqual(q0, q1)) {\n      return q0;\n    }\n\n    // clamp the dot product just in case of numerical instability\n    final dot = q0.dot(q1).clamp(-1.0, 1.0);\n    if (1 - dot.abs() < epsilon) {\n      // The quaternions are very close, so the linear interpolation algorithm\n      // will be a good approximation.\n      // This will prevent a NaN from the slerp algorithm.\n      return lerp(q0, q1, t).normalized();\n    }\n\n    final angle = acos(dot.abs());\n\n    final q1Prime = dot >= 0 ? q1 : q1.scaled(-1);\n    final a = sin((1 - t) * angle);\n    final b = sin(t * angle);\n\n    return (q0.scaled(a) + q1Prime.scaled(b)) / sin(angle);\n  }\n\n  static Quaternion lerp(Quaternion q0, Quaternion q1, double t) {\n    return q0 + (q1 - q0).scaled(t);\n  }\n\n  static bool isEqual(Quaternion a, Quaternion b) {\n    return a.x == b.x && a.y == b.y && a.z == b.z && a.w == b.w;\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/extensions/vector2.dart",
    "content": "import 'package:flame_3d/game.dart';\n\n/// Represents an immutable [Vector2].\ntypedef ImmutableVector2 = ({double x, double y});\n\nextension Vector2Extension on Vector2 {\n  /// Returns an immutable representation of the vector.\n  ImmutableVector2 get immutable => (x: x, y: y);\n\n  Vector2 lerp(Vector2 other, double t) {\n    return Vector2Utils.lerp(this, other, t);\n  }\n}\n\nfinal class Vector2Utils {\n  Vector2Utils._();\n\n  static Vector2 lerp(Vector2 a, Vector2 b, double t) {\n    return a + (b - a).scaled(t);\n  }\n}\n\nextension Vector2Math on ImmutableVector2 {\n  List<double> get storage => [x, y];\n\n  Vector2 get mutable => Vector2(x, y);\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/extensions/vector3.dart",
    "content": "import 'package:flame_3d/game.dart';\n\n/// Represents an immutable [Vector3].\ntypedef ImmutableVector3 = ({double x, double y, double z});\n\nextension Vector3Extension on Vector3 {\n  /// Returns an immutable representation of the vector.\n  ImmutableVector3 get immutable => (x: x, y: y, z: z);\n\n  Vector3 lerp(Vector3 other, double t) {\n    return Vector3Utils.lerp(this, other, t);\n  }\n}\n\nclass Vector3Utils {\n  Vector3Utils._();\n\n  static Vector3 lerp(Vector3 a, Vector3 b, double t) {\n    return a + (b - a).scaled(t);\n  }\n}\n\nextension Vector3Math on ImmutableVector3 {\n  List<double> get storage => [x, y, z];\n\n  ImmutableVector3 operator -(Object v) {\n    if (v is ImmutableVector3) {\n      return (x: x - v.x, y: y - v.y, z: z - v.z);\n    } else if (v is Vector3) {\n      return (x: x - v.x, y: y - v.y, z: z - v.z);\n    }\n    throw UnsupportedError('${v.runtimeType}');\n  }\n\n  Vector3 get mutable => Vector3(x, y, z);\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/extensions/vector4.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame_3d/game.dart';\n\n/// Represents an immutable [Vector3].\ntypedef ImmutableVector4 = ({double x, double y, double z, double w});\n\nextension Vector4Extension on Vector4 {\n  /// Returns an immutable representation of the vector.\n  ImmutableVector4 get immutable => (x: x, y: y, z: z, w: w);\n\n  Vector4 lerp(Vector4 other, double t) {\n    return Vector4Utils.lerp(this, other, t);\n  }\n}\n\nclass Vector4Utils {\n  Vector4Utils._();\n\n  static Vector4 lerp(Vector4 a, Vector4 b, double t) {\n    return a + (b - a).scaled(t);\n  }\n\n  static Vector4 fromColor(Color color) {\n    return Vector4(color.r, color.g, color.b, color.a);\n  }\n}\n\nextension Vector4Math on ImmutableVector4 {\n  List<double> get storage => [x, y, z, w];\n\n  Vector4 get mutable => Vector4(x, y, z, w);\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/game/flame_game_3d.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/game.dart';\nimport 'package:flame_3d/camera.dart';\n\nclass FlameGame3D<W extends World3D, C extends CameraComponent3D>\n    extends FlameGame<W> {\n  FlameGame3D({\n    super.children,\n    W? world,\n    C? camera,\n  }) : super(\n         world: world ?? World3D(clearColor: const Color(0xFFFFFFFF)) as W,\n         camera: camera ?? CameraComponent3D() as C,\n       );\n\n  @override\n  C get camera => super.camera as C;\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/game/notifying_quaternion.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame_3d/game.dart';\nimport 'package:flutter/foundation.dart';\n\n/// {@template notifying_quaternion}\n/// Extension of the standard [Quaternion] class, implementing the\n/// [ChangeNotifier] functionality. This allows any interested party to be\n/// notified when the value of this quaternion changes.\n///\n/// This class can be used as a regular [Quaternion] class. However, if you do\n/// subscribe to notifications, don't forget to eventually unsubscribe in\n/// order to avoid resource leaks.\n///\n/// Direct modification of this quaternion's [storage] is not allowed.\n/// {@endtemplate}\nclass NotifyingQuaternion extends Quaternion with ChangeNotifier {\n  /// {@macro notifying_quaternion}\n  ///\n  /// Constructs a quaternion using the raw values [x], [y], [z], and [w].\n  factory NotifyingQuaternion(double x, double y, double z, double w) =>\n      NotifyingQuaternion._()..setValues(x, y, z, w);\n  NotifyingQuaternion._() : super.fromFloat32List(Float32List(4));\n\n  /// {@macro notifying_quaternion}\n  ///\n  /// Constructs a quaternion from a rotation matrix [rotationMatrix].\n  factory NotifyingQuaternion.fromRotation(Matrix3 rotationMatrix) =>\n      NotifyingQuaternion._()..setFromRotation(rotationMatrix);\n\n  /// {@macro notifying_quaternion}\n  ///\n  /// Constructs a quaternion from a rotation of [angle] around [axis].\n  factory NotifyingQuaternion.axisAngle(Vector3 axis, double angle) =>\n      NotifyingQuaternion._()..setAxisAngle(axis, angle);\n\n  /// {@macro notifying_quaternion}\n  ///\n  /// Constructs a quaternion as a copy of [other].\n  factory NotifyingQuaternion.copy(Quaternion other) =>\n      NotifyingQuaternion._()..setFrom(other);\n\n  /// {@macro notifying_quaternion}\n  ///\n  /// Constructs a quaternion from time derivative of [q] with angular\n  /// velocity [omega].\n  factory NotifyingQuaternion.dq(Quaternion q, Vector3 omega) =>\n      NotifyingQuaternion._()..setDQ(q, omega);\n\n  /// {@macro notifying_quaternion}\n  ///\n  /// Constructs a quaternion from [yaw], [pitch] and [roll].\n  factory NotifyingQuaternion.euler(double yaw, double pitch, double roll) =>\n      NotifyingQuaternion._()..setEuler(yaw, pitch, roll);\n\n  @override\n  void setValues(double x, double y, double z, double w) {\n    super.setValues(x, y, z, w);\n    notifyListeners();\n  }\n\n  @override\n  void setAxisAngle(Vector3 axis, double radians) {\n    super.setAxisAngle(axis, radians);\n    notifyListeners();\n  }\n\n  @override\n  void setDQ(Quaternion q, Vector3 omega) {\n    super.setDQ(q, omega);\n    notifyListeners();\n  }\n\n  @override\n  void setEuler(double yaw, double pitch, double roll) {\n    super.setEuler(yaw, pitch, roll);\n    notifyListeners();\n  }\n\n  @override\n  void setFromRotation(Matrix3 rotationMatrix) {\n    super.setFromRotation(rotationMatrix);\n    notifyListeners();\n  }\n\n  @override\n  void setFromTwoVectors(Vector3 a, Vector3 b) {\n    super.setFromTwoVectors(a, b);\n    notifyListeners();\n  }\n\n  @override\n  void setRandom(Random rn) {\n    super.setRandom(rn);\n    notifyListeners();\n  }\n\n  @override\n  void setFrom(Quaternion source) {\n    super.setFrom(source);\n    notifyListeners();\n  }\n\n  @override\n  void operator []=(int i, double arg) {\n    super[i] = arg;\n    notifyListeners();\n  }\n\n  @override\n  double normalize() {\n    final l = super.normalize();\n    notifyListeners();\n    return l;\n  }\n\n  @override\n  void add(Quaternion arg) {\n    super.add(arg);\n    notifyListeners();\n  }\n\n  @override\n  void sub(Quaternion arg) {\n    super.sub(arg);\n    notifyListeners();\n  }\n\n  @override\n  void scale(double scale) {\n    super.scale(scale);\n    notifyListeners();\n  }\n\n  @override\n  set x(double x) {\n    super.x = x;\n    notifyListeners();\n  }\n\n  @override\n  set y(double y) {\n    super.y = y;\n    notifyListeners();\n  }\n\n  @override\n  set z(double z) {\n    super.z = z;\n    notifyListeners();\n  }\n\n  @override\n  set w(double w) {\n    super.w = w;\n    notifyListeners();\n  }\n\n  @override\n  void conjugate() {\n    super.conjugate();\n    notifyListeners();\n  }\n\n  @override\n  void inverse() {\n    super.inverse();\n    notifyListeners();\n  }\n\n  @override\n  Float32List get storage => super.storage.asUnmodifiableView();\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/game/notifying_vector3.dart",
    "content": "import 'package:flame_3d/game.dart';\nimport 'package:flutter/foundation.dart';\n\n/// {@template notifying_vector_3}\n/// Extension of the standard [Vector3] class, implementing the [ChangeNotifier]\n/// functionality. This allows any interested party to be notified when the\n/// value of this vector changes.\n///\n/// This class can be used as a regular [Vector3] class. However, if you do\n/// subscribe to notifications, don't forget to eventually unsubscribe in\n/// order to avoid resource leaks.\n///\n/// Direct modification of this vector's [storage] is not allowed.\n/// {@endtemplate}\nclass NotifyingVector3 extends Vector3 with ChangeNotifier {\n  /// {@macro notifying_vector_3}\n  ///\n  /// Constructs a vector using the raw values [x], [y], and [z].\n  factory NotifyingVector3(double x, double y, double z) =>\n      NotifyingVector3.zero()..setValues(x, y, z);\n\n  /// {@macro notifying_vector_3}\n  ///\n  /// Create an empty vector.\n  NotifyingVector3.zero() : super.zero();\n\n  /// {@macro notifying_vector_3}\n  ///\n  /// Create an vector whose values are all [v].\n  factory NotifyingVector3.all(double v) => NotifyingVector3.zero()..splat(v);\n\n  /// {@macro notifying_vector_3}\n  ///\n  /// Create a copy of the [other] vector.\n  factory NotifyingVector3.copy(Vector3 other) =>\n      NotifyingVector3.zero()..setFrom(other);\n\n  @override\n  void setValues(double x, double y, double z) {\n    super.setValues(x, y, z);\n    notifyListeners();\n  }\n\n  @override\n  void setFrom(Vector3 other) {\n    super.setFrom(other);\n    notifyListeners();\n  }\n\n  @override\n  void setZero() {\n    super.setZero();\n    notifyListeners();\n  }\n\n  @override\n  void splat(double arg) {\n    super.splat(arg);\n    notifyListeners();\n  }\n\n  @override\n  void operator []=(int i, double v) {\n    super[i] = v;\n    notifyListeners();\n  }\n\n  @override\n  set length(double l) {\n    super.length = l;\n    notifyListeners();\n  }\n\n  @override\n  double normalize() {\n    final l = super.normalize();\n    notifyListeners();\n    return l;\n  }\n\n  @override\n  void postmultiply(Matrix3 arg) {\n    super.postmultiply(arg);\n    notifyListeners();\n  }\n\n  @override\n  void add(Vector3 arg) {\n    super.add(arg);\n    notifyListeners();\n  }\n\n  @override\n  void addScaled(Vector3 arg, double factor) {\n    super.addScaled(arg, factor);\n    notifyListeners();\n  }\n\n  @override\n  void sub(Vector3 arg) {\n    super.sub(arg);\n    notifyListeners();\n  }\n\n  @override\n  void multiply(Vector3 arg) {\n    super.multiply(arg);\n    notifyListeners();\n  }\n\n  @override\n  void divide(Vector3 arg) {\n    super.divide(arg);\n    notifyListeners();\n  }\n\n  @override\n  void scale(double arg) {\n    super.scale(arg);\n    notifyListeners();\n  }\n\n  @override\n  void negate() {\n    super.negate();\n    notifyListeners();\n  }\n\n  @override\n  void absolute() {\n    super.absolute();\n    notifyListeners();\n  }\n\n  @override\n  void clamp(Vector3 min, Vector3 max) {\n    super.clamp(min, max);\n    notifyListeners();\n  }\n\n  @override\n  void clampScalar(double min, double max) {\n    super.clampScalar(min, max);\n    notifyListeners();\n  }\n\n  @override\n  void floor() {\n    super.floor();\n    notifyListeners();\n  }\n\n  @override\n  void ceil() {\n    super.ceil();\n    notifyListeners();\n  }\n\n  @override\n  void round() {\n    super.round();\n    notifyListeners();\n  }\n\n  @override\n  void roundToZero() {\n    super.roundToZero();\n    notifyListeners();\n  }\n\n  @override\n  void copyFromArray(List<double> array, [int offset = 0]) {\n    super.copyFromArray(array, offset);\n    notifyListeners();\n  }\n\n  @override\n  set xy(Vector2 arg) {\n    super.xy = arg;\n    notifyListeners();\n  }\n\n  @override\n  set yx(Vector2 arg) {\n    super.yx = arg;\n    notifyListeners();\n  }\n\n  @override\n  set x(double x) {\n    super.x = x;\n    notifyListeners();\n  }\n\n  @override\n  set y(double y) {\n    super.y = y;\n    notifyListeners();\n  }\n\n  @override\n  set z(double z) {\n    super.z = z;\n    notifyListeners();\n  }\n\n  @override\n  Float32List get storage => super.storage.asUnmodifiableView();\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/game/transform_3d.dart",
    "content": "import 'package:flame_3d/game.dart';\nimport 'package:flutter/foundation.dart' show ChangeNotifier;\n\n/// {@template transform_3d}\n/// This class describes a generic 3D transform, which is a combination of\n/// translations, rotations and scaling. These transforms are combined into a\n/// single matrix, that can be either applied to a graphical device like the\n/// canvas, composed with another transform, or used directly to convert\n/// coordinates.\n///\n/// The transform can be visualized as 2 reference frames: a \"global\" and\n/// a \"local\". At first, these two reference frames coincide. Then, the\n/// following sequence of transforms is applied:\n///   - translation to point [position];\n///   - rotate using the [rotation];\n///   - scaling in X, Y and Z directions by [scale] factors.\n///\n/// The class is optimized for repeated use: the transform matrix is cached\n/// and then recalculated only when the underlying properties change. Moreover,\n/// recalculation of the transform is postponed until the matrix is actually\n/// requested by the user. Thus, modifying multiple properties at once does\n/// not incur the penalty of unnecessary recalculations.\n///\n/// This class implements the [ChangeNotifier] API, allowing you to subscribe\n/// for notifications whenever the transform matrix changes. In addition, you\n/// can subscribe to get notified when individual components of the transform\n/// change: [position], [scale], and [rotation].\n/// {@endtemplate}\nclass Transform3D extends ChangeNotifier {\n  /// {@macro transform_3d}\n  Transform3D()\n    : _recalculate = true,\n      _position = NotifyingVector3.zero(),\n      _rotation = NotifyingQuaternion(0, 0, 0, 0),\n      _scale = NotifyingVector3.all(1),\n      _transformMatrix = Matrix4.zero() {\n    _position.addListener(_markAsModified);\n    _scale.addListener(_markAsModified);\n    _rotation.addListener(_markAsModified);\n  }\n\n  /// {@macro transform_3d}\n  ///\n  /// Create a copy of the [other] transform.\n  factory Transform3D.copy(Transform3D other) => Transform3D()..setFrom(other);\n\n  /// {@macro transform_3d}\n  ///\n  /// Create an instance of [Transform3D] and apply the [matrix] on it.\n  factory Transform3D.fromMatrix4(Matrix4 matrix) {\n    return Transform3D()..setFromMatrix4(matrix);\n  }\n\n  /// Creates a [Transform3D] from the given broken down\n  /// parameters and sensible defaults:\n  /// - [position] defaults to no translation;\n  /// - [rotation] defaults to no rotation;\n  /// - [scale] defaults to no scaling.\n  factory Transform3D.compose({\n    Vector3? position,\n    Quaternion? rotation,\n    Vector3? scale,\n  }) {\n    final matrix = matrix4(\n      position: position,\n      rotation: rotation,\n      scale: scale,\n    );\n    return Transform3D.fromMatrix4(matrix);\n  }\n\n  /// Creates a transform-3d-type [Matrix4] from the given broken down\n  /// parameters and sensible defaults:\n  /// - [position] defaults to no translation;\n  /// - [rotation] defaults to no rotation;\n  /// - [scale] defaults to no scaling.\n  static Matrix4 matrix4({\n    Vector3? position,\n    Quaternion? rotation,\n    Vector3? scale,\n  }) {\n    return Matrix4.compose(\n      position ?? Vector3.zero(),\n      rotation ?? Quaternion.identity(),\n      scale ?? Vector3.all(1),\n    );\n  }\n\n  /// Clone of this.\n  Transform3D clone() => Transform3D.copy(this);\n\n  /// The translation part of the transform. This translation is applied\n  /// relative to the global coordinate space.\n  ///\n  /// The returned vector can be modified by the user, and the changes\n  /// will be propagated back to the transform matrix.\n  NotifyingVector3 get position => _position;\n  set position(Vector3 position) => _position.setFrom(position);\n  final NotifyingVector3 _position;\n\n  /// X coordinate of the translation transform.\n  double get x => _position.x;\n  set x(double x) => _position.x = x;\n\n  /// Y coordinate of the translation transform.\n  double get y => _position.y;\n  set y(double y) => _position.y = y;\n\n  /// Z coordinate of the translation transform.\n  double get z => _position.z;\n  set z(double y) => _position.z = z;\n\n  NotifyingQuaternion get rotation => _rotation;\n  set rotation(Quaternion rotation) => _rotation.setFrom(rotation);\n  final NotifyingQuaternion _rotation;\n\n  /// The scale part of the transform. The default scale factor is (1, 1, 1),\n  /// a scale greater than 1 corresponds to expansion, and less than 1 is\n  /// contraction. A negative scale is also allowed, and it corresponds\n  /// to a mirror reflection around the corresponding axis.\n  /// Scale factors can be different for X, Y and Z directions.\n  ///\n  /// The returned vector can be modified by the user, and the changes\n  /// will be propagated back to the transform matrix.\n  NotifyingVector3 get scale => _scale;\n  set scale(Vector3 scale) => _scale.setFrom(scale);\n  final NotifyingVector3 _scale;\n\n  /// The total transformation matrix for the component. This matrix combines\n  /// translation, rotation and scale transforms into a single entity. The\n  /// matrix is cached and gets recalculated only when necessary.\n  ///\n  /// The returned matrix must not be modified by the user.\n  Matrix4 get transformMatrix {\n    if (_recalculate) {\n      _transformMatrix.setFromTranslationRotationScale(\n        _position,\n        _rotation,\n        _scale,\n      );\n      _recalculate = false;\n    }\n    return _transformMatrix;\n  }\n\n  final Matrix4 _transformMatrix;\n  bool _recalculate;\n\n  /// Set this to the values of the [other] [Transform3D].\n  void setFrom(Transform3D other) {\n    rotation.setFrom(other.rotation);\n    position.setFrom(other.position);\n    scale.setFrom(other.scale);\n  }\n\n  void setFromMatrix4(Matrix4 matrix) {\n    matrix.decompose(position, rotation, scale);\n  }\n\n  /// Check whether this transform is equal to [other], up to the given\n  /// [tolerance]. Setting tolerance to zero will check for exact equality.\n  /// Transforms are considered equal if their rotation angles are the same\n  /// or differ by a multiple of 2π, and if all other transform parameters:\n  /// translation, scale, and offset are the same.\n  ///\n  /// The [tolerance] parameter is in absolute units, not relative.\n  bool closeTo(Transform3D other, {double tolerance = 1e-10}) {\n    return (position.x - other.position.x).abs() <= tolerance &&\n        (position.y - other.position.y).abs() <= tolerance &&\n        (position.z - other.position.z).abs() <= tolerance &&\n        (rotation.x - other.rotation.x).abs() <= tolerance &&\n        (rotation.y - other.rotation.y).abs() <= tolerance &&\n        (rotation.z - other.rotation.z).abs() <= tolerance &&\n        (rotation.w - other.rotation.w).abs() <= tolerance &&\n        (scale.x - other.scale.x).abs() <= tolerance &&\n        (scale.y - other.scale.y).abs() <= tolerance &&\n        (scale.z - other.scale.z).abs() <= tolerance;\n  }\n\n  void _markAsModified() {\n    _recalculate = true;\n    notifyListeners();\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/graphics/gpu_context_wrapper.dart",
    "content": "import 'package:flutter_gpu/gpu.dart' as gpu;\n\n// TODO(luan): for now, we need to support both old (returns T?) and\n//             newer (returns T!) versions of some Flutter GPU context methods.\nclass GpuContextWrapper {\n  static final Map<gpu.GpuContext, GpuContextWrapper> _instances = {};\n\n  final gpu.GpuContext _gpuContext;\n\n  factory GpuContextWrapper(gpu.GpuContext gpuContext) {\n    return _instances.putIfAbsent(\n      gpuContext,\n      () => GpuContextWrapper._(gpuContext),\n    );\n  }\n\n  GpuContextWrapper._(this._gpuContext);\n\n  gpu.Texture createTexture(\n    gpu.StorageMode storageMode,\n    int width,\n    int height, {\n    gpu.PixelFormat format = gpu.PixelFormat.r8g8b8a8UNormInt,\n  }) {\n    return unwrap(\n      _gpuContext.createTexture(\n        storageMode,\n        width,\n        height,\n        format: format,\n      ),\n    );\n  }\n\n  gpu.DeviceBuffer createDeviceBuffer(\n    gpu.StorageMode storageMode,\n    int sizeInBytes,\n  ) {\n    return unwrap(_gpuContext.createDeviceBuffer(storageMode, sizeInBytes));\n  }\n\n  static T unwrap<T>(T? t) {\n    return t!;\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/graphics/graphics_device.dart",
    "content": "import 'dart:typed_data';\nimport 'dart:ui';\n\nimport 'package:flame_3d/game.dart';\nimport 'package:flame_3d/resources.dart';\nimport 'package:flame_3d/src/graphics/gpu_context_wrapper.dart';\nimport 'package:flame_3d/src/graphics/joints_info.dart';\nimport 'package:flutter_gpu/gpu.dart' as gpu;\n\nenum BlendState {\n  additive,\n  alphaBlend,\n  opaque,\n}\n\nenum DepthStencilState {\n  standard,\n  depthRead,\n  none,\n}\n\n/// Face culling mode. Controls which triangle faces are discarded before\n/// rasterization.\n///\n/// Assign to [Material.cullMode] to control per-material culling.\nenum CullMode {\n  /// No culling, both front and back faces are rendered.\n  none,\n\n  /// Cull front-facing triangles (render only back faces).\n  frontFace,\n\n  /// Cull back-facing triangles (render only front faces).\n  backFace,\n}\n\n/// {@template graphics_device}\n/// The Graphical Device provides a way for developers to interact with the GPU\n/// by binding different resources to it.\n///\n/// A single render call starts with a call to [begin] and only ends when [end]\n/// is called. Any resource that gets bound to the device in between these two\n/// method calls will be uploaded to the GPU and returns as an [Image] in [end].\n/// {@endtemplate}\nclass GraphicsDevice {\n  /// {@macro graphics_device}\n  GraphicsDevice({\n    this.clearValue = const Color(0x00000000),\n    gpu.GpuContext? gpuContext,\n  }) : _gpuContext = gpuContext ?? gpu.gpuContext;\n\n  final gpu.GpuContext _gpuContext;\n\n  /// The clear value, used to clear out the screen.\n  final Color clearValue;\n\n  late gpu.CommandBuffer _commandBuffer;\n  late gpu.HostBuffer _hostBuffer;\n  late gpu.RenderPass _renderPass;\n  late gpu.RenderTarget _renderTarget;\n\n  Matrix4 get model => _modelMatrix;\n  final Matrix4 _modelMatrix = Matrix4.zero();\n\n  Matrix4 get view => _viewMatrix;\n  final Matrix4 _viewMatrix = Matrix4.zero();\n\n  Matrix4 get projection => _projectionMatrix;\n  final Matrix4 _projectionMatrix = Matrix4.zero();\n\n  Size _previousSize = Size.zero;\n\n  /// Must be set by the rendering pipeline before elements are bound.\n  /// Can be accessed by elements in their bind method.\n  final JointsInfo jointsInfo = JointsInfo();\n\n  /// Must be set by the rendering pipeline before elements are bound.\n  /// Can be accessed by elements in their bind method.\n  final LightingInfo lightingInfo = LightingInfo();\n\n  /// Begin a new rendering batch.\n  ///\n  /// After [begin] is called the graphics device can be used to bind resources\n  /// like [Mesh]s, [Material]s and [Texture]s.\n  ///\n  /// Once you have executed all your bindings you can submit the batch to the\n  /// GPU with [end].\n  void begin(\n    Size size, {\n    // TODO(wolfenrain): unused at the moment\n    BlendState blendState = BlendState.alphaBlend,\n    // TODO(wolfenrain): used incorrectly\n    DepthStencilState depthStencilState = DepthStencilState.depthRead,\n  }) {\n    _commandBuffer = _gpuContext.createCommandBuffer();\n    _hostBuffer = _gpuContext.createHostBuffer();\n\n    _renderPass = _commandBuffer.createRenderPass(_getRenderTarget(size))\n      ..setWindingOrder(gpu.WindingOrder.counterClockwise)\n      ..setColorBlendEnable(true)\n      ..setColorBlendEquation(\n        gpu.ColorBlendEquation(\n          destinationAlphaBlendFactor: blendState == BlendState.alphaBlend\n              ? gpu.BlendFactor.oneMinusSourceAlpha\n              : gpu.BlendFactor.zero,\n        ),\n      )\n      ..setDepthWriteEnable(depthStencilState == DepthStencilState.depthRead)\n      ..setDepthCompareOperation(\n        switch (depthStencilState) {\n          DepthStencilState.none => gpu.CompareFunction.never,\n          DepthStencilState.standard => gpu.CompareFunction.lessEqual,\n          DepthStencilState.depthRead => gpu.CompareFunction.less,\n        },\n      );\n  }\n\n  /// Submit the rendering batch and it's the commands to the GPU and return\n  /// the result.\n  Image end() {\n    _commandBuffer.submit();\n    return _renderTarget.colorAttachments[0].texture.asImage();\n  }\n\n  void clearBindings() {\n    _renderPass.clearBindings();\n  }\n\n  /// Bind a [mesh].\n  void bindMesh(Mesh mesh) {\n    mesh.bind(this);\n  }\n\n  /// Bind a [surface].\n  void bindSurface(Surface surface) {\n    _renderPass.clearBindings();\n    bindMaterial(surface.material);\n\n    _renderPass.bindVertexBuffer(\n      gpu.BufferView(\n        surface.resource!,\n        offsetInBytes: 0,\n        lengthInBytes: surface.verticesBytes,\n      ),\n      surface.vertexCount,\n    );\n\n    _renderPass.bindIndexBuffer(\n      gpu.BufferView(\n        surface.resource!,\n        offsetInBytes: surface.verticesBytes,\n        lengthInBytes: surface.indicesBytes,\n      ),\n      gpu.IndexType.int16,\n      surface.indexCount,\n    );\n\n    _renderPass.draw();\n  }\n\n  /// Bind a [material] and set up the buffer correctly.\n  void bindMaterial(Material material) {\n    _renderPass\n      ..bindPipeline(material.resource)\n      ..setCullMode(gpu.CullMode.values[material.cullMode.index]);\n\n    material.bind(this);\n    material.vertexShader.bind(this);\n    material.fragmentShader.bind(this);\n  }\n\n  /// Bind a uniform [slot] to the [buffer].\n  void bindUniform(gpu.UniformSlot slot, ByteBuffer buffer) {\n    _renderPass.bindUniform(slot, _hostBuffer.emplace(buffer.asByteData()));\n  }\n\n  /// Bind a uniform [slot] to the [texture].\n  void bindTexture(gpu.UniformSlot slot, Texture texture) {\n    _renderPass.bindTexture(slot, texture.resource);\n  }\n\n  gpu.RenderTarget _getRenderTarget(Size size) {\n    if (_previousSize != size) {\n      _previousSize = size;\n\n      final gpuContext = GpuContextWrapper(_gpuContext);\n      final colorTexture = gpuContext.createTexture(\n        gpu.StorageMode.devicePrivate,\n        size.width.toInt(),\n        size.height.toInt(),\n      );\n\n      final depthTexture = gpuContext.createTexture(\n        gpu.StorageMode.deviceTransient,\n        size.width.toInt(),\n        size.height.toInt(),\n        format: _gpuContext.defaultDepthStencilFormat,\n      );\n\n      _renderTarget = gpu.RenderTarget.singleColor(\n        gpu.ColorAttachment(\n          texture: colorTexture,\n          clearValue: Vector4Utils.fromColor(clearValue),\n        ),\n        depthStencilAttachment: gpu.DepthStencilAttachment(\n          texture: depthTexture,\n          depthClearValue: 1.0,\n        ),\n      );\n    }\n\n    return _renderTarget;\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/graphics/joints_info.dart",
    "content": "import 'package:flame_3d/core.dart';\n\nclass JointsInfo {\n  /// Joints per surface index\n  Map<int, List<Matrix4>> jointTransformsPerSurface = {};\n\n  /// Joints for the current surface\n  List<Matrix4> jointTransforms = [];\n\n  void setSurface(int surfaceIndex) {\n    jointTransforms = jointTransformsPerSurface[surfaceIndex] ?? [];\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/model/animation_state.dart",
    "content": "import 'package:flame_3d/core.dart';\nimport 'package:flame_3d/src/model/model_animation.dart';\n\n/// Couples an immutable [ModelAnimation] (animation data) with a clock, which\n/// allows it to be ticked by the game loop and sampled for rendering.\nclass AnimationState {\n  ModelAnimation? animationRef;\n  double clock = 0.0;\n\n  void startAnimation(\n    ModelAnimation? animation, {\n    bool resetClock = true,\n  }) {\n    animationRef = animation;\n    if (resetClock) {\n      reset();\n    }\n  }\n\n  void update(double dt) {\n    final animation = animationRef;\n    if (animation == null) {\n      return;\n    }\n\n    clock += dt;\n    while (clock > animation.lastTime) {\n      clock -= animation.lastTime;\n    }\n  }\n\n  Matrix4 maybeTransform(int nodeIndex, Matrix4 transform) {\n    final animation = animationRef?.nodes[nodeIndex];\n    if (animation == null) {\n      return transform;\n    }\n\n    final result = transform.clone();\n    animation.sampleInto(clock, result);\n    return result;\n  }\n\n  void reset() {\n    clock = 0.0;\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/model/model.dart",
    "content": "import 'dart:collection';\n\nimport 'package:flame_3d/game.dart';\nimport 'package:flame_3d/model.dart';\nimport 'package:flame_3d/resources.dart';\n\n/// A file-type agnostic representation of a 3D model parsed, including:\n/// * a list of nodes\n/// * a list of associated animations\nclass Model {\n  final Map<int, ModelNode> nodes;\n  final List<ModelAnimation> animations;\n\n  Model({\n    required this.nodes,\n    required this.animations,\n  });\n\n  Model.simple({\n    required Mesh mesh,\n  }) : nodes = {\n         0: ModelNode.simple(\n           nodeIndex: 0,\n           mesh: mesh,\n         ),\n       },\n       animations = [];\n\n  Aabb3 get aabb => _aabb ??= _calculateBoundingBox();\n  Aabb3? _aabb;\n\n  Aabb3 _calculateBoundingBox() {\n    final box = Aabb3();\n    for (final entry in nodes.entries) {\n      final mesh = entry.value.mesh;\n      if (mesh != null) {\n        box.hull(mesh.aabb);\n      }\n    }\n    return box;\n  }\n\n  Set<String> get animationNames {\n    return {for (final animation in animations) ?animation.name};\n  }\n\n  Set<String> get nodeNames {\n    return {for (final node in nodes.values) ?node.name};\n  }\n\n  Map<int, ProcessedNode> processNodes(AnimationState animation) {\n    final processedNodes = <int, ProcessedNode>{};\n\n    final inDegree = <int, int>{};\n    final adjacencyMap = <int, Set<int>>{};\n\n    for (final node in nodes.values) {\n      final index = node.nodeIndex;\n      final deps = node.dependencies;\n      inDegree[index] = deps.length;\n      for (final dep in deps) {\n        (adjacencyMap[dep] ??= {}).add(index);\n      }\n    }\n\n    final queue = Queue<int>.from(\n      [\n        for (final node in nodes.values)\n          if (inDegree[node.nodeIndex] == 0) node.nodeIndex,\n      ],\n    );\n\n    while (queue.isNotEmpty) {\n      final index = queue.removeFirst();\n      final node = nodes[index]!;\n      node.processNode(processedNodes, animation);\n      final adjacency = adjacencyMap[index] ?? {};\n      for (final dependency in adjacency) {\n        inDegree[dependency] = inDegree[dependency]! - 1;\n        if (inDegree[dependency] == 0) {\n          queue.add(dependency);\n        }\n      }\n    }\n\n    if (processedNodes.length != nodes.length) {\n      throw StateError('Failed to process all nodes');\n    }\n\n    return processedNodes;\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/model/model_animation.dart",
    "content": "import 'dart:math';\n\n// TODO(spydon): Remove this import once Flutter 3.35.0 is the minimum version.\n// ignore: unnecessary_import\nimport 'package:flame/extensions.dart';\n// ignore: unnecessary_import\nimport 'package:flame_3d/core.dart';\nimport 'package:flame_3d/model.dart';\nimport 'package:flame_3d/src/parser/gltf/animation_interpolation.dart';\n\n/// A single keyframe inside an animation; [T] is the type of value being\n/// animated.\ntypedef AnimationKeyframe<T> = ({double time, T value});\n\n/// Base class for animation splines. Animations are a sequence of\n/// keyframes over either:\n/// * position ([TranslationAnimationSpline]),\n/// * angle ([RotationAnimationSpline]),\n/// * scale ([ScaleAnimationSpline]).\nabstract class AnimationSpline<T> {\n  final AnimationInterpolation interpolation;\n  final List<AnimationKeyframe<T>> values;\n\n  AnimationSpline({\n    required this.interpolation,\n    required this.values,\n  });\n\n  AnimationSpline.from({\n    required this.interpolation,\n    required List<double> times,\n    required List<T> values,\n  }) : values = List.generate(times.length, (index) {\n         return (time: times[index], value: values[index]);\n       });\n\n  T lerp(T a, T b, double t);\n  void transform(Matrix4 matrix, T value);\n}\n\n/// An animation spline over the position of a transformation (translation).\nclass TranslationAnimationSpline extends AnimationSpline<Vector3> {\n  TranslationAnimationSpline.from({\n    required super.interpolation,\n    required super.times,\n    required super.values,\n  }) : super.from();\n\n  @override\n  Vector3 lerp(Vector3 a, Vector3 b, double t) {\n    return interpolation.lerp(a, b, t);\n  }\n\n  @override\n  void transform(Matrix4 matrix, Vector3 value) {\n    matrix.setTranslation(value);\n  }\n}\n\n/// An animation spline over the angle of a transformation (rotation).\nclass RotationAnimationSpline extends AnimationSpline<Quaternion> {\n  RotationAnimationSpline.from({\n    required super.interpolation,\n    required super.times,\n    required super.values,\n  }) : super.from();\n\n  @override\n  Quaternion lerp(Quaternion a, Quaternion b, double t) {\n    return interpolation.slerp(a, b, t);\n  }\n\n  @override\n  void transform(Matrix4 matrix, Quaternion value) {\n    matrix.setRotation(value.asRotationMatrix());\n  }\n}\n\n/// An animation spline over the scale of a transformation (scaling).\nclass ScaleAnimationSpline extends AnimationSpline<Vector3> {\n  ScaleAnimationSpline.from({\n    required super.interpolation,\n    required super.times,\n    required super.values,\n  }) : super.from();\n\n  @override\n  Vector3 lerp(Vector3 a, Vector3 b, double t) {\n    return interpolation.lerp(a, b, t);\n  }\n\n  @override\n  void transform(Matrix4 matrix, Vector3 value) {\n    matrix.scaleByVector3(value);\n  }\n}\n\n/// Allows sampling of an animation by interpolating keyframes.\nclass AnimationController<T> {\n  final AnimationSpline<T> animation;\n  final double lastTime;\n\n  AnimationController({\n    required this.animation,\n  }) : lastTime = animation.values.last.time;\n\n  T sample(double time) {\n    final values = animation.values;\n\n    if (time < values.first.time) {\n      return values.first.value;\n    }\n\n    if (time > values.last.time) {\n      return values.last.value;\n    }\n\n    for (var i = 0; i < values.length - 1; i++) {\n      final t0 = values[i].time;\n      final t1 = values[i + 1].time;\n\n      if (time >= t0 && time < t1) {\n        final t = (time - t0) / (t1 - t0);\n        final value0 = values[i].value;\n        final value1 = values[i + 1].value;\n        return animation.lerp(value0, value1, t);\n      }\n    }\n\n    throw Exception('This should never happen');\n  }\n\n  void sampleInto(double time, Matrix4 matrix) {\n    final value = sample(time);\n    animation.transform(matrix, value);\n  }\n}\n\n/// Groups the animations of a single node in a [Model], which can be\n/// controlled by multiple animation channels.\nclass NodeAnimation {\n  final List<AnimationController> channels;\n  final double lastTime;\n\n  NodeAnimation({\n    required this.channels,\n  }) : lastTime = channels.map((e) => e.lastTime).reduce(max);\n\n  void sampleInto(double time, Matrix4 matrix) {\n    for (final channel in channels) {\n      channel.sampleInto(time, matrix);\n    }\n  }\n}\n\n/// Groups the animations for all nodes of a [Model].\nclass ModelAnimation {\n  final String? name;\n  final Map<int, NodeAnimation> nodes;\n  final double lastTime;\n\n  ModelAnimation({\n    required this.name,\n    required this.nodes,\n  }) : lastTime = nodes.values.map((e) => e.lastTime).reduce(max);\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/model/model_component.dart",
    "content": "import 'package:flame_3d/camera.dart';\nimport 'package:flame_3d/components.dart';\nimport 'package:flame_3d/core.dart';\nimport 'package:flame_3d/graphics.dart';\nimport 'package:flame_3d/src/model/animation_state.dart';\nimport 'package:flame_3d/src/model/model.dart';\n\n/// A component wrapper over a 3D [Model], using the [AnimationState] to keep\n/// track and manage its animations.\nclass ModelComponent extends Object3D {\n  final Model model;\n\n  final Set<int> _hiddenNodes = {};\n  final AnimationState _animation = AnimationState();\n\n  ModelComponent({\n    required this.model,\n    super.position,\n    super.rotation,\n    super.scale,\n  });\n\n  Aabb3 get aabb => _aabb\n    ..setFrom(model.aabb)\n    ..transform(transformMatrix);\n  final Aabb3 _aabb = Aabb3();\n\n  @override\n  void bind(GraphicsDevice device) {\n    final nodes = model.processNodes(_animation);\n    for (final MapEntry(key: index, value: node) in nodes.entries) {\n      if (_hiddenNodes.contains(index)) {\n        continue;\n      }\n\n      final mesh = node.node.mesh;\n      if (mesh != null) {\n        device.jointsInfo.jointTransformsPerSurface = node.jointTransforms;\n        world.device\n          ..model.setFrom(transformMatrix.multiplied(node.combinedTransform))\n          ..bindMesh(mesh);\n      }\n    }\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    _animation.update(dt);\n  }\n\n  Set<String> get animationNames => model.animations\n      .map((e) => e.name)\n      .whereType<String>() // filter not null\n      .toSet();\n\n  void playAnimationByName(String name, {bool resetClock = true}) {\n    final animation = model.animations.where((e) => e.name == name).firstOrNull;\n    if (animation == null) {\n      throw ArgumentError('No animation with name $name');\n    }\n    _animation.startAnimation(animation, resetClock: resetClock);\n  }\n\n  void playAnimationByIndex(int index, {bool resetClock = true}) {\n    final animation = model.animations[index];\n    _animation.startAnimation(animation, resetClock: resetClock);\n  }\n\n  void stopAnimation() {\n    _animation.startAnimation(null);\n  }\n\n  void hideNodeByName(String name, {bool hidden = true}) {\n    final node = model.nodes.entries.firstWhere((e) => e.value.name == name);\n    if (hidden) {\n      _hiddenNodes.add(node.key);\n    } else {\n      _hiddenNodes.remove(node.key);\n    }\n  }\n\n  @override\n  bool shouldCull(CameraComponent3D camera) {\n    // TODO(luan): this actually does not work because of animations\n    // it might end up culling something that is actually visible\n    return camera.frustum.intersectsWithAabb3(aabb);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/model/model_node.dart",
    "content": "import 'package:collection/collection.dart';\nimport 'package:flame_3d/game.dart';\nimport 'package:flame_3d/resources.dart';\nimport 'package:flame_3d/src/model/animation_state.dart';\n\n/// A node wraps over a mesh as part of a 3D model, including its joints and\n/// local transformations.\nclass ModelNode {\n  final String? name;\n  final int nodeIndex;\n  final int? parentNodeIndex;\n  final Matrix4 transform;\n  final Mesh? mesh;\n  final Map<int, ModelJoint> joints;\n\n  ModelNode({\n    required this.name,\n    required this.nodeIndex,\n    required this.parentNodeIndex,\n    required this.transform,\n    required this.mesh,\n    required this.joints,\n  });\n\n  List<int> get dependencies => [\n    ?parentNodeIndex,\n    for (final joint in joints.values) joint.nodeIndex,\n  ];\n\n  ModelNode.simple({\n    required this.nodeIndex,\n    required this.mesh,\n  }) : name = null,\n       parentNodeIndex = null,\n       transform = Matrix4.identity(),\n       joints = {};\n\n  void processNode(\n    Map<int, ProcessedNode> processedNodes,\n    AnimationState animation,\n  ) {\n    final resultMatrix = Matrix4.identity();\n\n    // parent\n    final parentNodeIndex = this.parentNodeIndex;\n    if (parentNodeIndex != null) {\n      resultMatrix.multiply(processedNodes[parentNodeIndex]!.combinedTransform);\n    }\n\n    // local + animation\n    final localTransform = animation.maybeTransform(nodeIndex, transform);\n    resultMatrix.multiply(localTransform);\n\n    final jointTransforms = computeJointsPerSurface(processedNodes);\n\n    processedNodes[nodeIndex] = ProcessedNode(\n      node: this,\n      combinedTransform: resultMatrix,\n      jointTransforms: jointTransforms,\n    );\n  }\n\n  Map<int, List<Matrix4>> computeJointsPerSurface(\n    Map<int, ProcessedNode> processedNodes,\n  ) {\n    final jointTransformsPerSurface = <int, List<Matrix4>>{};\n    final surfaces = mesh?.surfaces ?? [];\n    for (final (index, surface) in surfaces.indexed) {\n      final globalToLocalJointMap = surface.jointMap;\n      if (globalToLocalJointMap == null) {\n        continue;\n      }\n\n      final jointTransforms =\n          (globalToLocalJointMap.entries..sortedBy((e) => e.value))\n              .map((e) => e.key)\n              .map((jointIndex) {\n                final joint = joints[jointIndex];\n                if (joint == null) {\n                  throw StateError('Missing joint $jointIndex');\n                }\n\n                final jointNodeIndex = joint.nodeIndex;\n                final jointNode = processedNodes[jointNodeIndex];\n\n                final transform = Matrix4.identity()\n                  ..multiply(jointNode?.combinedTransform ?? Matrix4.identity())\n                  ..multiply(joint.inverseBindMatrix);\n\n                return transform;\n              })\n              .toList();\n\n      jointTransformsPerSurface[index] = jointTransforms;\n    }\n\n    return jointTransformsPerSurface;\n  }\n}\n\n/// A single join inside a [ModelNode].\nclass ModelJoint {\n  final int nodeIndex;\n  final Matrix4 inverseBindMatrix;\n\n  ModelJoint({\n    required this.nodeIndex,\n    required this.inverseBindMatrix,\n  });\n}\n\n/// A post-processed node including the final transforms after animations are\n/// applied.\nclass ProcessedNode {\n  final ModelNode node;\n  final Matrix4 combinedTransform;\n  // for each surface within the node's mesh, a list of up to 4 joint transforms\n  // with localized indexes according to the surface's localJoints\n  final Map<int, List<Matrix4>> jointTransforms;\n\n  ProcessedNode({\n    required this.node,\n    required this.combinedTransform,\n    required this.jointTransforms,\n  });\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/glb_parser.dart",
    "content": "import 'dart:convert';\nimport 'dart:typed_data';\n\nimport 'package:flame/flame.dart';\nimport 'package:flame_3d/src/model/model.dart';\nimport 'package:flame_3d/src/parser/gltf/component_type.dart';\nimport 'package:flame_3d/src/parser/gltf/glb_chunk.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/model_parser.dart';\n\n/// Parses GLB and GLTF file formats as per specified by:\n/// https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.pdf\nclass GlbParser extends ModelParser {\n  @override\n  Future<Model> parseModel(String filePath) async {\n    final root = await parseRoot(filePath);\n    return root.toFlameModel();\n  }\n\n  Future<GltfRoot> parseRoot(String filePath) async {\n    final glb = await parseGlb(filePath);\n    return glb.parse();\n  }\n\n  Future<Glb> parseGlb(String filePath) async {\n    final content = await Flame.assets.readBinaryFile(filePath);\n\n    var cursor = 0;\n    Uint8List read(int bytes) {\n      cursor += bytes;\n      return content.sublist(cursor - bytes, cursor);\n    }\n\n    final magic = _parseString(read(4));\n    if (magic != 'glTF') {\n      throw Exception('Invalid magic number $magic');\n    }\n\n    final version = _parseInt(read(4));\n    if (version != 2) {\n      throw Exception('Invalid version $version');\n    }\n\n    final length = _parseInt(read(4));\n\n    final chunks = <GlbChunk>[];\n    while (cursor < content.length) {\n      final chunkLength = _parseInt(read(4));\n      final chunkType = _parseString(read(4));\n      final chunkData = read(chunkLength);\n\n      chunks.add(\n        GlbChunk(\n          length: chunkLength,\n          type: chunkType,\n          data: chunkData,\n        ),\n      );\n    }\n\n    return Glb(\n      prefix: ModelParser.prefix(filePath),\n      version: version,\n      length: length,\n      chunks: chunks,\n    );\n  }\n}\n\nclass Glb {\n  final String prefix;\n  final int version;\n  final int length;\n  final List<GlbChunk> chunks;\n\n  Glb({\n    required this.prefix,\n    required this.version,\n    required this.length,\n    required this.chunks,\n  });\n\n  Map<String, Object?> jsonChunk() {\n    final chunk = chunks.firstWhere((GlbChunk chunk) => chunk.type == 'JSON');\n    return jsonDecode(_parseString(chunk.data)) as Map<String, Object?>;\n  }\n\n  Iterable<GlbChunk> binaryChunks() {\n    return chunks.where((GlbChunk chunk) => chunk.type == 'BIN\\x00');\n  }\n\n  Future<GltfRoot> parse() async {\n    final json = jsonChunk();\n    final chunks = binaryChunks().toList();\n    return GltfRoot.from(\n      prefix: prefix,\n      json: json,\n      chunks: chunks,\n    );\n  }\n}\n\nint _parseInt(Uint8List bytes) {\n  final byteData = ByteData.view(bytes.buffer);\n  return ComponentType.unsignedInt.parseData(byteData).toInt();\n}\n\nString _parseString(Uint8List bytes) {\n  return String.fromCharCodes(bytes);\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/accessor.dart",
    "content": "import 'package:flame_3d/core.dart';\nimport 'package:flame_3d/src/parser/gltf/accessor_type.dart';\nimport 'package:flame_3d/src/parser/gltf/buffer_view.dart';\nimport 'package:flame_3d/src/parser/gltf/component_type.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_ref.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/gltf/sparse_accessor.dart';\n\n/// A untyped GLTF accessor; it is typically wrapped into a specific accessor\n/// type on the data model. This provides the backing implementation.\nclass RawAccessor extends GltfNode {\n  /// The reference to the buffer view.\n  /// When undefined, the accessor **MUST** be initialized with zeros; `sparse`\n  /// property or extensions **MAY** override zeros with actual values.\n  final GltfRef<BufferView> bufferView;\n\n  /// The offset relative to the start of the buffer view in bytes.\n  ///\n  /// This **MUST** be a multiple of the size of the component datatype.\n  /// This property **MUST NOT** be defined when `bufferView` is undefined.\n  final int byteOffset;\n\n  /// The datatype of the accessor's components.\n  /// UNSIGNED_INT type **MUST NOT** be used for any accessor that is not\n  /// referenced by `mesh.primitive.indices`.\n  final ComponentType componentType;\n\n  /// Specifies whether integer data values are normalized (`true`) to [0, 1]\n  /// (for unsigned types) or to [-1, 1] (for signed types) when they are\n  /// accessed.\n  ///\n  /// This property **MUST NOT** be set to `true` for accessors with `FLOAT` or\n  /// `UNSIGNED_INT` component type.\n  final bool normalized;\n\n  /// The number of elements referenced by this accessor, not to be confused\n  /// with the number of bytes or number of components.\n  final int count;\n\n  /// Specifies if the accessor's elements are scalars, vectors, or matrices.\n  /// This should match the type used for a [TypedAccessor].\n  final AccessorType type;\n\n  /// Maximum value of each component in this accessor.\n  /// Array elements **MUST** be treated as having the same data type as\n  /// accessor's `componentType`.\n  ///\n  /// Both `min` and `max` arrays have the same length.\n  ///\n  /// The length is determined by the value of the `type` property;\n  /// it can be 1, 2, 3, 4, 9, or 16.\n  ///\n  /// `normalized` property has no effect on array values: they always\n  /// correspond to the actual values stored in the buffer.\n  ///\n  /// When the accessor is sparse, this property **MUST** contain maximum\n  /// values of accessor data with sparse substitution applied.\n  final List<double>? max;\n\n  /// Minimum value of each component in this accessor.\n  ///\n  /// Array elements **MUST** be treated as having the same data type as\n  /// accessor's `componentType`.\n  ///\n  /// Both `min` and `max` arrays have the same length.\n  ///\n  /// The length is determined by the value of the `type` property;\n  /// it can be 1, 2, 3, 4, 9, or 16.\n  ///\n  /// `normalized` property has no effect on array values: they always\n  /// correspond to the actual values stored in the buffer.\n  ///\n  /// When the accessor is sparse, this property **MUST** contain minimum\n  /// values of accessor data with sparse substitution applied.\n  final List<double>? min;\n\n  /// Sparse storage of elements that deviate from their initialization value.\n  final SparseAccessor? sparse;\n\n  Iterable<num> data() sync* {\n    final buffer = bufferView.get();\n    final bytes = buffer.data();\n\n    final byteData = bytes.buffer.asByteData();\n\n    final step = componentType.byteSize;\n    if ((bytes.lengthInBytes - byteOffset) % step != 0) {\n      throw Exception(\n        'Accessor data length ${bytes.lengthInBytes} '\n        'is not a multiple of the stride $step.',\n      );\n    }\n\n    for (\n      var cursor = byteOffset;\n      cursor < bytes.lengthInBytes;\n      cursor += step\n    ) {\n      yield componentType.parseData(byteData, cursor: cursor);\n    }\n  }\n\n  List<T> _typedData<T>(int size, T Function(List<num>) producer) {\n    _verifyNotSparse();\n    _verifyAccessorType(size);\n\n    final buffer = bufferView.get();\n    final view = data().toList();\n\n    final int step;\n    final byteStride = buffer.byteStride;\n    if (byteStride != null) {\n      step = byteStride ~/ componentType.byteSize;\n    } else {\n      step = size;\n    }\n    if (step == 0) {\n      throw Exception('Step cannot be 0');\n    }\n    if (view.length % step != 0) {\n      throw Exception(\n        'Accessor data length ${view.length}'\n        ' is not a multiple of the step $step',\n      );\n    }\n\n    final result = <T>[];\n    for (var i = 0; i < view.length; i += step) {\n      result.add(producer(view.sublist(i, i + size)));\n      if (result.length == count) {\n        break;\n      }\n    }\n    return result;\n  }\n\n  void _verifyAccessorType(int size) {\n    if (type.size != size) {\n      throw Exception('Accessor type mismatch: $type != $size');\n    }\n  }\n\n  void _verifyNotSparse() {\n    if (sparse != null) {\n      throw Exception('Accessor is sparse: not supported yet.');\n    }\n  }\n\n  IntAccessor asInt() => IntAccessor(root: root, accessor: this);\n  FloatAccessor asFloat() => FloatAccessor(root: root, accessor: this);\n  Vector2Accessor asVector2() => Vector2Accessor(root: root, accessor: this);\n  Vector3Accessor asVector3() => Vector3Accessor(root: root, accessor: this);\n  Vector4Accessor asVector4() => Vector4Accessor(root: root, accessor: this);\n  QuaternionAccessor asQuaternion() => QuaternionAccessor(\n    root: root,\n    accessor: this,\n  );\n  Matrix4Accessor asMatrix4() => Matrix4Accessor(root: root, accessor: this);\n\n  RawAccessor({\n    required super.root,\n    required this.bufferView,\n    required this.byteOffset,\n    required this.componentType,\n    required this.normalized,\n    required this.count,\n    required this.type,\n    required this.max,\n    required this.min,\n    required this.sparse,\n  });\n\n  RawAccessor.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        bufferView: Parser.ref(root, map, 'bufferView')!,\n        byteOffset: Parser.integer(map, 'byteOffset') ?? 0,\n        componentType: ComponentType.parse(map, 'componentType')!,\n        normalized: Parser.boolean(map, 'normalized') ?? false,\n        count: Parser.integer(map, 'count')!,\n        type: AccessorType.parse(map, 'type')!,\n        max: Parser.floatList(map, 'max'),\n        min: Parser.floatList(map, 'min'),\n        sparse: Parser.object(root, map, 'sparse', SparseAccessor.parse),\n      );\n}\n\nabstract class TypedAccessor<T> extends GltfNode {\n  final RawAccessor rawAccessor;\n\n  TypedAccessor({\n    required super.root,\n    required RawAccessor accessor,\n  }) : rawAccessor = accessor;\n\n  List<T> typedData();\n\n  void _checkAccessorType(AccessorType expected) {\n    final type = rawAccessor.type;\n    if (type != expected) {\n      throw Exception('Accessor type mismatch: $type != $expected');\n    }\n  }\n}\n\nclass IntAccessor extends TypedAccessor<int> {\n  IntAccessor({\n    required super.root,\n    required super.accessor,\n  });\n\n  @override\n  List<int> typedData() {\n    _checkAccessorType(AccessorType.scalar);\n    return rawAccessor._typedData(1, (list) => list[0].toInt());\n  }\n}\n\nclass FloatAccessor extends TypedAccessor<double> {\n  FloatAccessor({\n    required super.root,\n    required super.accessor,\n  });\n\n  @override\n  List<double> typedData() {\n    _checkAccessorType(AccessorType.scalar);\n    return rawAccessor._typedData(1, (list) => list[0].toDouble());\n  }\n}\n\nclass Vector2Accessor extends TypedAccessor<Vector2> {\n  Vector2Accessor({\n    required super.root,\n    required super.accessor,\n  });\n\n  @override\n  List<Vector2> typedData() {\n    _checkAccessorType(AccessorType.vec2);\n    return rawAccessor._typedData(2, (values) {\n      final doubles = Parser.coerceFloatList(values);\n      return Vector2.array(doubles);\n    });\n  }\n}\n\nclass Vector3Accessor extends TypedAccessor<Vector3> {\n  Vector3Accessor({\n    required super.root,\n    required super.accessor,\n  });\n\n  @override\n  List<Vector3> typedData() {\n    _checkAccessorType(AccessorType.vec3);\n    return rawAccessor._typedData(3, (values) {\n      final doubles = Parser.coerceFloatList(values);\n      return Vector3.array(doubles);\n    });\n  }\n}\n\nclass Vector4Accessor extends TypedAccessor<Vector4> {\n  Vector4Accessor({\n    required super.root,\n    required super.accessor,\n  });\n\n  @override\n  List<Vector4> typedData() {\n    _checkAccessorType(AccessorType.vec4);\n    return rawAccessor._typedData(4, (values) {\n      final doubles = Parser.coerceFloatList(values);\n      return Vector4.array(doubles);\n    });\n  }\n}\n\nclass QuaternionAccessor extends TypedAccessor<Quaternion> {\n  QuaternionAccessor({\n    required super.root,\n    required super.accessor,\n  });\n\n  @override\n  List<Quaternion> typedData() {\n    _checkAccessorType(AccessorType.vec4);\n    return rawAccessor._typedData(4, (values) {\n      final doubles = Parser.coerceFloatList(values);\n      return Quaternion(doubles[0], doubles[1], doubles[2], doubles[3]);\n    });\n  }\n}\n\nclass Matrix4Accessor extends TypedAccessor<Matrix4> {\n  Matrix4Accessor({\n    required super.root,\n    required super.accessor,\n  });\n\n  @override\n  List<Matrix4> typedData() {\n    _checkAccessorType(AccessorType.mat4);\n    return rawAccessor._typedData(16, (values) {\n      final doubles = Parser.coerceFloatList(values);\n      return Matrix4.fromList(doubles);\n    });\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/accessor_type.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\n\n/// Specifies if the accessor's elements are scalars, vectors, or matrices.\nenum AccessorType {\n  scalar('SCALAR', 1),\n  vec2('VEC2', 2),\n  vec3('VEC3', 3),\n  vec4('VEC4', 4),\n  mat2('MAT2', 4),\n  mat3('MAT3', 9),\n  mat4('MAT4', 16)\n  ;\n\n  final String name;\n  final int size;\n\n  const AccessorType(this.name, this.size);\n\n  static AccessorType valueOf(String name) {\n    return values.firstWhere((e) => e.name == name);\n  }\n\n  static AccessorType? parse(Map<String, Object?> map, String key) {\n    return Parser.stringEnum(map, key, valueOf);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/alpha_mode.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\n\n/// The material's alpha rendering mode enumeration specifying the\n/// interpretation of the alpha value of the base color.\nenum AlphaMode {\n  /// The alpha value is ignored, and the rendered output is fully opaque.\n  opaque('OPAQUE'),\n\n  /// The rendered output is either fully opaque or fully transparent depending\n  /// on the alpha value and the specified `alphaCutoff` value;\n  /// the exact appearance of the edges **MAY** be subject to\n  /// implementation-specific techniques such as \"Alpha-to-Coverage\"\n  mask('MASK'),\n\n  /// The alpha value is used to composite the source and destination areas.\n  /// The rendered output is combined with the background using the normal\n  /// painting operation (i.e. the Porter and Duff over operator).\n  blend('BLEND')\n  ;\n\n  final String value;\n\n  const AlphaMode(this.value);\n\n  static AlphaMode valueOf(String value) {\n    return values.firstWhere((e) => e.value == value);\n  }\n\n  static AlphaMode? parse(Map<String, Object?> map, String key) {\n    return Parser.stringEnum(map, key, valueOf);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/animation.dart",
    "content": "import 'package:flame_3d/src/model/model_animation.dart';\nimport 'package:flame_3d/src/parser/gltf/animation_channel.dart';\nimport 'package:flame_3d/src/parser/gltf/animation_path.dart';\nimport 'package:flame_3d/src/parser/gltf/animation_sampler.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\n\n/// A keyframe animation.\nclass Animation extends GltfNode {\n  final String? name;\n\n  /// An array of animation channels.\n  /// An animation channel combines an animation sampler with a target property\n  /// being animated.\n  /// Different channels of the same animation **MUST NOT** have the same\n  /// targets.\n  final List<AnimationChannel> channels;\n\n  /// An array of animation samplers.\n  /// An animation sampler combines timestamps with a sequence of output values\n  /// and defines an interpolation algorithm.\n  final List<AnimationSampler> samplers;\n\n  Animation({\n    required super.root,\n    required this.name,\n    required this.channels,\n    required this.samplers,\n  });\n\n  Animation.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        name: Parser.string(map, 'name'),\n        channels: Parser.objectList(\n          root,\n          map,\n          'channels',\n          AnimationChannel.parse,\n        )!,\n        samplers: Parser.objectList(\n          root,\n          map,\n          'samplers',\n          AnimationSampler.parse,\n        )!,\n      );\n\n  ModelAnimation toFlameAnimation() {\n    final controllers = <int, List<AnimationController>>{};\n    for (final channel in channels) {\n      final path = channel.target.path;\n      final sampler = samplers[channel.sampler];\n\n      final times = sampler.input.get().typedData();\n      final values = sampler.output.get();\n\n      final spline =\n          switch (path) {\n                AnimationPath.translation => TranslationAnimationSpline.from(\n                  interpolation: sampler.interpolation,\n                  times: times,\n                  values: values.asVector3().typedData(),\n                ),\n                AnimationPath.scale => ScaleAnimationSpline.from(\n                  interpolation: sampler.interpolation,\n                  times: times,\n                  values: values.asVector3().typedData(),\n                ),\n                AnimationPath.rotation => RotationAnimationSpline.from(\n                  interpolation: sampler.interpolation,\n                  times: times,\n                  values: values.asQuaternion().typedData(),\n                ),\n                AnimationPath.weights => throw UnimplementedError(),\n              }\n              as AnimationSpline;\n\n      final nodeIndex = channel.target.node.index;\n      (controllers[nodeIndex] ??= []).add(\n        AnimationController(\n          animation: spline,\n        ),\n      );\n    }\n    final nodes = controllers.map(\n      (key, value) => MapEntry(\n        key,\n        NodeAnimation(\n          channels: value,\n        ),\n      ),\n    );\n    return ModelAnimation(\n      name: name,\n      nodes: nodes,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/animation_channel.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/animation_target.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\n\nclass AnimationChannel extends GltfNode {\n  /// The ID of a sampler in this animation used to compute the value for the\n  /// target, e.g., a node's translation, rotation, or scale (TRS).\n  final int sampler;\n\n  /// The descriptor of the animated property.\n  final AnimationTarget target;\n\n  AnimationChannel({\n    required super.root,\n    required this.sampler,\n    required this.target,\n  });\n\n  AnimationChannel.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        sampler: Parser.integer(map, 'sampler')!,\n        target: Parser.object(root, map, 'target', AnimationTarget.parse)!,\n      );\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/animation_interpolation.dart",
    "content": "import 'package:flame_3d/core.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\n\n/// Interpolation algorithm.\nenum AnimationInterpolation {\n  /// The animated values are linearly interpolated between keyframes.\n  /// When targeting a rotation, spherical linear interpolation (slerp)\n  /// **SHOULD** be used to interpolate quaternions.\n  /// The number of output elements **MUST** equal the number of input elements.\n  linear('LINEAR'),\n\n  /// The animated values remain constant to the output of the first keyframe,\n  /// until the next keyframe.\n  /// The number of output elements **MUST** equal the number of input elements.\n  step('STEP'),\n\n  /// The animation's interpolation is computed using a cubic spline with\n  /// specified tangents.\n  /// The number of output elements **MUST** equal three times the number of\n  /// input elements.\n  /// For each input element, the output stores three elements, an in-tangent,\n  /// a spline vertex, and an out-tangent.\n  /// There **MUST** be at least two keyframes when using this interpolation.\n  cubicSpline('CUBICSPLINE')\n  ; // cSpell:ignore CUBICSPLINE\n\n  final String value;\n\n  const AnimationInterpolation(this.value);\n\n  static AnimationInterpolation valueOf(String value) {\n    return values.firstWhere((e) => e.value == value);\n  }\n\n  static AnimationInterpolation? parse(Map<String, Object?> map, String key) {\n    return Parser.stringEnum(map, key, valueOf);\n  }\n\n  Vector3 lerp(Vector3 a, Vector3 b, double t) {\n    return switch (this) {\n      linear => Vector3Utils.lerp(a, b, t),\n      step => a,\n      cubicSpline => throw UnimplementedError(),\n    };\n  }\n\n  Quaternion slerp(Quaternion a, Quaternion b, double t) {\n    return switch (this) {\n      linear => QuaternionUtils.slerp(a, b, t),\n      step => a,\n      cubicSpline => throw UnimplementedError(),\n    };\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/animation_path.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\n\n/// The name of the node's TRS property to modify.\nenum AnimationPath {\n  translation('translation'),\n  rotation('rotation'),\n  scale('scale'),\n  weights('weights')\n  ;\n\n  final String value;\n\n  const AnimationPath(this.value);\n\n  static AnimationPath valueOf(String value) {\n    return values.firstWhere((e) => e.value == value);\n  }\n\n  static AnimationPath? parse(Map<String, Object?> map, String key) {\n    return Parser.stringEnum(map, key, valueOf);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/animation_sampler.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/accessor.dart';\nimport 'package:flame_3d/src/parser/gltf/animation_interpolation.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_ref.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\n\n/// An animation sampler combines timestamps with a sequence of output values\n/// and defines an interpolation algorithm.\nclass AnimationSampler extends GltfNode {\n  /// The index of an accessor containing keyframe timestamps.\n  /// The accessor **MUST** be of scalar type with floating-point components.\n  /// The values represent time in seconds with `time[0] >= 0.0`, and strictly\n  /// increasing values, i.e., `time[n + 1] > time[n]`.\n  final GltfRef<FloatAccessor> input;\n\n  /// Interpolation algorithm.\n  final AnimationInterpolation interpolation;\n\n  /// The index of an accessor, containing keyframe output values.\n  final GltfRef<RawAccessor> output;\n\n  AnimationSampler({\n    required super.root,\n    required this.input,\n    required this.interpolation,\n    required this.output,\n  });\n\n  AnimationSampler.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        input: Parser.ref(root, map, 'input')!,\n        interpolation:\n            AnimationInterpolation.parse(map, 'interpolation') ??\n            AnimationInterpolation.linear,\n        output: Parser.ref(root, map, 'output')!,\n      );\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/animation_target.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/animation_path.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_ref.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/gltf/node.dart';\n\n/// The descriptor of the animated property.\nclass AnimationTarget extends GltfNode {\n  /// The reference to the node to animate. When undefined, the animated object\n  /// **MAY** be defined by an extension.\n  final GltfRef<Node> node;\n\n  /// The name of the node's TRS property to animate, or the `weights` of the\n  /// Morph Targets it instantiates.\n  /// For the `translation` property, the values that are provided by the\n  /// sampler are the translation along the X, Y, and Z axes.\n  /// For the `rotation` property, the values are a quaternion in the order\n  /// (x, y, z, w), where w is the scalar.\n  /// For the `scale` property, the values are the scaling factors along the\n  /// X, Y, and Z axes.\n  final AnimationPath path;\n\n  AnimationTarget({\n    required super.root,\n    required this.node,\n    required this.path,\n  });\n\n  AnimationTarget.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        node: Parser.ref(root, map, 'node')!,\n        path: AnimationPath.parse(map, 'path')!,\n      );\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/buffer.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\n\n/// A buffer points to binary geometry, animation, or skins.\nclass Buffer extends GltfNode {\n  /// The length of the buffer in bytes.\n  final int byteLength;\n\n  /// The URI of the buffer.\n  final String? uri;\n\n  Buffer({\n    required super.root,\n    required this.byteLength,\n    required this.uri,\n  });\n\n  Buffer.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        byteLength: Parser.integer(map, 'byteLength')!,\n        uri: Parser.string(map, 'uri'),\n      );\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/buffer_view.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:flame_3d/src/parser/gltf/buffer.dart';\nimport 'package:flame_3d/src/parser/gltf/buffer_view_target.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node_with_data.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_ref.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\n\n/// A view into a buffer generally representing a subset of the buffer.\nclass BufferView extends GltfNode with GltfNodeWithData<Uint8List> {\n  /// The reference to the buffer.\n  final GltfRef<Buffer> buffer;\n\n  /// The length of the bufferView in bytes.\n  final int byteLength;\n\n  /// The offset into the buffer in bytes.\n  final int byteOffset;\n\n  /// The stride, in bytes, between vertex attributes.\n  ///\n  /// When this is not defined, data is tightly packed.\n  /// When two or more accessors use the same buffer view, this field **MUST**\n  /// be defined.\n  final int? byteStride;\n\n  /// The hint representing the intended GPU buffer type to use with this\n  /// buffer view.\n  final BufferViewTarget? target;\n\n  BufferView({\n    required super.root,\n    required this.buffer,\n    required this.byteLength,\n    required this.byteOffset,\n    required this.byteStride,\n    required this.target,\n  });\n\n  BufferView.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        buffer: Parser.ref(root, map, 'buffer')!,\n        byteLength: Parser.integer(map, 'byteLength')!,\n        byteOffset: Parser.integer(map, 'byteOffset') ?? 0,\n        byteStride: Parser.integer(map, 'byteStride'),\n        target: BufferViewTarget.parse(map, 'target'),\n      );\n\n  @override\n  Future<Uint8List> loadData() async {\n    return root.readChunk(buffer);\n  }\n\n  Uint8List data() {\n    final data = getData();\n    return data.sublist(\n      byteOffset,\n      byteOffset + byteLength,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/buffer_view_target.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\n\n/// The target that the WebGL buffer should be bound to.\nenum BufferViewTarget {\n  arrayBuffer(34962),\n  elementArrayBuffer(34963)\n  ;\n\n  final int value;\n\n  const BufferViewTarget(this.value);\n\n  static BufferViewTarget valueOf(int value) {\n    return values.firstWhere((e) => e.value == value);\n  }\n\n  static BufferViewTarget? parse(Map<String, Object?> map, String key) {\n    return Parser.integerEnum(map, key, valueOf);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/camera.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/camera_orthographic.dart';\nimport 'package:flame_3d/src/parser/gltf/camera_perspective.dart';\nimport 'package:flame_3d/src/parser/gltf/camera_type.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\n\n/// A camera's projection.\n///\n/// A node **MAY** reference a camera to apply a transform to place the camera\n/// in the scene.\nclass Camera extends GltfNode {\n  /// Specifies if the camera uses a perspective or orthographic projection.\n  /// Based on this, either the camera's `perspective` or `orthographic`\n  /// property **MUST** be defined.\n  final CameraType type;\n\n  /// An orthographic camera containing properties to create an orthographic\n  /// projection matrix.\n  /// This property **MUST NOT** be defined when `perspective` is defined.\n  final CameraOrthographic? orthographic;\n\n  /// A perspective camera containing properties to create a perspective\n  /// projection matrix.\n  /// This property **MUST NOT** be defined when `orthographic` is defined.\n  final CameraPerspective? perspective;\n\n  Camera({\n    required super.root,\n    required this.type,\n    required this.orthographic,\n    required this.perspective,\n  });\n\n  Camera.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        type: CameraType.parse(map, 'type')!,\n        orthographic: Parser.object(\n          root,\n          map,\n          'orthographic',\n          CameraOrthographic.parse,\n        ),\n        perspective: Parser.object(\n          root,\n          map,\n          'perspective',\n          CameraPerspective.parse,\n        ),\n      );\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/camera_orthographic.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\n\n/// An orthographic camera containing properties to create an orthographic\n/// projection matrix.\nclass CameraOrthographic extends GltfNode {\n  /// The floating-point horizontal magnification of the view.\n  /// This value **MUST NOT** be equal to zero.\n  /// This value **SHOULD NOT** be negative.\n  final double xMag;\n\n  /// The floating-point vertical magnification of the view.\n  /// This value **MUST NOT** be equal to zero.\n  /// This value **SHOULD NOT** be negative.\n  final double yMag;\n\n  /// The floating-point distance to the far clipping plane.\n  /// This value **MUST NOT** be equal to zero.\n  /// `zFar` **MUST** be greater than `zNear`.\n  final double xFar;\n\n  /// The floating-point distance to the near clipping plane.\n  final double zNear;\n\n  CameraOrthographic({\n    required super.root,\n    required this.xMag,\n    required this.yMag,\n    required this.xFar,\n    required this.zNear,\n  });\n\n  CameraOrthographic.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        xMag: Parser.float(map, 'xmag')!, // cSpell:ignore xmag\n        yMag: Parser.float(map, 'ymag')!, // cSpell:ignore ymag\n        xFar: Parser.float(map, 'zfar')!, // cSpell:ignore zfar\n        zNear: Parser.float(map, 'znear')!, // cSpell:ignore znear\n      );\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/camera_perspective.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\n\n/// A perspective camera containing properties to create a perspective\n/// projection matrix.\nclass CameraPerspective extends GltfNode {\n  /// The floating-point aspect ratio of the field of view.\n  /// When undefined, the aspect ratio of the rendering viewport **MUST** be\n  /// used.\n  final double? aspectRatio;\n\n  /// The floating-point vertical field of view in radians.\n  /// This value **SHOULD** be less than π.\n  final double yFov;\n\n  /// The floating-point distance to the far clipping plane.\n  /// When defined, `zFar` **MUST** be greater than `zNear`.\n  /// If `zFar` is undefined, client implementations **SHOULD** use\n  /// infinite projection matrix.\n  final double? zFar;\n\n  /// The floating-point distance to the near clipping plane.\n  final double zNear;\n\n  CameraPerspective({\n    required super.root,\n    required this.aspectRatio,\n    required this.yFov,\n    required this.zFar,\n    required this.zNear,\n  });\n\n  CameraPerspective.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        aspectRatio: Parser.float(map, 'aspectRatio'),\n        yFov: Parser.float(map, 'yfov')!, // cSpell:ignore yfov\n        zFar: Parser.float(map, 'zfar'), // cSpell:ignore zfar\n        zNear: Parser.float(map, 'znear')!, // cSpell:ignore znear\n      );\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/camera_type.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\n\n/// Specifies if the camera uses a perspective or orthographic projection.\nenum CameraType {\n  perspective,\n  orthographic\n  ;\n\n  static CameraType valueOf(String value) {\n    return values.firstWhere((e) => e.name == value);\n  }\n\n  static CameraType? parse(Map<String, Object?> map, String key) {\n    return Parser.stringEnum(map, key, valueOf);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/component_type.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\n\n/// The datatype of the accessor's components.\nenum ComponentType {\n  byte(value: 5120, byteSize: 1),\n  unsignedByte(value: 5121, byteSize: 1),\n  short(value: 5122, byteSize: 2),\n  unsignedShort(value: 5123, byteSize: 2),\n  unsignedInt(value: 5125, byteSize: 4),\n  float(value: 5126, byteSize: 4)\n  ;\n\n  final int value;\n  final int byteSize;\n\n  const ComponentType({\n    required this.value,\n    required this.byteSize,\n  });\n\n  num parseData(ByteData byteData, {int cursor = 0}) {\n    return switch (this) {\n      ComponentType.byte => byteData.getInt8(cursor),\n      ComponentType.unsignedByte => byteData.getUint8(cursor),\n      ComponentType.short => byteData.getInt16(cursor, Endian.little),\n      ComponentType.unsignedShort => byteData.getUint16(cursor, Endian.little),\n      ComponentType.unsignedInt => byteData.getUint32(cursor, Endian.little),\n      ComponentType.float => byteData.getFloat32(cursor, Endian.little),\n    };\n  }\n\n  static ComponentType valueOf(int value) {\n    return values.firstWhere((e) => e.value == value);\n  }\n\n  static ComponentType? parse(Map<String, Object?> map, String key) {\n    return Parser.integerEnum(map, key, valueOf);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/glb_chunk.dart",
    "content": "import 'dart:typed_data';\n\n/// Each data chunk in a GLB file.\nclass GlbChunk {\n  final int length;\n  final String type;\n  final Uint8List data;\n\n  GlbChunk({\n    required this.length,\n    required this.type,\n    required this.data,\n  });\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/gltf_node.dart",
    "content": "import 'package:flame_3d/core.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_ref.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\n\n/// A base class for all data classes representing the GLTF schema.\n/// It holds a reference to the [GltfRoot] that contains it, which allows\n/// for [GltfRef]s to be resolved.\nabstract class GltfNode {\n  final GltfRoot root;\n\n  GltfNode({\n    required this.root,\n  });\n}\n\n/// A helper utility class to group different methods for parsing GLTF data\n/// into data classes.\nclass Parser {\n  Parser._();\n\n  static GltfRef<T>? ref<T extends GltfNode>(\n    GltfRoot root,\n    Map<String, Object?> map,\n    String key,\n  ) {\n    final index = map[key];\n    if (index == null) {\n      return null;\n    }\n    return GltfRef<T>(\n      root: root,\n      index: index as int,\n    );\n  }\n\n  static List<GltfRef<T>>? refList<T extends GltfNode>(\n    GltfRoot root,\n    Map<String, Object?> map,\n    String key,\n  ) {\n    return (map[key] as List<Object?>?)?.map((e) {\n      return GltfRef<T>(\n        root: root,\n        index: e! as int,\n      );\n    }).toList();\n  }\n\n  static Vector3? vector3(\n    GltfRoot root,\n    Map<String, Object?> map,\n    String key,\n  ) {\n    final entries = floatList(map, key);\n    return entries?.let(Vector3.array);\n  }\n\n  static Matrix4? matrix4(\n    GltfRoot root,\n    Map<String, Object?> map,\n    String key,\n  ) {\n    final entries = floatList(map, key);\n    return entries?.let(Matrix4.fromList);\n  }\n\n  static Vector4? vector4(\n    GltfRoot root,\n    Map<String, Object?> map,\n    String key,\n  ) {\n    final entries = floatList(map, key);\n    return entries?.let(Vector4.array);\n  }\n\n  static Quaternion? quaternion(\n    GltfRoot root,\n    Map<String, Object?> map,\n    String key,\n  ) {\n    final entries = floatList(map, key);\n    return entries?.let((e) => Quaternion(e[0], e[1], e[2], e[3]));\n  }\n\n  static int? integer(\n    Map<String, Object?> map,\n    String key,\n  ) {\n    return (map[key] as num?)?.toInt();\n  }\n\n  static double? float(\n    Map<String, Object?> map,\n    String key,\n  ) {\n    return (map[key] as num?)?.toDouble();\n  }\n\n  static bool? boolean(\n    Map<String, Object?> map,\n    String key,\n  ) {\n    return map[key] as bool?;\n  }\n\n  static List<double>? floatList(\n    Map<String, Object?> map,\n    String key,\n  ) {\n    return (map[key] as List<Object?>?)?.let(coerceFloatList);\n  }\n\n  static List<double> coerceFloatList(\n    List<Object?> value,\n  ) {\n    return value.map((e) => (e! as num).toDouble()).toList();\n  }\n\n  static String? string(\n    Map<String, Object?> map,\n    String key,\n  ) {\n    return map[key] as String?;\n  }\n\n  static T? object<T>(\n    GltfRoot root,\n    Map<String, Object?> map,\n    String key,\n    T Function(GltfRoot, Map<String, Object?>) parser,\n  ) {\n    final value = map[key];\n    if (value == null) {\n      return null;\n    }\n    return parser(root, value as Map<String, Object?>);\n  }\n\n  static T? integerEnum<T extends Enum>(\n    Map<String, Object?> map,\n    String key,\n    T Function(int) valueOf,\n  ) {\n    final value = map[key];\n    if (value == null) {\n      return null;\n    }\n    return valueOf(value as int);\n  }\n\n  static T? stringEnum<T extends Enum>(\n    Map<String, Object?> map,\n    String key,\n    T Function(String) valueOf,\n  ) {\n    final value = map[key];\n    if (value == null) {\n      return null;\n    }\n    return valueOf(value as String);\n  }\n\n  static List<T>? objectList<T>(\n    GltfRoot root,\n    Map<String, Object?> map,\n    String key,\n    T Function(GltfRoot, Map<String, Object?>) parser,\n  ) {\n    return (map[key] as List<Object?>?)\n        ?.map((e) => parser(root, e! as Map<String, Object?>))\n        .toList();\n  }\n\n  static Map<String, int>? mapInt(\n    Map<String, Object?> map,\n    String key,\n  ) {\n    return (map[key] as Map<String, Object?>?)?.map(\n      (key, value) => MapEntry(key, value! as int),\n    );\n  }\n}\n\nextension _Let<T> on T {\n  R let<R>(R Function(T) block) {\n    if (this == null) {\n      return null as R;\n    }\n    return block(this!);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/gltf_node_with_data.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\n\n/// A [GltfNode] with some sort of associated data that needs to be loaded\n/// with an async [init] method.\nmixin GltfNodeWithData<T> on GltfNode {\n  late T _data;\n\n  Future<void> init() async {\n    _data = await loadData();\n  }\n\n  Future<T> loadData();\n  T getData() => _data;\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/gltf_ref.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\n\n/// A wrapper over an index reference within the GLTF spec, allowing the data\n/// structure to be navigated with ease.\nclass GltfRef<T extends GltfNode> extends GltfNode {\n  final int index;\n\n  GltfRef({\n    required super.root,\n    required this.index,\n  });\n\n  T get() {\n    return root.resolve<T>(index);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/gltf_root.dart",
    "content": "import 'dart:convert';\nimport 'dart:typed_data';\n\nimport 'package:flame/flame.dart';\nimport 'package:flame_3d/model.dart';\nimport 'package:flame_3d/src/parser/gltf/accessor.dart';\nimport 'package:flame_3d/src/parser/gltf/animation.dart';\nimport 'package:flame_3d/src/parser/gltf/buffer.dart';\nimport 'package:flame_3d/src/parser/gltf/buffer_view.dart';\nimport 'package:flame_3d/src/parser/gltf/camera.dart';\nimport 'package:flame_3d/src/parser/gltf/glb_chunk.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node_with_data.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_ref.dart';\nimport 'package:flame_3d/src/parser/gltf/image.dart';\nimport 'package:flame_3d/src/parser/gltf/material.dart';\nimport 'package:flame_3d/src/parser/gltf/mesh.dart';\nimport 'package:flame_3d/src/parser/gltf/node.dart';\nimport 'package:flame_3d/src/parser/gltf/sampler.dart';\nimport 'package:flame_3d/src/parser/gltf/scene.dart';\nimport 'package:flame_3d/src/parser/gltf/skin.dart';\nimport 'package:flame_3d/src/parser/gltf/texture.dart';\n\n/// The root schema of a GLTF file.\nclass GltfRoot {\n  /// Path prefix used to resolve relative paths.\n  final String _prefix;\n\n  late final List<Buffer> buffers;\n  late final List<BufferView> bufferViews;\n  late final List<RawAccessor> accessors;\n\n  late final int scene;\n  late final List<Scene> scenes;\n\n  late final List<Node> nodes;\n  late final List<Camera> cameras;\n  late final List<Skin> skins;\n  late final List<Mesh> meshes;\n  late final List<Material> materials;\n  late final List<Texture> textures;\n  late final List<Animation> animations;\n  late final List<Sampler> samplers;\n  late final List<Image> images;\n\n  late final List<GlbChunk> chunks;\n\n  GltfRoot._({\n    required String prefix,\n  }) : _prefix = prefix;\n\n  Future<Uint8List> readChunk(GltfRef<Buffer> ref) async {\n    if (chunks.isNotEmpty) {\n      final chunk = chunks[ref.index];\n      return chunk.data;\n    }\n    final buffer = ref.get();\n    return readChunkFrom(buffer.uri!);\n  }\n\n  Future<Uint8List> readChunkFrom(String uri) async {\n    if (uri.startsWith('data:')) {\n      const prefixes = [\n        'data:application/gltf-buffer;base64,',\n        'data:application/octet-stream;base64,',\n      ];\n      for (final prefix in prefixes) {\n        if (uri.startsWith(prefix)) {\n          return base64Decode(uri.substring(prefix.length));\n        }\n      }\n      throw Exception('Unsupported data URI: $uri');\n    } else {\n      final path = '$_prefix/$uri';\n      return Flame.assets.readBinaryFile(path);\n    }\n  }\n\n  T resolve<T extends GltfNode>(int index) {\n    return switch (T) {\n          const (Scene) => scenes[index],\n          const (Node) => nodes[index],\n          const (Mesh) => meshes[index],\n          const (Material) => materials[index],\n          const (Camera) => cameras[index],\n          const (Skin) => skins[index],\n          const (BufferView) => bufferViews[index],\n          const (Buffer) => buffers[index],\n          const (Texture) => textures[index],\n          const (Animation) => animations[index],\n          const (Sampler) => samplers[index],\n          const (Image) => images[index],\n          const (IntAccessor) => accessors[index].asInt(),\n          const (FloatAccessor) => accessors[index].asFloat(),\n          const (Vector2Accessor) => accessors[index].asVector2(),\n          const (Vector3Accessor) => accessors[index].asVector3(),\n          const (Vector4Accessor) => accessors[index].asVector4(),\n          const (QuaternionAccessor) => accessors[index].asQuaternion(),\n          const (Matrix4Accessor) => accessors[index].asMatrix4(),\n          const (RawAccessor) => accessors[index],\n          _ => throw UnimplementedError('Cannot resolve type $T'),\n        }\n        as T;\n  }\n\n  static Future<GltfRoot> from({\n    required String prefix,\n    required Map<String, dynamic> json,\n    required List<GlbChunk> chunks,\n  }) async {\n    final root = GltfRoot._(prefix: prefix);\n    root.chunks = chunks;\n\n    Future<List<T>> parse<T>(\n      String key,\n      T Function(GltfRoot, Map<String, Object?>) parser,\n    ) async {\n      final objects = Parser.objectList(root, json, key, parser) ?? [];\n      for (final object in objects) {\n        if (object is GltfNodeWithData) {\n          await object.init();\n        }\n      }\n      return objects;\n    }\n\n    root.buffers = await parse('buffers', Buffer.parse);\n    root.bufferViews = await parse('bufferViews', BufferView.parse);\n    root.accessors = await parse('accessors', RawAccessor.parse);\n\n    root.scenes = await parse('scenes', Scene.parse);\n    root.scene = Parser.integer(json, 'scene')!;\n\n    root.nodes = await parse('nodes', Node.parse);\n    root.cameras = await parse('cameras', Camera.parse);\n    root.skins = await parse('skins', Skin.parse);\n    root.meshes = await parse('meshes', Mesh.parse);\n    root.materials = await parse('materials', Material.parse);\n    root.textures = await parse('textures', Texture.parse);\n    root.animations = await parse('animations', Animation.parse);\n    root.samplers = await parse('samplers', Sampler.parse);\n    root.images = await parse('images', Image.parse);\n\n    return root;\n  }\n\n  Map<int, ModelNode> toFlameNodes([int? scene]) {\n    return scenes[scene ?? this.scene].toFlameNodes();\n  }\n\n  Model toFlameModel([int? scene]) {\n    return Model(\n      nodes: toFlameNodes(scene),\n      animations: animations\n          .map((animation) => animation.toFlameAnimation())\n          .toList(),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/image.dart",
    "content": "import 'dart:typed_data';\nimport 'dart:ui' as dart;\n\nimport 'package:flame_3d/resources.dart' as flame3d;\nimport 'package:flame_3d/src/parser/gltf/buffer_view.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node_with_data.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_ref.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/gltf/mime_type.dart';\n\n/// Image data used to create a texture.\nclass Image extends GltfNode with GltfNodeWithData<flame3d.ImageTexture> {\n  /// The URI (or IRI) of the image.\n  ///\n  /// Relative paths are relative to the current glTF asset.\n  /// Instead of referencing an external file, this field **MAY** contain a\n  /// `data:`-URI.\n  /// This field **MUST NOT** be defined when `bufferView` is defined.\n  final String? uri;\n\n  /// The image's media type.\n  ///\n  /// This field **MUST** be defined when `bufferView` is defined.\n  final MimeType? mimeType;\n\n  /// The reference to the bufferView that contains the image.\n  /// This field **MUST NOT** be defined when `uri` is defined.\n  final GltfRef<BufferView>? bufferView;\n\n  Image({\n    required super.root,\n    required this.uri,\n    required this.mimeType,\n    required this.bufferView,\n  });\n\n  Image.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        uri: Parser.string(map, 'uri'),\n        mimeType: MimeType.parse(map, 'mimeType'),\n        bufferView: Parser.ref(root, map, 'bufferView'),\n      );\n\n  Future<Uint8List> data() async {\n    final uri = this.uri;\n    if (uri != null) {\n      return root.readChunkFrom(uri);\n    } else {\n      final bufferView = this.bufferView?.get();\n      if (bufferView == null) {\n        throw Exception('Either `uri` or `bufferView` must be defined');\n      }\n      return bufferView.data();\n    }\n  }\n\n  Future<dart.Image> parseDartImage() async {\n    final bytes = await data();\n    final codec = await dart.instantiateImageCodec(bytes);\n    final frameInfo = await codec.getNextFrame();\n    return frameInfo.image;\n  }\n\n  @override\n  Future<flame3d.ImageTexture> loadData() async {\n    return flame3d.ImageTexture.create(await parseDartImage());\n  }\n\n  flame3d.ImageTexture toFlameTexture() {\n    return getData();\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/mag_filter.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\n\n/// Magnification filter. Valid values correspond to WebGL enums.\nenum MagFilter {\n  nearest('NEAREST', 9728),\n  linear('LINEAR', 9729)\n  ;\n\n  final String name;\n  final int value;\n\n  const MagFilter(this.name, this.value);\n\n  static MagFilter valueOf(int value) {\n    return values.firstWhere((e) => e.value == value);\n  }\n\n  static MagFilter? parse(Map<String, Object?> map, String key) {\n    return Parser.integerEnum(map, key, valueOf);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/material.dart",
    "content": "import 'package:flame_3d/core.dart';\nimport 'package:flame_3d/resources.dart' as flame_3d;\nimport 'package:flame_3d/src/parser/gltf/alpha_mode.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/gltf/normal_texture_info.dart';\nimport 'package:flame_3d/src/parser/gltf/occlusion_texture_info.dart';\nimport 'package:flame_3d/src/parser/gltf/pbr_metallic_roughness.dart';\nimport 'package:flame_3d/src/parser/gltf/texture_info.dart';\n\n/// The material appearance of a primitive.\nclass Material extends GltfNode {\n  final String? name;\n\n  /// A set of parameter values that are used to define the metallic-roughness\n  /// material model from Physically Based Rendering (PBR) methodology.\n  /// When undefined, all the default values of `pbrMetallicRoughness` **MUST**\n  /// apply.\n  final PBRMetallicRoughness? pbrMetallicRoughness;\n\n  /// The tangent space normal texture. The texture encodes RGB components with\n  /// linear transfer function.\n  /// Each texel represents the XYZ components of a normal vector in tangent\n  /// space.\n  /// The normal vectors use the convention +X is right and +Y is up. +Z points\n  /// toward the viewer.\n  /// If a fourth component (A) is present, it **MUST** be ignored.\n  /// When undefined, the material does not have a tangent space normal texture.\n  final NormalTextureInfo? normalTexture;\n\n  /// The occlusion texture. The occlusion values are linearly sampled from the\n  /// R channel.\n  /// Higher values indicate areas that receive full indirect lighting and lower\n  /// values indicate no indirect lighting.\n  /// If other channels are present (GBA), they **MUST** be ignored for\n  /// occlusion calculations.\n  /// When undefined, the material does not have an occlusion texture.\n  final OcclusionTextureInfo? occlusionTexture;\n\n  /// The emissive texture. It controls the color and intensity of the light\n  /// being emitted by the material.\n  /// This texture contains RGB components encoded with the sRGB transfer\n  /// function.\n  /// If a fourth component (A) is present, it **MUST** be ignored.\n  /// When undefined, the texture **MUST** be sampled as having `1.0` in RGB\n  /// components.\n  final TextureInfo? emissiveTexture;\n\n  /// The factors for the emissive color of the material. This value defines\n  /// linear multipliers for the sampled texels of the emissive texture.\n  final Vector3 emissiveFactor;\n\n  /// The material's alpha rendering mode enumeration specifying the\n  /// interpretation of the alpha value of the base color.\n  final AlphaMode alphaMode;\n\n  /// Specifies whether the material is double sided. When this value is false,\n  /// back-face culling is enabled.\n  /// When this value is true, back-face culling is disabled and double-sided\n  /// lighting is enabled.\n  /// The back-face **MUST** have its normals reversed before the lighting\n  /// equation is evaluated.\n  final bool doubleSided;\n\n  Material({\n    required super.root,\n    required this.name,\n    required this.pbrMetallicRoughness,\n    required this.normalTexture,\n    required this.occlusionTexture,\n    required this.emissiveTexture,\n    required this.emissiveFactor,\n    required this.alphaMode,\n    required this.doubleSided,\n  });\n\n  Material.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        name: Parser.string(map, 'name'),\n        pbrMetallicRoughness: Parser.object(\n          root,\n          map,\n          'pbrMetallicRoughness',\n          PBRMetallicRoughness.parse,\n        ),\n        normalTexture: Parser.object(\n          root,\n          map,\n          'normalTexture',\n          NormalTextureInfo.parse,\n        ),\n        occlusionTexture: Parser.object(\n          root,\n          map,\n          'occlusionTexture',\n          OcclusionTextureInfo.parse,\n        ),\n        emissiveTexture: Parser.object(\n          root,\n          map,\n          'emissiveTexture',\n          TextureInfo.parse,\n        ),\n        emissiveFactor:\n            Parser.vector3(root, map, 'emissiveFactor') ?? Vector3.all(0),\n        alphaMode: AlphaMode.parse(map, 'alphaMode') ?? AlphaMode.opaque,\n        doubleSided: Parser.boolean(map, 'doubleSided') ?? false,\n      );\n\n  flame_3d.Material? toFlameMaterial() {\n    return pbrMetallicRoughness?.toFlameSpatialMaterial();\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/mesh.dart",
    "content": "import 'package:flame_3d/core.dart';\nimport 'package:flame_3d/resources.dart' as flame_3d;\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/gltf/primitive.dart';\n\n/// A set of primitives to be rendered.\n///\n/// Its global transform is defined by a node that references it.\nclass Mesh extends GltfNode {\n  /// An array of primitives, each defining geometry to be rendered.\n  final List<Primitive> primitives;\n\n  /// Array of weights to be applied to the morph targets.\n  /// The number of array elements **MUST** match the number of morph targets\n  final List<double>? weights;\n\n  Mesh({\n    required super.root,\n    required this.primitives,\n    required this.weights,\n  });\n\n  Mesh.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        primitives:\n            Parser.objectList(root, map, 'primitives', Primitive.parse) ?? [],\n        weights: Parser.floatList(map, 'weights'),\n      );\n\n  // TODO(luan): remove the transform parameter\n  flame_3d.Mesh toFlameMesh([Matrix4? transform]) {\n    final mesh = flame_3d.Mesh();\n    for (final primitive in primitives) {\n      mesh.addSurface(primitive.toFlameSurface(transform));\n    }\n    return mesh;\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/mime_type.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\n\nenum MimeType {\n  jpeg('image/jpeg'),\n  png('image/png'),\n  string('string')\n  ;\n\n  final String value;\n\n  const MimeType(this.value);\n\n  static MimeType valueOf(String value) {\n    return values.firstWhere((e) => e.value == value);\n  }\n\n  static MimeType? parse(Map<String, Object?> map, String key) {\n    return Parser.stringEnum(map, key, valueOf);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/min_filter.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\n\n/// Minification filter. Valid values correspond to WebGL enums.\nenum MinFilter {\n  nearest('NEAREST', 9728),\n  linear('LINEAR', 9729),\n  nearestMipmapNearest('NEAREST_MIPMAP_NEAREST', 9984),\n  linearMipmapNearest('LINEAR_MIPMAP_NEAREST', 9985),\n  nearestMipmapLinear('NEAREST_MIPMAP_LINEAR', 9986),\n  linearMipmapLinear('LINEAR_MIPMAP_LINEAR', 9987)\n  ;\n\n  final String name;\n  final int value;\n\n  const MinFilter(this.name, this.value);\n\n  static MinFilter valueOf(int value) {\n    return values.firstWhere((e) => e.value == value);\n  }\n\n  static MinFilter? parse(Map<String, Object?> map, String key) {\n    return Parser.integerEnum(map, key, valueOf);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/morph_target.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/accessor.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_ref.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\n\n/// A plain JSON object specifying attributes displacements in a morph target,\n/// where:\n/// * each key corresponds to one of the three supported attribute semantic\n///   (`POSITION`, `NORMAL`, or `TANGENT`); and\n/// * each value is the index of the accessor containing the attribute\n///   displacements' data.\nclass MorphTarget extends GltfNode {\n  Map<MorphTargetType, GltfRef<RawAccessor>> attributes = {};\n\n  MorphTarget({\n    required super.root,\n    required this.attributes,\n  });\n\n  MorphTarget.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        attributes: map.map(\n          (key, value) => MapEntry(\n            MorphTargetType.valueOf(key),\n            GltfRef<RawAccessor>(root: root, index: value! as int),\n          ),\n        ),\n      );\n}\n\nenum MorphTargetType {\n  position('POSITION'),\n  normal('NORMAL'),\n  tangent('TANGENT')\n  ;\n\n  final String value;\n\n  const MorphTargetType(this.value);\n\n  static MorphTargetType valueOf(String value) {\n    return values.firstWhere((e) => e.value == value);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/node.dart",
    "content": "import 'package:flame_3d/core.dart';\nimport 'package:flame_3d/src/parser/gltf/camera.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_ref.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/gltf/mesh.dart';\nimport 'package:flame_3d/src/parser/gltf/skin.dart';\n\n/// A node in the node hierarchy.\n///\n/// When the node contains `skin`, all `mesh.primitives` **MUST** contain\n/// `JOINTS_0` and `WEIGHTS_0` attributes.\n///\n/// A node **MAY** have either a `matrix` or any combination of\n/// `translation`/`rotation`/`scale` (TRS) properties.\n/// TRS properties are converted to matrices and post-multiplied in the\n/// `T * R * S` order to compose the transformation matrix;\n/// first the scale is applied to the vertices, then the rotation, and then the\n/// translation.\n/// If none are provided, the transform is the identity.\n///\n/// When a node is targeted for animation (referenced by an\n/// animation.channel.target), `matrix` **MUST NOT** be present.\nclass Node extends GltfNode {\n  /// The reference to the camera referenced by this node.\n  final GltfRef<Camera>? camera;\n\n  /// The references to this node's children.\n  final List<GltfRef<Node>> children;\n\n  /// The reference to skeleton nodes.\n  ///\n  /// Each node defines a subtree, which has a `jointName` of the corresponding\n  /// element in the referenced `skin.jointNames`.\n  final List<GltfRef<Node>> skeletons;\n\n  /// The reference to the skin referenced by this node.\n  /// When a skin is referenced by a node within a scene, all joints used by\n  /// the skin **MUST** belong to the same scene.\n  /// When defined, `mesh` **MUST** also be defined.\n  final GltfRef<Skin>? skin;\n\n  /// Name used when this node is a joint in a skin.\n  final String? jointName;\n\n  /// A floating-point 4x4 transformation matrix stored in column-major order.\n  final Matrix4? matrix;\n\n  /// The reference to the mesh in this node.\n  final GltfRef<Mesh>? mesh;\n\n  /// The node's unit quaternion rotation in the order (x, y, z, w),\n  /// where w is the scalar.\n  final Quaternion? rotation;\n\n  /// The node's non-uniform scale, given as the scaling factors along\n  /// the x, y, and z axes.\n  final Vector3? scale;\n\n  /// The node's translation along the x, y, and z axes.\".\n  final Vector3? translation;\n\n  /// The weights of the instantiated morph target.\n  /// The number of array elements **MUST** match the number of morph targets\n  /// of the referenced mesh.\n  /// When defined, `mesh` **MUST** also be defined.\n  final List<double>? weights;\n\n  final String? name;\n\n  Node({\n    required super.root,\n    required this.camera,\n    required this.children,\n    required this.skeletons,\n    required this.skin,\n    required this.jointName,\n    required this.matrix,\n    required this.mesh,\n    required this.rotation,\n    required this.scale,\n    required this.translation,\n    required this.weights,\n    required this.name,\n  });\n\n  Node.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        camera: Parser.ref(root, map, 'camera'),\n        children: Parser.refList(root, map, 'children') ?? [],\n        skeletons: Parser.refList(root, map, 'skeletons') ?? [],\n        skin: Parser.ref(root, map, 'skin'),\n        jointName: Parser.string(map, 'jointName'),\n        matrix: Parser.matrix4(root, map, 'matrix'),\n        mesh: Parser.ref(root, map, 'mesh'),\n        rotation: Parser.quaternion(root, map, 'rotation'),\n        scale: Parser.vector3(root, map, 'scale'),\n        translation: Parser.vector3(root, map, 'translation'),\n        weights: Parser.floatList(map, 'weights'),\n        name: Parser.string(map, 'name'),\n      );\n\n  Matrix4? get _trs {\n    if (translation == null && rotation == null && scale == null) {\n      return null;\n    }\n    return Matrix4.compose(\n      translation ?? Vector3.zero(),\n      rotation ?? Quaternion.identity(),\n      scale ?? Vector3.all(1.0),\n    );\n  }\n\n  Matrix4 get transform => matrix ?? _trs ?? Matrix4.identity();\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/normal_texture_info.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/gltf/texture_info.dart';\n\n/// Material Normal Texture Info.\nclass NormalTextureInfo extends TextureInfo {\n  /// The scalar parameter applied to each normal vector of the texture.\n  ///\n  /// This value scales the normal vector in X and Y directions using the\n  /// formula:\n  ///\n  /// ```\n  ///   scaledNormal =  normalize((<sampled normal texture value> * 2.0 - 1.0)\n  ///                       * vec3(<normal scale>, <normal scale>, 1.0))\n  /// ```\n  final double scale;\n\n  NormalTextureInfo({\n    required super.root,\n    required super.index,\n    required super.texCoord,\n    required this.scale,\n  });\n\n  NormalTextureInfo.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        index: Parser.ref(root, map, 'index')!,\n        texCoord: Parser.integer(map, 'texCoord'),\n        scale: Parser.float(map, 'scale') ?? 1.0,\n      );\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/occlusion_texture_info.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/gltf/texture_info.dart';\n\n/// The occlusion texture.\nclass OcclusionTextureInfo extends TextureInfo {\n  /// A scalar parameter controlling the amount of occlusion applied.\n  ///\n  /// A value of `0.0` means no occlusion. A value of `1.0` means full\n  /// occlusion.\n  ///\n  /// This value affects the final occlusion value as:\n  /// ```'\n  ///   1.0 + strength * (<sampled occlusion texture value> - 1.0)\n  /// ```\n  final double strength;\n\n  OcclusionTextureInfo({\n    required super.root,\n    required super.index,\n    required super.texCoord,\n    required this.strength,\n  });\n\n  OcclusionTextureInfo.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        index: Parser.ref(root, map, 'index')!,\n        texCoord: Parser.integer(map, 'texCoord'),\n        strength: Parser.float(map, 'strength') ?? 1.0,\n      );\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/pbr_metallic_roughness.dart",
    "content": "import 'dart:ui' show Color;\n\nimport 'package:flame_3d/core.dart';\nimport 'package:flame_3d/resources.dart' as flame_3d;\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/gltf/texture_info.dart';\n\n/// A set of parameter values that are used to define the metallic-roughness\n/// material model from Physically-Based Rendering (PBR) methodology.\nclass PBRMetallicRoughness extends GltfNode {\n  /// The factors for the base color of the material.\n  /// This value defines linear multipliers for the sampled texels of the base\n  /// color texture.\n  final Vector4? baseColorFactor;\n\n  /// The base color texture.\n  ///\n  /// The first three components (RGB) **MUST** be encoded with the sRGB\n  /// transfer function.\n  /// They specify the base color of the material. If the fourth component\n  /// (A) is present, it represents the linear alpha coverage of the material.\n  /// Otherwise, the alpha coverage is equal to `1.0`.\n  ///\n  /// The `material.alphaMode` property specifies how alpha is interpreted.\n  /// The stored texels **MUST NOT** be premultiplied. When undefined,\n  /// the texture **MUST** be sampled as having `1.0` in all components.\"\n  final TextureInfo? baseColorTexture;\n\n  /// The factor for the metalness of the material.\n  /// This value defines a linear multiplier for the sampled metalness values\n  /// of the metallic-roughness texture.\n  /// Goes from [0, 1]. Default value is 1.\n  final double metallicFactor;\n\n  /// The factor for the roughness of the material.\n  /// This value defines a linear multiplier for the sampled roughness values\n  /// of the metallic-roughness texture.\n  /// Goes from [0, 1]. Default value is 1.\n  final double roughnessFactor;\n\n  /// The metallic-roughness texture.\n  /// The metalness values are sampled from the B channel.\n  /// The roughness values are sampled from the G channel.\n  /// These values **MUST** be encoded with a linear transfer function.\n  /// If other channels are present (R or A), they **MUST** be ignored for\n  /// metallic-roughness calculations.\n  /// When undefined, the texture **MUST** be sampled as having `1.0` in\n  /// G and B components.\"\n  final TextureInfo? metallicRoughnessTexture;\n\n  PBRMetallicRoughness({\n    required super.root,\n    required this.baseColorFactor,\n    required this.baseColorTexture,\n    required this.metallicFactor,\n    required this.roughnessFactor,\n    required this.metallicRoughnessTexture,\n  });\n\n  PBRMetallicRoughness.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        baseColorFactor: Parser.vector4(root, map, 'baseColorFactor'),\n        baseColorTexture: Parser.object(\n          root,\n          map,\n          'baseColorTexture',\n          TextureInfo.parse,\n        ),\n        metallicFactor: Parser.float(map, 'metallicFactor') ?? 1.0,\n        roughnessFactor: Parser.float(map, 'roughnessFactor') ?? 1.0,\n        metallicRoughnessTexture: Parser.object(\n          root,\n          map,\n          'metallicRoughnessTexture',\n          TextureInfo.parse,\n        ),\n      );\n\n  flame_3d.SpatialMaterial? toFlameSpatialMaterial() {\n    return flame_3d.SpatialMaterial(\n      albedoColor: baseColorFactor?.toColor() ?? const Color(0xFFFFFFFF),\n      albedoTexture: baseColorTexture?.toFlameTexture(),\n      metallic: metallicFactor,\n      roughness: roughnessFactor,\n    );\n  }\n}\n\nextension _ToColor on Vector4 {\n  Color toColor() {\n    return Color.fromARGB(\n      (a * 255).toInt(),\n      (r * 255).toInt(),\n      (g * 255).toInt(),\n      (b * 255).toInt(),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/primitive.dart",
    "content": "import 'dart:math';\nimport 'dart:ui' show Color;\n\nimport 'package:flame_3d/core.dart';\nimport 'package:flame_3d/resources.dart' as flame_3d;\nimport 'package:flame_3d/src/parser/gltf/accessor.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_ref.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/gltf/material.dart';\nimport 'package:flame_3d/src/parser/gltf/morph_target.dart';\nimport 'package:flame_3d/src/parser/gltf/primitive_mode.dart';\n\n// cSpell:ignore TEXCOORD\n// (used in GLTF as the key for texture coordinate attributes)\n\n/// Geometry to be rendered with the given material.\nclass Primitive extends GltfNode {\n  /// The topology type of primitives to render.\n  final PrimitiveMode mode;\n\n  /// A plain JSON object, where each key corresponds to a mesh attribute\n  /// semantic and each value is the index of the accessor containing\n  /// attribute's data.\n  ///\n  /// Typical keys include: `POSITION`, `NORMAL`, `TEXCOORD_0`, etc.\n  final Map<String, int> attributes;\n\n  /// The reference to the accessor that contains the vertex indices.\n  /// When this is undefined, the primitive defines non-indexed geometry.\n  /// When defined, the accessor **MUST** have `SCALAR` type and an unsigned\n  /// integer component type.\n  final GltfRef<IntAccessor>? indices;\n\n  /// The reference to the material to apply to this primitive when rendering.\n  final GltfRef<Material>? material;\n\n  /// An array of morph targets.\n  final List<MorphTarget> targets;\n\n  Primitive({\n    required super.root,\n    required this.mode,\n    required this.attributes,\n    required this.indices,\n    required this.material,\n    required this.targets,\n  });\n\n  GltfRef<Vector3Accessor>? get positions => _accessor('POSITION');\n  GltfRef<Vector3Accessor>? get normals => _accessor('NORMAL');\n  GltfRef<Vector2Accessor>? get texCoords => _accessor('TEXCOORD_0');\n  GltfRef<Vector4Accessor>? get joints => _accessor('JOINTS_0');\n  GltfRef<Vector4Accessor>? get weights => _accessor('WEIGHTS_0');\n\n  GltfRef<T>? _accessor<T extends GltfNode>(String key) {\n    final joints = attributes[key];\n    if (joints == null) {\n      return null;\n    }\n    return GltfRef<T>(\n      root: root,\n      index: joints,\n    );\n  }\n\n  (List<flame_3d.Vertex>, List<int>) toFlameVertices(\n    JointData jointData,\n    Matrix4 transform,\n  ) {\n    assert(mode == PrimitiveMode.triangles);\n\n    final positions = this.positions!.get().typedData();\n    final indices =\n        this.indices?.get().typedData() ??\n        // for non-indexed geometries\n        List.generate(positions.length, (i) => i);\n    final texCoords = this.texCoords?.get().typedData();\n    final normals =\n        this.normals?.get().typedData() ??\n        flame_3d.Vertex.calculateVertexNormals(positions, indices);\n\n    Vector3? process(Vector3? v) {\n      if (v == null) {\n        return null;\n      }\n      return transform.transform3(v.clone());\n    }\n\n    final maxIndex = indices.reduce(max);\n    final vertices = <flame_3d.Vertex>[];\n    for (var i = 0; i <= maxIndex; i++) {\n      vertices.add(\n        flame_3d.Vertex(\n          position: process(positions[i])!,\n          texCoord: texCoords?.elementAtOrNull(i) ?? Vector2.zero(),\n          normal: process(normals.elementAtOrNull(i)),\n          joints: jointData.localizedJoint(i),\n          weights: jointData.weight(i),\n        ),\n      );\n    }\n\n    return (vertices, indices);\n  }\n\n  flame_3d.Surface toFlameSurface([Matrix4? transform]) {\n    final jointData = computeJointData();\n\n    final (vertices, indices) = toFlameVertices(\n      jointData,\n      transform ?? Matrix4.identity(),\n    );\n\n    return flame_3d.Surface(\n      vertices: vertices.toList(),\n      indices: indices,\n      jointMap: jointData.jointMap,\n      material:\n          material?.get().toFlameMaterial() ??\n          flame_3d.SpatialMaterial(\n            albedoColor: const Color(0xFFFF00FF),\n          ),\n    );\n  }\n\n  JointData computeJointData() {\n    final weights = this.weights?.get().typedData() ?? [];\n    // this are the indexes (0, 1, 2, 3) that have any relevance at all\n    final relevantIndexes = weights.expand((w) {\n      return w.storage.indexed\n          .where((e) => e.$2 > 0.0)\n          .map((e) => e.$1)\n          .toSet();\n    }).toSet();\n\n    final joints = this.joints?.get().typedData() ?? [];\n    final globalToLocalJointMap = Map.fromEntries(\n      joints\n          .expand((e) {\n            return e.storage.indexed\n                .where((e) => relevantIndexes.contains(e.$1))\n                .map((e) => e.$2);\n          })\n          .toSet()\n          .indexed\n          .map((e) => MapEntry(e.$2.toInt(), e.$1)),\n    );\n\n    final localizedJoints = joints.map((joint) {\n      return Vector4.array(\n        joint.storage.map((e) {\n          final index = e.toInt();\n          if (index != e) {\n            throw StateError('Invalid joint index: $e');\n          }\n          // TODO(luan): remove this logic entirely once we support arrays\n          if (e == 0.0 && globalToLocalJointMap[index] == null) {\n            // this must be a 0 weight value that just happens to be id = 0\n            return 0.0;\n          }\n          return globalToLocalJointMap[index]!.toDouble();\n        }).toList(),\n      );\n    }).toList();\n\n    return JointData(\n      weights: weights,\n      localizedJoints: localizedJoints,\n      jointMap: globalToLocalJointMap,\n    );\n  }\n\n  Primitive.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        mode: PrimitiveMode.parse(map, 'mode') ?? PrimitiveMode.triangles,\n        attributes: Parser.mapInt(map, 'attributes') ?? {},\n        indices: Parser.ref(root, map, 'indices'),\n        material: Parser.ref(root, map, 'material'),\n        targets:\n            Parser.objectList<MorphTarget>(\n              root,\n              map,\n              'targets',\n              MorphTarget.parse,\n            ) ??\n            [],\n      );\n}\n\nclass JointData {\n  final List<Vector4> weights;\n  final List<Vector4> localizedJoints;\n  final Map<int, int> jointMap;\n\n  JointData({\n    required this.weights,\n    required this.localizedJoints,\n    required this.jointMap,\n  });\n\n  Vector4 weight(int index) {\n    return weights.elementAtOrNull(index) ?? Vector4.zero();\n  }\n\n  Vector4 localizedJoint(int index) {\n    return localizedJoints.elementAtOrNull(index) ?? Vector4.zero();\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/primitive_mode.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\n\n/// The topology type of primitives to render.\nenum PrimitiveMode {\n  points(0),\n  lines(1),\n  lineLoop(2),\n  lineStrip(3),\n  triangles(4),\n  triangleStrip(5),\n  triangleFan(6)\n  ;\n\n  final int value;\n\n  const PrimitiveMode(this.value);\n\n  static PrimitiveMode valueOf(int value) {\n    return values.firstWhere((e) => e.value == value);\n  }\n\n  static PrimitiveMode? parse(Map<String, Object?> map, String key) {\n    return Parser.integerEnum(map, key, valueOf);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/sampler.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/gltf/mag_filter.dart';\nimport 'package:flame_3d/src/parser/gltf/min_filter.dart';\nimport 'package:flame_3d/src/parser/gltf/wrap_mode.dart';\n\n/// Texture sampler properties for filtering and wrapping modes.\nclass Sampler extends GltfNode {\n  /// Magnification filter.\n  final MagFilter magFilter;\n\n  /// Minification filter.\n  final MinFilter minFilter;\n\n  /// The wrap mode for the s coordinate.\n  final WrapMode wrapS;\n\n  /// The wrap mode for the t coordinate.\n  final WrapMode wrapT;\n\n  Sampler({\n    required super.root,\n    required this.magFilter,\n    required this.minFilter,\n    required this.wrapS,\n    required this.wrapT,\n  });\n\n  Sampler.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        magFilter: MagFilter.parse(map, 'magFilter') ?? MagFilter.linear,\n        minFilter:\n            MinFilter.parse(map, 'minFilter') ?? MinFilter.nearestMipmapLinear,\n        wrapS: WrapMode.parse(map, 'wrapS') ?? WrapMode.repeat,\n        wrapT: WrapMode.parse(map, 'wrapT') ?? WrapMode.repeat,\n      );\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/scene.dart",
    "content": "import 'package:flame_3d/core.dart';\nimport 'package:flame_3d/model.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_ref.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/gltf/node.dart';\n\n/// The root nodes of a scene.\nclass Scene extends GltfNode {\n  /// The references to each root node.\n  final List<GltfRef<Node>> nodes;\n\n  Scene({\n    required super.root,\n    required this.nodes,\n  });\n\n  Scene.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        nodes: Parser.refList<Node>(root, map, 'nodes')!,\n      );\n\n  Map<int, ModelNode> toFlameNodes() {\n    final nodes = <int, ModelNode>{};\n    for (final nodeRef in this.nodes) {\n      _processNode(\n        nodes: nodes,\n        parent: null,\n        nodeRef: nodeRef,\n      );\n    }\n    return nodes;\n  }\n\n  void _processNode({\n    required Map<int, ModelNode> nodes,\n    required ModelNode? parent,\n    required GltfRef<Node> nodeRef,\n  }) {\n    final gltfNode = nodeRef.get();\n\n    final mesh = gltfNode.mesh?.get().toFlameMesh();\n\n    final skin = gltfNode.skin?.get();\n    final inverseBindMatrices = skin?.inverseBindMatrices?.get().typedData();\n    final joints = skin?.joints ?? [];\n\n    final modelJoints = <int, ModelJoint>{};\n    for (final (i, joint) in joints.indexed) {\n      modelJoints[i] = ModelJoint(\n        nodeIndex: joint.index,\n        inverseBindMatrix:\n            inverseBindMatrices?.elementAt(i) ?? Matrix4.identity(),\n      );\n    }\n\n    final node = ModelNode(\n      name: gltfNode.name,\n      nodeIndex: nodeRef.index,\n      parentNodeIndex: parent?.nodeIndex,\n      transform: gltfNode.transform,\n      mesh: mesh,\n      joints: modelJoints,\n    );\n    nodes[nodeRef.index] = node;\n\n    for (final child in gltfNode.children) {\n      _processNode(\n        nodes: nodes,\n        parent: node,\n        nodeRef: child,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/skin.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/accessor.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_ref.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/gltf/node.dart';\n\n/// Joints and matrices defining a skin.\nclass Skin extends GltfNode {\n  /// The reference to the accessor containing the floating-point 4x4\n  /// inverse-bind matrices.\n  /// Its `accessor.count` property **MUST** be greater than or equal to the\n  /// number of elements of the `joints` array.\n  /// When undefined, each matrix is a 4x4 identity matrix.\n  final GltfRef<Matrix4Accessor>? inverseBindMatrices;\n\n  /// The reference to the node used as a skeleton root.\n  /// The node **MUST** be the closest common root of the joints hierarchy or a\n  /// direct or indirect parent node of the closest common root.\n  final GltfRef<Node>? skeleton;\n\n  /// Indices of skeleton nodes, used as joints in this skin.\n  final List<GltfRef<Node>> joints;\n\n  Skin({\n    required super.root,\n    required this.inverseBindMatrices,\n    required this.skeleton,\n    required this.joints,\n  });\n\n  Skin.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        inverseBindMatrices: Parser.ref(root, map, 'inverseBindMatrices'),\n        skeleton: Parser.ref(root, map, 'skeleton'),\n        joints: Parser.refList<Node>(root, map, 'joints') ?? [],\n      );\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/sparse_accessor.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/gltf/sparse_accessor_indices.dart';\nimport 'package:flame_3d/src/parser/gltf/sparse_accessor_values.dart';\n\n/// Sparse storage of accessor values that deviate from their initialization\n/// value.\nclass SparseAccessor extends GltfNode {\n  /// Number of deviating accessor values stored in the sparse array.\n  final int count;\n\n  /// An object pointing to a buffer view containing the indices of deviating\n  /// accessor values.\n  /// The number of indices is equal to `count`.\n  /// Indices **MUST** strictly increase.\n  final SparseAccessorIndices indices;\n\n  /// An object pointing to a buffer view containing the deviating\n  /// accessor values.\n  final SparseAccessorValues values;\n\n  SparseAccessor({\n    required super.root,\n    required this.count,\n    required this.indices,\n    required this.values,\n  });\n\n  SparseAccessor.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        count: Parser.integer(map, 'count') ?? 0,\n        indices: Parser.object(\n          root,\n          map,\n          'indices',\n          SparseAccessorIndices.parse,\n        )!,\n        values: Parser.object(\n          root,\n          map,\n          'values',\n          SparseAccessorValues.parse,\n        )!,\n      );\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/sparse_accessor_indices.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/buffer_view.dart';\nimport 'package:flame_3d/src/parser/gltf/component_type.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_ref.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\n\n/// An object pointing to a buffer view containing the indices of deviating\n/// accessor values.\n/// The number of indices is equal to `accessor.sparse.count`. Indices **MUST**\n/// strictly increase.\nclass SparseAccessorIndices extends GltfNode {\n  /// The reference to the buffer view with sparse indices.\n  /// The referenced buffer view **MUST NOT** have its `target` or `byteStride`\n  /// properties defined.\n  /// The buffer view and the optional `byteOffset` **MUST** be aligned to the\n  /// `componentType` byte length.\"\n  final GltfRef<BufferView> bufferView;\n\n  /// The offset relative to the start of the buffer view in bytes.\n  final int byteOffset;\n\n  /// The indices data type.\n  final ComponentType componentType;\n\n  SparseAccessorIndices({\n    required super.root,\n    required this.bufferView,\n    required this.byteOffset,\n    required this.componentType,\n  });\n\n  SparseAccessorIndices.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        bufferView: Parser.ref(root, map, 'bufferView')!,\n        byteOffset: Parser.integer(map, 'byteOffset') ?? 0,\n        componentType: ComponentType.parse(map, 'componentType')!,\n      );\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/sparse_accessor_values.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/buffer_view.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_ref.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\n\n/// An object pointing to a buffer view containing the deviating accessor\n/// values.\n/// The number of elements is equal to `accessor.sparse.count` times number of\n/// components.\n/// The elements have the same component type as the base accessor.\n/// The elements are tightly packed. Data **MUST** be aligned following the\n/// same rules as the base accessor.\nclass SparseAccessorValues extends GltfNode {\n  /// The index of the bufferView with sparse values.\n  /// The referenced buffer view **MUST NOT** have its `target` or `byteStride`\n  /// properties defined.\n  final GltfRef<BufferView> bufferView;\n\n  /// The offset relative to the start of the bufferView in bytes.\n  final int byteOffset;\n\n  SparseAccessorValues({\n    required super.root,\n    required this.bufferView,\n    required this.byteOffset,\n  });\n\n  SparseAccessorValues.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        bufferView: Parser.ref(root, map, 'bufferView')!,\n        byteOffset: Parser.integer(map, 'byteOffset') ?? 0,\n      );\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/texture.dart",
    "content": "import 'package:flame_3d/resources.dart' as flame_3d;\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_ref.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/gltf/image.dart';\nimport 'package:flame_3d/src/parser/gltf/sampler.dart';\nimport 'package:flame_3d/src/parser/gltf/texture_format.dart';\nimport 'package:flame_3d/src/parser/gltf/texture_target.dart';\nimport 'package:flame_3d/src/parser/gltf/texture_type.dart';\n\nclass Texture extends GltfNode {\n  /// The texture's format. Defaults to `6408` (RGBA).\n  final TextureFormat format;\n\n  /// The texture's internal format. Defaults to `6408` (RGBA).\n  final TextureFormat internalFormat;\n\n  /// The reference to the sampler used by this texture.\n  final GltfRef<Sampler>? sampler;\n\n  /// The reference to the image used by this texture.\n  final GltfRef<Image> source;\n\n  /// The target that the WebGL texture should be bound to.\n  ///\n  /// Valid values correspond to WebGL enums: `3553` (TEXTURE_2D).\n  final TextureTarget target;\n\n  /// Texel datatype. Defaults to `5121` (UNSIGNED_BYTE).\n  final TextureType type;\n\n  Texture({\n    required super.root,\n    required this.format,\n    required this.internalFormat,\n    required this.sampler,\n    required this.source,\n    required this.target,\n    required this.type,\n  });\n\n  Texture.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        format: TextureFormat.parse(map, 'format') ?? TextureFormat.rgba,\n        internalFormat:\n            TextureFormat.parse(map, 'internalFormat') ?? TextureFormat.rgba,\n        sampler: Parser.ref<Sampler>(root, map, 'sampler'),\n        source: Parser.ref<Image>(root, map, 'source')!,\n        target: TextureTarget.parse(map, 'target') ?? TextureTarget.texture2d,\n        type: TextureType.parse(map, 'type') ?? TextureType.unsignedByte,\n      );\n\n  flame_3d.Texture toFlameTexture() {\n    // TODO(luan): consider other parameters, such as sampler, type, etc\n    return source.get().toFlameTexture();\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/texture_format.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\n\n/// Texture formats; values correspond to WebGL enums.\nenum TextureFormat {\n  alpha('ALPHA', 6406),\n  rgb('RGB', 6407),\n  rgba('RGBA', 6408),\n  luminance('LUMINANCE', 6409),\n  luminanceAlpha('LUMINANCE_ALPHA', 6410)\n  ;\n\n  final String name;\n  final int value;\n\n  const TextureFormat(this.name, this.value);\n\n  static TextureFormat valueOf(String name) {\n    return values.firstWhere((e) => e.name == name);\n  }\n\n  static TextureFormat? parse(Map<String, Object?> map, String key) {\n    return Parser.stringEnum(map, key, valueOf);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/texture_info.dart",
    "content": "import 'package:flame_3d/resources.dart' as flame_3d;\nimport 'package:flame_3d/src/parser/gltf/gltf_node.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_ref.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/gltf/texture.dart';\n\n// cSpell:ignore TEXCOORD\n// (used in GLTF as the key for texture coordinate attributes)\n\n/// Reference to a texture.\nclass TextureInfo extends GltfNode {\n  /// The reference to the texture.\n  final GltfRef<Texture> index;\n\n  /// This integer value is used to construct a string in the format\n  /// `TEXCOORD_<set index>`, which is a reference to a key in\n  /// `mesh.primitives.attributes`\n  /// (e.g. a value of `0` corresponds to `TEXCOORD_0`).\n  ///\n  /// A mesh primitive **MUST** have the corresponding texture coordinate\n  /// attributes for the material to be applicable to it.\n  final int? texCoord;\n\n  TextureInfo({\n    required super.root,\n    required this.index,\n    this.texCoord,\n  });\n\n  TextureInfo.parse(\n    GltfRoot root,\n    Map<String, Object?> map,\n  ) : this(\n        root: root,\n        index: Parser.ref(root, map, 'index')!,\n        texCoord: Parser.integer(map, 'texCoord'),\n      );\n\n  flame_3d.Texture toFlameTexture() {\n    return index.get().toFlameTexture();\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/texture_target.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\n\n/// Texture targets; values correspond to WebGL enums.\nenum TextureTarget {\n  texture2d('TEXTURE_2D', 3553)\n  ;\n\n  final String name;\n  final int value;\n\n  const TextureTarget(this.name, this.value);\n\n  static TextureTarget valueOf(String name) {\n    return values.firstWhere((e) => e.name == name);\n  }\n\n  static TextureTarget? parse(Map<String, Object?> map, String key) {\n    return Parser.stringEnum(map, key, valueOf);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/texture_type.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\n\n/// Texture formats; values correspond to WebGL enums.\nenum TextureType {\n  unsignedByte('UNSIGNED_BYTE', 5121),\n  unsignedShort565('UNSIGNED_SHORT_5_6_5', 33635),\n  unsignedShort4444('UNSIGNED_SHORT_4_4_4_4', 32819),\n  unsignedShort5551('UNSIGNED_SHORT_5_5_5_1', 32820)\n  ;\n\n  final String name;\n  final int value;\n\n  const TextureType(this.name, this.value);\n\n  static TextureType valueOf(int value) {\n    return values.firstWhere((e) => e.value == value);\n  }\n\n  static TextureType? parse(Map<String, Object?> map, String key) {\n    return Parser.integerEnum(map, key, valueOf);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf/wrap_mode.dart",
    "content": "import 'package:flame_3d/src/parser/gltf/gltf_node.dart';\n\n/// Wrapping mode. Valid values correspond to WebGL enums.\nenum WrapMode {\n  clampToEdge('CLAMP_TO_EDGE', 33071),\n  mirroredRepeat('MIRRORED_REPEAT', 33648),\n  repeat('REPEAT', 10497)\n  ;\n\n  final String name;\n  final int value;\n\n  const WrapMode(this.name, this.value);\n\n  static WrapMode valueOf(int value) {\n    return values.firstWhere((e) => e.value == value);\n  }\n\n  static WrapMode? parse(Map<String, Object?> map, String key) {\n    return Parser.integerEnum(map, key, valueOf);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/gltf_parser.dart",
    "content": "import 'package:flame/flame.dart';\nimport 'package:flame_3d/src/model/model.dart';\nimport 'package:flame_3d/src/parser/gltf/gltf_root.dart';\nimport 'package:flame_3d/src/parser/model_parser.dart';\n\n/// Parses GLB and GLTF file formats as per specified by:\n/// https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.pdf\nclass GltfParser extends ModelParser {\n  @override\n  Future<Model> parseModel(String filePath) async {\n    final root = await parseGltf(filePath);\n    return root.toFlameModel();\n  }\n\n  Future<GltfRoot> parseGltf(String filePath) async {\n    final content = await Flame.assets.readJson(filePath);\n    return GltfRoot.from(\n      prefix: ModelParser.prefix(filePath),\n      json: content,\n      chunks: [],\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/model_parser.dart",
    "content": "import 'package:flame_3d/src/model/model.dart';\nimport 'package:flame_3d/src/parser/glb_parser.dart';\nimport 'package:flame_3d/src/parser/gltf_parser.dart';\nimport 'package:flame_3d/src/parser/obj_parser.dart';\n\nabstract class ModelParser {\n  Future<Model> parseModel(String filePath);\n\n  static Future<Model> parse(String filePath) async {\n    final parser = _getParser(filePath);\n    return parser.parseModel(filePath);\n  }\n\n  static ModelParser _getParser(String filePath) {\n    if (filePath.endsWith('.gltf')) {\n      return gltf;\n    } else if (filePath.endsWith('.glb')) {\n      return glb;\n    } else if (filePath.endsWith('.obj')) {\n      return obj;\n    } else {\n      throw ArgumentError('Unsupported file type: $filePath');\n    }\n  }\n\n  static GltfParser gltf = GltfParser();\n  static GlbParser glb = GlbParser();\n  static ObjParser obj = ObjParser();\n\n  static String prefix(String filePath) {\n    return filePath.substring(0, filePath.lastIndexOf('/') + 1);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/obj/surface_tool.dart",
    "content": "// ignore_for_file: use_setters_to_change_properties\n\nimport 'dart:collection';\nimport 'dart:ui';\n\nimport 'package:flame_3d/game.dart';\nimport 'package:flame_3d/resources.dart';\n\n// TODO(wolfenrain): heavily inspired by the Godot one, maybe this should be\n// part of the core package\nclass SurfaceTool {\n  final List<Vertex> _vertices = [];\n  final List<int> _indices = [];\n\n  Color _lastColor = const Color(0xFFFFFFFF);\n  Vector3? _lastNormal;\n  final Vector2 _lastTexCoord = Vector2.zero();\n  Material _lastMaterial = SpatialMaterial();\n\n  void setColor(Color color) => _lastColor = color;\n\n  void setNormal(Vector3? normal) {\n    if (normal == null) {\n      _lastNormal = null;\n    } else {\n      (_lastNormal ??= Vector3.zero()).setFrom(normal);\n    }\n  }\n\n  void setTexCoord(Vector2 texCoord) => _lastTexCoord.setFrom(texCoord);\n\n  void setMaterial(Material material) => _lastMaterial = material;\n\n  void addTriangleFan(\n    List<Vector3> vertices,\n    List<Vector2> texCoords, [\n    List<Vector3> normals = const [],\n    List<Color> colors = const [],\n    // TODO(wolfenrain): support tangents\n  ]) {\n    assert(vertices.length == 3, 'Expected a length of 3 for vertices');\n\n    void addPoint(int n) {\n      if (texCoords.length > n) {\n        setTexCoord(texCoords[n]);\n      }\n      if (colors.length > n) {\n        setColor(colors[n]);\n      }\n      if (normals.length > n) {\n        setNormal(normals[n]);\n      } else {\n        setNormal(null);\n      }\n      // TODO(wolfenrain): tangents\n      addVertex(vertices[n]);\n    }\n\n    for (var i = 0; i < vertices.length - 2; i++) {\n      addPoint(0);\n      addPoint(i + 1);\n      addPoint(i + 2);\n    }\n  }\n\n  void addVertex(Vector3 position) {\n    final vertex = Vertex(\n      position: position,\n      texCoord: _lastTexCoord,\n      normal: _lastNormal,\n      color: _lastColor,\n    );\n\n    _vertices.add(vertex);\n  }\n\n  void addIndex(int index) {\n    _indices.add(index);\n  }\n\n  void index() {\n    if (_indices.isNotEmpty) {\n      return;\n    }\n\n    final indexMap = HashMap<Vertex, int>();\n    final oldVertices = List<Vertex>.from(_vertices);\n    _vertices.clear();\n\n    for (final vertex in oldVertices) {\n      var index = indexMap[vertex];\n      if (index == null) {\n        index = indexMap.length;\n        _vertices.add(vertex);\n        indexMap[vertex] = index;\n      }\n      _indices.add(index);\n    }\n  }\n\n  Mesh apply([Mesh? mesh]) {\n    index();\n    mesh ??= Mesh();\n    return mesh..addSurface(\n      Surface(\n        vertices: _vertices,\n        indices: _indices,\n        material: _lastMaterial,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/parser/obj_parser.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/flame.dart';\nimport 'package:flame_3d/game.dart';\nimport 'package:flame_3d/resources.dart';\nimport 'package:flame_3d/src/model/model.dart';\nimport 'package:flame_3d/src/parser/model_parser.dart';\nimport 'package:flame_3d/src/parser/obj/surface_tool.dart';\n\n// These are keywords used in the OBJ syntax.\n// cSpell:ignore usemtl newmtl mtllib\n\nclass ObjParser extends ModelParser {\n  @override\n  Future<Model> parseModel(String filePath) async {\n    final mesh = await parseMesh(filePath);\n    return Model.simple(mesh: mesh);\n  }\n\n  Future<Mesh> parseMesh(String filePath, {Mesh? applyTo}) async {\n    final vertices = <Vector3>[];\n    final normals = <Vector3>[];\n    final texCoords = <Vector2>[];\n    final faces = <String, List<Face>>{};\n\n    final lines = (await Flame.assets.readFile(filePath)).split('\\n');\n\n    // if not material is specified, this will be used, and the default\n    // material will be applied.\n    var matName = '__default__';\n\n    final materials = <String, SpatialMaterial>{};\n    for (final line in lines) {\n      final [type, ...parts] = line.split(' ');\n\n      switch (type) {\n        // Comment\n        case '#':\n          continue;\n        // Vertex\n        case 'v':\n          vertices.add(Vector3.array(parts.map(double.parse).toList()));\n        // Normal\n        case 'vn':\n          normals.add(Vector3.array(parts.map(double.parse).toList()));\n        // UV\n        case 'vt':\n          texCoords.add(Vector2.array(parts.map(double.parse).toList()));\n        // Face\n        case 'f':\n          if (parts.length == 3) {\n            // Single triangle\n\n            final face = Face.empty();\n            for (final value in parts) {\n              // format is <vertex/texture/normal>, with vertex and normal being optional\n              final indices = value.split('/');\n              face.vertex.add(int.parse(indices[0]) - 1);\n              if (indices.length > 1) {\n                if (indices[1].isNotEmpty) {\n                  face.texCoord.add(int.parse(indices[1]) - 1);\n                }\n                if (indices.length > 2) {\n                  face.normal.add(int.parse(indices[2]) - 1);\n                }\n              }\n            }\n            (faces[matName] ??= []).add(face);\n          } else if (parts.length > 4) {\n            // TODO(luan): implement triangulation\n            throw UnimplementedError(\n              'Triangulation not implemented for ObjParser',\n            );\n          }\n        // Material library\n        case 'mtllib':\n          final relative = (filePath.split('/')..removeLast()).join('/');\n          materials.addAll(\n            await _parseMaterial('$relative/${parts[0]}'.trim()),\n          );\n        // Material\n        case 'usemtl':\n          matName = parts[0].trim();\n\n          if (!faces.containsKey(matName)) {\n            if (!materials.containsKey(matName)) {\n              throw AssertionError('Material not found: $matName');\n            }\n            faces[matName] = [];\n          }\n      }\n    }\n\n    var mesh = applyTo ?? Mesh();\n    for (final materialGroup in faces.keys) {\n      final material = materials[materialGroup] ?? Material.defaultMaterial;\n      final surface = SurfaceTool()..setMaterial(material);\n\n      for (final face in faces[materialGroup]!) {\n        if (face.vertex.length == 3) {\n          // Vertices\n          final fanVertices = [\n            vertices[face.vertex[0]],\n            vertices[face.vertex[1]],\n            vertices[face.vertex[2]],\n          ];\n\n          // Tex coords\n          final fanTexCoords = <Vector2>[];\n          if (face.texCoord.isNotEmpty) {\n            for (final k in [0, 2, 1]) {\n              final f = face.texCoord[k];\n              if (f > -1) {\n                final uv = texCoords[f];\n                fanTexCoords.add(uv);\n              }\n            }\n          }\n\n          // Normals\n          final fanNormals = [\n            if (face.normal.isNotEmpty) ...[\n              normals[face.normal[0]],\n              normals[face.normal[1]],\n              normals[face.normal[2]],\n            ],\n          ];\n\n          surface.addTriangleFan(fanVertices, fanTexCoords, fanNormals);\n        }\n      }\n      mesh = surface.apply(mesh);\n    }\n    return mesh;\n  }\n\n  Future<Map<String, SpatialMaterial>> _parseMaterial(\n    String filePath,\n  ) async {\n    final lines = (await Flame.assets.readFile(filePath)).split('\\n');\n\n    final materials = <String, SpatialMaterial>{};\n    SpatialMaterial? currentMat;\n    for (final line in lines) {\n      final [type, ...parts] = line.split(' ');\n      switch (type) {\n        // Comment\n        case '#':\n          continue;\n        // Creating a new material\n        case 'newmtl':\n          currentMat = SpatialMaterial(\n            albedoTexture: ColorTexture(const Color(0xFFFFFFFF)),\n          );\n          materials[parts[0].trim()] = currentMat;\n        // Diffuse color\n        case 'Kd':\n          currentMat?.albedoColor = Color.from(\n            alpha: 1.0,\n            red: double.parse(parts[0]),\n            green: double.parse(parts[1]),\n            blue: double.parse(parts[2]),\n          );\n      }\n    }\n    return materials;\n  }\n}\n\nclass Face {\n  const Face(this.vertex, this.texCoord, this.normal);\n\n  final List<int> vertex;\n  final List<int> texCoord;\n  final List<int> normal;\n\n  Face.empty() : vertex = [], texCoord = [], normal = [];\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/light/ambient_light.dart",
    "content": "import 'dart:ui' show Color;\n\nimport 'package:flame_3d/resources.dart';\n\nclass AmbientLight extends LightSource {\n  AmbientLight({\n    super.color = const Color(0xFFFFFFFF),\n    super.intensity = 0.2,\n  });\n\n  void apply(Shader shader) {\n    shader.setColor('AmbientLight.color', color);\n    shader.setFloat('AmbientLight.intensity', intensity);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/light/light.dart",
    "content": "import 'package:flame_3d/game.dart';\nimport 'package:flame_3d/resources.dart';\n\n/// {@template light}\n/// A [Resource] that represents a light source that is positioned in the scene\n/// and changes how other objects are rendered.\n///\n/// This class isn't a true resource, it does not upload it self to the GPU.\n/// Instead, it is used to modify how other resources are uploaded.\n///\n/// {@endtemplate}\nclass Light extends Resource<void> {\n  final Transform3D transform;\n  final LightSource source;\n\n  /// {@macro light}\n  Light({\n    required this.transform,\n    required this.source,\n  });\n\n  @override\n  void createResource() {}\n\n  void apply(int index, Shader shader) {\n    shader.setVector3('Light$index.position', transform.position);\n    shader.setColor('Light$index.color', source.color);\n    shader.setFloat('Light$index.intensity', source.intensity);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/light/light_source.dart",
    "content": "import 'dart:ui' show Color;\n\nimport 'package:flame_3d/resources.dart';\n\n/// Describes the properties of a light source.\n/// There are three types of light sources: point, directional, and spot.\n/// Currently only [PointLight] is implemented.\nabstract class LightSource {\n  final Color color;\n  final double intensity;\n\n  LightSource({\n    required this.color,\n    required this.intensity,\n  });\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/light/lighting_info.dart",
    "content": "import 'package:flame_3d/resources.dart';\n\nclass LightingInfo {\n  Iterable<Light> lights = [];\n\n  void apply(Shader shader) {\n    _applyAmbientLight(shader);\n    _applyPointLights(shader);\n  }\n\n  void _applyAmbientLight(Shader shader) {\n    final ambient = _extractAmbientLight(lights);\n    ambient.apply(shader);\n  }\n\n  void _applyPointLights(Shader shader) {\n    final pointLights = lights.where((e) => e.source is PointLight);\n    final numLights = pointLights.length;\n    if (numLights > 3) {\n      // temporary, until we support dynamic arrays\n      throw Exception('At most 3 point lights are allowed');\n    }\n\n    // NOTE: using floats because Android GLES does not support integer uniforms\n    // Refer to https://github.com/flutter/engine/pull/55329\n    shader.setFloat('LightsInfo.numLights', numLights.toDouble());\n    for (final (index, light) in pointLights.indexed) {\n      light.apply(index, shader);\n    }\n  }\n\n  AmbientLight _extractAmbientLight(Iterable<Light> lights) {\n    final ambient = lights.where((e) => e.source is AmbientLight);\n    if (ambient.isEmpty) {\n      return AmbientLight();\n    }\n    if (ambient.length > 1) {\n      throw Exception('At most one ambient light is allowed');\n    }\n    return ambient.first.source as AmbientLight;\n  }\n\n  static List<UniformSlot> shaderSlots = [\n    UniformSlot.value('AmbientLight', {'color', 'intensity'}),\n    UniformSlot.value('LightsInfo', {'numLights'}),\n    UniformSlot.value('Light0', {'position', 'color', 'intensity'}),\n    UniformSlot.value('Light1', {'position', 'color', 'intensity'}),\n    UniformSlot.value('Light2', {'position', 'color', 'intensity'}),\n  ];\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/light/point_light.dart",
    "content": "import 'package:flame_3d/resources.dart';\n\n/// A point light that emits light in all directions equally.\nclass PointLight extends LightSource {\n  PointLight({\n    required super.color,\n    required super.intensity,\n  });\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/light.dart",
    "content": "export 'light/ambient_light.dart';\nexport 'light/light.dart';\nexport 'light/light_source.dart';\nexport 'light/lighting_info.dart';\nexport 'light/point_light.dart';\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/material/material.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame_3d/graphics.dart';\nimport 'package:flame_3d/resources.dart';\nimport 'package:flutter_gpu/gpu.dart' as gpu;\n\n/// {@template material}\n/// Base material [Resource], it holds the shader library that should be used\n/// for the texture.\n/// {@endtemplate}\nabstract class Material extends Resource<gpu.RenderPipeline> {\n  /// {@macro material}\n  Material({\n    required Shader vertexShader,\n    required Shader fragmentShader,\n  }) : _vertexShader = vertexShader,\n       _fragmentShader = fragmentShader;\n\n  static Material defaultMaterial = SpatialMaterial()\n    ..albedoColor = const Color(0xFFFF00FF);\n\n  @override\n  gpu.RenderPipeline createResource() {\n    return gpu.gpuContext.createRenderPipeline(\n      _vertexShader.compile().resource,\n      _fragmentShader.compile().resource,\n    );\n  }\n\n  Shader get vertexShader => _vertexShader;\n  Shader _vertexShader;\n  set vertexShader(Shader shader) {\n    _vertexShader = shader;\n    recreateResource = true;\n  }\n\n  Shader get fragmentShader => _fragmentShader;\n  Shader _fragmentShader;\n  set fragmentShader(Shader shader) {\n    _fragmentShader = shader;\n    recreateResource = true;\n  }\n\n  /// Face culling mode for this material. Defaults to [CullMode.none].\n  CullMode cullMode = CullMode.none;\n\n  void bind(GraphicsDevice device) {}\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/material/spatial_material.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame_3d/game.dart';\nimport 'package:flame_3d/graphics.dart';\nimport 'package:flame_3d/resources.dart';\n\nclass SpatialMaterial extends Material {\n  SpatialMaterial({\n    this.albedoColor = const Color(0xFFFFFFFF),\n    Texture? albedoTexture,\n    this.metallic = 0.8,\n    this.roughness = 0.6,\n  }) : albedoTexture = albedoTexture ?? Texture.standard,\n       super(\n         vertexShader: Shader.vertex(\n           asset:\n               'packages/flame_3d/assets/shaders/spatial_material.shaderbundle',\n           slots: [\n             UniformSlot.value('VertexInfo', {\n               'model',\n               'view',\n               'projection',\n             }),\n             UniformSlot.value(\n               'JointMatrices',\n               List.generate(_maxJoints, (index) => 'joint$index').toSet(),\n             ),\n           ],\n         ),\n         fragmentShader: Shader.fragment(\n           asset:\n               'packages/flame_3d/assets/shaders/spatial_material.shaderbundle',\n           slots: [\n             UniformSlot.sampler('albedoTexture'),\n             UniformSlot.value('Material', {\n               'albedoColor',\n               'metallic',\n               'roughness',\n             }),\n             ...LightingInfo.shaderSlots,\n             UniformSlot.value('Camera', {'position'}),\n           ],\n         ),\n       );\n\n  /// The material's base color.\n  Color albedoColor;\n\n  /// The texture that will be multiplied by [albedoColor].\n  Texture albedoTexture;\n\n  double metallic;\n\n  double roughness;\n\n  @override\n  void bind(GraphicsDevice device) {\n    _bindVertexInfo(device);\n    _bindJointMatrices(device);\n    _bindMaterial(device);\n    _bindCamera(device);\n  }\n\n  void _bindVertexInfo(GraphicsDevice device) {\n    vertexShader\n      ..setMatrix4('VertexInfo.model', device.model)\n      ..setMatrix4('VertexInfo.view', device.view)\n      ..setMatrix4('VertexInfo.projection', device.projection);\n  }\n\n  void _bindJointMatrices(GraphicsDevice device) {\n    final jointTransforms = device.jointsInfo.jointTransforms;\n    if (jointTransforms.length > _maxJoints) {\n      throw Exception(\n        'At most $_maxJoints joints per surface are supported;'\n        ' found ${jointTransforms.length}',\n      );\n    }\n    for (final (index, transform) in jointTransforms.indexed) {\n      vertexShader.setMatrix4('JointMatrices.joint$index', transform);\n    }\n  }\n\n  void _bindMaterial(GraphicsDevice device) {\n    _applyLights(device);\n    fragmentShader\n      ..setTexture('albedoTexture', albedoTexture)\n      ..setColor('Material.albedoColor', albedoColor)\n      ..setFloat('Material.metallic', metallic)\n      ..setFloat('Material.roughness', roughness);\n  }\n\n  void _bindCamera(GraphicsDevice device) {\n    final invertedView = Matrix4.inverted(device.view);\n    final cameraPosition = invertedView.transform3(Vector3.zero());\n    fragmentShader.setVector3('Camera.position', cameraPosition);\n  }\n\n  void _applyLights(GraphicsDevice device) {\n    device.lightingInfo.apply(fragmentShader);\n  }\n\n  static const _maxJoints = 16;\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/material.dart",
    "content": "export 'material/material.dart';\nexport 'material/spatial_material.dart';\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/mesh/cone_mesh.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/geometry.dart';\nimport 'package:flame_3d/core.dart';\nimport 'package:flame_3d/resources.dart';\n\n/// A conical mesh, with base on the x-z plane, centered origin; and tip\n/// pointing upwards parallel to the y-axis.\n///\n/// The default texture mapping follows the standard arrangement of the circular\n/// base on the top left quadrant and the flattened side on the bottom half.\nclass ConeMesh extends Mesh {\n  /// Creates a conical mesh with a circular base of radius [radius] on the x-z\n  /// plane and a height of [height] pointing upwards parallel to the y-axis.\n  ///\n  /// You can optionally specify the number of segments used for the\n  /// triangulation (the higher, the more \"high-res\" the cone will be).\n  ConeMesh({\n    required double radius,\n    required double height,\n    required Material material,\n    int segments = 32,\n  }) {\n    _addBaseSurface(\n      radius: radius,\n      material: material,\n      segments: segments,\n    );\n    _addSideSurface(\n      radius: radius,\n      height: height,\n      material: material,\n      segments: segments,\n    );\n  }\n\n  void _addBaseSurface({\n    required double radius,\n    required Material material,\n    required int segments,\n  }) {\n    final vertices = <Vertex>[];\n    final indices = <int>[];\n\n    // Texture mapping: circle radius 0.25 in UV\n    const centerV = 0.75;\n\n    // Base center\n    vertices.add(\n      Vertex(position: Vector3.zero(), texCoord: Vector2(0.5, centerV)),\n    );\n\n    // Base triangulation\n    for (var i = 0; i < segments; i++) {\n      final theta = tau * i / segments;\n      final x = radius * cos(theta);\n      final z = radius * sin(theta);\n      final u = 0.5 + 0.25 * cos(theta);\n      final v = centerV + 0.25 * sin(theta);\n      vertices.add(\n        Vertex(position: Vector3(x, 0, z), texCoord: Vector2(u, v)),\n      );\n    }\n\n    for (var i = 0; i < segments; i++) {\n      indices.addAll([0, i + 1, ((i + 1) % segments) + 1]);\n    }\n\n    addSurface(\n      Surface(\n        vertices: vertices,\n        indices: indices,\n        material: material,\n      ),\n    );\n  }\n\n  void _addSideSurface({\n    required double radius,\n    required double height,\n    required Material material,\n    required int segments,\n  }) {\n    final vertices = <Vertex>[];\n    final indices = <int>[];\n\n    // Texture mapping: base edge along arc in upper half\n    const centerV = 0.75;\n    const tipV = 0.25;\n\n    // Tip\n    vertices.add(\n      Vertex(position: Vector3(0, height, 0), texCoord: Vector2(0.5, tipV)),\n    );\n\n    // Side triangulation\n    for (var i = 0; i < segments; i++) {\n      final theta = tau * i / segments;\n      final x = radius * cos(theta);\n      final z = radius * sin(theta);\n      final u = i / segments;\n      vertices.add(\n        Vertex(position: Vector3(x, 0, z), texCoord: Vector2(u, centerV)),\n      );\n    }\n\n    for (var i = 0; i < segments; i++) {\n      indices.addAll([0, i + 1, ((i + 1) % segments) + 1]);\n    }\n\n    addSurface(\n      Surface(\n        vertices: vertices,\n        indices: indices,\n        material: material,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/mesh/cuboid_mesh.dart",
    "content": "import 'package:flame_3d/game.dart';\nimport 'package:flame_3d/resources.dart';\n\n/// {@template cuboid_mesh}\n/// Represents a Cuboid's geometry with a single surface.\n/// {@endtemplate}\nclass CuboidMesh extends Mesh {\n  /// {@macro cuboid_mesh}\n  CuboidMesh({\n    required Vector3 size,\n    Material? material,\n    bool useFaceNormals = true,\n  }) {\n    final Vector3(:x, :y, :z) = size / 2;\n\n    Vertex vertex({\n      required Vector3 position,\n      required Vector2 texCoord,\n      required Vector3 normal,\n    }) {\n      return Vertex(\n        position: position,\n        texCoord: texCoord,\n        normal: useFaceNormals ? normal : null,\n      );\n    }\n\n    final vertices = [\n      // Face 1 (front)\n      vertex(\n        position: Vector3(-x, -y, -z),\n        texCoord: Vector2(0, 0),\n        normal: Vector3(0, 0, -1),\n      ),\n      vertex(\n        position: Vector3(x, -y, -z),\n        texCoord: Vector2(1, 0),\n        normal: Vector3(0, 0, -1),\n      ),\n      vertex(\n        position: Vector3(x, y, -z),\n        texCoord: Vector2(1, 1),\n        normal: Vector3(0, 0, -1),\n      ),\n      vertex(\n        position: Vector3(-x, y, -z),\n        texCoord: Vector2(0, 1),\n        normal: Vector3(0, 0, -1),\n      ),\n\n      // Face 2 (back)\n      vertex(\n        position: Vector3(-x, -y, z),\n        texCoord: Vector2(0, 0),\n        normal: Vector3(0, 0, 1),\n      ),\n      vertex(\n        position: Vector3(x, -y, z),\n        texCoord: Vector2(1, 0),\n        normal: Vector3(0, 0, 1),\n      ),\n      vertex(\n        position: Vector3(x, y, z),\n        texCoord: Vector2(1, 1),\n        normal: Vector3(0, 0, 1),\n      ),\n      vertex(\n        position: Vector3(-x, y, z),\n        texCoord: Vector2(0, 1),\n        normal: Vector3(0, 0, 1),\n      ),\n\n      // Face 3 (left)\n      vertex(\n        position: Vector3(-x, -y, z),\n        texCoord: Vector2(0, 0),\n        normal: Vector3(-1, 0, 0),\n      ),\n      vertex(\n        position: Vector3(-x, -y, -z),\n        texCoord: Vector2(1, 0),\n        normal: Vector3(-1, 0, 0),\n      ),\n      vertex(\n        position: Vector3(-x, y, -z),\n        texCoord: Vector2(1, 1),\n        normal: Vector3(-1, 0, 0),\n      ),\n      vertex(\n        position: Vector3(-x, y, z),\n        texCoord: Vector2(0, 1),\n        normal: Vector3(-1, 0, 0),\n      ),\n\n      // Face 4 (right)\n      vertex(\n        position: Vector3(x, -y, -z),\n        texCoord: Vector2(0, 0),\n        normal: Vector3(1, 0, 0),\n      ),\n      vertex(\n        position: Vector3(x, -y, z),\n        texCoord: Vector2(1, 0),\n        normal: Vector3(1, 0, 0),\n      ),\n      vertex(\n        position: Vector3(x, y, z),\n        texCoord: Vector2(1, 1),\n        normal: Vector3(1, 0, 0),\n      ),\n      vertex(\n        position: Vector3(x, y, -z),\n        texCoord: Vector2(0, 1),\n        normal: Vector3(1, 0, 0),\n      ),\n\n      // Face 5 (top)\n      vertex(\n        position: Vector3(-x, y, -z),\n        texCoord: Vector2(0, 0),\n        normal: Vector3(0, 1, 0),\n      ),\n      vertex(\n        position: Vector3(x, y, -z),\n        texCoord: Vector2(1, 0),\n        normal: Vector3(0, 1, 0),\n      ),\n      vertex(\n        position: Vector3(x, y, z),\n        texCoord: Vector2(1, 1),\n        normal: Vector3(0, 1, 0),\n      ),\n      vertex(\n        position: Vector3(-x, y, z),\n        texCoord: Vector2(0, 1),\n        normal: Vector3(0, 1, 0),\n      ),\n\n      // Face 6 (bottom)\n      vertex(\n        position: Vector3(-x, -y, z),\n        texCoord: Vector2(0, 0),\n        normal: Vector3(0, -1, 0),\n      ),\n      vertex(\n        position: Vector3(x, -y, z),\n        texCoord: Vector2(1, 0),\n        normal: Vector3(0, -1, 0),\n      ),\n      vertex(\n        position: Vector3(x, -y, -z),\n        texCoord: Vector2(1, 1),\n        normal: Vector3(0, -1, 0),\n      ),\n      vertex(\n        position: Vector3(-x, -y, -z),\n        texCoord: Vector2(0, 1),\n        normal: Vector3(0, -1, 0),\n      ),\n    ];\n\n    final indices = [\n      0, 1, 2, 2, 3, 0, // Face 1\n      4, 5, 6, 6, 7, 4, // Face 2\n      8, 9, 10, 10, 11, 8, // Face 3\n      12, 13, 14, 14, 15, 12, // Face 4\n      16, 17, 18, 18, 19, 16, // Face 5\n      20, 21, 22, 22, 23, 20, // Face 6\n    ];\n\n    addSurface(\n      Surface(\n        vertices: vertices,\n        indices: indices,\n        material: material,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/mesh/cylinder_mesh.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:flame/geometry.dart';\nimport 'package:flame_3d/game.dart';\nimport 'package:flame_3d/resources.dart';\n\nclass CylinderMesh extends Mesh {\n  CylinderMesh({\n    required double radius,\n    required double height,\n    int segments = 16,\n    Material? material,\n  }) {\n    final vertices = <Vertex>[];\n    final halfHeight = height / 2.0;\n\n    // top circle\n    for (var i = 0; i <= segments; i++) {\n      final theta = i * tau / segments;\n      final x = radius * math.cos(theta);\n      final y = halfHeight;\n      final z = radius * math.sin(theta);\n\n      final u = (1 + math.cos(theta)) * 0.5;\n      final v = (1 + math.sin(theta)) * 0.5;\n\n      vertices.add(\n        Vertex(position: Vector3(x, y, z), texCoord: Vector2(u, v)),\n      );\n    }\n\n    // top center\n    vertices.add(\n      Vertex(position: Vector3(0, halfHeight, 0), texCoord: Vector2(0.5, 0.5)),\n    );\n\n    // bottom circle\n    for (var i = 0; i <= segments; i++) {\n      final theta = i * tau / segments;\n      final x = radius * math.cos(theta);\n      final y = -halfHeight;\n      final z = radius * math.sin(theta);\n\n      final u = (1 + math.cos(theta)) * 0.5;\n      final v = (1 + math.sin(theta)) * 0.5;\n\n      vertices.add(\n        Vertex(position: Vector3(x, y, z), texCoord: Vector2(u, v)),\n      );\n    }\n\n    // bottom center\n    vertices.add(\n      Vertex(position: Vector3(0, -halfHeight, 0), texCoord: Vector2(0.5, 0.5)),\n    );\n\n    final indices = <int>[];\n\n    // top circle indices\n    for (var i = 0; i < segments; i++) {\n      indices.add(i);\n      indices.add(i + 1);\n      indices.add(segments + 1); // center index\n    }\n\n    // bottom circle indices\n    for (var i = 0; i < segments; i++) {\n      indices.add(segments + 1 + i);\n      indices.add(segments + 1 + i + 1);\n      indices.add(segments * 2 + 2); // center index\n    }\n\n    // side indices\n    for (var i = 0; i < segments; i++) {\n      final topIndex = i;\n      final bottomIndex = i + segments + 1;\n      indices.add(topIndex);\n      indices.add(bottomIndex);\n      indices.add(topIndex + 1);\n\n      indices.add(bottomIndex);\n      indices.add(bottomIndex + 1);\n      indices.add(topIndex + 1);\n    }\n\n    addSurface(\n      Surface(\n        vertices: vertices,\n        indices: indices,\n        material: material,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/mesh/mesh.dart",
    "content": "import 'package:flame_3d/game.dart';\nimport 'package:flame_3d/graphics.dart';\nimport 'package:flame_3d/resources.dart';\n\n/// {@template mesh}\n/// A [Resource] that represents a geometric shape that is divided up in one or\n/// more [Surface]s.\n///\n/// This class isn't a true resource, it does not upload it self to the GPU.\n/// Instead it uploads [Surface]s, it acts as a proxy.\n/// {@endtemplate}\nclass Mesh extends Resource<void> {\n  /// {@macro mesh}\n  Mesh() : _surfaces = [];\n\n  final List<Surface> _surfaces;\n  Aabb3? _aabb;\n\n  /// The AABB of the mesh.\n  ///\n  /// This is the sum of all the AABB's of the surfaces it contains.\n  Aabb3 get aabb => _aabb ??= _recomputeAabb3();\n\n  int get vertexCount => _surfaces.fold(0, (p, e) => p + e.vertexCount);\n\n  void bind(GraphicsDevice device) {\n    for (final (index, surface) in _surfaces.indexed) {\n      device.jointsInfo.setSurface(index);\n      device.bindSurface(surface);\n    }\n  }\n\n  @override\n  void createResource() {}\n\n  /// The total surface count of the mesh.\n  int get surfaceCount => _surfaces.length;\n\n  /// An unmodifiable iterable over the list of the surfaces.\n  ///\n  /// Note: if you modify the geometry of any [Surface] within this list,\n  /// you will need to call [updateBounds] to update the mesh's bounds.\n  Iterable<Surface> get surfaces => _surfaces;\n\n  /// Add a new [surface] to this mesh.\n  /// Return the index of the newly added surface.\n  /// Surfaces are always added to the end of the list.\n  int addSurface(Surface surface) {\n    _surfaces.add(surface);\n    updateBounds();\n    return _surfaces.length - 1;\n  }\n\n  /// Replace the surface at [index] with [surface].\n  void updateSurface(int index, Surface surface) {\n    _surfaces[index] = surface;\n    updateBounds();\n  }\n\n  /// Remove the surface at [index].\n  void removeSurface(int index) {\n    _surfaces.removeAt(index);\n    updateBounds();\n  }\n\n  /// Update the surfaces of the mesh, making sure to recompute the bounds\n  /// after.\n  void updateSurfaces(void Function(List<Surface> surfaces) update) {\n    update(_surfaces);\n    updateBounds();\n  }\n\n  /// Must be called when the mesh has been modified.\n  void updateBounds() {\n    _aabb = null;\n  }\n\n  Aabb3 _recomputeAabb3() {\n    var aabb = Aabb3();\n    for (var i = 0; i < _surfaces.length; i++) {\n      if (i == 0) {\n        aabb = _surfaces[i].aabb;\n      } else {\n        aabb.hull(_surfaces[i].aabb);\n      }\n    }\n    return aabb;\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/mesh/plane_mesh.dart",
    "content": "import 'package:flame_3d/game.dart';\nimport 'package:flame_3d/resources.dart';\n\n/// {@template plane_mesh}\n/// Represents a 2D Plane's geometry with a single surface.\n/// {@endtemplate}\nclass PlaneMesh extends Mesh {\n  /// {@macro plane_mesh}\n  PlaneMesh({\n    required Vector2 size,\n    Material? material,\n  }) {\n    final Vector2(:x, :y) = size / 2;\n\n    final vertices = [\n      Vertex(\n        position: Vector3(-x, 0, -y),\n        texCoord: Vector2(0, 0),\n        normal: Vector3(0, 1, 0),\n      ),\n      Vertex(\n        position: Vector3(x, 0, -y),\n        texCoord: Vector2(1, 0),\n        normal: Vector3(0, 1, 0),\n      ),\n      Vertex(\n        position: Vector3(x, 0, y),\n        texCoord: Vector2(1, 1),\n        normal: Vector3(0, 1, 0),\n      ),\n      Vertex(\n        position: Vector3(-x, 0, y),\n        texCoord: Vector2(0, 1),\n        normal: Vector3(0, 1, 0),\n      ),\n    ];\n    addSurface(\n      Surface(\n        vertices: vertices,\n        indices: [0, 1, 2, 2, 3, 0],\n        material: material,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/mesh/sphere_mesh.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:flame_3d/game.dart';\nimport 'package:flame_3d/resources.dart';\n\n/// {@template sphere_mesh}\n/// Represents a Sphere's geometry with a single surface.\n/// {@endtemplate}\nclass SphereMesh extends Mesh {\n  /// {@macro sphere_mesh}\n  SphereMesh({\n    required double radius,\n    int segments = 64,\n    Material? material,\n  }) {\n    final vertices = <Vertex>[];\n    for (var i = 0; i <= segments; i++) {\n      final theta = i * (2 * math.pi) / segments;\n      for (var j = 0; j <= segments; j++) {\n        final phi = j * math.pi / segments;\n\n        final x = radius * math.sin(phi) * math.cos(theta);\n        final y = radius * math.cos(phi);\n        final z = radius * math.sin(phi) * math.sin(theta);\n\n        final u = theta / (2 * math.pi);\n        final v = phi / math.pi;\n\n        vertices.add(\n          Vertex(position: Vector3(x, y, z), texCoord: Vector2(u, v)),\n        );\n      }\n    }\n\n    final indices = <int>[];\n    for (var i = 0; i < segments; i++) {\n      for (var j = 0; j < segments; j++) {\n        final first = i * (segments + 1) + j;\n        final second = first + segments + 1;\n\n        indices.add(first);\n        indices.add(second);\n        indices.add(first + 1);\n\n        indices.add(second);\n        indices.add(second + 1);\n        indices.add(first + 1);\n      }\n    }\n\n    addSurface(\n      Surface(\n        vertices: vertices,\n        indices: indices,\n        material: material,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/mesh/surface.dart",
    "content": "import 'dart:math' as math;\nimport 'dart:typed_data';\n\nimport 'package:flame_3d/game.dart';\nimport 'package:flame_3d/resources.dart';\nimport 'package:flame_3d/src/graphics/gpu_context_wrapper.dart';\nimport 'package:flutter_gpu/gpu.dart' as gpu;\n\nenum PrimitiveType {\n  triangles,\n}\n\n/// {@template surface}\n/// Base surface [Resource], it describes a single surface to be rendered.\n/// {@endtemplate}\nclass Surface extends Resource<gpu.DeviceBuffer?> {\n  /// {@macro surface}\n  Surface({\n    required List<Vertex> vertices,\n    required List<int> indices,\n    Material? material,\n    this.jointMap,\n    /**\n     * If `true`, the normals will be calculated if they are not provided.\n     */\n    bool calculateNormals = true,\n  }) : material = material ?? Material.defaultMaterial {\n    final normalizedVertices = _normalize(\n      vertices: vertices,\n      indices: indices,\n      calculateNormals: calculateNormals,\n    );\n    // `TODO`(bdero): This should have an attribute map instead and be fully SoA\n    // but vertex attributes in Impeller aren't flexible enough yet.\n    // See also https://github.com/flutter/flutter/issues/116168.\n    _vertices = Float32List.fromList(\n      normalizedVertices.fold([], (p, v) => p..addAll(v.storage)),\n    ).buffer;\n    _vertexCount = normalizedVertices.length;\n\n    _indices = Uint16List.fromList(indices).buffer;\n    _indexCount = indices.length;\n\n    _calculateAabb(normalizedVertices);\n  }\n\n  Material material;\n  Map<int, int>? jointMap;\n\n  Aabb3 get aabb => _aabb;\n  late Aabb3 _aabb;\n\n  int get verticesBytes => _vertices.lengthInBytes;\n  late ByteBuffer _vertices;\n\n  int get vertexCount => _vertexCount;\n  late int _vertexCount;\n\n  int get indicesBytes => _indices.lengthInBytes;\n  late ByteBuffer _indices;\n\n  int get indexCount => _indexCount;\n  late int _indexCount;\n\n  int? resourceSizeInByes;\n\n  @override\n  bool get recreateResource {\n    final sizeInBytes = _vertices.lengthInBytes + _indices.lengthInBytes;\n    return resourceSizeInByes != sizeInBytes;\n  }\n\n  @override\n  gpu.DeviceBuffer? createResource() {\n    final sizeInBytes = _vertices.lengthInBytes + _indices.lengthInBytes;\n    resourceSizeInByes = sizeInBytes;\n    return GpuContextWrapper(gpu.gpuContext).createDeviceBuffer(\n        gpu.StorageMode.hostVisible,\n        sizeInBytes,\n      )\n      ..overwrite(_vertices.asByteData())\n      ..overwrite(\n        _indices.asByteData(),\n        destinationOffsetInBytes: _vertices.lengthInBytes,\n      );\n  }\n\n  void _calculateAabb(List<Vertex> vertices) {\n    var minX = double.infinity;\n    var minY = double.infinity;\n    var minZ = double.infinity;\n    var maxX = double.negativeInfinity;\n    var maxY = double.negativeInfinity;\n    var maxZ = double.negativeInfinity;\n\n    for (final vertex in vertices) {\n      minX = math.min(minX, vertex.position.x);\n      minY = math.min(minY, vertex.position.y);\n      minZ = math.min(minZ, vertex.position.z);\n      maxX = math.max(maxX, vertex.position.x);\n      maxY = math.max(maxY, vertex.position.y);\n      maxZ = math.max(maxZ, vertex.position.z);\n    }\n\n    _aabb = Aabb3.minMax(\n      Vector3(minX, minY, minZ),\n      Vector3(maxX, maxY, maxZ),\n    );\n  }\n\n  static List<Vertex> _normalize({\n    required List<Vertex> vertices,\n    required List<int> indices,\n    required bool calculateNormals,\n  }) {\n    final recalculate =\n        calculateNormals && vertices.any((e) => e.normal == null);\n    if (!recalculate) {\n      return vertices;\n    }\n\n    final normals = Vertex.calculateVertexNormals(\n      vertices.map((e) => e.position.mutable).toList(),\n      indices,\n    );\n    return [\n      for (final (i, v) in vertices.indexed)\n        v.copyWith(normal: v.normal?.mutable ?? normals[i]),\n    ];\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/mesh/vertex.dart",
    "content": "import 'dart:typed_data';\nimport 'dart:ui' show Color;\n\nimport 'package:flame_3d/game.dart';\nimport 'package:meta/meta.dart';\n\n/// {@template vertex}\n/// Represents a vertex in 3D space.\n///\n/// A vertex consists out of space coordinates, UV/texture coordinates and a\n/// color.\n/// {@endtemplate}\n@immutable\nclass Vertex {\n  /// {@macro vertex}\n  Vertex({\n    required Vector3 position,\n    required Vector2 texCoord,\n    this.color = const Color(0xFFFFFFFF),\n    Vector3? normal,\n    Vector4? joints,\n    Vector4? weights,\n  }) : position = position.immutable,\n       texCoord = texCoord.immutable,\n       normal = normal?.immutable,\n       joints = joints?.immutable,\n       weights = weights?.immutable,\n       _storage = Float32List.fromList([\n         ...position.storage, // 1, 2, 3\n         ...texCoord.storage, // 4, 5\n         ...[color.r, color.g, color.b, color.a], // 6, 7, 8, 9\n         ...(normal ?? Vector3.zero()).storage, // 10, 11, 12\n         ...(joints ?? Vector4.zero()).storage, // 13, 14, 15, 16\n         ...(weights ?? Vector4.zero()).storage, // 17, 18, 19, 20\n       ]);\n\n  Float32List get storage => _storage;\n  final Float32List _storage;\n\n  /// The position of the vertex in 3D space.\n  final ImmutableVector3 position;\n\n  /// The UV coordinates of the texture to map.\n  final ImmutableVector2 texCoord;\n\n  /// The normal vector of the vertex.\n  final ImmutableVector3? normal;\n\n  /// The joints of the vertex.\n  final ImmutableVector4? joints;\n\n  /// The weights of the vertex.\n  final ImmutableVector4? weights;\n\n  /// The color on the vertex.\n  final Color color;\n\n  @override\n  bool operator ==(Object other) =>\n      other is Vertex &&\n      position == other.position &&\n      texCoord == other.texCoord &&\n      normal == other.normal &&\n      color == other.color &&\n      joints == other.joints &&\n      weights == other.weights;\n\n  @override\n  int get hashCode => Object.hashAll([\n    position,\n    texCoord,\n    normal,\n    color,\n    joints,\n    weights,\n  ]);\n\n  Vertex copyWith({\n    Vector3? position,\n    Vector2? texCoord,\n    Vector3? normal,\n    Color? color,\n    Vector4? joints,\n    Vector4? weights,\n  }) {\n    // TODO(wolfenrain): optimize this.\n    return Vertex(\n      position: position ?? this.position.mutable,\n      texCoord: texCoord ?? this.texCoord.mutable,\n      normal: normal ?? this.normal?.mutable,\n      color: color ?? this.color,\n      joints: joints ?? this.joints?.mutable,\n      weights: weights ?? this.weights?.mutable,\n    );\n  }\n\n  static List<Vector3> calculateVertexNormals(\n    List<Vector3> vertices,\n    List<int> indices,\n  ) {\n    final normals = List.filled(vertices.length, Vector3.zero());\n    for (var i = 0; i < indices.length; i += 3) {\n      final i0 = indices[i];\n      final i1 = indices[i + 1];\n      final i2 = indices[i + 2];\n\n      final v0 = vertices[i0];\n      final v1 = vertices[i1];\n      final v2 = vertices[i2];\n\n      final edge1 = v1 - v0;\n      final edge2 = v2 - v0;\n      final faceNormal = edge1.cross(edge2)..normalize();\n\n      normals[i0] += faceNormal;\n      normals[i1] += faceNormal;\n      normals[i2] += faceNormal;\n    }\n    for (final normal in normals) {\n      normal.normalize();\n    }\n    return normals;\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/mesh.dart",
    "content": "export 'mesh/cone_mesh.dart';\nexport 'mesh/cuboid_mesh.dart';\nexport 'mesh/cylinder_mesh.dart';\nexport 'mesh/mesh.dart';\nexport 'mesh/plane_mesh.dart';\nexport 'mesh/sphere_mesh.dart';\nexport 'mesh/surface.dart';\nexport 'mesh/vertex.dart';\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/resource.dart",
    "content": "// TODO(wolfenrain): in the long run it would be nice of we can make it\n// automatically refer to same type of objects to prevent memory leaks\n\n/// {@template resource}\n/// A Resource is the base class for any resource typed classes. The primary\n/// use case is to be a data container.\n/// {@endtemplate}\nabstract class Resource<R> {\n  R? _resource;\n  bool recreateResource = true;\n\n  R createResource();\n\n  R get resource {\n    if (recreateResource) {\n      _resource = createResource();\n      recreateResource = false;\n    }\n    return _resource!;\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/shader/shader.dart",
    "content": "import 'dart:typed_data';\nimport 'dart:ui';\n\nimport 'package:flame_3d/game.dart';\nimport 'package:flame_3d/graphics.dart';\nimport 'package:flame_3d/resources.dart';\nimport 'package:flutter_gpu/gpu.dart' as gpu;\n\nMap<Color, Float32List> _colorBytesCache = {};\n\n/// {@template shader_resource}\n///\n/// {@endtemplate}\nclass ShaderResource extends Resource<gpu.Shader> {\n  final gpu.Shader shader;\n\n  /// {@macro shader_resource}\n  factory ShaderResource.createFromAsset({\n    required String asset,\n    required String shaderName,\n    required List<UniformSlot> slots,\n  }) {\n    final library = gpu.ShaderLibrary.fromAsset(asset)!;\n\n    final shader = library[shaderName];\n    if (shader == null) {\n      throw StateError('Shader \"$shaderName\" not found in library \"$asset\"');\n    }\n    return ShaderResource._(shader: shader, slots: slots);\n  }\n\n  ShaderResource._({\n    required this.shader,\n    List<UniformSlot> slots = const [],\n  }) {\n    for (final slot in slots) {\n      slot.uniformSlot = resource.getUniformSlot(slot.name);\n    }\n  }\n\n  @override\n  gpu.Shader createResource() => shader;\n}\n\nclass Shader {\n  final String asset;\n  final String name;\n  final List<UniformSlot> slots;\n  final Map<String, UniformInstance> instances = {};\n\n  Shader({\n    required this.asset,\n    required this.name,\n    required this.slots,\n  });\n\n  Shader.vertex({required String asset, required List<UniformSlot> slots})\n    : this(asset: asset, name: 'TextureVertex', slots: slots);\n\n  Shader.fragment({required String asset, required List<UniformSlot> slots})\n    : this(asset: asset, name: 'TextureFragment', slots: slots);\n\n  /// Set a [Texture] at the given [key] on the buffer.\n  void setTexture(String key, Texture texture) => _setTypedValue(key, texture);\n\n  /// Set a [Vector2] at the given [key] on the buffer.\n  void setVector2(String key, Vector2 vector) => _setValue(key, vector.storage);\n\n  /// Set a [Vector3] at the given [key] on the buffer.\n  void setVector3(String key, Vector3 vector) => _setValue(key, vector.storage);\n\n  /// Set a [Vector4] at the given [key] on the buffer.\n  void setVector4(String key, Vector4 vector) => _setValue(key, vector.storage);\n\n  /// Set an [int] (encoded as uint) at the given [key] on the buffer.\n  void setUint(String key, int value) {\n    _setValue(key, _encodeUint32(value, Endian.little));\n  }\n\n  /// Set a [double] at the given [key] on the buffer.\n  void setFloat(String key, double value) {\n    _setValue(key, _encodeFloat32(value));\n  }\n\n  /// Set a [Matrix2] at the given [key] on the buffer.\n  void setMatrix2(String key, Matrix2 matrix) => _setValue(key, matrix.storage);\n\n  /// Set a [Matrix3] at the given [key] on the buffer.\n  void setMatrix3(String key, Matrix3 matrix) => _setValue(key, matrix.storage);\n\n  /// Set a [Matrix4] at the given [key] on the buffer.\n  void setMatrix4(String key, Matrix4 matrix) => _setValue(key, matrix.storage);\n\n  void setColor(String key, Color color) =>\n      _setValue(key, _colorBytesCache.putIfAbsent(color, () => color.storage));\n\n  void bind(GraphicsDevice device) {\n    for (final slot in slots) {\n      instances[slot.name]?.bind(device);\n    }\n  }\n\n  /// Set the [data] to the [UniformSlot] identified by [key].\n  void _setValue(String key, Float32List data) {\n    _setTypedValue(key, data.buffer);\n  }\n\n  List<String?> parseKey(String key) {\n    // examples: albedoTexture, Light[2].position, or Foo.bar\n    final regex = RegExp(r'^(\\w+)(?:\\[(\\d+)\\])?(?:\\.(\\w+))?$');\n    return regex.firstMatch(key)?.groups([1, 2, 3]) ?? [];\n  }\n\n  /// Get the slot for the [key], it only calculates it once for every unique\n  /// [key].\n  void _setTypedValue<K, T>(String key, T value) {\n    final groups = parseKey(key);\n\n    final object = groups[0]; // e.g. Light, albedoTexture\n    final index = _maybeParseInt(groups[1]); // e.g. 2 (optional)\n    final field = groups[2]; // e.g. position (optional)\n\n    if (object == null) {\n      throw StateError('Uniform \"$key\" is missing an object');\n    }\n\n    final instance =\n        instances.putIfAbsent(object, () {\n              final slot = slots.firstWhere(\n                (e) => e.name == object,\n                orElse: () => throw StateError('Uniform \"$object\" is unmapped'),\n              );\n              return slot.create();\n            })\n            as UniformInstance<K, T>;\n\n    final k = instance.makeKey(index, field);\n    instance.set(k, value);\n  }\n\n  static Float32List _encodeUint32(int value, Endian endian) {\n    return (ByteData(16)..setUint32(0, value, endian)).buffer.asFloat32List();\n  }\n\n  static Float32List _encodeFloat32(double value) {\n    return Float32List.fromList([value]);\n  }\n\n  static int? _maybeParseInt(String? value) {\n    if (value == null) {\n      return null;\n    }\n    return int.parse(value);\n  }\n\n  ShaderResource compile() {\n    return ShaderResource.createFromAsset(\n      asset: asset,\n      shaderName: name,\n      slots: slots,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/shader/uniform_array.dart",
    "content": "import 'dart:collection';\nimport 'dart:typed_data';\n\nimport 'package:flame_3d/graphics.dart';\nimport 'package:flame_3d/resources.dart';\n\ntypedef UniformArrayKey = ({int index, String field});\n\n/// {@template uniform_value}\n/// Instance of a uniform array. Represented by a [ByteBuffer].\n/// {@endtemplate}\nclass UniformArray extends UniformInstance<UniformArrayKey, ByteBuffer> {\n  /// {@macro uniform_value}\n  UniformArray(super.slot);\n\n  final List<Map<int, ({int hash, List<double> data})>> _storage = [];\n\n  @override\n  ByteBuffer createResource() {\n    final data = <double>[];\n    for (final element in _storage) {\n      var previousIndex = -1;\n      for (final entry in element.entries) {\n        if (previousIndex + 1 != entry.key) {\n          final field = slot.fields.indexed.firstWhere(\n            (e) => e.$1 == previousIndex + 1,\n          );\n          throw StateError(\n            'Uniform ${slot.name}.${field.$2} was not set',\n          );\n        }\n        previousIndex = entry.key;\n        data.addAll(entry.value.data);\n      }\n    }\n    return Float32List.fromList(data).buffer;\n  }\n\n  Map<int, ({int hash, List<double> data})> _get(int index) {\n    while (index >= _storage.length) {\n      _storage.add(HashMap());\n    }\n    return _storage[index];\n  }\n\n  List<double>? get(int index, String key) {\n    return _get(index)[slot.indexOf(key)]?.data;\n  }\n\n  @override\n  void set(UniformArrayKey key, ByteBuffer buffer) {\n    final storage = _get(key.index);\n    final index = slot.indexOf(key.field);\n\n    // Ensure that we are only setting new data if the hash has changed.\n    final data = buffer.asFloat32List();\n    final hash = Object.hashAll(data);\n    if (storage[index]?.hash == hash) {\n      return;\n    }\n\n    // Store the storage at the given slot index.\n    storage[index] = (data: data, hash: hash);\n\n    // Clear the cache.\n    recreateResource = true;\n  }\n\n  @override\n  UniformArrayKey makeKey(int? index, String? field) {\n    if (index == null) {\n      throw StateError('index is required for ${slot.name}');\n    }\n    if (field == null) {\n      throw StateError('field is required for ${slot.name}');\n    }\n\n    return (index: index, field: field);\n  }\n\n  @override\n  void bind(GraphicsDevice device) {\n    device.bindUniform(slot.resource!, resource!);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/shader/uniform_instance.dart",
    "content": "import 'package:flame_3d/graphics.dart';\nimport 'package:flame_3d/resources.dart';\n\n/// {@template uniform_instance}\n/// An instance of a [UniformSlot] that can cache the [resource] that will be\n/// bound to a [Shader].\n/// {@endtemplate}\nabstract class UniformInstance<K, T> extends Resource<T?> {\n  /// {@macro uniform_instance}\n  UniformInstance(this.slot);\n\n  /// The slot this instance belongs too.\n  final UniformSlot slot;\n\n  void bind(GraphicsDevice device);\n\n  void set(K key, T value);\n\n  K makeKey(int? index, String? field);\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/shader/uniform_sampler.dart",
    "content": "import 'package:flame_3d/graphics.dart';\nimport 'package:flame_3d/resources.dart';\n\n/// {@template uniform_sampler}\n/// Instance of a uniform sampler. Represented by a [Texture].\n/// {@endtemplate}\nclass UniformSampler extends UniformInstance<void, Texture> {\n  Texture? texture;\n\n  /// {@macro uniform_sampler}\n  UniformSampler(super.slot);\n\n  @override\n  void bind(GraphicsDevice device) {\n    device.bindTexture(slot.resource!, resource!);\n  }\n\n  @override\n  void set(void key, Texture value) {\n    texture = value;\n    recreateResource = true;\n  }\n\n  @override\n  Texture createResource() => texture!;\n\n  @override\n  void makeKey(int? index, String? field) {}\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/shader/uniform_slot.dart",
    "content": "import 'package:flame_3d/resources.dart';\nimport 'package:flutter_gpu/gpu.dart' as gpu;\n\n/// {@template uniform_slot}\n/// Class that maps a uniform slot in such a way that it is easier to do memory\n/// allocation.\n///\n/// This allows the [Shader] to create [UniformInstance]s that bind themselves\n/// to the shader without the shader needing to the inner workings.\n/// {@endtemplate}\nclass UniformSlot extends Resource<gpu.UniformSlot?> {\n  UniformSlot._(this.name, this.fields, this._instanceCreator)\n    : _fieldIndices = {for (var (index, key) in fields.indexed) key: index};\n\n  /// {@macro uniform_slot}\n  ///\n  /// Used for struct uniforms in shaders.\n  ///\n  /// The [fields] should be defined in order as they appear in the struct.\n  UniformSlot.value(String name, Set<String> fields)\n    : this._(name, fields, UniformValue.new);\n\n  /// {@macro uniform_slot}\n  ///\n  /// Used for array uniforms in shaders.\n  ///\n  /// The [fields] should be defined in order as they appear in the struct.\n  UniformSlot.array(String name, Set<String> fields)\n    : this._(name, fields, UniformArray.new);\n\n  /// {@macro uniform_slot}\n  ///\n  /// Used for sampler uniforms in shaders.\n  UniformSlot.sampler(String name) : this._(name, {}, UniformSampler.new);\n\n  /// The uniform slot's name.\n  final String name;\n\n  /// The fields in the uniform and the order in which the memory should be\n  /// allocated.\n  ///\n  /// This is empty if the slot is a sampler.\n  final Set<String> fields;\n\n  /// Cache of the fields mapped to their index.\n  final Map<String, int> _fieldIndices;\n\n  final UniformInstance Function(UniformSlot) _instanceCreator;\n\n  int indexOf(String field) => _fieldIndices[field]!;\n\n  UniformInstance create() => _instanceCreator.call(this);\n\n  gpu.UniformSlot? _uniformSlot;\n\n  set uniformSlot(gpu.UniformSlot value) {\n    _uniformSlot = value;\n    recreateResource = true;\n  }\n\n  @override\n  gpu.UniformSlot? createResource() => _uniformSlot;\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/shader/uniform_value.dart",
    "content": "import 'dart:collection';\nimport 'dart:typed_data';\n\nimport 'package:collection/collection.dart';\nimport 'package:flame_3d/graphics.dart';\nimport 'package:flame_3d/resources.dart';\n\n/// {@template uniform_value}\n/// Instance of a uniform value. Represented by a [ByteBuffer].\n///\n/// The `[]` operator can be used to set the raw data of a field. If the data is\n/// different from the last set it will recalculated the [resource].\n/// {@endtemplate}\nclass UniformValue extends UniformInstance<String, ByteBuffer> {\n  /// {@macro uniform_value}\n  UniformValue(super.slot);\n\n  final Map<int, ({int hash, Float32List data})> _storage = HashMap();\n\n  @override\n  ByteBuffer createResource() {\n    var previousIndex = -1;\n    final entries = _storage.entries.sortedBy((c) => c.key);\n\n    final packed = <double>[];\n    var offsetBytes = 0; // current write cursor in bytes\n\n    for (final e in entries) {\n      if (previousIndex + 1 != e.key) {\n        final field = slot.fields.indexed.firstWhere(\n          (e) => e.$1 == previousIndex + 1,\n        );\n        throw StateError('Uniform ${slot.name}.${field.$2} was not set');\n      }\n      previousIndex = e.key;\n\n      final values = e.value.data; // original component values\n      final componentCount = values.length;\n\n      final type = _UniformValueType.fromComponentCount(componentCount);\n\n      // align current offset\n      final aligned = type.align(offsetBytes);\n      if (aligned != offsetBytes) {\n        final padFloats = (aligned - offsetBytes) ~/ 4;\n        for (var i = 0; i < padFloats; i++) {\n          packed.add(0.0);\n        }\n        offsetBytes = aligned;\n      }\n\n      packed.addAll(values);\n      // add padding if needed\n      final paddingNeeded = type.paddedComponentCount - componentCount;\n      for (var i = 0; i < paddingNeeded; i++) {\n        packed.add(0.0);\n      }\n\n      offsetBytes += type.sizeBytes;\n    }\n\n    return Float32List.fromList(packed).buffer;\n  }\n\n  Float32List? operator [](String key) => _storage[slot.indexOf(key)]?.data;\n\n  void operator []=(String key, Float32List data) {\n    final index = slot.indexOf(key);\n\n    // Ensure that we are only setting new data if the hash has changed.\n    final hash = Object.hashAll(data);\n    if (_storage[index]?.hash == hash) {\n      return;\n    }\n\n    // Store the storage at the given slot index.\n    _storage[index] = (data: data, hash: hash);\n\n    // Clear the cache.\n    recreateResource = true;\n  }\n\n  @override\n  String makeKey(int? index, String? field) {\n    if (index != null) {\n      throw StateError('index is not supported for ${slot.name}');\n    }\n    if (field == null) {\n      throw StateError('field is required for ${slot.name}');\n    }\n\n    return field;\n  }\n\n  @override\n  void bind(GraphicsDevice device) {\n    device.bindUniform(slot.resource!, resource!);\n  }\n\n  @override\n  void set(String key, ByteBuffer value) {\n    if (!slot.fields.contains(key)) {\n      throw StateError('Field \"$key\" is unmapped for \"${slot.name}\"');\n    }\n    this[key] = value.asFloat32List();\n  }\n}\n\n/// According to std140 packing, we might need to add additional padding\n/// bytes depending on the data type of each field.\n/// We infer the data type by the component count of the provided field.\nenum _UniformValueType {\n  float(\n    componentCount: 1,\n    baseAlignmentBytes: 4,\n    sizeBytes: 4,\n    paddedComponentCount: 1,\n  ),\n  vec2(\n    componentCount: 2,\n    baseAlignmentBytes: 8,\n    sizeBytes: 8,\n    paddedComponentCount: 2,\n  ),\n  vec3(\n    componentCount: 3,\n    baseAlignmentBytes: 16,\n    sizeBytes: 16,\n    paddedComponentCount: 4, // padded to 4 according to std140\n  ),\n  vec4(\n    componentCount: 4,\n    baseAlignmentBytes: 16,\n    sizeBytes: 16,\n    paddedComponentCount: 4,\n  ),\n  mat4(\n    componentCount: 16,\n    baseAlignmentBytes: 16,\n    sizeBytes: 64,\n    paddedComponentCount: 16,\n  )\n  ;\n\n  const _UniformValueType({\n    required this.componentCount,\n    required this.baseAlignmentBytes,\n    required this.sizeBytes,\n    required this.paddedComponentCount,\n  });\n\n  final int componentCount;\n  final int baseAlignmentBytes;\n  final int sizeBytes;\n  final int paddedComponentCount;\n\n  int align(int offset) {\n    return (offset + baseAlignmentBytes - 1) & ~(baseAlignmentBytes - 1);\n  }\n\n  static _UniformValueType fromComponentCount(int count) {\n    return _UniformValueType.values.firstWhere(\n      (e) => e.componentCount == count,\n      orElse: () => throw StateError(\n        'Unsupported uniform field component count $count',\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/shader.dart",
    "content": "export 'shader/shader.dart';\nexport 'shader/uniform_array.dart';\nexport 'shader/uniform_instance.dart';\nexport 'shader/uniform_sampler.dart';\nexport 'shader/uniform_slot.dart';\nexport 'shader/uniform_value.dart';\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/texture/color_texture.dart",
    "content": "import 'dart:typed_data';\nimport 'dart:ui';\n\nimport 'package:flame_3d/resources.dart';\n\n/// {@template color_texture}\n/// A texture that holds a single color. By default it creates a 1x1 texture.\n/// {@endtemplate}\nclass ColorTexture extends Texture {\n  /// {@macro color_texture}\n  ColorTexture(Color color, {int width = 1, int height = 1})\n    : super(\n        Uint32List.fromList(\n          List.filled(\n            width * height,\n            // Convert to a 32 bit value representing this color.\n            (color.a * 255).round() << 24 |\n                (color.r * 255).round() << 16 |\n                (color.g * 255).round() << 8 |\n                (color.b * 255).round(),\n          ),\n        ).buffer.asByteData(),\n        width: width,\n        height: height,\n        format: PixelFormat.bgra8888,\n      );\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/texture/image_texture.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame_3d/resources.dart';\n\n/// {@template image_texture}\n/// A texture that holds an image as it's render-able texture.\n/// {@endtemplate}\nclass ImageTexture extends Texture {\n  /// {@macro image_texture}\n  ImageTexture(super.source, {required super.width, required super.height});\n\n  /// Create a [ImageTexture] from the given [image].\n  static Future<ImageTexture> create(Image image) async {\n    final Image(:toByteData, :width, :height) = image;\n    return ImageTexture((await toByteData())!, width: width, height: height);\n  }\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/texture/texture.dart",
    "content": "import 'dart:typed_data';\nimport 'dart:ui';\n\nimport 'package:flame_3d/resources.dart';\nimport 'package:flame_3d/src/graphics/gpu_context_wrapper.dart';\nimport 'package:flutter_gpu/gpu.dart' as gpu;\n\n/// {@template texture}\n/// Base texture [Resource], represents an image/texture on the GPU.\n/// {@endtemplate}\nclass Texture extends Resource<gpu.Texture> {\n  final ByteData sourceData;\n  final int width;\n  final int height;\n  final PixelFormat format;\n\n  /// {@macro texture}\n  Texture(\n    this.sourceData, {\n    required this.width,\n    required this.height,\n    this.format = PixelFormat.rgba8888,\n  });\n\n  @override\n  gpu.Texture createResource() {\n    return GpuContextWrapper(gpu.gpuContext).createTexture(\n      gpu.StorageMode.hostVisible,\n      width,\n      height,\n      format: switch (format) {\n        PixelFormat.rgba8888 => gpu.PixelFormat.r8g8b8a8UNormInt,\n        PixelFormat.bgra8888 => gpu.PixelFormat.b8g8r8a8UNormInt,\n        PixelFormat.rgbaFloat32 => gpu.PixelFormat.r32g32b32a32Float,\n        _ => throw UnsupportedError('Unsupported pixel format: $format'),\n      },\n    )..overwrite(sourceData);\n  }\n\n  Image toImage() => resource.asImage();\n\n  /// A transparent single pixel texture.\n  static final empty = ColorTexture(const Color(0x00000000));\n\n  /// A white single pixel texture.\n  static final standard = ColorTexture(const Color(0xFFFFFFFF));\n}\n"
  },
  {
    "path": "packages/flame_3d/lib/src/resources/texture.dart",
    "content": "export 'texture/color_texture.dart';\nexport 'texture/image_texture.dart';\nexport 'texture/texture.dart';\n"
  },
  {
    "path": "packages/flame_3d/pubspec.yaml",
    "content": "name: flame_3d\nresolution: workspace\ndescription: Experimental 3D support for the Flame Engine\nversion: 0.1.1+7\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_3d\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  collection: ^1.19.1\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  flutter_gpu:\n    sdk: flutter\n  meta: ^1.12.0\n  ordered_set: ^8.0.0\n  vector_math: ^2.1.4\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flame_test: ^2.2.3\n  flutter_test:\n    sdk: flutter\n\nflutter:\n  assets:\n    - assets/shaders/\n"
  },
  {
    "path": "packages/flame_3d/shaders/spatial_material.frag",
    "content": "#version 460 core\n\n// implementation based on https://learnopengl.com/PBR/Lighting\n\n// #define NUM_LIGHTS 8\n#define PI 3.14159265359\n#define EPSILON 0.0001\n\nin vec2 fragTexCoord;\nin vec4 fragColor;\nin vec3 fragPosition;\nsmooth in vec3 fragNormal;\n\nout vec4 outColor;\n\nuniform sampler2D albedoTexture;  // Albedo texture\n\n// material info\n\nuniform Material {\n  vec4 albedoColor;\n  float metallic;\n  float roughness;\n} material;\n\n// light info\n\nuniform AmbientLight {\n  vec4 color;\n  float intensity;\n} ambientLight;\n\nuniform LightsInfo {\n  float numLights;\n} lightsInfo;\n\n// uniform Light {\n//   vec3 position;\n//   vec3 color;\n//   float intensity;\n// } lights[NUM_LIGHTS];\n\nuniform Light0 {\n  vec3 position;\n  vec4 color;\n  float intensity;\n} light0;\n\nuniform Light1 {\n  vec3 position;\n  vec4 color;\n  float intensity;\n} light1;\n\nuniform Light2 {\n  vec3 position;\n  vec4 color;\n  float intensity;\n} light2;\n\n// camera info\n\nuniform Camera {\n  vec3 position;\n} camera;\n\nvec3 fresnelSchlick(float cosTheta, vec3 f0) {\n  return f0 + (1.0 - f0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);\n}\n\nfloat distributionGGX(vec3 normal, vec3 halfwayDir, float roughness) {\n  float a = roughness * roughness;\n  float a2 = a * a;\n  float num = a2;\n\n  float NdotH = max(dot(normal, halfwayDir), 0.0);\n  float NdotH2 = NdotH * NdotH;\n  float b = (NdotH2 * (a2 - 1.0) + 1.0);\n  float denom = PI * b * b;\n\n  return num / denom;\n}\n\nfloat geometrySchlickGGX(float NdotV, float roughness) {\n  float r = (roughness + 1.0);\n  float k = (r * r) / 8.0;\n\n  float num = NdotV;\n  float denom = NdotV * (1.0 - k) + k;\n\n  return num / denom;\n}\n\nfloat geometrySmith(vec3 normal, vec3 viewDir, vec3 lightDir, float roughness) {\n  float NdotV = max(dot(normal, viewDir), 0.0);\n  float NdotL = max(dot(normal, lightDir), 0.0);\n  float ggx2 = geometrySchlickGGX(NdotV, roughness);\n  float ggx1 = geometrySchlickGGX(NdotL, roughness);\n\n  return ggx1 * ggx2;\n}\n\nvec3 processLight(\n  vec3 lightPos,\n  vec3 lightColor,\n  float lightIntensity,\n  vec3 baseColor,\n  vec3 normal,\n  vec3 viewDir,\n  vec3 diffuse\n) {\n  vec3 lightDirVec = lightPos - fragPosition;\n  vec3 lightDir = normalize(lightDirVec);\n  float distance = length(lightDirVec) + EPSILON;\n  vec3 halfwayDir = normalize(viewDir + lightDir);\n\n  float attenuation = lightIntensity / (distance * distance);\n  vec3 radiance = lightColor * attenuation;\n\n  // cook-torrance brdf\n  float ndf = distributionGGX(normal, halfwayDir, material.roughness);\n  float g = geometrySmith(normal, viewDir, lightDir, material.roughness);\n  vec3 f = fresnelSchlick(max(dot(halfwayDir, viewDir), 0.0), diffuse);\n\n  vec3 kS = f; // reflection/specular fraction\n  vec3 kD = (vec3(1.0) - kS) * (1.0 - material.metallic); // refraction/diffuse fraction\n\n  vec3 numerator = ndf * g * f;\n  float denominator = 4.0 * max(dot(normal, viewDir), 0.0) * max(dot(normal, lightDir), 0.0) + EPSILON;\n  vec3 specular = numerator / denominator;\n\n  // add to outgoing radiance Lo\n  float NdotL = max(dot(normal, lightDir), 0.0);\n  return (kD * baseColor / PI + specular) * radiance * NdotL;\n}\n\nvoid main() {\n  vec3 normal = normalize(fragNormal);\n  vec3 viewDir = normalize(camera.position - fragPosition);\n\n  vec3 baseColor = material.albedoColor.rgb;\n  baseColor *= texture(albedoTexture, fragTexCoord).rgb;\n\n  vec3 f0 = vec3(0.04);\n  vec3 diffuse = mix(f0, baseColor, material.metallic);\n\n  vec3 kS = fresnelSchlick(max(dot(normal, viewDir), 0.0), diffuse);\n  vec3 kD = (vec3(1.0) - kS) * (1.0 - material.metallic);\n  vec3 ambient = kD * baseColor * ambientLight.color.rgb * ambientLight.intensity;\n\n  vec3 lo = vec3(0.0);\n\n  if (lightsInfo.numLights > 0) {\n    vec3 light0Pos = light0.position;\n    vec3 light0Color = light0.color.rgb;\n    float light0Intensity = light0.intensity;\n\n    lo += processLight(light0Pos, light0Color, light0Intensity, baseColor, normal, viewDir, diffuse);\n  }\n\n  if (lightsInfo.numLights > 1) {\n    vec3 light1Pos = light1.position;\n    vec3 light1Color = light1.color.rgb;\n    float light1Intensity = light1.intensity;\n\n    lo += processLight(light1Pos, light1Color, light1Intensity, baseColor, normal, viewDir, diffuse);\n  }\n\n  if (lightsInfo.numLights > 2) {\n    vec3 light2Pos = light2.position;\n    vec3 light2Color = light2.color.rgb;\n    float light2Intensity = light2.intensity;\n\n    lo += processLight(light2Pos, light2Color, light2Intensity, baseColor, normal, viewDir, diffuse);\n  }\n\n  vec3 color = ambient + lo;\n\n  color = color / (color + vec3(1.0));\n  color = pow(color, vec3(1.0 / 2.2));\n\n  outColor = vec4(color, 1.0);\n}"
  },
  {
    "path": "packages/flame_3d/shaders/spatial_material.vert",
    "content": "#version 460 core\n\nin vec3 vertexPosition;\nin vec2 vertexTexCoord;\nin vec4 vertexColor;\nin vec3 vertexNormal;\nin vec4 vertexJoints;\nin vec4 vertexWeights;\n\nout vec2 fragTexCoord;\nout vec4 fragColor;\nout vec3 fragPosition;\nout vec3 fragNormal;\n\nuniform VertexInfo {\n  mat4 model;\n  mat4 view;\n  mat4 projection;\n} vertex_info;\n\nuniform JointMatrices {\n  mat4 joint0;\n  mat4 joint1;\n  mat4 joint2;\n  mat4 joint3;\n  mat4 joint4;\n  mat4 joint5;\n  mat4 joint6;\n  mat4 joint7;\n  mat4 joint8;\n  mat4 joint9;\n  mat4 joint10;\n  mat4 joint11;\n  mat4 joint12;\n  mat4 joint13;\n  mat4 joint14;\n  mat4 joint15;\n} joints;\n\nmat4 jointMat(float jointIndex) {\n  if (jointIndex == 0.0) {\n    return joints.joint0;\n  } else if (jointIndex == 1.0) {\n    return joints.joint1;\n  } else if (jointIndex == 2.0) {\n    return joints.joint2;\n  } else if (jointIndex == 3.0) {\n    return joints.joint3;\n  } else if (jointIndex == 4.0) {\n    return joints.joint4;\n  } else if (jointIndex == 5.0) {\n    return joints.joint5;\n  } else if (jointIndex == 6.0) {\n    return joints.joint6;\n  } else if (jointIndex == 7.0) {\n    return joints.joint7;\n  } else if (jointIndex == 8.0) {\n    return joints.joint8;\n  } else if (jointIndex == 9.0) {\n    return joints.joint9;\n  } else if (jointIndex == 10.0) {\n    return joints.joint10;\n  } else if (jointIndex == 11.0) {\n    return joints.joint11;\n  } else if (jointIndex == 12.0) {\n    return joints.joint12;\n  } else if (jointIndex == 13.0) {\n    return joints.joint13;\n  } else if (jointIndex == 14.0) {\n    return joints.joint14;\n  } else if (jointIndex == 15.0) {\n    return joints.joint15;\n  } else {\n    return mat4(0.0);\n  }\n}\n\nmat4 computeSkinMatrix() {\n  if (vertexWeights.x == 0.0 && vertexWeights.y == 0.0 && vertexWeights.z == 0.0 && vertexWeights.w == 0.0) {\n    // no weights, skip skinning\n    return mat4(1.0);\n  }\n\n  return vertexWeights.x * jointMat(vertexJoints.x) +\n    vertexWeights.y * jointMat(vertexJoints.y) +\n    vertexWeights.z * jointMat(vertexJoints.z) +\n    vertexWeights.w * jointMat(vertexJoints.w);\n}\n\nvoid main() {\n  mat4 skinMatrix = computeSkinMatrix();\n  vec3 position = (skinMatrix * vec4(vertexPosition, 1.0)).xyz;\n  vec3 normal = normalize((skinMatrix * vec4(vertexNormal, 0.0)).xyz);\n\n  // Calculate the modelview projection matrix\n  mat4 modelViewProjection = vertex_info.projection * vertex_info.view * vertex_info.model;\n\n  // Transform the vertex position\n  gl_Position = modelViewProjection * vec4(position, 1.0);\n\n  // Pass the interpolated values to the fragment shader\n  fragTexCoord = vertexTexCoord;\n  fragColor = vertexColor;\n\n  // Calculate the world-space position and normal\n  fragPosition = vec3(vertex_info.model * vec4(position, 1.0));\n  fragNormal = mat3(transpose(inverse(vertex_info.model))) * normal;\n}\n"
  },
  {
    "path": "packages/flame_3d/test/components/line_3d_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame_3d/components.dart';\nimport 'package:flame_3d/core.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Line 3D Mesh Component', () {\n    test('can create vertical line', () {\n      final line = Line3D.generate(\n        start: Vector3(0, -1, 0),\n        end: Vector3(0, 1, 0),\n        color: const Color(0xFFFFFFFF),\n      );\n\n      expect(\n        line.transform.transformMatrix,\n        equals(Matrix4.identity()),\n      );\n    });\n\n    test('can create horizontal line', () {\n      final line = Line3D.generate(\n        start: Vector3(-1, 0, 0),\n        end: Vector3(1, 0, 0),\n        color: const Color(0xFFFFFFFF),\n      );\n\n      expect(\n        line.transform.transformMatrix,\n        closeToMatrix4(\n          Matrix4.columns(\n            Vector4(0, -1, 0, 0),\n            Vector4(1, 0, 0, 0),\n            Vector4(0, 0, 1, 0),\n            Vector4(0, 0, 0, 1),\n          ),\n          10e-6,\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_3d/test/mesh/cone_mesh_test.dart",
    "content": "import 'package:flame_3d/resources.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Cone Mesh', () {\n    test('can create a simple cone', () {\n      const segments = 17;\n\n      final cone = ConeMesh(\n        radius: 1.0,\n        height: 1.0,\n        segments: segments,\n        material: SpatialMaterial(),\n      );\n\n      expect(\n        cone.surfaces,\n        hasLength(2),\n      );\n\n      expect(\n        cone.surfaces.map((s) => s.vertexCount).toSet(),\n        equals({segments + 1}),\n      );\n\n      expect(\n        cone.surfaces.map((s) => s.indexCount).toSet(),\n        equals({segments * 3}),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_3d/test/quaternion_extensions_test.dart",
    "content": "import 'package:flame_3d/game.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Quaternion extensions', () {\n    test('can lerp', () {\n      final a = Quaternion(1, 2, 3, 4);\n      final b = Quaternion(3, 4, 5, 6);\n\n      final result = a.lerp(b, 0.5);\n      expect(result.x, 2);\n      expect(result.y, 3);\n      expect(result.z, 4);\n      expect(result.w, 5);\n    });\n\n    test('can slerp', () {\n      const angle1 = 1.2;\n      const angle2 = 0.7;\n      const angle3 = angle2 + _epsilon * 10;\n\n      final axis = Vector3(1, 0, 0);\n\n      final quaternion1 = Quaternion.axisAngle(axis, angle1);\n      final quaternion2 = Quaternion.axisAngle(axis, angle2);\n      final quaternion3 = Quaternion.axisAngle(axis, angle3);\n\n      final slerp1 = QuaternionUtils.slerp(quaternion1, quaternion2, 0.5);\n      expect(\n        angle1 - 0.5 * (angle1 - angle2),\n        closeTo(slerp1.radians, _epsilon),\n      );\n\n      final slerp2 = QuaternionUtils.slerp(quaternion2, quaternion3, 0.75);\n      expect(\n        angle2 + 0.75 * (angle3 - angle2),\n        closeTo(slerp2.radians, _epsilon),\n      );\n\n      final slerp3 = QuaternionUtils.slerp(quaternion2, quaternion2, 0.5);\n      expect(angle2, closeTo(slerp3.radians, _epsilon));\n    });\n\n    test('slerp edge cases', () {\n      const angle1 = 1.2;\n      const angle2 = angle1 + _epsilon;\n\n      final axis = Vector3(1, 0, 0);\n\n      final quaternion1 = Quaternion.axisAngle(axis, angle1);\n      final quaternion2 = Quaternion.axisAngle(axis, angle2);\n\n      final slerp1 = QuaternionUtils.slerp(quaternion1, quaternion2, 0);\n      expect(angle1, closeTo(slerp1.radians, _epsilon));\n\n      final slerp2 = QuaternionUtils.slerp(quaternion1, quaternion2, 1);\n      expect(angle2, closeTo(slerp2.radians, _epsilon));\n    });\n  });\n}\n\nconst _epsilon = 10e-6;\n"
  },
  {
    "path": "packages/flame_3d/test/resources/shader/uniform_binding_test.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:flame_3d/core.dart';\nimport 'package:flame_3d/resources.dart';\nimport 'package:flame_3d/src/resources/shader.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('uniform bindings', () {\n    test('can bind a vec3 a slot', () {\n      final slot = UniformSlot.value('Vertex', {'position'});\n      final shader = _createShader([slot]);\n\n      shader.setVector3('Vertex.position', Vector3(7, 8, 9));\n\n      final bytes = shader.instances['Vertex']!.resource as ByteBuffer;\n      final result = Vector3.fromBuffer(bytes, 0);\n      expect(result, Vector3(7, 8, 9));\n    });\n\n    test('can bind multiple vector slots', () {\n      final slot = UniformSlot.value('AmbientLight', {'color', 'position'});\n      final shader = _createShader([slot]);\n\n      shader.setVector3('AmbientLight.position', Vector3(7, 8, 9));\n      shader.setVector4('AmbientLight.color', Vector4(4, 3, 2, 1));\n\n      final bytes = shader.instances['AmbientLight']!.resource as ByteBuffer;\n\n      final color = Vector4.fromBuffer(bytes, 0);\n      expect(color, Vector4(4, 3, 2, 1));\n\n      final position = Vector3.fromBuffer(bytes, color.storage.lengthInBytes);\n      expect(position, Vector3(7, 8, 9));\n    });\n\n    test('can bind a mat4 a slot', () {\n      final slot = UniformSlot.value('Vertex', {'camera'});\n      final shader = _createShader([slot]);\n\n      shader.setMatrix4('Vertex.camera', Matrix4.identity());\n\n      final bytes = shader.instances['Vertex']!.resource as ByteBuffer;\n      final result = Matrix4.fromBuffer(bytes, 0);\n      expect(result, Matrix4.identity());\n    });\n\n    test('can bind a vec3 to an array slot', () {\n      final slot = UniformSlot.array('Light', {'position'});\n      final shader = _createShader([slot]);\n\n      shader.setVector3('Light[0].position', Vector3(7, 8, 9));\n\n      final bytes = shader.instances['Light']!.resource as ByteBuffer;\n      final result = Vector3.fromBuffer(bytes, 0);\n\n      expect(result, Vector3(7, 8, 9));\n    });\n\n    test('can bind multiple slots', () {\n      final slots = [\n        UniformSlot.value('Vertex', {'position'}),\n        UniformSlot.value('Material', {'color', 'metallic'}),\n        UniformSlot.array('Light', {'position', 'color'}),\n      ];\n      final shader = _createShader(slots);\n\n      shader.setVector3('Vertex.position', Vector3(1, 2, 3));\n      shader.setVector4('Material.color', Vector4(4, 3, 2, 1));\n      shader.setFloat('Material.metallic', 0.5);\n      shader.setVector3('Light[0].position', Vector3(11, 12, 13));\n      shader.setVector4('Light[0].color', Vector4(14, 15, 16, 17));\n      shader.setVector3('Light[1].position', Vector3(-1, -2, -3));\n      shader.setVector4('Light[1].color', Vector4(-11, -12, -13, -14));\n\n      final vertex = shader.instances['Vertex']!.resource as ByteBuffer;\n      final vertexResult = Vector3.fromBuffer(vertex, 0);\n      expect(vertexResult, Vector3(1, 2, 3));\n\n      final material = shader.instances['Material']!.resource as ByteBuffer;\n      final color = Vector4.fromBuffer(material, 0);\n      expect(color, Vector4(4, 3, 2, 1));\n      final metallic = Float32List.view(material, color.storage.lengthInBytes);\n      expect(metallic[0], 0.5);\n\n      final light = shader.instances['Light']!.resource as ByteBuffer;\n\n      var cursor = 0;\n\n      final light0Position = Vector3.fromBuffer(light, cursor);\n      expect(light0Position, Vector3(11, 12, 13));\n      cursor += light0Position.storage.lengthInBytes;\n\n      final light0Color = Vector4.fromBuffer(light, cursor);\n      expect(light0Color, Vector4(14, 15, 16, 17));\n      cursor += light0Color.storage.lengthInBytes;\n\n      final light1Position = Vector3.fromBuffer(light, cursor);\n      expect(light1Position, Vector3(-1, -2, -3));\n      cursor += light1Position.storage.lengthInBytes;\n\n      final light1Color = Vector4.fromBuffer(light, cursor);\n      expect(light1Color, Vector4(-11, -12, -13, -14));\n    });\n  });\n}\n\nShader _createShader(List<UniformSlot> slots) {\n  return Shader(\n    asset: 'none',\n    name: '-test-',\n    slots: slots,\n  );\n}\n"
  },
  {
    "path": "packages/flame_3d/test/transform_3d_test.dart",
    "content": "import 'package:flame_3d/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nconst epsilon = 1e-6;\n\nvoid main() {\n  group('Transform3D', () {\n    test('position, rotation and scale can be composed and decomposed', () {\n      final position = Vector3(1, 2, 3);\n      final rotation = Quaternion(0.1, 0.2, 0.3, 0.4).normalized();\n      final scale = Vector3(4, 5, 6);\n\n      final matrix = Matrix4.compose(position, rotation, scale);\n\n      final transform = Transform3D.fromMatrix4(matrix);\n      expect(transform.position, closeToVector3(position, epsilon));\n      expect(transform.rotation, closeToQuaternion(rotation, epsilon));\n      expect(transform.scale, closeToVector3(scale, epsilon));\n    });\n\n    test('can set from matrix or transform', () {\n      final transform = Transform3D();\n\n      final matrix1 = Matrix4.compose(\n        Vector3(1, 2, 3),\n        Quaternion(0.1, 0.2, 0.3, 0.4).normalized(),\n        Vector3(4, 5, 6),\n      );\n\n      transform.setFromMatrix4(matrix1);\n      expect(\n        transform.transformMatrix,\n        closeToMatrix4(matrix1, epsilon),\n      );\n\n      final matrix2 = Matrix4.compose(\n        Vector3(7, 8, 9),\n        Quaternion(0.5, 0.6, 0.7, 0.8).normalized(),\n        Vector3(10, 11, 12),\n      );\n      transform.setFrom(Transform3D.fromMatrix4(matrix2));\n\n      expect(\n        transform.transformMatrix,\n        closeToMatrix4(matrix2, epsilon),\n      );\n    });\n\n    test('can create matrix4 using some parameters with sensible defaults', () {\n      final matrix1 = Transform3D.matrix4(\n        position: Vector3(1, 2, 3),\n      );\n      final transform1 = Transform3D.fromMatrix4(matrix1);\n      expect(transform1.position, closeToVector3(Vector3(1, 2, 3), epsilon));\n      expect(\n        transform1.rotation,\n        closeToQuaternion(Quaternion.identity(), epsilon),\n      );\n      expect(transform1.scale, closeToVector3(Vector3.all(1), epsilon));\n\n      final transform2 = Transform3D.compose(\n        rotation: Quaternion(0.1, 0.2, 0.3, 0.4).normalized(),\n      );\n      expect(transform2.position, closeToVector3(Vector3.zero(), epsilon));\n      expect(\n        transform2.rotation,\n        closeToQuaternion(Quaternion(0.1, 0.2, 0.3, 0.4).normalized(), epsilon),\n      );\n      expect(transform2.scale, closeToVector3(Vector3.all(1), epsilon));\n\n      final matrix3 = Transform3D.matrix4(\n        scale: Vector3(4, 5, 6),\n      );\n      final transform3 = Transform3D()..setFromMatrix4(matrix3);\n      expect(transform3.position, closeToVector3(Vector3.zero(), epsilon));\n      expect(\n        transform3.rotation,\n        closeToQuaternion(Quaternion.identity(), epsilon),\n      );\n      expect(transform3.scale, closeToVector3(Vector3(4, 5, 6), epsilon));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_3d/test/vector2_extensions_test.dart",
    "content": "import 'package:flame_3d/core.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Vector2 extensions', () {\n    test('can convert back and forth to mutable', () {\n      final src = Vector2(1, 2);\n\n      final immutable = src.immutable;\n      expect(immutable.x, 1);\n      expect(immutable.y, 2);\n      expect(immutable.storage, [1, 2]);\n\n      final mutable = immutable.mutable;\n      expect(mutable.x, 1);\n      expect(mutable.y, 2);\n    });\n\n    test('can lerp', () {\n      final a = Vector2(1, 2);\n      final b = Vector2(3, 4);\n\n      final result = a.lerp(b, 0.5);\n      expect(result.x, 2);\n      expect(result.y, 3);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_3d/test/vector3_extensions_test.dart",
    "content": "import 'package:flame_3d/game.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Vector3 extensions', () {\n    test('can convert back and forth to mutable', () {\n      final src = Vector3(1, 2, 3);\n\n      final immutable = src.immutable;\n      expect(immutable.x, 1);\n      expect(immutable.y, 2);\n      expect(immutable.z, 3);\n      expect(immutable.storage, [1, 2, 3]);\n\n      final mutable = immutable.mutable;\n      expect(mutable.x, 1);\n      expect(mutable.y, 2);\n      expect(mutable.z, 3);\n    });\n\n    test('can subtract immutable vector3', () {\n      final a = Vector3(0, 10, 0).immutable;\n      final b = Vector3(1, 2, 3);\n\n      expect((a - b).storage, [-1, 8, -3]);\n      expect((a - b.immutable).storage, [-1, 8, -3]);\n    });\n\n    test('can lerp', () {\n      final a = Vector3(1, 2, 3);\n      final b = Vector3(3, 4, 5);\n\n      final result = a.lerp(b, 0.5);\n      expect(result.x, 2);\n      expect(result.y, 3);\n      expect(result.z, 4);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_3d/test/vector4_extensions_test.dart",
    "content": "import 'package:flame_3d/game.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Vector4 extensions', () {\n    test('can convert back and forth to mutable', () {\n      final src = Vector4(1, 2, 3, 4);\n\n      final immutable = src.immutable;\n      expect(immutable.x, 1);\n      expect(immutable.y, 2);\n      expect(immutable.z, 3);\n      expect(immutable.w, 4);\n      expect(immutable.storage, [1, 2, 3, 4]);\n\n      final mutable = immutable.mutable;\n      expect(mutable.x, 1);\n      expect(mutable.y, 2);\n      expect(mutable.z, 3);\n      expect(mutable.w, 4);\n    });\n\n    test('can lerp', () {\n      final a = Vector4(1, 2, 3, 4);\n      final b = Vector4(3, 4, 5, 6);\n\n      final result = a.lerp(b, 0.5);\n      expect(result.x, 2);\n      expect(result.y, 3);\n      expect(result.z, 4);\n      expect(result.w, 5);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_audio/CHANGELOG.md",
    "content": "## 2.12.0\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n - **FEAT**: Support package argument in asset loading methods and widgets ([#3835](https://github.com/flame-engine/flame/issues/3835)). ([3f6f95b0](https://github.com/flame-engine/flame/commit/3f6f95b0225df9e4035c74de6cfad501e8c45167))\n\n## 2.11.14\n\n - Update a dependency to the latest release.\n\n## 2.11.13\n\n - Update a dependency to the latest release.\n\n## 2.11.12\n\n - Update a dependency to the latest release.\n\n## 2.11.11\n\n - **DOCS**: Deprecate TapDetector in favour of TapCallbacks ([#2886](https://github.com/flame-engine/flame/issues/2886)). ([b173697b](https://github.com/flame-engine/flame/commit/b173697bfb7ea61287251c43cd3c9d2fdb448fe3))\n\n## 2.11.10\n\n - Update a dependency to the latest release.\n\n## 2.11.9\n\n - Update a dependency to the latest release.\n\n## 2.11.8\n\n - Update a dependency to the latest release.\n\n## 2.11.7\n\n - Update a dependency to the latest release.\n\n## 2.11.6\n\n - Update a dependency to the latest release.\n\n## 2.11.5\n\n - Update a dependency to the latest release.\n\n## 2.11.4\n\n - Update a dependency to the latest release.\n\n## 2.11.3\n\n - Update a dependency to the latest release.\n\n## 2.11.2\n\n - Update a dependency to the latest release.\n\n## 2.11.1\n\n - **REFACTOR**(flame_audio): Set AudioContext for AudioPool only ([#3511](https://github.com/flame-engine/flame/issues/3511)). ([d5ae35f2](https://github.com/flame-engine/flame/commit/d5ae35f2bbd214159fcb81e2e94e45085bdc2e66))\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 2.11.0\n\n - **FEAT**(audio): Set audio context AudioContextConfigFocus.mixWithOthers by default ([#3483](https://github.com/flame-engine/flame/issues/3483)). ([762e0ad8](https://github.com/flame-engine/flame/commit/762e0ad8423e7bf3920139ca71a03b186d09c063))\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 2.10.8\n\n - Update a dependency to the latest release.\n\n## 2.10.7\n\n - Update a dependency to the latest release.\n\n## 2.10.6\n\n - Update a dependency to the latest release.\n\n## 2.10.5\n\n - Update a dependency to the latest release.\n\n## 2.10.4\n\n - Update a dependency to the latest release.\n\n## 2.10.3\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 2.10.2\n\n - **DOCS**: Update flame_audio readme ([#3119](https://github.com/flame-engine/flame/issues/3119)). ([843984de](https://github.com/flame-engine/flame/commit/843984dee5f5f6afd351ef29ad2adb39650f30bb))\n\n## 2.10.1\n\n - Update a dependency to the latest release.\n\n## 2.10.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 2.1.8\n\n - Update a dependency to the latest release.\n\n## 2.1.7\n\n - Update a dependency to the latest release.\n\n## 2.1.6\n\n - Update a dependency to the latest release.\n\n## 2.1.5\n\n - Update a dependency to the latest release.\n\n## 2.1.4\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n## 2.1.3\n\n - Update a dependency to the latest release.\n\n## 2.1.2\n\n - **REFACTOR**: Remove unnecessary 'async' keyword across the codebase [DCM] ([#2803](https://github.com/flame-engine/flame/issues/2803)). ([2dfe0e5a](https://github.com/flame-engine/flame/commit/2dfe0e5a431213c7148ab6389e3e8c8dc49fbf3d))\n - **REFACTOR**: Mark semantically final variables as final (or const) proper [DCM] ([#2783](https://github.com/flame-engine/flame/issues/2783)). ([71f7b475](https://github.com/flame-engine/flame/commit/71f7b475e33dd6fa7224c4a3ab29ffdb89702c34))\n - **FIX**: Remove deprecations for 1.10.0 ([#2809](https://github.com/flame-engine/flame/issues/2809)). ([5b67b8f1](https://github.com/flame-engine/flame/commit/5b67b8f14ad4fdb38a249d0a41ecba49ba2fcc44))\n\n## 2.1.1\n\n - Update a dependency to the latest release.\n\n## 2.1.0\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **FEAT**(flame_audio): Adding an easy way of updating the prefix from FlameAudio ([#2751](https://github.com/flame-engine/flame/issues/2751)). ([d2c9dcec](https://github.com/flame-engine/flame/commit/d2c9dcecbe661896ba8c84d81b9500cdfa8c78c8))\n\n## 2.0.5\n\n - Update a dependency to the latest release.\n\n## 2.0.4\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n\n## 2.0.3\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n## 2.0.2\n\n - **FIX**: Release instead of dispose audioplayer in play ([#2513](https://github.com/flame-engine/flame/issues/2513)). ([e699b259](https://github.com/flame-engine/flame/commit/e699b259e99619bb97fe370ce0679157e65eb42b))\n\n## 2.0.1\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n## 2.0.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FEAT**: Update AudioPlayers to ^4.0.0 ([#2482](https://github.com/flame-engine/flame/issues/2482)). ([47372087](https://github.com/flame-engine/flame/commit/47372087f218e9c00d0fec82084f6edc7cbee5af))\n\nMigration instructions:\n\nAudioPool has moved to AudioPlayers, but we still export it from `flame_audio`,\nso the only thing you have to do if you import AudioPool directly is to\nchange the import to:\n`import 'package:flame_audio/flame_audio.dart';`\n\n## 1.4.1\n\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n\n## 1.4.0\n\n - Bump to audioplayers v3.0.0\n \n## 1.3.5\n\n - Update a dependency to the latest release.\n\n## 1.3.4\n\n## 1.3.3\n\n## 1.3.2\n\n - Update a dependency to the latest release.\n\n## 1.3.1\n\n - Update a dependency to the latest release.\n\n## 1.3.0\n\n> Note: This release has breaking changes.\n\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **BREAKING** **FEAT**: Update flame_audio to AP 1.0.0 ([#1724](https://github.com/flame-engine/flame/issues/1724)). ([d6bf920d](https://github.com/flame-engine/flame/commit/d6bf920d28eea5f08adcba2601104271078e7a3d))\n\n## 1.2.0\n\n> Note: This release has breaking changes.\n\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **BREAKING** **FEAT**: Update flame_audio to AP 1.0.0 ([#1724](https://github.com/flame-engine/flame/issues/1724)). ([d6bf920d](https://github.com/flame-engine/flame/commit/d6bf920d28eea5f08adcba2601104271078e7a3d))\n\n## 1.1.0\n\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **FIX**: Removed warnings using flutter v3 ([#1640](https://github.com/flame-engine/flame/issues/1640)). ([69214827](https://github.com/flame-engine/flame/commit/69214827a0edb563468951256eccecab408f89df))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n\n## 1.0.2\n\n - Update a dependency to the latest release.\n\n## 1.0.1\n\n - Graduate package to a stable release. See pre-releases prior to this version for changelog entries.\n\n## 1.0.1-releasecandidate.1\n\n## [1.0.0]\n* Bump to Flame 1.0.0\n\n## [1.0.0-rc.15]\n* Bump audioplayers version to 0.20.1\n\n## [1.0.0-rc.1]\n* Moved project to the mono-repo\n* Updated flame, audioplayers\n\n## [0.1.0-rc5]\n* Updated `audioplayers` version\n* Updated Flame version\n* Add support to null safety\n\n## [0.1.0-rc4]\n* Update audioplayers version\n\n## [0.1.0-rc3]\n* Bump flame version to rc5, upgrade code to match, update other dependencies\n* Add linter and fix code to comply\n\n## [0.1.0-rc2]\n* Bump flame version to rc2 and audioplayers version to 0.17\n\n## [0.1.0-rc1]\n* First real version, including all necessary files for 1.0.0\n\n## [0.0.1]\n* Empty release; in the future all flame audio related code will live here.\n"
  },
  {
    "path": "packages/flame_audio/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_audio/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nAdds audio support for <a href=\"https://github.com/flame-engine/flame\">Flame</a> using the <a href=\"https://github.com/luanpotter/audioplayers\">audioplayers</a> package.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_audio\" ><img src=\"https://img.shields.io/pub/v/flame_audio.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n# flame_audio\n\nThis package makes it easy to add audio capabilities to your games, integrating\n[Audioplayers](https://github.com/bluefireteam/audioplayers) features seamless into your Flame game\ncode.\n\nAdd this as a dependency to your Flame game if you want to play background music,\nambient sounds, sound effects, etc. For the full documentation, visit [flame_audio](https://docs.flame-engine.org/latest/bridge_packages/flame_audio/flame_audio.html).\n\n\n## How to use\n\nAdd sound files to `assets/audio`. Remember to run `pub get` after updating pubspec.yaml with:\n\n```dart\nassets:\n    - assets/audio\n```\n\n\n### General sounds\n\nUse these built-in methods to play sounds in your Flame game:\n\n```dart\nimport 'package:flame_audio/flame_audio.dart';\n\n// For shorter reused audio clips, like sound effects\nFlameAudio.play('explosion.mp3');\n\n// For looping an audio file\nFlameAudio.loop('music.mp3');\n\n// For playing a longer audio file\nFlameAudio.playLongAudio('music.mp3');\n\n// For looping a longer audio file\nFlameAudio.loopLongAudio('music.mp3');\n```\n\n\n### Background music\n\nStart by initializing FlameAudio bgm.\n\n```dart\nFlameAudio.bgm.initialize();\n```\n\nRemember to call dispose to remove the observer.\n\n```dart\nFlameAudio.bgm.dispose();\n```\n\nTo play a looping background music\n\n```dart\nimport 'package:flame_audio/flame_audio.dart';\n\n// play with optional volume param\nFlameAudio.bgm.play('music/world-map.mp3', volume: .25);\n```\n\nTo stop background music\n\n```dart\nFlameAudio.bgm.stop();\n```\n\nTo pause and resume background music\n\n```dart\nFlameAudio.bgm.pause();\nFlameAudio.bgm.resume();\n```\n\n\n### Caching\n\nYou can pre-load your sounds into the audioCache.\nThis prevents a delay for the first time an audio file is called.\nThe files are cached automatically after the first time.\n\n```dart\n// cache single track\nawait FlameAudio.audioCache.load('explosion.mp3');\n\n// cache multiple tracks\nawait FlameAudio.audioCache.loadAll(['explosion.mp3', 'music.mp3']);\n```\n\nTo clear the cache\n\n```dart\n// clear specific track\nFlameAudio.audioCache.clear('explosion.mp3');\n\n// clear whole cache\nFlameAudio.audioCache.clearCache();\n```\n\n\n### Audio pool\n\nUse AudioPools if you have extremely quick firing, repetitive or simultaneous sounds.\nTo create an AudioPool:\n\n```dart\nAudioPool audioPool = await FlameAudio.createPool('explosion.mp3', maxPlayers: 2);\naudioPool.start();\n```\n"
  },
  {
    "path": "packages/flame_audio/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    - public_member_api_docs\n"
  },
  {
    "path": "packages/flame_audio/example/CREDITS.md",
    "content": "# Credits\n\nCC-0 audio files for this examples obtained here:\n\n- sfx: [https://opengameart.org/content/63-digital-sound-effects-lasers-phasers-space-etc]\n- bg music: [https://opengameart.org/content/another-space-background-track]\n"
  },
  {
    "path": "packages/flame_audio/example/README.md",
    "content": "# flame audio example\n\nSimple project to showcase the usage of flame_audio\n"
  },
  {
    "path": "packages/flame_audio/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    public_member_api_docs: false\n"
  },
  {
    "path": "packages/flame_audio/example/lib/main.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame_audio/flame_audio.dart';\nimport 'package:flutter/widgets.dart' hide Animation;\n\nvoid main() {\n  runApp(GameWidget(game: AudioGame()));\n}\n\n/// This example game showcases three possible use cases:\n///\n/// 1. Use the static FlameAudio class to easily fire a sfx using the default\n/// configs for the button tap.\n/// 2. Uses a custom AudioPool for extremely efficient audio loading and pooling\n/// for tapping elsewhere.\n/// 3. Uses the Bgm utility for background music.\nclass AudioGame extends FlameGame with TapCallbacks {\n  static final Paint black = BasicPalette.black.paint();\n  static final Paint gray = const PaletteEntry(Color(0xFFCCCCCC)).paint();\n  static final TextPaint text = TextPaint(\n    style: TextStyle(color: BasicPalette.white.color),\n  );\n\n  late AudioPool pool;\n\n  @override\n  Future<void> onLoad() async {\n    pool = await FlameAudio.createPool(\n      'sfx/fire_2.mp3',\n      minPlayers: 3,\n      maxPlayers: 4,\n    );\n    startBgmMusic();\n  }\n\n  Rect get button => Rect.fromLTWH(20, size.y - 300, size.x - 40, 200);\n\n  Future<void> startBgmMusic() async {\n    await FlameAudio.bgm.initialize();\n    await FlameAudio.bgm.play('music/bg_music.ogg');\n  }\n\n  void fireOne() {\n    FlameAudio.play('sfx/fire_1.mp3');\n  }\n\n  void fireTwo() {\n    pool.start();\n  }\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    canvas.drawRect(size.toRect(), black);\n\n    text.render(\n      canvas,\n      '(click anywhere for 1)',\n      Vector2(size.x / 2, 200),\n      anchor: Anchor.topCenter,\n    );\n\n    canvas.drawRect(button, gray);\n\n    text.render(\n      canvas,\n      'click here for 2',\n      Vector2(size.x / 2, size.y - 200),\n      anchor: Anchor.bottomCenter,\n    );\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    if (button.containsPoint(event.canvasPosition)) {\n      fireTwo();\n    } else {\n      fireOne();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_audio/example/pubspec.yaml",
    "content": "name: flame_audio_example\nresolution: workspace\ndescription: Simple project to showcase the usage of flame_audio\n\npublish_to: 'none'\n\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_audio: ^2.12.0\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n\nflutter:\n  assets:\n    - assets/audio/music/bg_music.ogg\n    - assets/audio/sfx/fire_1.mp3\n    - assets/audio/sfx/fire_2.mp3\n"
  },
  {
    "path": "packages/flame_audio/lib/bgm.dart",
    "content": "import 'package:audioplayers/audioplayers.dart';\nimport 'package:flutter/widgets.dart';\n\n/// {@template _bgm}\n/// The looping background music class.\n///\n/// This class helps with looping background music management that reacts to\n/// application lifecycle state changes. On construction, the instance is added\n/// as an observer to the [WidgetsBinding] instance. A [dispose] function is\n/// provided in case this functionality needs to be unloaded but the app needs\n/// to keep running.\n/// {@endtemplate}\nclass Bgm extends WidgetsBindingObserver {\n  bool _isRegistered = false;\n\n  /// The [AudioPlayer] instance that is used to play the audio.\n  AudioPlayer audioPlayer;\n\n  /// Whether [Bgm] is playing or not.\n  bool isPlaying = false;\n\n  /// {@macro _bgm}\n  Bgm({AudioCache? audioCache})\n    : audioPlayer = AudioPlayer()\n        ..audioCache = audioCache ?? AudioCache.instance;\n\n  /// Registers a [WidgetsBinding] observer.\n  ///\n  /// This must be called for auto-pause and resume to work properly.\n  Future<void> initialize({AudioContext? audioContext}) async {\n    if (_isRegistered) {\n      return;\n    }\n    _isRegistered = true;\n\n    // Avoid requesting audio focus\n    audioContext ??= AudioContextConfig(\n      focus: AudioContextConfigFocus.mixWithOthers,\n    ).build();\n    await audioPlayer.setAudioContext(audioContext);\n\n    WidgetsBinding.instance.addObserver(this);\n  }\n\n  /// Dispose the [WidgetsBinding] observer.\n  Future<void> dispose() async {\n    await audioPlayer.dispose();\n    if (!_isRegistered) {\n      return;\n    }\n    WidgetsBinding.instance.removeObserver(this);\n    _isRegistered = false;\n  }\n\n  /// Plays and loops a background music file specified by [fileName].\n  ///\n  /// The volume can be specified in the optional named parameter [volume]\n  /// where `0` means off and `1` means max.\n  ///\n  /// It is safe to call this function even when a current BGM track is\n  /// playing.\n  Future<void> play(\n    String fileName, {\n    double volume = 1,\n    String? package,\n  }) async {\n    await audioPlayer.release();\n    await audioPlayer.setReleaseMode(ReleaseMode.loop);\n    await audioPlayer.setVolume(volume);\n    final path = package == null\n        ? fileName\n        : 'packages/$package/${audioPlayer.audioCache.prefix}$fileName';\n    if (package != null) {\n      final originalPrefix = audioPlayer.audioCache.prefix;\n      audioPlayer.audioCache.prefix = '';\n      await audioPlayer.setSource(AssetSource(path));\n      audioPlayer.audioCache.prefix = originalPrefix;\n    } else {\n      await audioPlayer.setSource(AssetSource(path));\n    }\n    await audioPlayer.resume();\n    isPlaying = true;\n  }\n\n  /// Stops the currently playing background music track (if any).\n  Future<void> stop() async {\n    isPlaying = false;\n    await audioPlayer.stop();\n  }\n\n  /// Resumes the currently played (but resumed) background music.\n  Future<void> resume() async {\n    isPlaying = true;\n    await audioPlayer.resume();\n  }\n\n  /// Pauses the background music without unloading or resetting the audio\n  /// player.\n  Future<void> pause() async {\n    isPlaying = false;\n    await audioPlayer.pause();\n  }\n\n  /// Handler for AppLifecycleState changes.\n  ///\n  /// This function handles the automatic pause and resume when the app\n  /// lifecycle state changes. There is NO NEED to call this function directly\n  /// directly.\n  @override\n  void didChangeAppLifecycleState(AppLifecycleState state) {\n    if (state == AppLifecycleState.resumed) {\n      if (isPlaying && audioPlayer.state == PlayerState.paused) {\n        audioPlayer.resume();\n      }\n    } else {\n      audioPlayer.pause();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_audio/lib/flame_audio.dart",
    "content": "import 'package:audioplayers/audioplayers.dart';\nimport 'package:flame_audio/bgm.dart';\n\nexport 'package:audioplayers/audioplayers.dart';\n\n/// A typedef for a function that creates an [AudioCache] instance.\ntypedef AudioCacheFactory = AudioCache Function({required String prefix});\n\n/// A typedef for a function that creates a [Bgm] instance.\ntypedef BgmFactory = Bgm Function({required AudioCache audioCache});\n\n/// This utility class holds static references to some global audio objects.\n///\n/// You can use as a helper to very simply play a sound or a background music.\n/// Alternatively you can create your own instances and control them yourself.\nclass FlameAudio {\n  /// The factory used to create the global [AudioCache] instance.\n  ///\n  /// Useful to override the default [AudioCache] constructor in testing\n  /// or edge cases where the developer needs control on how [AudioCache]\n  /// are created.\n  static AudioCacheFactory audioCacheFactory = AudioCache.new;\n\n  /// The factory used to create the global [Bgm] instance.\n  ///\n  /// Useful to override the default [Bgm] constructor in testing\n  /// or edge cases where the developer needs control on how [Bgm]\n  /// are created.\n  static BgmFactory bgmFactory = Bgm.new;\n\n  /// Access a shared instance of the [AudioCache] class.\n  static AudioCache audioCache = audioCacheFactory(\n    prefix: 'assets/audio/',\n  );\n\n  /// Access a shared instance of the [Bgm] class.\n  ///\n  /// This will use the same global audio cache from [FlameAudio].\n  static final Bgm bgm = bgmFactory(audioCache: audioCache);\n\n  /// Updates the prefix in the global [AudioCache] and [bgm] instances.\n  static void updatePrefix(String prefix) {\n    audioCache.prefix = prefix;\n    bgm.audioPlayer.audioCache.prefix = prefix;\n  }\n\n  static Future<AudioPlayer> _preparePlayer(\n    String file,\n    double volume,\n    ReleaseMode releaseMode,\n    PlayerMode playerMode, {\n    required AudioContext? audioContext,\n    String? package,\n  }) async {\n    final player = AudioPlayer();\n    if (package != null) {\n      player.audioCache = audioCacheFactory(prefix: '');\n    } else {\n      player.audioCache = audioCache;\n    }\n    audioContext ??= _defaultAudioContext;\n    await player.setAudioContext(audioContext);\n    await player.setReleaseMode(releaseMode);\n    final path = package == null\n        ? file\n        : 'packages/$package/${audioCache.prefix}$file';\n    await player.play(\n      AssetSource(path),\n      volume: volume,\n      mode: playerMode,\n    );\n    return player;\n  }\n\n  /// Plays a single run of the given [file], with a given [volume].\n  static Future<AudioPlayer> play(\n    String file, {\n    double volume = 1.0,\n    AudioContext? audioContext,\n    String? package,\n  }) {\n    return _preparePlayer(\n      file,\n      volume,\n      ReleaseMode.release,\n      PlayerMode.lowLatency,\n      audioContext: audioContext,\n      package: package,\n    );\n  }\n\n  /// Plays, and keeps looping the given [file].\n  static Future<AudioPlayer> loop(\n    String file, {\n    double volume = 1.0,\n    AudioContext? audioContext,\n    String? package,\n  }) {\n    return _preparePlayer(\n      file,\n      volume,\n      ReleaseMode.loop,\n      PlayerMode.lowLatency,\n      audioContext: audioContext,\n      package: package,\n    );\n  }\n\n  /// Plays a single run of the given file [file]\n  /// This method supports long audio files\n  static Future<AudioPlayer> playLongAudio(\n    String file, {\n    double volume = 1.0,\n    AudioContext? audioContext,\n    String? package,\n  }) {\n    return _preparePlayer(\n      file,\n      volume,\n      ReleaseMode.release,\n      PlayerMode.mediaPlayer,\n      audioContext: audioContext,\n      package: package,\n    );\n  }\n\n  /// Plays, and keep looping the given [file]\n  /// This method supports long audio files\n  ///\n  /// NOTE: Length audio files on Android have an audio gap between loop\n  /// iterations, this happens due to limitations on Android's native media\n  /// player features. If you need a gapless loop, prefer the loop method.\n  static Future<AudioPlayer> loopLongAudio(\n    String file, {\n    double volume = 1.0,\n    AudioContext? audioContext,\n    String? package,\n  }) {\n    return _preparePlayer(\n      file,\n      volume,\n      ReleaseMode.loop,\n      PlayerMode.mediaPlayer,\n      audioContext: audioContext,\n      package: package,\n    );\n  }\n\n  /// Creates a new Audio Pool using Flame's global Audio Cache.\n  static Future<AudioPool> createPool(\n    String sound, {\n    required int maxPlayers,\n    int minPlayers = 1,\n    AudioContext? audioContext,\n    String? package,\n  }) async {\n    audioContext ??= _defaultAudioContext;\n    final path = package == null\n        ? sound\n        : 'packages/$package/${audioCache.prefix}$sound';\n    return AudioPool.create(\n      source: AssetSource(path),\n      audioCache: package == null ? audioCache : audioCacheFactory(prefix: ''),\n      minPlayers: minPlayers,\n      maxPlayers: maxPlayers,\n      audioContext: audioContext,\n    );\n  }\n\n  static final AudioContext _defaultAudioContext = AudioContextConfig(\n    focus: AudioContextConfigFocus.mixWithOthers,\n  ).build();\n}\n"
  },
  {
    "path": "packages/flame_audio/pubspec.yaml",
    "content": "name: flame_audio\nresolution: workspace\ndescription: |\n  Audio support for the Flame game engine, basically a thin wrapper around the audioplayers package.\nversion: 2.12.0\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_audio\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - audio\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  audioplayers: ^6.2.0\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  synchronized: ^3.1.0\n\ndev_dependencies:\n  dartdoc: ^9.0.0\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n  mocktail: ^1.0.4\n"
  },
  {
    "path": "packages/flame_audio/test/flame_audio_test.dart",
    "content": "import 'package:flame_audio/bgm.dart';\nimport 'package:flame_audio/flame_audio.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass _MockAudioCache extends Mock implements AudioCache {}\n\nclass _MockAudioPlayer extends Mock implements AudioPlayer {}\n\nclass _MockBgmCache extends Mock implements Bgm {}\n\nvoid main() {\n  group('FlameAudio', () {\n    test('starts the audioCache with the default prefix', () {\n      expect(\n        FlameAudio.audioCache.prefix,\n        equals('assets/audio/'),\n      );\n    });\n\n    group('updatePrefix', () {\n      test('updates the prefix on both bgm and audioCache', () {\n        final audioCache = _MockAudioCache();\n\n        FlameAudio.audioCache = audioCache;\n\n        final bgm = _MockBgmCache();\n        final bgmAudioPlayer = _MockAudioPlayer();\n        when(() => bgm.audioPlayer).thenReturn(bgmAudioPlayer);\n        final bgmAudioCache = _MockAudioCache();\n        when(() => bgmAudioPlayer.audioCache).thenReturn(bgmAudioCache);\n\n        FlameAudio.bgmFactory = ({required AudioCache audioCache}) => bgm;\n\n        const newPrefix = 'newPrefix/';\n        FlameAudio.updatePrefix(newPrefix);\n\n        verify(() => audioCache.prefix = newPrefix).called(1);\n        verify(() => bgmAudioCache.prefix = newPrefix).called(1);\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/CHANGELOG.md",
    "content": "## 0.1.4+3\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 0.1.4+2\n\n - Update a dependency to the latest release.\n\n## 0.1.4+1\n\n - Update a dependency to the latest release.\n\n## 0.1.4\n\n - **FEAT**: Add `Blackboard` support for behavior trees ([#3756](https://github.com/flame-engine/flame/issues/3756)). ([955411d3](https://github.com/flame-engine/flame/commit/955411d34a6e85e25524b52938146578fa4aa3e3))\n\n## 0.1.3+17\n\n - Update a dependency to the latest release.\n\n## 0.1.3+16\n\n - Update a dependency to the latest release.\n\n## 0.1.3+15\n\n - Update a dependency to the latest release.\n\n## 0.1.3+14\n\n - Update a dependency to the latest release.\n\n## 0.1.3+13\n\n - Update a dependency to the latest release.\n\n## 0.1.3+12\n\n - Update a dependency to the latest release.\n\n## 0.1.3+11\n\n - Update a dependency to the latest release.\n\n## 0.1.3+10\n\n - Update a dependency to the latest release.\n\n## 0.1.3+9\n\n - Update a dependency to the latest release.\n\n## 0.1.3+8\n\n - Update a dependency to the latest release.\n\n## 0.1.3+7\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 0.1.3+6\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 0.1.3+5\n\n - Update a dependency to the latest release.\n\n## 0.1.3+4\n\n - Update a dependency to the latest release.\n\n## 0.1.3+3\n\n - Update a dependency to the latest release.\n\n## 0.1.3+2\n\n - Update a dependency to the latest release.\n\n## 0.1.3+1\n\n - Update a dependency to the latest release.\n\n## 0.1.3\n\n - **FIX**: Update version of lints to comply with new pub requirements ([#3223](https://github.com/flame-engine/flame/issues/3223)). ([1b0bee72](https://github.com/flame-engine/flame/commit/1b0bee726b5937f73d4be5e304bc8780aa3ca6f0))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n - **DOCS**: Fix capitalization of the Dart programming language on pubspec description field ([#3222](https://github.com/flame-engine/flame/issues/3222)). ([9404241e](https://github.com/flame-engine/flame/commit/9404241e8a14d8d510f693c8557ca62ed76bd390))\n\n## 0.1.2\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n\n## 0.1.1\n\n - **FEAT**: Add initial version of `behavior_tree` and `flame_behavior_tree` package ([#3045](https://github.com/flame-engine/flame/issues/3045)). ([faf2df4b](https://github.com/flame-engine/flame/commit/faf2df4b8c68015a1bfbdd96f93c950cb14963ef))\n\n"
  },
  {
    "path": "packages/flame_behavior_tree/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/flame_behavior_tree/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">This is a bridge package that integrates the <a href=\"https://github.com/flame-engine/flame/tree/main/packages/flame_behavior_tree/behavior_tree\">behavior_tree</a> dart package with <a href=\"https://flame-engine.org/\">Flame engine</a>.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_behavior_tree\" ><img src=\"https://img.shields.io/pub/v/flame_behavior_tree.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n\n## Features\n\nThis package provides a `HasBehaviorTree` mixin for Flame `Components`. It can be added to any\n`Component` and it takes care of ticking the behavior tree along with the component's update.\n\n\n## Getting started\n\nAdd this package to your Flutter project using:\n\n```bash\nflutter pub add flame_behavior_tree\n```\n\n\n## Usage\n\n- Add the `HasBehaviorTree` mixin to the component that wants to follow a certain AI behavior.\n\n  ```dart\n  class MyComponent extends Position with HasBehaviorTree {\n  \n  }\n  ```\n\n- Set-up a behavior tree and set its root as the `treeRoot` of the `HasBehaviorTree`.\n\n```dart\nclass MyComponent extends PositionComponent with HasBehaviorTree {\n  Future<void> onLoad() async {\n    treeRoot = Selector(\n      children: [\n        Sequence(children: [task1, condition, task2]),\n        Sequence(...),\n      ]\n    );\n    super.onLoad();\n  }\n}\n```\n\n- Increase the `tickInterval` to make the tree tick less frequently.\n\n```dart\nclass MyComponent extends PositionComponent with HasBehaviorTree {\n  Future<void> onLoad() async {\n    treeRoot = Selector(...);\n    tickInterval = 4;\n    super.onLoad();\n  }\n}\n```\n\n\n## Additional information\n\nWhen working with behavior trees, keep in mind that\n\n- nodes of a behavior tree do not necessarily update on every frame.\n- avoid storing data in nodes as much as possible because it can go out of sync with rest of the\ngame as nodes are not ticked on every frame.\n"
  },
  {
    "path": "packages/flame_behavior_tree/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options.yaml"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/CHANGELOG.md",
    "content": "## 0.1.5+1\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 0.1.5\n\n - **FEAT**: Add `Blackboard` support for behavior trees ([#3756](https://github.com/flame-engine/flame/issues/3756)). ([955411d3](https://github.com/flame-engine/flame/commit/955411d34a6e85e25524b52938146578fa4aa3e3))\n\n## 0.1.4\n - **CHORE**: Bump dependencies\n\n## 0.1.3+2\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 0.1.3+1\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 0.1.3\n\n - **FIX**: Update version of lints to comply with new pub requirements ([#3223](https://github.com/flame-engine/flame/issues/3223)). ([1b0bee72](https://github.com/flame-engine/flame/commit/1b0bee726b5937f73d4be5e304bc8780aa3ca6f0))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n - **DOCS**: Fix capitalization of the Dart programming language on pubspec description field ([#3222](https://github.com/flame-engine/flame/issues/3222)). ([9404241e](https://github.com/flame-engine/flame/commit/9404241e8a14d8d510f693c8557ca62ed76bd390))\n\n## 0.1.2\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n\n## 0.1.1\n\n - **FEAT**: Add initial version of `behavior_tree` and `flame_behavior_tree` package ([#3045](https://github.com/flame-engine/flame/issues/3045)). ([faf2df4b](https://github.com/flame-engine/flame/commit/faf2df4b8c68015a1bfbdd96f93c950cb14963ef))\n\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nThis package provides a simple and easy to use <a href=\"https://en.wikipedia.org/wiki/Behavior_tree\">behavior tree</a> API in pure dart.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/behavior_tree\" ><img src=\"https://img.shields.io/pub/v/behavior_tree.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n\nBehavior tree is a very common way of implementing AI behavior in game and robotics. Using this, you\ncan break-down a complex behavior of an in game AI, into multiple smaller nodes.\n\n\n## Features\n\n- Nodes\n  - Composite\n    - Sequence: Continues execution until one of the children fails.\n    - Selector: Continues execution until one of the children succeeds.\n  - Decorator\n    - Inverter: Flips the status of the child node.\n    - Limiter: Limits the number of ticks for child node.\n  - Task\n    - Task: Executes a given callback when ticked.\n    - AsyncTask: Executes an async callback when ticked.\n    - Condition: Checks a condition when ticked.\n\n\n## Getting started\n\nAdd this package to your dart project using,\n\n```bash\ndart pub add behavior_tree\n```\n\n\n## Usage\n\n- Create a behavior tree.\n\n```dart\nfinal treeRoot = Sequence(\n  children: [\n    Condition(() => isHungry),\n    Task(() => goToShop()),\n    Task(() => buyFood()),\n    Task(() => goToHome()),\n    Task(() => eatFood()),\n  ]\n);\n```\n\n- Tick the root node to update the tree.\n\n```dart\nfinal treeRoot = ...;\ntreeRoot.tick();\n```\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options.yaml\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/example/behavior_tree_example.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\n\nbool isHungry = true;\n\nvoid main() {\n  // Create a sequence of tasks\n  final treeRoot = Sequence(\n    children: [\n      Condition(() => isHungry),\n      Task(goToShop),\n      Task(buyFood),\n      Task(goToHome),\n      Task(eatFood),\n    ],\n  );\n\n  // Tick the tree\n  treeRoot.tick();\n}\n\nNodeStatus goToShop() {\n  // Go to the shop\n  return NodeStatus.success;\n}\n\nNodeStatus buyFood() {\n  // Buy food\n  return NodeStatus.success;\n}\n\nNodeStatus goToHome() {\n  // Go home\n  return NodeStatus.success;\n}\n\nNodeStatus eatFood() {\n  // Eat food\n  return NodeStatus.success;\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/lib/behavior_tree.dart",
    "content": "/// Behavior tree implementation in dart\nlibrary behavior_tree;\n\nexport 'src/base_node.dart';\nexport 'src/blackboard.dart';\nexport 'src/blackboard_provider.dart';\nexport 'src/composites/selector.dart';\nexport 'src/composites/sequence.dart';\nexport 'src/decorators/inverter.dart';\nexport 'src/decorators/limiter.dart';\nexport 'src/node.dart';\nexport 'src/tasks/async_task.dart';\nexport 'src/tasks/condition.dart';\nexport 'src/tasks/task.dart';\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/lib/src/base_node.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\nimport 'package:meta/meta.dart';\n\n/// A base class for all the nodes.\nabstract class BaseNode implements NodeInterface {\n  NodeStatus _status = NodeStatus.notStarted;\n\n  /// The parent node in the behavior tree.\n  ///\n  /// This is set automatically when nodes are added as children to composite\n  /// or decorator nodes. It enables nodes to query up the tree for the\n  /// blackboard without needing explicit propagation.\n  BaseNode? _parent;\n\n  /// The blackboard provider (only set on root node).\n  ///\n  /// This is typically set by components that implement [BlackboardProvider],\n  /// such as those using the HasBehaviorTree mixin. Only the root node needs\n  /// this set; child nodes will query up the tree.\n  BlackboardProvider? _blackboardProvider;\n\n  /// Gets the blackboard by querying up the tree hierarchy.\n  ///\n  /// Child nodes ask their parent for the blackboard, and the root node\n  /// fetches it from its [BlackboardProvider]. This means the blackboard\n  /// lives in the component, not in the tree nodes themselves.\n  ///\n  /// Example:\n  /// In a Flame component with HasBehaviorTree\n  /// ```dart\n  /// blackboard = Blackboard();\n  /// blackboard.set('health', 100);\n  /// treeRoot = Sequence(children: [...]);\n  /// ```\n  /// All nodes in the tree can now access the blackboard\n  Blackboard? get blackboard {\n    // If we have a provider (we're the root), get it from there.\n    // Otherwise, query our parent.\n    return _blackboardProvider?.blackboard ?? _parent?.blackboard;\n  }\n\n  /// Sets the blackboard provider on this node (only used on root).\n  ///\n  /// This is called automatically by mixins like HasBehaviorTree.\n  set blackboardProvider(BlackboardProvider? provider) {\n    _blackboardProvider = provider;\n  }\n\n  @override\n  NodeStatus get status => _status;\n\n  @override\n  set status(NodeStatus value) {\n    _status = value;\n  }\n\n  @override\n  @mustCallSuper\n  void reset() {\n    _status = NodeStatus.notStarted;\n  }\n\n  /// Internal method to set parent relationship when adding children.\n  @protected\n  void setParent(NodeInterface child) {\n    if (child is BaseNode) {\n      child._parent = this;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/lib/src/blackboard.dart",
    "content": "/// Shared memory for behavior tree nodes to read and write data.\n///\n/// The Blackboard provides a centralized location for storing and retrieving\n/// data that needs to be shared between nodes in a behavior tree.\nclass Blackboard {\n  final Map<String, dynamic> _data = {};\n\n  /// Gets a value from the blackboard.\n  ///\n  /// Returns the value associated with [key], or [defaultValue] if the key\n  /// doesn't exist. If no default is provided and the key doesn't exist,\n  /// throws a [StateError].\n  T get<T>(String key, {T? defaultValue}) {\n    if (!_data.containsKey(key)) {\n      if (defaultValue != null) {\n        return defaultValue;\n      }\n      throw StateError('Key \"$key\" not found in blackboard');\n    }\n    return _data[key] as T;\n  }\n\n  /// Sets a value in the blackboard.\n  ///\n  /// Associates [value] with [key]. If [key] already exists, its value is\n  /// replaced.\n  void set<T>(String key, T value) {\n    _data[key] = value;\n  }\n\n  /// Checks if a key exists in the blackboard.\n  bool has(String key) => _data.containsKey(key);\n\n  /// Removes a key and its value from the blackboard.\n  ///\n  /// Returns the value that was associated with [key], or null if [key]\n  /// was not in the blackboard.\n  dynamic remove(String key) => _data.remove(key);\n\n  /// Removes all entries from the blackboard.\n  void clear() => _data.clear();\n\n  /// Returns all keys in the blackboard.\n  Iterable<String> get keys => _data.keys;\n\n  /// Returns the number of key-value pairs in the blackboard.\n  int get length => _data.length;\n\n  /// Returns true if the blackboard is empty.\n  bool get isEmpty => _data.isEmpty;\n\n  /// Returns true if the blackboard is not empty.\n  bool get isNotEmpty => _data.isNotEmpty;\n\n  /// Creates a copy of this blackboard.\n  Blackboard copy() {\n    final copy = Blackboard();\n    copy._data.addAll(_data);\n    return copy;\n  }\n\n  @override\n  String toString() {\n    return 'Blackboard($_data)';\n  }\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/lib/src/blackboard_provider.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\n\n/// An interface for components that provide a blackboard to behavior trees.\n///\n/// This should typically be implemented by game components that have behavior\n/// trees, allowing the tree root to fetch the blackboard from its owning\n/// component.\nabstract interface class BlackboardProvider {\n  /// Gets the blackboard for this provider.\n  Blackboard? get blackboard;\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/lib/src/composites/selector.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\n\n/// A composite node that stops at its first non-failing child node.\nclass Selector extends BaseNode implements NodeInterface {\n  /// Creates a selector node for given [children] nodes.\n  Selector({List<NodeInterface>? children})\n    : _children = children ?? <NodeInterface>[] {\n    _children.forEach(setParent);\n  }\n\n  final List<NodeInterface> _children;\n\n  @override\n  void tick() {\n    for (final node in _children) {\n      node.tick();\n\n      if (node.status != NodeStatus.failure) {\n        status = node.status;\n        return;\n      }\n    }\n    status = NodeStatus.failure;\n  }\n\n  @override\n  void reset() {\n    for (final node in _children) {\n      node.reset();\n    }\n    super.reset();\n  }\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/lib/src/composites/sequence.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\n\n/// A composite node that stops at its first successful child node.\nclass Sequence extends BaseNode implements NodeInterface {\n  /// Creates a sequence node for given [children] nodes.\n  Sequence({List<NodeInterface>? children})\n    : _children = children ?? <NodeInterface>[] {\n    _children.forEach(setParent);\n  }\n\n  final List<NodeInterface> _children;\n\n  @override\n  void tick() {\n    for (final node in _children) {\n      node.tick();\n\n      if (node.status != NodeStatus.success) {\n        status = node.status;\n        return;\n      }\n    }\n    status = NodeStatus.success;\n  }\n\n  @override\n  void reset() {\n    for (final node in _children) {\n      node.reset();\n    }\n    super.reset();\n  }\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/lib/src/decorators/inverter.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\n\n/// A decorator node that inverts [child]'s status if it is not\n/// [NodeStatus.running].\nclass Inverter extends BaseNode implements NodeInterface {\n  /// Creates an inverter node for given [child] node.\n  Inverter(this.child) {\n    setParent(child);\n    _invertStatus();\n  }\n\n  /// The child node whose status needs to be inverted.\n  final NodeInterface child;\n\n  @override\n  void tick() {\n    child.tick();\n    _invertStatus();\n  }\n\n  void _invertStatus() {\n    status = switch (child.status) {\n      NodeStatus.notStarted => NodeStatus.notStarted,\n      NodeStatus.running => NodeStatus.running,\n      NodeStatus.success => NodeStatus.failure,\n      NodeStatus.failure => NodeStatus.success,\n    };\n  }\n\n  @override\n  void reset() {\n    super.reset();\n    child.reset();\n    _invertStatus();\n  }\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/lib/src/decorators/limiter.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\n\n/// A decorator node that limits the number of times [child] can be ticked.\nclass Limiter extends BaseNode implements NodeInterface {\n  /// Creates a limiter node for given [child] node and [limit].\n  ///\n  /// Once this node has been ticked [limit] number of times, it stops ticking\n  /// the child node. After this, [status] will keep returning the status of\n  /// child the last time it was ticked. This behavior can be overridden by\n  /// providing an optional [statusAfterLimit].\n  Limiter(\n    this.child,\n    this.limit, {\n    NodeStatus? statusAfterLimit,\n  }) : _statusAfterLimit = statusAfterLimit {\n    setParent(child);\n    status = (_tickCount < limit)\n        ? child.status\n        : _statusAfterLimit ?? child.status;\n  }\n\n  var _tickCount = 0;\n  final NodeStatus? _statusAfterLimit;\n\n  /// The child node whose ticks are to be limited.\n  final NodeInterface child;\n\n  /// The max number of times [child] can be ticked.\n  final int limit;\n\n  /// Returns the number of times [child] has been ticked.\n  int get tickCount => _tickCount;\n\n  @override\n  void tick() {\n    if (_tickCount < limit) {\n      child.tick();\n      ++_tickCount;\n    }\n    status = (_tickCount < limit)\n        ? child.status\n        : _statusAfterLimit ?? child.status;\n  }\n\n  @override\n  void reset() {\n    _tickCount = 0;\n    child.reset();\n    super.reset();\n  }\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/lib/src/node.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\n\n/// The valid values for status of a node.\nenum NodeStatus {\n  /// Indicates that the node has not been ticked yet.\n  notStarted,\n\n  /// Indicates that the node is running.\n  running,\n\n  /// Indicates that the node has completed successfully.\n  success,\n\n  /// Indicates that the node has failed.\n  failure,\n}\n\n/// An interface which all the nodes implement.\n///\n/// Some examples are [Selector], [Sequence], [Inverter] and [Limiter].\nabstract interface class NodeInterface {\n  /// Returns the current status of this node.\n  NodeStatus get status;\n\n  /// Sets the status of this node.\n  set status(NodeStatus value);\n\n  /// Updates the node and re-evaluates its status.\n  void tick();\n\n  /// Resets the node to its initial state.\n  void reset();\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/lib/src/tasks/async_task.dart",
    "content": "import 'dart:async';\n\nimport 'package:behavior_tree/behavior_tree.dart';\n\ntypedef AsyncTaskCallback = Future<NodeStatus> Function();\n\n/// This is a leaf node that will execute the given async task when ticked.\n/// While the callback is executing, this node will report [status] as\n/// [NodeStatus.running]. Once the callback finishes, the status will be updated\n/// to the returned value of the callback.\nclass AsyncTask extends BaseNode implements NodeInterface {\n  /// Creates an async task node for given [callback].\n  AsyncTask(AsyncTaskCallback callback) : _callback = callback;\n\n  final AsyncTaskCallback _callback;\n\n  @override\n  void tick() {\n    if (status != NodeStatus.running) {\n      status = NodeStatus.running;\n\n      _callback().then((returnedStatus) {\n        status = returnedStatus;\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/lib/src/tasks/condition.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\n\ntypedef ConditionCallback = bool Function();\n\n/// This is a leaf node that will updates its [status] based on\n/// [conditionCallback].\nclass Condition extends BaseNode {\n  /// Creates a condition node for given [conditionCallback].\n  Condition(this.conditionCallback);\n\n  /// The callback that will be executed when the condition is ticked.\n  final ConditionCallback conditionCallback;\n\n  @override\n  void tick() {\n    status = conditionCallback() ? NodeStatus.success : NodeStatus.failure;\n  }\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/lib/src/tasks/task.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\n\n/// The type of callback used by the [Task] node.\ntypedef TaskCallback = NodeStatus Function();\n\n/// This is a leaf node that will execute the given task when ticked.\nclass Task extends BaseNode implements NodeInterface {\n  /// Creates a task node for given [taskCallback].\n  Task(this.taskCallback);\n\n  /// The callback that will be executed when the task is ticked.\n  /// It should return the status of the task.\n  final TaskCallback taskCallback;\n\n  @override\n  void tick() => status = taskCallback();\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/pubspec.yaml",
    "content": "name: behavior_tree\nresolution: workspace\ndescription: A behavior tree implementation written in Dart. This package is designed to be used for implementing AI in games.\nversion: 0.1.5+1\nrepository: https://github.com/flame-engine/flame/tree/main/packages/flame_behavior_tree/behavior_tree\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  meta: ^1.12.0\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  mocktail: ^1.0.4\n  test: any\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/test/async_task_test.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('AsyncTask', () {\n    test('sets status to running when ticked.', () {\n      final asyncTask = AsyncTask(() async => NodeStatus.success);\n      asyncTask.tick();\n      expect(asyncTask.status, NodeStatus.running);\n    });\n\n    test('updates status to the returned value of the callback.', () async {\n      final asyncTask = AsyncTask(() async => NodeStatus.failure);\n      asyncTask.tick();\n      await Future.delayed(Duration.zero); // Wait for the callback to complete\n      expect(asyncTask.status, equals(NodeStatus.failure));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/test/blackboard_test.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('Blackboard', () {\n    late Blackboard blackboard;\n\n    setUp(() {\n      blackboard = Blackboard();\n    });\n\n    group('set and get', () {\n      test('can store and retrieve values', () {\n        blackboard.set('key', 42);\n        expect(blackboard.get<int>('key'), 42);\n      });\n\n      test('can store different types', () {\n        blackboard.set('int', 42);\n        blackboard.set('string', 'hello');\n        blackboard.set('bool', true);\n        blackboard.set('double', 3.14);\n\n        expect(blackboard.get<int>('int'), 42);\n        expect(blackboard.get<String>('string'), 'hello');\n        expect(blackboard.get<bool>('bool'), true);\n        expect(blackboard.get<double>('double'), 3.14);\n      });\n\n      test('can store complex objects', () {\n        final list = [1, 2, 3];\n        final map = {'a': 1, 'b': 2};\n\n        blackboard.set('list', list);\n        blackboard.set('map', map);\n\n        expect(blackboard.get<List<int>>('list'), list);\n        expect(blackboard.get<Map<String, int>>('map'), map);\n      });\n\n      test('can update existing values', () {\n        blackboard.set('key', 42);\n        expect(blackboard.get<int>('key'), 42);\n\n        blackboard.set('key', 100);\n        expect(blackboard.get<int>('key'), 100);\n      });\n\n      test('throws error for non-existent keys without default', () {\n        expect(\n          () => blackboard.get<int>('nonexistent'),\n          throwsA(isA<StateError>()),\n        );\n      });\n\n      test('returns default value for non-existent keys', () {\n        expect(blackboard.get<int>('nonexistent', defaultValue: 0), 0);\n      });\n    });\n\n    group('has', () {\n      test('returns true for existing keys', () {\n        blackboard.set('key', 42);\n        expect(blackboard.has('key'), isTrue);\n      });\n\n      test('returns false for non-existent keys', () {\n        expect(blackboard.has('nonexistent'), isFalse);\n      });\n\n      test('returns true even if value is null', () {\n        blackboard.set('key', null);\n        expect(blackboard.has('key'), isTrue);\n      });\n    });\n\n    group('remove', () {\n      test('removes existing keys', () {\n        blackboard.set('key', 42);\n        expect(blackboard.has('key'), isTrue);\n\n        blackboard.remove('key');\n        expect(blackboard.has('key'), isFalse);\n        expect(\n          () => blackboard.get<int>('key'),\n          throwsA(isA<StateError>()),\n        );\n      });\n\n      test('does nothing for non-existent keys', () {\n        expect(() => blackboard.remove('nonexistent'), returnsNormally);\n      });\n    });\n\n    group('clear', () {\n      test('removes all entries', () {\n        blackboard.set('key1', 1);\n        blackboard.set('key2', 2);\n        blackboard.set('key3', 3);\n\n        expect(blackboard.has('key1'), isTrue);\n        expect(blackboard.has('key2'), isTrue);\n        expect(blackboard.has('key3'), isTrue);\n\n        blackboard.clear();\n\n        expect(blackboard.has('key1'), isFalse);\n        expect(blackboard.has('key2'), isFalse);\n        expect(blackboard.has('key3'), isFalse);\n      });\n\n      test('works on empty blackboard', () {\n        expect(() => blackboard.clear(), returnsNormally);\n      });\n    });\n\n    group('copy', () {\n      test('creates independent copy', () {\n        blackboard.set('key1', 1);\n        blackboard.set('key2', 'hello');\n\n        final copy = blackboard.copy();\n\n        expect(copy.get<int>('key1'), 1);\n        expect(copy.get<String>('key2'), 'hello');\n\n        // Modifying copy doesn't affect original\n        copy.set('key1', 999);\n        expect(blackboard.get<int>('key1'), 1);\n        expect(copy.get<int>('key1'), 999);\n\n        // Modifying original doesn't affect copy\n        blackboard.set('key2', 'world');\n        expect(blackboard.get<String>('key2'), 'world');\n        expect(copy.get<String>('key2'), 'hello');\n      });\n\n      test('copies empty blackboard', () {\n        final copy = blackboard.copy();\n        expect(copy.has('anything'), isFalse);\n      });\n    });\n  });\n\n  group('Blackboard integration with nodes', () {\n    late Blackboard blackboard;\n    late _MockBlackboardProvider provider;\n\n    setUp(() {\n      blackboard = Blackboard();\n      provider = _MockBlackboardProvider(blackboard);\n    });\n\n    test('nodes can access blackboard through parent chain', () {\n      blackboard.set('value', 42);\n\n      final task = _TestTask();\n      final sequence = Sequence(children: [task]);\n\n      sequence.blackboardProvider = provider;\n\n      sequence.tick();\n\n      expect(task.accessedValue, 42);\n    });\n\n    test('nested nodes can access blackboard', () {\n      blackboard.set('value', 100);\n\n      final deepTask = _TestTask();\n      final innerSequence = Sequence(children: [deepTask]);\n      final outerSequence = Sequence(children: [innerSequence]);\n\n      outerSequence.blackboardProvider = provider;\n\n      outerSequence.tick();\n\n      expect(deepTask.accessedValue, 100);\n    });\n\n    test('nodes can modify blackboard', () {\n      blackboard.set('counter', 0);\n\n      final incrementTask = _IncrementTask();\n      final sequence = Sequence(children: [incrementTask]);\n\n      sequence.blackboardProvider = provider;\n\n      sequence.tick();\n      expect(blackboard.get<int>('counter'), 1);\n\n      sequence.reset();\n      sequence.tick();\n      expect(blackboard.get<int>('counter'), 2);\n    });\n\n    test('multiple nodes share same blackboard', () {\n      blackboard.set('shared', 0);\n\n      final task1 = _IncrementSharedTask();\n      final task2 = _IncrementSharedTask();\n      final task3 = _IncrementSharedTask();\n\n      final sequence = Sequence(children: [task1, task2, task3]);\n      sequence.blackboardProvider = provider;\n\n      sequence.tick();\n\n      expect(blackboard.get<int>('shared'), 3);\n    });\n\n    test('blackboard works with selectors', () {\n      blackboard.set('flag', false);\n\n      final checkTask = _CheckFlagTask();\n      final fallbackTask = _SetFlagTask();\n\n      final selector = Selector(children: [checkTask, fallbackTask]);\n      selector.blackboardProvider = provider;\n\n      // First tick: flag is false, so fallbackTask runs\n      selector.tick();\n      expect(blackboard.get<bool>('flag'), true);\n\n      // Second tick: flag is now true, so checkTask succeeds\n      selector.reset();\n      selector.tick();\n      expect(checkTask.wasExecuted, true);\n    });\n\n    test('blackboard works with inverter', () {\n      blackboard.set('condition', false);\n\n      final checkTask = _ConditionTask();\n      final inverter = Inverter(checkTask);\n      final sequence = Sequence(children: [inverter]);\n\n      sequence.blackboardProvider = provider;\n\n      sequence.tick();\n\n      // Condition is false, inverted to success\n      expect(sequence.status, NodeStatus.success);\n    });\n\n    test('blackboard works with limiter', () {\n      blackboard.set('executeCount', 0);\n\n      final countTask = _CountExecutionTask();\n      final limiter = Limiter(countTask, 3);\n      final sequence = Sequence(children: [limiter]);\n\n      sequence.blackboardProvider = provider;\n\n      // Execute 5 times, but task is limited to 3 executions\n      for (var i = 0; i < 5; i++) {\n        sequence.tick();\n      }\n\n      expect(blackboard.get<int>('executeCount'), 3);\n    });\n\n    test('nodes without provider get null blackboard', () {\n      final task = _TestTask();\n      final sequence = Sequence(children: [task]);\n\n      // No provider set\n      sequence.tick();\n\n      expect(task.accessedValue, isNull);\n    });\n  });\n}\n\n// Test helper classes\n\nclass _MockBlackboardProvider implements BlackboardProvider {\n  _MockBlackboardProvider(this.blackboard);\n\n  @override\n  final Blackboard blackboard;\n}\n\nclass _TestTask extends BaseNode {\n  int? accessedValue;\n\n  @override\n  void tick() {\n    accessedValue = blackboard?.get<int>('value');\n    status = NodeStatus.success;\n  }\n}\n\nclass _IncrementTask extends BaseNode {\n  @override\n  void tick() {\n    final current = blackboard?.get<int>('counter') ?? 0;\n    blackboard?.set('counter', current + 1);\n    status = NodeStatus.success;\n  }\n}\n\nclass _IncrementSharedTask extends BaseNode {\n  @override\n  void tick() {\n    final current = blackboard?.get<int>('shared') ?? 0;\n    blackboard?.set('shared', current + 1);\n    status = NodeStatus.success;\n  }\n}\n\nclass _CheckFlagTask extends BaseNode {\n  bool wasExecuted = false;\n\n  @override\n  void tick() {\n    wasExecuted = true;\n    final flag = blackboard?.get<bool>('flag') ?? false;\n    status = flag ? NodeStatus.success : NodeStatus.failure;\n  }\n}\n\nclass _SetFlagTask extends BaseNode {\n  @override\n  void tick() {\n    blackboard?.set('flag', true);\n    status = NodeStatus.success;\n  }\n}\n\nclass _ConditionTask extends BaseNode {\n  @override\n  void tick() {\n    final condition = blackboard?.get<bool>('condition') ?? false;\n    status = condition ? NodeStatus.success : NodeStatus.failure;\n  }\n}\n\nclass _CountExecutionTask extends BaseNode {\n  @override\n  void tick() {\n    final count = blackboard?.get<int>('executeCount') ?? 0;\n    blackboard?.set('executeCount', count + 1);\n    status = NodeStatus.success;\n  }\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/test/conditon_test.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('Condition', () {\n    test('status is success when condition returns true', () {\n      final condition = Condition(() => true);\n      condition.tick();\n      expect(condition.status, NodeStatus.success);\n    });\n\n    test('status is failure when condition returns false', () {\n      final condition = Condition(() => false);\n      condition.tick();\n      expect(condition.status, NodeStatus.failure);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/test/inverter_test.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('Inverter', () {\n    final alwaysFailure = _MockNode();\n    final alwaysSuccess = _MockNode();\n    final alwaysRunning = _MockNode();\n\n    setUp(() {\n      reset(alwaysFailure);\n      reset(alwaysSuccess);\n      reset(alwaysRunning);\n\n      when(() => alwaysFailure.status).thenReturn(NodeStatus.failure);\n      when(() => alwaysSuccess.status).thenReturn(NodeStatus.success);\n      when(() => alwaysRunning.status).thenReturn(NodeStatus.running);\n    });\n\n    test('can be instantiated.', () {\n      expect(() => Inverter(alwaysRunning), returnsNormally);\n    });\n\n    test('default status is inverted child status.', () {\n      final inverter = Inverter(alwaysSuccess);\n      expect(inverter.status, NodeStatus.failure);\n    });\n\n    test('inverts status of child.', () {\n      final inverter1 = Inverter(alwaysSuccess)..tick();\n      expect(inverter1.status, NodeStatus.failure);\n\n      final inverter2 = Inverter(alwaysFailure)..tick();\n      expect(inverter2.status, NodeStatus.success);\n    });\n\n    test('keeping running if child is running.', () {\n      final inverter = Inverter(alwaysRunning)..tick();\n      expect(inverter.status, NodeStatus.running);\n    });\n  });\n}\n\nclass _MockNode extends Mock implements NodeInterface {}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/test/limiter_test.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('Limiter', () {\n    final alwaysFailure = _MockNode();\n    final alwaysSuccess = _MockNode();\n    final alwaysRunning = _MockNode();\n\n    setUp(() {\n      reset(alwaysFailure);\n      reset(alwaysSuccess);\n      reset(alwaysRunning);\n\n      when(() => alwaysFailure.status).thenReturn(NodeStatus.failure);\n      when(() => alwaysSuccess.status).thenReturn(NodeStatus.success);\n      when(() => alwaysRunning.status).thenReturn(NodeStatus.running);\n    });\n\n    test('can be instantiated.', () {\n      expect(() => Limiter(alwaysRunning, 5), returnsNormally);\n    });\n\n    test('default status same as status of child.', () {\n      final limiter = Limiter(alwaysSuccess, 5);\n      expect(limiter.status, alwaysSuccess.status);\n    });\n\n    test('limits tick count of child.', () {\n      const limit = 5;\n      final limiter = Limiter(alwaysRunning, limit);\n\n      var count = 0;\n      while (count < 23) {\n        limiter.tick();\n        ++count;\n      }\n\n      expect(limiter.tickCount, limit);\n      expect(limiter.status, alwaysRunning.status);\n    });\n\n    test('overrides status after crossing limit.', () {\n      const limit = 5;\n      final failAfterLimit = Limiter(\n        alwaysSuccess,\n        limit,\n        statusAfterLimit: NodeStatus.failure,\n      );\n\n      final succeedAfterLimit = Limiter(\n        alwaysRunning,\n        limit,\n        statusAfterLimit: NodeStatus.success,\n      );\n\n      var count = 0;\n      while (count < 23) {\n        failAfterLimit.tick();\n        succeedAfterLimit.tick();\n        ++count;\n      }\n\n      expect(failAfterLimit.status, NodeStatus.failure);\n      expect(succeedAfterLimit.status, NodeStatus.success);\n    });\n  });\n}\n\nclass _MockNode extends Mock implements NodeInterface {}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/test/selector_test.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('Selector', () {\n    const nTries = 20;\n    final alwaysFailure = _MockNode();\n    final alwaysSuccess = _MockNode();\n    final alwaysRunning = _MockNode();\n    final successAfterTries = _StatusAfterNTries(nTries, NodeStatus.success);\n    final failureAfterTries = _StatusAfterNTries(nTries, NodeStatus.failure);\n\n    setUp(() {\n      reset(alwaysFailure);\n      reset(alwaysSuccess);\n      reset(alwaysRunning);\n\n      when(() => alwaysFailure.status).thenReturn(NodeStatus.failure);\n      when(() => alwaysSuccess.status).thenReturn(NodeStatus.success);\n      when(() => alwaysRunning.status).thenReturn(NodeStatus.running);\n\n      successAfterTries.reset();\n      failureAfterTries.reset();\n    });\n\n    test('can be instantiated without the children.', () {\n      expect(Selector.new, returnsNormally);\n    });\n\n    test('can be instantiated with the children.', () {\n      expect(\n        () => Selector(children: [Selector(), Selector()]),\n        returnsNormally,\n      );\n    });\n\n    test('default status is not started.', () {\n      final selector = Selector();\n      expect(selector.status, NodeStatus.notStarted);\n    });\n\n    test('can be ticked without the children.', () {\n      final selector = Selector();\n      expect(selector.tick, returnsNormally);\n    });\n\n    test('can be ticked with the children.', () {\n      final selector = Selector(children: [Selector(), Selector()]);\n      expect(selector.tick, returnsNormally);\n    });\n\n    test('succeeds if any one of the children succeeds.', () {\n      final selector = Selector(children: [alwaysFailure, alwaysSuccess])\n        ..tick();\n      expect(selector.status, NodeStatus.success);\n    });\n\n    test('fails if all of the children fail.', () {\n      final selector = Selector(children: [alwaysFailure, alwaysFailure])\n        ..tick();\n      expect(selector.status, NodeStatus.failure);\n    });\n\n    test('runs until all children fail.', () {\n      final selector = Selector(\n        children: [alwaysFailure, failureAfterTries],\n      );\n\n      var count = 0;\n      while (count <= nTries) {\n        selector.tick();\n\n        expect(\n          selector.status,\n          count == nTries ? NodeStatus.failure : NodeStatus.running,\n        );\n\n        ++count;\n      }\n\n      verify(alwaysFailure.tick).called(count);\n      expect(failureAfterTries.tickCount, count);\n    });\n\n    test('runs until one of the children succeeds.', () {\n      final selector = Selector(children: [successAfterTries, alwaysFailure]);\n\n      var count = 0;\n      while (count <= nTries) {\n        selector.tick();\n\n        expect(\n          selector.status,\n          count == nTries ? NodeStatus.success : NodeStatus.running,\n        );\n\n        ++count;\n      }\n\n      verifyNever(alwaysFailure.tick);\n      expect(successAfterTries.tickCount, count);\n    });\n  });\n}\n\nclass _MockNode extends Mock implements NodeInterface {}\n\nclass _StatusAfterNTries extends BaseNode implements NodeInterface {\n  _StatusAfterNTries(this.nTries, this.statusAfterTries);\n\n  final int nTries;\n  final NodeStatus statusAfterTries;\n\n  var _tickCount = 0;\n  int get tickCount => _tickCount;\n\n  @override\n  void tick() {\n    status = _tickCount++ < nTries ? NodeStatus.running : statusAfterTries;\n  }\n\n  @override\n  void reset() {\n    super.reset();\n    _tickCount = 0;\n  }\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/test/sequence_test.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('Sequence', () {\n    const nTries = 20;\n    final alwaysFailure = _MockNode();\n    final alwaysSuccess = _MockNode();\n    final alwaysRunning = _MockNode();\n    final successAfterTries = _StatusAfterNTries(nTries, NodeStatus.success);\n    final failureAfterTries = _StatusAfterNTries(nTries, NodeStatus.failure);\n\n    setUp(() {\n      reset(alwaysFailure);\n      reset(alwaysSuccess);\n      reset(alwaysRunning);\n\n      when(() => alwaysFailure.status).thenReturn(NodeStatus.failure);\n      when(() => alwaysSuccess.status).thenReturn(NodeStatus.success);\n      when(() => alwaysRunning.status).thenReturn(NodeStatus.running);\n\n      successAfterTries.reset();\n      failureAfterTries.reset();\n    });\n\n    test('can be instantiated without the children.', () {\n      expect(Sequence.new, returnsNormally);\n    });\n\n    test('can be instantiated with the children.', () {\n      expect(\n        () => Sequence(children: [Sequence(), Sequence()]),\n        returnsNormally,\n      );\n    });\n\n    test('default status is not started.', () {\n      final sequence = Sequence();\n      expect(sequence.status, NodeStatus.notStarted);\n    });\n\n    test('can be ticked without the children.', () {\n      final selector = Sequence();\n      expect(selector.tick, returnsNormally);\n    });\n\n    test('can be ticked with the children.', () {\n      final selector = Sequence(children: [Sequence(), Sequence()]);\n      expect(selector.tick, returnsNormally);\n    });\n\n    test('succeeds if all of the children succeed.', () {\n      final sequence = Sequence(children: [alwaysSuccess, alwaysSuccess])\n        ..tick();\n      expect(sequence.status, NodeStatus.success);\n    });\n\n    test('fails if any of the children fails.', () {\n      final sequence = Sequence(children: [alwaysSuccess, alwaysFailure])\n        ..tick();\n      expect(sequence.status, NodeStatus.failure);\n    });\n\n    test('runs until first failure.', () {\n      final sequence = Sequence(\n        children: [alwaysSuccess, failureAfterTries],\n      );\n\n      var count = 0;\n      while (count <= nTries) {\n        sequence.tick();\n\n        expect(\n          sequence.status,\n          count == nTries ? NodeStatus.failure : NodeStatus.running,\n        );\n\n        ++count;\n      }\n\n      verify(alwaysSuccess.tick).called(count);\n      expect(failureAfterTries.tickCount, count);\n    });\n\n    test('runs until all children succeed.', () {\n      final sequence = Sequence(children: [alwaysSuccess, successAfterTries]);\n\n      var count = 0;\n      while (count <= nTries) {\n        sequence.tick();\n\n        expect(\n          sequence.status,\n          count == nTries ? NodeStatus.success : NodeStatus.running,\n        );\n\n        ++count;\n      }\n\n      verify(alwaysSuccess.tick).called(count);\n      expect(successAfterTries.tickCount, count);\n    });\n  });\n}\n\nclass _MockNode extends Mock implements NodeInterface {}\n\nclass _StatusAfterNTries extends BaseNode implements NodeInterface {\n  _StatusAfterNTries(this.nTries, this.statusAfterTries);\n\n  final int nTries;\n  final NodeStatus statusAfterTries;\n\n  var _tickCount = 0;\n  int get tickCount => _tickCount;\n\n  @override\n  void tick() {\n    status = _tickCount++ < nTries ? NodeStatus.running : statusAfterTries;\n  }\n\n  @override\n  void reset() {\n    super.reset();\n    _tickCount = 0;\n  }\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/behavior_tree/test/task_test.dart",
    "content": "import 'package:behavior_tree/behavior_tree.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('Task', () {\n    test('returns the status returned by the task callback', () {\n      const expectedStatus = NodeStatus.success;\n      final task = Task(() => expectedStatus);\n\n      task.tick();\n      expect(task.status, equals(expectedStatus));\n    });\n\n    test('executes the task callback when ticked', () {\n      var executed = false;\n      final task = Task(() {\n        executed = true;\n        return NodeStatus.success;\n      });\n\n      task.tick();\n      expect(executed, isTrue);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options.yaml\n"
  },
  {
    "path": "packages/flame_behavior_tree/example/lib/main.dart",
    "content": "import 'dart:async';\n\nimport 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flame_behavior_tree/flame_behavior_tree.dart';\nimport 'package:flutter/material.dart';\n\ntypedef MyGame = FlameGame<GameWorld>;\nconst gameWidth = 320.0;\nconst gameHeight = 180.0;\n\nvoid main() {\n  runApp(const MainApp());\n}\n\nclass MainApp extends StatelessWidget {\n  const MainApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: Scaffold(\n        body: GameWidget<MyGame>.controlled(\n          gameFactory: () => MyGame(\n            world: GameWorld(),\n            camera: CameraComponent.withFixedResolution(\n              width: gameWidth,\n              height: gameHeight,\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass GameWorld extends World with HasGameReference {\n  @override\n  Future<void> onLoad() async {\n    game.camera.moveTo(Vector2(gameWidth * 0.5, gameHeight * 0.5));\n\n    final house = RectangleComponent(\n      size: Vector2(100, 100),\n      position: Vector2(gameWidth * 0.5, 10),\n      paint: BasicPalette.cyan.paint()\n        ..strokeWidth = 5\n        ..style = PaintingStyle.stroke,\n      anchor: Anchor.topCenter,\n    );\n\n    final door = Door(\n      size: Vector2(20, 4),\n      position: Vector2(40, house.size.y),\n      anchor: Anchor.centerLeft,\n    );\n\n    final agent = Agent(\n      door: door,\n      house: house,\n      position: Vector2(gameWidth * 0.76, gameHeight * 0.9),\n    );\n\n    await house.add(door);\n    await addAll([house, agent]);\n  }\n}\n\nclass Door extends RectangleComponent with TapCallbacks {\n  Door({super.position, super.size, super.anchor})\n    : super(paint: BasicPalette.brown.paint());\n\n  bool isOpen = false;\n  bool _isInProgress = false;\n  bool _isKnocking = false;\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    if (!_isInProgress) {\n      _isInProgress = true;\n      add(\n        RotateEffect.to(\n          isOpen ? 0 : -pi * 0.5,\n          EffectController(duration: 0.5, curve: Curves.easeInOut),\n          onComplete: () {\n            isOpen = !isOpen;\n            _isInProgress = false;\n          },\n        ),\n      );\n    }\n  }\n\n  void knock() {\n    if (!_isKnocking) {\n      _isKnocking = true;\n      add(\n        MoveEffect.by(\n          Vector2(0, -1),\n          EffectController(\n            alternate: true,\n            duration: 0.1,\n            repeatCount: 2,\n          ),\n          onComplete: () {\n            _isKnocking = false;\n          },\n        ),\n      );\n    }\n  }\n}\n\nclass Agent extends PositionComponent with HasBehaviorTree {\n  Agent({required this.door, required this.house, required Vector2 position})\n    : _startPosition = position.clone(),\n      super(position: position);\n\n  final Door door;\n  final PositionComponent house;\n  final Vector2 _startPosition;\n\n  @override\n  Future<void> onLoad() async {\n    await add(CircleComponent(radius: 3, anchor: Anchor.center));\n    _setupBehaviorTree();\n    super.onLoad();\n  }\n\n  void _setupBehaviorTree() {\n    // Initialize blackboard with agent state\n    blackboard = Blackboard();\n    blackboard!.set('isInside', false);\n    blackboard!.set('isAtTheDoor', false);\n    blackboard!.set('isAtCenterOfHouse', false);\n    blackboard!.set('isMoving', false);\n    blackboard!.set('wantsToGoOutside', false);\n\n    final walkTowardsDoorInside = WalkTowardsDoorInsideTask(\n      door: door,\n      agent: this,\n    );\n\n    final stepOutTheDoor = StepOutTheDoorTask(\n      door: door,\n      agent: this,\n    );\n\n    final walkTowardsInitialPosition = WalkTowardsInitialPositionTask(\n      startPosition: _startPosition,\n      agent: this,\n    );\n\n    final walkTowardsDoorOutside = WalkTowardsDoorOutsideTask(\n      door: door,\n      agent: this,\n    );\n\n    final walkTowardsCenterOfTheHouse = WalkTowardsCenterOfTheHouseTask(\n      house: house,\n      agent: this,\n    );\n\n    final checkIfDoorIsOpen = CheckIfDoorIsOpenCondition(door);\n    final knockTheDoor = KnockTheDoorTask(door);\n    final checkIfMoving = CheckMovingCondition();\n    final checkWantsToGoOutside = CheckWantsToGoOutsideCondition();\n    final checkIsInside = CheckIsInsideCondition();\n    final checkIsOutside = CheckIsOutsideCondition();\n\n    final goOutsideSequence = Sequence(\n      children: [\n        checkWantsToGoOutside,\n        Selector(\n          children: [\n            Sequence(\n              children: [\n                checkIsInside,\n                walkTowardsDoorInside,\n                Selector(\n                  children: [\n                    Sequence(\n                      children: [\n                        checkIfDoorIsOpen,\n                        stepOutTheDoor,\n                      ],\n                    ),\n                    knockTheDoor,\n                  ],\n                ),\n              ],\n            ),\n            walkTowardsInitialPosition,\n          ],\n        ),\n      ],\n    );\n\n    final goInsideSequence = Sequence(\n      children: [\n        Inverter(checkWantsToGoOutside),\n        Selector(\n          children: [\n            Sequence(\n              children: [\n                checkIsOutside,\n                walkTowardsDoorOutside,\n                Selector(\n                  children: [\n                    Sequence(\n                      children: [\n                        checkIfDoorIsOpen,\n                        walkTowardsCenterOfTheHouse,\n                      ],\n                    ),\n                    knockTheDoor,\n                  ],\n                ),\n              ],\n            ),\n          ],\n        ),\n      ],\n    );\n\n    treeRoot = Selector(\n      children: [\n        checkIfMoving,\n        goOutsideSequence,\n        goInsideSequence,\n      ],\n    );\n    tickInterval = 2;\n  }\n}\n\n// Custom behavior tree nodes that use the blackboard\n\nclass CheckMovingCondition extends BaseNode {\n  @override\n  void tick() {\n    final isMoving = blackboard?.get<bool>('isMoving') ?? false;\n    status = isMoving ? NodeStatus.success : NodeStatus.failure;\n  }\n}\n\nclass CheckWantsToGoOutsideCondition extends BaseNode {\n  @override\n  void tick() {\n    final wantsToGoOutside = blackboard?.get<bool>('wantsToGoOutside') ?? false;\n    status = wantsToGoOutside ? NodeStatus.success : NodeStatus.failure;\n  }\n}\n\nclass CheckIsInsideCondition extends BaseNode {\n  @override\n  void tick() {\n    final isInside = blackboard?.get<bool>('isInside') ?? false;\n    status = isInside ? NodeStatus.success : NodeStatus.failure;\n  }\n}\n\nclass CheckIsOutsideCondition extends BaseNode {\n  @override\n  void tick() {\n    final isInside = blackboard?.get<bool>('isInside') ?? false;\n    status = !isInside ? NodeStatus.success : NodeStatus.failure;\n  }\n}\n\nclass CheckIfDoorIsOpenCondition extends BaseNode {\n  CheckIfDoorIsOpenCondition(this.door);\n  final Door door;\n\n  @override\n  void tick() {\n    status = door.isOpen ? NodeStatus.success : NodeStatus.failure;\n  }\n}\n\nclass KnockTheDoorTask extends BaseNode {\n  KnockTheDoorTask(this.door);\n  final Door door;\n\n  @override\n  void tick() {\n    door.knock();\n    status = NodeStatus.success;\n  }\n}\n\nclass WalkTowardsDoorInsideTask extends BaseNode {\n  WalkTowardsDoorInsideTask({required this.door, required this.agent});\n  final Door door;\n  final Agent agent;\n\n  @override\n  void tick() {\n    final isAtTheDoor = blackboard?.get<bool>('isAtTheDoor') ?? false;\n\n    if (!isAtTheDoor) {\n      blackboard?.set('isMoving', true);\n\n      agent.add(\n        MoveEffect.to(\n          door.absolutePosition + Vector2(door.size.x * 0.8, -15),\n          EffectController(\n            duration: 3,\n            curve: Curves.easeInOut,\n          ),\n          onComplete: () {\n            blackboard?.set('isMoving', false);\n            blackboard?.set('isAtTheDoor', true);\n            blackboard?.set('isAtCenterOfHouse', false);\n          },\n        ),\n      );\n    }\n    status = isAtTheDoor ? NodeStatus.success : NodeStatus.running;\n  }\n}\n\nclass StepOutTheDoorTask extends BaseNode {\n  StepOutTheDoorTask({required this.door, required this.agent});\n  final Door door;\n  final Agent agent;\n\n  @override\n  void tick() {\n    final isInside = blackboard?.get<bool>('isInside') ?? false;\n\n    if (isInside) {\n      blackboard?.set('isMoving', true);\n      agent.add(\n        MoveEffect.to(\n          door.absolutePosition + Vector2(door.size.x * 0.5, 10),\n          EffectController(\n            duration: 2,\n            curve: Curves.easeInOut,\n          ),\n          onComplete: () {\n            blackboard?.set('isMoving', false);\n            blackboard?.set('isInside', false);\n          },\n        ),\n      );\n    }\n    status = !isInside ? NodeStatus.success : NodeStatus.running;\n  }\n}\n\nclass WalkTowardsInitialPositionTask extends BaseNode {\n  WalkTowardsInitialPositionTask({\n    required this.startPosition,\n    required this.agent,\n  });\n  final Vector2 startPosition;\n  final Agent agent;\n\n  @override\n  void tick() {\n    final isAtTheDoor = blackboard?.get<bool>('isAtTheDoor') ?? false;\n    final wantsToGoOutside = blackboard?.get<bool>('wantsToGoOutside') ?? false;\n\n    if (isAtTheDoor) {\n      blackboard?.set('isMoving', true);\n      blackboard?.set('isAtTheDoor', false);\n\n      agent.add(\n        MoveEffect.to(\n          startPosition,\n          EffectController(\n            duration: 3,\n            curve: Curves.easeInOut,\n          ),\n          onComplete: () {\n            blackboard?.set('isMoving', false);\n            blackboard?.set('wantsToGoOutside', false);\n          },\n        ),\n      );\n    }\n\n    status = !wantsToGoOutside ? NodeStatus.success : NodeStatus.running;\n  }\n}\n\nclass WalkTowardsDoorOutsideTask extends BaseNode {\n  WalkTowardsDoorOutsideTask({required this.door, required this.agent});\n  final Door door;\n  final Agent agent;\n\n  @override\n  void tick() {\n    final isAtTheDoor = blackboard?.get<bool>('isAtTheDoor') ?? false;\n\n    if (!isAtTheDoor) {\n      blackboard?.set('isMoving', true);\n      agent.add(\n        MoveEffect.to(\n          door.absolutePosition + Vector2(door.size.x * 0.5, 10),\n          EffectController(\n            duration: 3,\n            curve: Curves.easeInOut,\n          ),\n          onComplete: () {\n            blackboard?.set('isMoving', false);\n            blackboard?.set('isAtTheDoor', true);\n          },\n        ),\n      );\n    }\n    status = isAtTheDoor ? NodeStatus.success : NodeStatus.running;\n  }\n}\n\nclass WalkTowardsCenterOfTheHouseTask extends BaseNode {\n  WalkTowardsCenterOfTheHouseTask({required this.house, required this.agent});\n  final PositionComponent house;\n  final Agent agent;\n\n  @override\n  void tick() {\n    final isAtCenterOfHouse =\n        blackboard?.get<bool>('isAtCenterOfHouse') ?? false;\n\n    if (!isAtCenterOfHouse) {\n      blackboard?.set('isMoving', true);\n      blackboard?.set('isInside', true);\n\n      agent.add(\n        MoveEffect.to(\n          house.absoluteCenter,\n          EffectController(\n            duration: 3,\n            curve: Curves.easeInOut,\n          ),\n          onComplete: () {\n            blackboard?.set('isMoving', false);\n            blackboard?.set('wantsToGoOutside', true);\n            blackboard?.set('isAtTheDoor', false);\n            blackboard?.set('isAtCenterOfHouse', true);\n          },\n        ),\n      );\n    }\n    status = (blackboard?.get<bool>('isInside') ?? false)\n        ? NodeStatus.success\n        : NodeStatus.running;\n  }\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/example/pubspec.yaml",
    "content": "name: flame_behavior_tree_example\nresolution: workspace\ndescription: \"A simple demo to show usage of behavior trees in flame.\"\npublish_to: 'none'\nversion: 0.1.0\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_behavior_tree: ^0.1.4+3\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n\nflutter:\n  uses-material-design: true\n"
  },
  {
    "path": "packages/flame_behavior_tree/lib/flame_behavior_tree.dart",
    "content": "/// A bridge package that integrates behavior_tree package with flame.\nlibrary flame_behavior_tree;\n\nexport 'package:behavior_tree/behavior_tree.dart';\nexport 'src/has_behavior_tree.dart';\n"
  },
  {
    "path": "packages/flame_behavior_tree/lib/src/has_behavior_tree.dart",
    "content": "import 'dart:async';\n\nimport 'package:behavior_tree/behavior_tree.dart';\nimport 'package:flame/components.dart';\nimport 'package:flutter/foundation.dart';\n\n/// A mixin on [Component] to indicate that the component has a behavior tree.\n///\n/// Reference to the behavior tree for this component can be set or accessed\n/// via [treeRoot]. The update frequency of the tree can be reduced by\n/// increasing [tickInterval]. By default, the tree will be updated on every\n/// update of the component.\n///\n/// An optional [blackboard] can be provided to share data between nodes in\n/// the behavior tree. The blackboard is stored in this component and accessed\n/// by nodes through their parent chain, so it doesn't need to be stored in\n/// each node.\nmixin HasBehaviorTree<T extends NodeInterface> on Component\n    implements BlackboardProvider {\n  T? _treeRoot;\n  Timer? _timer;\n  double _tickInterval = 0;\n  Blackboard? _blackboard;\n\n  /// The delay between any two ticks of the behavior tree.\n  double get tickInterval => _tickInterval;\n  set tickInterval(double interval) {\n    _tickInterval = interval;\n\n    if (_tickInterval > 0) {\n      _timer ??= Timer(interval, repeat: true);\n      _timer?.limit = interval;\n    } else {\n      _timer?.onTick = null;\n      _timer = null;\n      _tickInterval = 0;\n    }\n  }\n\n  /// The blackboard for sharing data between nodes.\n  ///\n  /// If not set, nodes will receive null when they access the blackboard.\n  /// Create a blackboard and assign it to enable data sharing:\n  ///\n  /// ```dart\n  /// blackboard = Blackboard();\n  /// blackboard.set('health', 100);\n  /// ```\n  ///\n  /// The blackboard is stored in this component and accessed by nodes\n  /// through their parent chain, eliminating the need to store it in each node.\n  @override\n  Blackboard? get blackboard => _blackboard;\n  set blackboard(Blackboard? value) => _blackboard = value;\n\n  /// The root node of the behavior tree.\n  T get treeRoot => _treeRoot!;\n  set treeRoot(T value) {\n    _treeRoot = value;\n    // Set this component as the blackboard provider for the root node\n    if (value is BaseNode) {\n      value.blackboardProvider = this;\n    }\n    _timer?.onTick = _treeRoot!.tick;\n  }\n\n  @override\n  @mustCallSuper\n  Future<void> onLoad() async {\n    super.onLoad();\n    _timer?.onTick = _treeRoot?.tick;\n  }\n\n  @override\n  @mustCallSuper\n  void update(double dt) {\n    super.update(dt);\n    if (_tickInterval > 0) {\n      _timer?.update(dt);\n    } else {\n      _treeRoot?.tick();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_behavior_tree/pubspec.yaml",
    "content": "name: flame_behavior_tree\nresolution: workspace\ndescription: A bridge package that integrates behavior_tree package with flame.\nversion: 0.1.4+3\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_behavior_tree\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  behavior_tree: ^0.1.5+1\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flame_test: ^2.2.3\n  flutter_test:\n    sdk: flutter\n  mocktail: ^1.0.4\n"
  },
  {
    "path": "packages/flame_behavior_tree/test/has_behavior_tree_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_behavior_tree/flame_behavior_tree.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nvoid main() {\n  group('HasBehaviorTree', () {\n    final alwaysFailure = _MockNode();\n    final alwaysSuccess = _MockNode();\n    final alwaysRunning = _MockNode();\n\n    setUp(() {\n      reset(alwaysFailure);\n      reset(alwaysSuccess);\n      reset(alwaysRunning);\n\n      when(() => alwaysFailure.status).thenReturn(NodeStatus.failure);\n      when(() => alwaysSuccess.status).thenReturn(NodeStatus.success);\n      when(() => alwaysRunning.status).thenReturn(NodeStatus.running);\n    });\n\n    testWithFlameGame(\n      'updates with null tree.',\n      (game) async {\n        final component = _BehaviorTreeComponent();\n        expect(() => game.add(component), returnsNormally);\n      },\n    );\n\n    test('tick interval can be changed', () {\n      final component = _BehaviorTreeComponent();\n      expect(component.tickInterval, 0);\n\n      component.tickInterval = 3;\n      expect(component.tickInterval, 3);\n\n      component.tickInterval = -53;\n      expect(component.tickInterval, 0);\n    });\n\n    test('throws if treeNode is accessed before setting.', () {\n      final component = _BehaviorTreeComponent();\n      expect(() => component.treeRoot, throwsA(isA<TypeError>()));\n\n      component.treeRoot = _MockNode();\n      expect(() => component.treeRoot, returnsNormally);\n    });\n\n    testWithFlameGame(\n      'updates without errors with a valid tree.',\n      (game) async {\n        final component = _BehaviorTreeComponent()\n          ..treeRoot = Sequence(\n            children: [alwaysSuccess, alwaysFailure, alwaysRunning],\n          );\n\n        expect(() async => await game.add(component), returnsNormally);\n\n        await game.ready();\n        expect(() => game.update(10), returnsNormally);\n\n        verify(alwaysSuccess.tick).called(1);\n        verify(alwaysFailure.tick).called(1);\n        verifyNever(alwaysRunning.tick);\n      },\n    );\n\n    testWithFlameGame(\n      'tree updates at a slower rate.',\n      (game) async {\n        final component = _BehaviorTreeComponent()\n          ..treeRoot = Sequence(\n            children: [alwaysSuccess, alwaysFailure, alwaysRunning],\n          )\n          ..tickInterval = 1;\n\n        await game.add(component);\n        await game.ready();\n\n        const dt = 1 / 60;\n        const gameTime = 3.0;\n        var elapsedTime = 0.0;\n\n        while (elapsedTime < gameTime) {\n          game.update(dt);\n          elapsedTime += dt;\n        }\n\n        verify(alwaysSuccess.tick).called(gameTime.toInt());\n        verify(alwaysFailure.tick).called(gameTime.toInt());\n        verifyNever(alwaysRunning.tick);\n      },\n    );\n\n    group('Blackboard support', () {\n      test('blackboard can be set and retrieved', () {\n        final component = _BehaviorTreeComponent();\n        expect(component.blackboard, isNull);\n\n        final blackboard = Blackboard();\n        component.blackboard = blackboard;\n\n        expect(component.blackboard, same(blackboard));\n      });\n\n      test('blackboard can be set to null', () {\n        final component = _BehaviorTreeComponent();\n        final blackboard = Blackboard();\n        component.blackboard = blackboard;\n\n        component.blackboard = null;\n        expect(component.blackboard, isNull);\n      });\n\n      test('implements BlackboardProvider', () {\n        final component = _BehaviorTreeComponent();\n        expect(component, isA<BlackboardProvider>());\n      });\n\n      testWithFlameGame(\n        'nodes can access blackboard through component',\n        (game) async {\n          final component = _BehaviorTreeComponent();\n          final blackboard = Blackboard();\n          blackboard.set('testValue', 42);\n\n          component.blackboard = blackboard;\n\n          final task = _TestTask();\n          component.treeRoot = Sequence(children: [task]);\n\n          await game.add(component);\n          await game.ready();\n\n          game.update(0.1);\n\n          expect(task.retrievedValue, 42);\n        },\n      );\n\n      testWithFlameGame(\n        'nested nodes can access blackboard',\n        (game) async {\n          final component = _BehaviorTreeComponent();\n          final blackboard = Blackboard();\n          blackboard.set('nestedValue', 'hello');\n\n          component.blackboard = blackboard;\n\n          final deepTask = _TestTask();\n          final innerSequence = Sequence(children: [deepTask]);\n          final outerSequence = Sequence(children: [innerSequence]);\n          component.treeRoot = outerSequence;\n\n          await game.add(component);\n          await game.ready();\n\n          game.update(0.1);\n\n          expect(deepTask.retrievedValue, 'hello');\n        },\n      );\n\n      testWithFlameGame(\n        'nodes can modify blackboard',\n        (game) async {\n          final component = _BehaviorTreeComponent();\n          final blackboard = Blackboard();\n          blackboard.set('counter', 0);\n\n          component.blackboard = blackboard;\n\n          final incrementTask = _IncrementTask();\n          component.treeRoot = Sequence(children: [incrementTask]);\n\n          await game.add(component);\n          await game.ready();\n\n          game.update(0.1);\n          expect(blackboard.get<int>('counter'), 1);\n\n          component.treeRoot.reset();\n          game.update(0.1);\n          expect(blackboard.get<int>('counter'), 2);\n        },\n      );\n\n      testWithFlameGame(\n        'multiple nodes share same blackboard',\n        (game) async {\n          final component = _BehaviorTreeComponent();\n          final blackboard = Blackboard();\n          blackboard.set('shared', 0);\n\n          component.blackboard = blackboard;\n\n          final task1 = _IncrementSharedTask();\n          final task2 = _IncrementSharedTask();\n          final task3 = _IncrementSharedTask();\n\n          component.treeRoot = Sequence(children: [task1, task2, task3]);\n\n          await game.add(component);\n          await game.ready();\n\n          game.update(0.1);\n\n          expect(blackboard.get<int>('shared'), 3);\n        },\n      );\n\n      testWithFlameGame(\n        'nodes work without blackboard set',\n        (game) async {\n          final component = _BehaviorTreeComponent();\n          // No blackboard set\n\n          final task = _TestTask();\n          component.treeRoot = Sequence(children: [task]);\n\n          await game.add(component);\n          await game.ready();\n\n          expect(() => game.update(0.1), returnsNormally);\n          expect(task.retrievedValue, isNull);\n        },\n      );\n\n      testWithFlameGame(\n        'blackboard provider is set on root node',\n        (game) async {\n          final component = _BehaviorTreeComponent();\n          final blackboard = Blackboard();\n          component.blackboard = blackboard;\n\n          final rootNode = Sequence(children: []);\n          component.treeRoot = rootNode;\n\n          await game.add(component);\n          await game.ready();\n\n          // Root node should have access to blackboard\n          expect(rootNode.blackboard, same(blackboard));\n        },\n      );\n\n      testWithFlameGame(\n        'changing blackboard updates accessible data',\n        (game) async {\n          final component = _BehaviorTreeComponent();\n          final blackboard1 = Blackboard();\n          blackboard1.set('value', 'first');\n\n          component.blackboard = blackboard1;\n\n          final task = _TestTask();\n          component.treeRoot = Sequence(children: [task]);\n\n          await game.add(component);\n          await game.ready();\n\n          game.update(0.1);\n          expect(task.retrievedValue, 'first');\n\n          // Change to a different blackboard\n          final blackboard2 = Blackboard();\n          blackboard2.set('value', 'second');\n          component.blackboard = blackboard2;\n\n          component.treeRoot.reset();\n          game.update(0.1);\n          expect(task.retrievedValue, 'second');\n        },\n      );\n    });\n  });\n}\n\nclass _BehaviorTreeComponent extends Component with HasBehaviorTree {}\n\nclass _MockNode extends Mock implements NodeInterface {}\n\n// Test helper nodes for blackboard testing\n\nclass _TestTask extends BaseNode {\n  Object? retrievedValue;\n\n  @override\n  void tick() {\n    if (blackboard?.has('testValue') ?? false) {\n      retrievedValue = blackboard?.get('testValue');\n    } else if (blackboard?.has('nestedValue') ?? false) {\n      retrievedValue = blackboard?.get('nestedValue');\n    } else if (blackboard?.has('value') ?? false) {\n      retrievedValue = blackboard?.get('value');\n    }\n    status = NodeStatus.success;\n  }\n}\n\nclass _IncrementTask extends BaseNode {\n  @override\n  void tick() {\n    final current = blackboard?.get<int>('counter') ?? 0;\n    blackboard?.set('counter', current + 1);\n    status = NodeStatus.success;\n  }\n}\n\nclass _IncrementSharedTask extends BaseNode {\n  @override\n  void tick() {\n    final current = blackboard?.get<int>('shared') ?? 0;\n    blackboard?.set('shared', current + 1);\n    status = NodeStatus.success;\n  }\n}\n"
  },
  {
    "path": "packages/flame_behaviors/CHANGELOG.md",
    "content": "## 1.3.4\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 1.3.3\n\n - Update a dependency to the latest release.\n\n## 1.3.2\n\n - Update a dependency to the latest release.\n\n## 1.3.1\n\n - Update a dependency to the latest release.\n\n## 1.3.0\n\n - **FEAT**: Add flame_behaviors package ([#3717](https://github.com/flame-engine/flame/issues/3717)). ([e950d79e](https://github.com/flame-engine/flame/commit/e950d79e56bf5902f2a48367a1e899e9b8903dc4))\n\n# [1.2.0](https://github.com/VeryGoodOpenSource/flame_behaviors/compare/flame_behaviors-v1.1.0...flame_behaviors-v1.2.0) (2024-08-27)\n\n- chore: tighten dependencies ([#64](https://github.com/VeryGoodOpenSource/flame_behaviors/pull/64))\n- feat: Flame 1.19 support ([#69](https://github.com/VeryGoodOpenSource/flame_behaviors/pull/69))\n\n# [1.1.0](https://github.com/VeryGoodOpenSource/flame_behaviors/compare/flame_behaviors-v1.0.0...flame_behaviors-v1.1.0) (2024-01-11)\n\n### Features\n\n- add `priority` and `key` to the constructor ([#57](https://github.com/VeryGoodOpenSource/flame_behaviors/pull/57)) ([cc6cb4a](https://github.com/VeryGoodOpenSource/flame_behaviors/commit/cc6cb4a635109a74d5002d7e16d0e5b3d7e0dce6))\n\n# [1.0.0](https://github.com/VeryGoodOpenSource/flame_behaviors/compare/v0.2.0...flame_behaviors-1.0.0) (2023-10-18)\n\n### Breaking Changes\n\n- migrate to flame v1.7.0 ([#43](https://github.com/VeryGoodOpenSource/flame_behaviors/pull/43)) ([08580f6](https://github.com/VeryGoodOpenSource/flame_behaviors/commit/08580f656abb12f38c1b16913c9cf5397e2b95a8))\n- migrate to flame v1.10.0 ([#46](https://github.com/VeryGoodOpenSource/flame_behaviors/pull/46)) ([9963591](https://github.com/VeryGoodOpenSource/flame_behaviors/commit/9963591d4c0cc1da389ba8446740f8747549b775))\n\n### Bug Fixes\n\n- make `PropagatingCollisionBehavior` more open ([#42](https://github.com/VeryGoodOpenSource/flame_behaviors/pull/42)) ([4ae0553](https://github.com/VeryGoodOpenSource/flame_behaviors/commit/4ae05534458ec7b66caf04e87afc5e8c25fba9ae))\n\n# [0.2.0](https://github.com/VeryGoodOpenSource/flame_behaviors/compare/v0.1.1...v0.2.0) (2023-01-23)\n\n### Features\n\n- add dependabot ([#31](https://github.com/VeryGoodOpenSource/flame_behaviors/issues/31)) ([4b601a1](https://github.com/VeryGoodOpenSource/flame_behaviors/commit/4b601a1ff3a516e36ae850857f5c5e5a9de7303f))\n- update version constraints ([#26](https://github.com/VeryGoodOpenSource/flame_behaviors/issues/26)) ([05303be](https://github.com/VeryGoodOpenSource/flame_behaviors/commit/05303beeae80df6055f3b3fc8f5630f2643d40fe))\n\n### Breaking Changes\n- make the entity into a generic component ([#34](https://github.com/VeryGoodOpenSource/flame_behaviors/pull/34))([d09964](https://github.com/VeryGoodOpenSource/flame_behaviors/commit/d0996471370a0764331da82433a874a1edecba20))\n- update flame dependency to v1.6.0  ([#35](https://github.com/VeryGoodOpenSource/flame_behaviors/pull/35))([867d7a](https://github.com/VeryGoodOpenSource/flame_behaviors/commit/867d7a283980a643bd5c49eae4be147d4df3469e))\n\n## [0.1.1](https://github.com/VeryGoodOpenSource/flame_behaviors/compare/v0.1.0...v0.1.1) (2022-06-20)\n\n### Bug Fixes\n\n- `PropagatingCollisionBehavior` should also work for non entity components ([#20](https://github.com/VeryGoodOpenSource/flame_behaviors/issues/20)) ([ca5fc6c](https://github.com/VeryGoodOpenSource/flame_behaviors/commit/ca5fc6c2862d58348a7b2a72814f58d370161982))\n\n# [0.1.0](https://github.com/VeryGoodOpenSource/flame_behaviors/compare/v0.0.1-dev.1...v0.1.0) (2022-06-13)\n\n### Bug Fixes\n\n- entity behavior cache is never cleared ([#10](https://github.com/VeryGoodOpenSource/flame_behaviors/issues/10)) ([6751ae8](https://github.com/VeryGoodOpenSource/flame_behaviors/commit/6751ae85eefdc848dd8f4c6c221c3f5d49303aed))\n- missing arguments on entity ([#8](https://github.com/VeryGoodOpenSource/flame_behaviors/issues/8)) ([e161daf](https://github.com/VeryGoodOpenSource/flame_behaviors/commit/e161daf360f49423355b4822ff4342bad00c6977))\n\n### Features\n\n- add `hasBehavior` method ([#11](https://github.com/VeryGoodOpenSource/flame_behaviors/issues/11)) ([fb36bc6](https://github.com/VeryGoodOpenSource/flame_behaviors/commit/fb36bc67c152d17fa98b56f88f42b1d86452c4ab))\n- add touch based behaviors ([#7](https://github.com/VeryGoodOpenSource/flame_behaviors/issues/7)) ([f7f3d35](https://github.com/VeryGoodOpenSource/flame_behaviors/commit/f7f3d35cade614ca404dc84937777752dca3f5be))\n- initial `flame_behaviors` implementation ([#2](https://github.com/VeryGoodOpenSource/flame_behaviors/issues/2)) ([766ebe6](https://github.com/VeryGoodOpenSource/flame_behaviors/commit/766ebe6f398cdb96e93425d86713760c0664075d))\n- make the internal find behavior logic more clear on when it can find something ([#12](https://github.com/VeryGoodOpenSource/flame_behaviors/issues/12)) ([e778b00](https://github.com/VeryGoodOpenSource/flame_behaviors/commit/e778b00c06f0bdd3d973548458402e7a3fa051b1))\n- proxy `debugMode` down to individual behaviors ([#9](https://github.com/VeryGoodOpenSource/flame_behaviors/issues/9)) ([eaab29f](https://github.com/VeryGoodOpenSource/flame_behaviors/commit/eaab29f3fd17412072e975bd11ebf2828adf548a))\n\n## 0.0.1-dev.1 (2022-05-04)\n"
  },
  {
    "path": "packages/flame_behaviors/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_behaviors/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nFlame Behaviors applies separation of concerns to game logic in the form of Entities and Behaviors.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_behaviors\" ><img src=\"https://img.shields.io/pub/v/flame_behaviors.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n# flame_behaviors\n\n\nFlame Behaviors applies separation of concerns to game logic in the form of Entities and Behaviors.\n\nDeveloped with 💙 and 🔥 by [Very Good Ventures][very_good_ventures_link] 🦄\n\n---\n\n\n## Quick Start 🚀\n\n\n### Installing 🧑‍💻\n\nIn order to use Flame Behaviors you must have the [Flame package][flame_package_link] added to\nyour project.\n\n> **Note**: Flame Behaviors requires Flame `\">=1.10.0 <2.0.0\"`\n\n\n### Adding the package\n\n```shell\n# 📦 Install the flame_behaviors package from pub.dev\nflutter pub add flame_behaviors\n```\n\n\n## Creating Entities and Behaviors\n\nUse Flame Behaviors to define your entities in your game and their behavioral aspects. Or follow\nthe [Introduction to Flame Behaviors][flame_behaviors_article] article to get you familiar with\nthe concepts!\n\n\n## Documentation 📝\n\nView the [full documentation][flame_behaviors_documentation].\n\n[very_good_ventures_link]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=CLI\n[flame_package_link]: https://pub.dev/packages/flame\n[flame_behaviors_article]: https://verygood.ventures/blog/build-games-with-flame-behaviors\n[flame_behaviors_documentation]: https://github.com/flame-engine/flame/blob/main/doc/bridge_packages/flame_behaviors/getting-started.md\n"
  },
  {
    "path": "packages/flame_behaviors/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml"
  },
  {
    "path": "packages/flame_behaviors/example/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.lock\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/*\n\n# Visual Studio Code related\n.classpath\n.project\n.settings/\n.vscode/*\n\n# Flutter repo-specific\n/bin/cache/\n/bin/mingit/\n/dev/benchmarks/mega_gallery/\n/dev/bots/.recipe_deps\n/dev/bots/android_tools/\n/dev/docs/doc/\n/dev/docs/flutter.docs.zip\n/dev/docs/lib/\n/dev/docs/pubspec.yaml\n/dev/integration_tests/**/xcuserdata\n/dev/integration_tests/**/Pods\n/packages/flutter/coverage/\nversion\n\n# packages file containing multi-root paths\n.packages.generated\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\nbuild/\nflutter_*.png\nlinked_*.ds\nunlinked.ds\nunlinked_spec.ds\n.fvm/\n\n# Android related\n**/android/**/gradle-wrapper.jar\n**/android/.gradle\n**/android/captures/\n**/android/gradlew\n**/android/gradlew.bat\n**/android/local.properties\n**/android/**/GeneratedPluginRegistrant.java\n**/android/key.properties\n**/android/.idea/\n*.jks\n\n# iOS/XCode related\n**/ios/**/*.mode1v3\n**/ios/**/*.mode2v3\n**/ios/**/*.moved-aside\n**/ios/**/*.pbxuser\n**/ios/**/*.perspectivev3\n**/ios/**/*sync/\n**/ios/**/.sconsign.dblite\n**/ios/**/.tags*\n**/ios/**/.vagrant/\n**/ios/**/DerivedData/\n**/ios/**/Icon?\n**/ios/**/Pods/\n**/ios/**/.symlinks/\n**/ios/**/profile\n**/ios/**/xcuserdata\n**/ios/.generated/\n**/ios/Flutter/App.framework\n**/ios/Flutter/Flutter.framework\n**/ios/Flutter/Flutter.podspec\n**/ios/Flutter/Generated.xcconfig\n**/ios/Flutter/app.flx\n**/ios/Flutter/app.zip\n**/ios/Flutter/.last_build_id\n**/ios/Flutter/flutter_assets/\n**/ios/Flutter/flutter_export_environment.sh\n**/ios/ServiceDefinitions.json\n**/ios/Runner/GeneratedPluginRegistrant.*\n\n# Coverage\ncoverage/\n\n# Submodules\n!pubspec.lock\npackages/**/pubspec.lock\n\n# Web related\nlib/generated_plugin_registrant.dart\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Exceptions to the above rules.\n!**/ios/**/default.mode1v3\n!**/ios/**/default.mode2v3\n!**/ios/**/default.pbxuser\n!**/ios/**/default.perspectivev3\n!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages\n!/dev/ci/**/Gemfile.lock\n!.vscode/extensions.json\n!.vscode/launch.json\n!.idea/codeStyles/\n!.idea/dictionaries/\n!.idea/runConfigurations/\n\nandroid/\nios/\nmacos/\nwindows/\nlinux/\nweb/\npubspec.lock"
  },
  {
    "path": "packages/flame_behaviors/example/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "packages/flame_behaviors/example/README.md",
    "content": "# Example\n\n[![License: MIT][license_badge]][license_link]\n\nGenerated by the [Very Good CLI][very_good_cli_link] 🤖\n\nAn example for the Flame Behavior Pattern.\n\n[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg\n[license_link]: https://opensource.org/licenses/MIT\n[very_good_cli_link]: https://github.com/VeryGoodOpenSource/very_good_cli\n"
  },
  {
    "path": "packages/flame_behaviors/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml"
  },
  {
    "path": "packages/flame_behaviors/example/lib/behaviors/behaviors.dart",
    "content": "export 'spawning_behavior.dart';\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/behaviors/spawning_behavior.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_behaviors_example/entities/entities.dart';\nimport 'package:flame_behaviors_example/main.dart';\n\nclass SpawningBehavior extends TappableBehavior<ExampleGame> {\n  final _rng = Random();\n\n  @override\n  Future<void> onLoad() async {\n    await parent.add(nextRandomEntity(parent.size / 2));\n\n    for (var i = 0; i < 5; i++) {\n      await parent.add(nextRandomEntity(parent.size / 2));\n    }\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    if (event.handled) {\n      return;\n    }\n    parent.add(nextRandomEntity(event.canvasPosition));\n  }\n\n  PositionedEntity nextRandomEntity(Vector2 position) {\n    final size = Vector2.all(50) + Vector2.random(_rng) * 100;\n    final rotationSpeed = 0.5 - _rng.nextDouble();\n    final velocity = (Vector2.random(_rng) - Vector2.random(_rng)) * 300;\n    final shapeType = Shapes.values[_rng.nextInt(Shapes.values.length)];\n\n    return switch (shapeType) {\n      Shapes.circle => Circle(\n        position: position,\n        size: size,\n        velocity: velocity,\n        rotationSpeed: rotationSpeed,\n      ),\n      Shapes.rectangle => Rectangle(\n        position: position,\n        size: size,\n        velocity: velocity,\n        rotationSpeed: rotationSpeed,\n      ),\n    };\n  }\n}\n\nenum Shapes { circle, rectangle }\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/entities/behaviors/behaviors.dart",
    "content": "export 'moving_behavior.dart';\nexport 'rotating_behavior.dart';\nexport 'screen_colliding_behavior.dart';\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/entities/behaviors/moving_behavior.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\n\nclass MovingBehavior extends Behavior<PositionedEntity> {\n  MovingBehavior({required this.velocity});\n\n  final Vector2 velocity;\n\n  @override\n  void update(double dt) {\n    parent.position.add(velocity * dt);\n  }\n}\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/entities/behaviors/rotating_behavior.dart",
    "content": "import 'dart:async';\nimport 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\n\nclass RotatingBehavior extends Behavior<PositionedEntity>\n    with HasGameReference {\n  RotatingBehavior({required this.rotationSpeed});\n\n  final double rotationSpeed;\n\n  late final ScreenHitbox screenHitbox;\n\n  @override\n  FutureOr<void> onLoad() {\n    screenHitbox = game.children.whereType<ScreenHitbox>().first;\n  }\n\n  @override\n  void update(double dt) {\n    final angleDelta = dt * rotationSpeed;\n    parent.angle = (parent.angle + angleDelta) % (2 * pi);\n  }\n}\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/entities/behaviors/screen_colliding_behavior.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\n\n/// Simplified \"screen wrapping\" behavior, while not perfect it does showcase\n/// the possibility of acting on collision with non-entities.\nclass ScreenCollidingBehavior\n    extends CollisionBehavior<ScreenHitbox, PositionedEntity> {\n  @override\n  void onCollisionEnd(ScreenHitbox other) {\n    if (parent.position.x < other.position.x) {\n      parent.position.x = other.position.x + other.scaledSize.x;\n    } else if (parent.position.x > other.position.x + other.scaledSize.x) {\n      parent.position.x = other.position.x;\n    }\n\n    if (parent.position.y < other.position.y) {\n      parent.position.y = other.position.y + other.scaledSize.y;\n    } else if (parent.position.y > other.position.y + other.scaledSize.y) {\n      parent.position.y = other.position.y;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/entities/circle/behaviors/behaviors.dart",
    "content": "export 'circle_collision_behavior.dart';\nexport 'dragging_behavior.dart';\nexport 'rectangle_collision_behavior.dart';\nexport 'tapping_behavior.dart';\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/entities/circle/behaviors/circle_collision_behavior.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_behaviors_example/entities/entities.dart';\nimport 'package:flutter/material.dart';\n\nclass CircleCollisionBehavior extends CollisionBehavior<Circle, Circle> {\n  final _collisionColor = Colors.green.withValues(alpha: 0.8);\n\n  @override\n  void onCollisionStart(Set<Vector2> intersectionPoints, Circle other) {\n    parent.paint.color = _collisionColor;\n  }\n\n  @override\n  void onCollisionEnd(Circle other) {\n    if (!isColliding) {\n      parent.paint.color = parent.defaultColor;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/entities/circle/behaviors/dragging_behavior.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_behaviors_example/entities/entities.dart';\n\nclass DraggingBehavior extends DraggableBehavior<Circle> {\n  MovingBehavior? movement;\n\n  Vector2? originalVelocity;\n\n  @override\n  void onMount() {\n    movement = parent.findBehavior<MovingBehavior>();\n    return super.onMount();\n  }\n\n  @override\n  void onDragStart(DragStartEvent event) {\n    originalVelocity = movement?.velocity.clone();\n    movement?.velocity.setFrom(Vector2.zero());\n    return super.onDragStart(event);\n  }\n\n  @override\n  void onDragCancel(DragCancelEvent event) {\n    movement?.velocity.setFrom(originalVelocity ?? Vector2.zero());\n    return super.onDragCancel(event);\n  }\n\n  @override\n  void onDragEnd(DragEndEvent event) {\n    movement?.velocity.setFrom(event.velocity);\n    return super.onDragEnd(event);\n  }\n\n  @override\n  void onDragUpdate(DragUpdateEvent event) {\n    parent.position.add(event.localDelta);\n  }\n}\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/entities/circle/behaviors/rectangle_collision_behavior.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_behaviors_example/entities/entities.dart';\nimport 'package:flutter/material.dart';\n\nclass RectangleCollisionBehavior extends CollisionBehavior<Rectangle, Circle> {\n  final _collisionColor = Colors.yellow.withValues(alpha: 0.8);\n\n  @override\n  void onCollisionStart(Set<Vector2> intersectionPoints, Rectangle other) {\n    parent.paint.color = _collisionColor;\n  }\n\n  @override\n  void onCollisionEnd(Rectangle other) {\n    if (!isColliding) {\n      parent.paint.color = parent.defaultColor;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/entities/circle/behaviors/tapping_behavior.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_behaviors_example/entities/entities.dart';\n\n/// This behavior ensures that SpawningBehavior of the game does not spawn\n/// anything when we click on a circle (for dragging).\nclass TappingBehavior extends TappableBehavior<Circle> {\n  @override\n  void onTapDown(TapDownEvent event) {\n    event.handled = true;\n  }\n}\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/entities/circle/circle.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_behaviors_example/entities/circle/behaviors/behaviors.dart';\nimport 'package:flame_behaviors_example/entities/entities.dart';\nimport 'package:flutter/material.dart';\n\nclass Circle extends PositionedEntity with HasPaint {\n  Circle({\n    required double rotationSpeed,\n    required Vector2 velocity,\n    super.position,\n    super.size,\n  }) : super(\n         anchor: Anchor.center,\n         behaviors: [\n           PropagatingCollisionBehavior(CircleHitbox()),\n           CircleCollisionBehavior(),\n           RectangleCollisionBehavior(),\n           ScreenCollidingBehavior(),\n           MovingBehavior(velocity: velocity),\n           RotatingBehavior(rotationSpeed: rotationSpeed),\n           TappingBehavior(),\n           DraggingBehavior(),\n         ],\n       );\n\n  final defaultColor = Colors.blue.withValues(alpha: 0.8);\n\n  @override\n  void onMount() {\n    paint.color = defaultColor;\n    super.onMount();\n  }\n\n  @override\n  void render(Canvas canvas) {\n    final center = size / 2;\n    canvas.drawCircle(center.toOffset(), min(size.x, size.y) / 2, paint);\n  }\n}\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/entities/entities.dart",
    "content": "export 'behaviors/behaviors.dart';\nexport 'circle/circle.dart';\nexport 'rectangle/rectangle.dart';\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/entities/rectangle/behaviors/behaviors.dart",
    "content": "export 'circle_colliding_behavior.dart';\nexport 'freezing_behavior.dart';\nexport 'rectangle_colliding_behavior.dart';\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/entities/rectangle/behaviors/circle_colliding_behavior.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_behaviors_example/entities/entities.dart';\nimport 'package:flutter/material.dart';\n\nclass CircleCollidingBehavior extends CollisionBehavior<Circle, Rectangle> {\n  final _collisionColor = Colors.green.withValues(alpha: 0.8);\n\n  @override\n  void onCollisionStart(Set<Vector2> intersectionPoints, Circle other) {\n    parent.paint.color = _collisionColor;\n  }\n\n  @override\n  void onCollisionEnd(Circle other) {\n    if (!isColliding) {\n      parent.paint.color = parent.defaultColor;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/entities/rectangle/behaviors/freezing_behavior.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_behaviors_example/entities/entities.dart';\n\nclass FreezingBehavior extends TappableBehavior<Rectangle> {\n  MovingBehavior? movement;\n\n  Vector2? originalVelocity;\n\n  @override\n  void onMount() {\n    movement = parent.findBehavior<MovingBehavior>();\n    return super.onMount();\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    if (movement?.velocity.isZero() ?? false) {\n      movement?.velocity.setFrom(originalVelocity ?? Vector2.zero());\n    } else {\n      originalVelocity = movement?.velocity.clone();\n      movement?.velocity.setFrom(Vector2.zero());\n    }\n    event.handled = true;\n  }\n}\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/entities/rectangle/behaviors/rectangle_colliding_behavior.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_behaviors_example/entities/entities.dart';\nimport 'package:flutter/material.dart';\n\nclass RectangleCollidingBehavior\n    extends CollisionBehavior<Rectangle, Rectangle> {\n  final _collisionColor = Colors.yellow.withValues(alpha: 0.8);\n\n  @override\n  void onCollisionStart(Set<Vector2> intersectionPoints, Rectangle other) {\n    parent.paint.color = _collisionColor;\n  }\n\n  @override\n  void onCollisionEnd(Rectangle other) {\n    if (!isColliding) {\n      parent.paint.color = parent.defaultColor;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/entities/rectangle/rectangle.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_behaviors_example/entities/entities.dart';\nimport 'package:flame_behaviors_example/entities/rectangle/behaviors/behaviors.dart';\nimport 'package:flutter/material.dart';\n\nclass Rectangle extends PositionedEntity with HasPaint {\n  Rectangle({\n    required double rotationSpeed,\n    required Vector2 velocity,\n    super.position,\n    super.size,\n  }) : super(\n         anchor: Anchor.center,\n         behaviors: [\n           PropagatingCollisionBehavior(RectangleHitbox()),\n           RectangleCollidingBehavior(),\n           CircleCollidingBehavior(),\n           ScreenCollidingBehavior(),\n           MovingBehavior(velocity: velocity),\n           RotatingBehavior(rotationSpeed: rotationSpeed),\n           FreezingBehavior(),\n         ],\n       );\n\n  final defaultColor = Colors.red.withValues(alpha: 0.8);\n\n  @override\n  void onMount() {\n    paint.color = defaultColor;\n    super.onMount();\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(Vector2.zero() & size, paint);\n  }\n}\n"
  },
  {
    "path": "packages/flame_behaviors/example/lib/main.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_behaviors_example/behaviors/behaviors.dart';\nimport 'package:flutter/material.dart';\n\nclass ExampleGame extends FlameGame with EntityMixin, HasCollisionDetection {\n  @override\n  Future<void> onLoad() async {\n    await add(FpsTextComponent(position: Vector2.zero()));\n    await add(ScreenHitbox());\n\n    // Game-specific behaviors\n    await add(SpawningBehavior());\n\n    return super.onLoad();\n  }\n}\n\nvoid main() {\n  runApp(GameWidget(game: ExampleGame()));\n}\n"
  },
  {
    "path": "packages/flame_behaviors/example/pubspec.yaml",
    "content": "name: flame_behaviors_example\nresolution: workspace\ndescription: A Very Good Example for the Flame Behavior Pattern.\nversion: 1.0.0+1\npublish_to: none\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_behaviors: ^1.3.4\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n\n"
  },
  {
    "path": "packages/flame_behaviors/lib/flame_behaviors.dart",
    "content": "/// Flame Behaviors applies separation of concerns to game logic in the form of\n/// Entities and Behaviors, originally built by Very Good Ventures.\nlibrary;\n\nexport 'src/behaviors/behaviors.dart';\nexport 'src/entity.dart';\n"
  },
  {
    "path": "packages/flame_behaviors/lib/src/behaviors/behavior.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\n\n/// {@template behavior}\n/// A behavior is a component that defines how an entity behaves. It can be\n/// attached to an [Entity] and handle a specific behavior for that entity.\n///\n/// A behavior can have it's own [Component]s for adding extra functionality\n/// related to the behavior. It cannot, however, have its own [Behavior]s.\n/// {@endtemplate}\nabstract class Behavior<Parent extends EntityMixin> extends Component\n    with ParentIsA<Parent> {\n  /// {@macro behavior}\n  Behavior({super.children, super.priority, super.key});\n\n  @override\n  FutureOr<void> add(Component component) {\n    assert(component is! EntityMixin, 'Behaviors cannot have entities.');\n    assert(component is! Behavior, 'Behaviors cannot have behaviors.');\n    return super.add(component);\n  }\n\n  @override\n  @Deprecated('Will be removed in a future version of Flame')\n  bool containsPoint(Vector2 point) {\n    return parent.containsPoint(point);\n  }\n\n  @override\n  bool containsLocalPoint(Vector2 point) {\n    return parent.containsLocalPoint(point);\n  }\n\n  @override\n  bool get debugMode => parent.debugMode;\n}\n"
  },
  {
    "path": "packages/flame_behaviors/lib/src/behaviors/behaviors.dart",
    "content": "export 'behavior.dart';\nexport 'events/events.dart';\nexport 'propagating_collision_behavior.dart';\n"
  },
  {
    "path": "packages/flame_behaviors/lib/src/behaviors/events/draggable_behavior.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\n\n/// {@template draggable_behavior}\n/// A behavior that makes an [Entity] draggable.\n/// {@endtemplate}\nabstract class DraggableBehavior<Parent extends EntityMixin>\n    extends Behavior<Parent>\n    with DragCallbacks {\n  /// {@macro draggable_behavior}\n  DraggableBehavior({super.children, super.priority, super.key});\n}\n"
  },
  {
    "path": "packages/flame_behaviors/lib/src/behaviors/events/events.dart",
    "content": "export 'package:flame/events.dart'\n    show\n        DragEndInfo,\n        DragStartInfo,\n        DragUpdateInfo,\n        PointerHoverInfo,\n        TapDownInfo,\n        TapUpInfo;\n\nexport 'draggable_behavior.dart';\nexport 'hoverable_behavior.dart';\nexport 'tappable_behavior.dart';\n"
  },
  {
    "path": "packages/flame_behaviors/lib/src/behaviors/events/hoverable_behavior.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\n\n/// {@template hoverable_behavior}\n/// A behavior that makes an [Entity] hoverable.\n///\n/// When using this behavior, also add `HasHoverables` to your game, which\n/// handles propagation of hover events from the root game to individual\n/// behaviors.\n/// {@endtemplate}\nabstract class HoverableBehavior<Parent extends EntityMixin>\n    extends Behavior<Parent>\n    with HoverCallbacks {\n  /// {@macro hoverable_behavior}\n  HoverableBehavior({super.children, super.priority, super.key});\n}\n"
  },
  {
    "path": "packages/flame_behaviors/lib/src/behaviors/events/tappable_behavior.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\n\n/// {@template tappable_behavior}\n/// A behavior that makes an [Entity] tappable.\n/// {@endtemplate}\nabstract class TappableBehavior<Parent extends EntityMixin>\n    extends Behavior<Parent>\n    with TapCallbacks {\n  /// {@macro tappable_behavior}\n  TappableBehavior({super.children, super.priority, super.key});\n}\n"
  },
  {
    "path": "packages/flame_behaviors/lib/src/behaviors/propagating_collision_behavior.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flutter/material.dart';\n\n/// {@template collision_behavior}\n/// This behavior is used for collision between entities. The\n/// [PropagatingCollisionBehavior] propagates the collision to this behavior if\n/// the entity that is colliding with the [Parent] is an instance of [Collider].\n/// {@endtemplate}\nabstract class CollisionBehavior<\n  Collider extends Component,\n  Parent extends EntityMixin\n>\n    extends Behavior<Parent> {\n  /// {@macro collision_behavior}\n  CollisionBehavior({super.children, super.priority, super.key});\n\n  /// Check if the given component is an instance of [Collider].\n  bool isValid(Component c) => c is Collider;\n\n  /// Called when the entity collides with [Collider].\n  void onCollision(Set<Vector2> intersectionPoints, Collider other) {}\n\n  /// Called when the entity starts to collides with [Collider].\n  void onCollisionStart(Set<Vector2> intersectionPoints, Collider other) {}\n\n  /// Called when the entity stops to collides with [Collider].\n  void onCollisionEnd(Collider other) {}\n\n  /// Whether the object is currently colliding with another [Collider] or not.\n  bool get isColliding {\n    final propagatingCollisionBehavior = parent\n        .findBehavior<PropagatingCollisionBehavior>();\n\n    return propagatingCollisionBehavior.activeCollisions\n        .map(propagatingCollisionBehavior.findEntity)\n        .whereType<Collider>()\n        .isNotEmpty;\n  }\n}\n\n/// {@template propagating_collision_behavior}\n/// This behavior is used to handle collisions between entities and propagates\n/// the collision through to any [CollisionBehavior]s that are attached to the\n/// entity.\n///\n/// The [CollisionBehavior]s are filtered by the [CollisionBehavior.isValid]\n/// method by checking if the colliding entity is valid for the given behavior\n/// and if the colliding entity is valid the [CollisionBehavior.onCollision] is\n/// called.\n///\n/// This allows for strongly typed collision detection. Without having to add\n/// multiple collision behaviors for different types of entities or adding more\n/// logic to a single collision detection behavior.\n///\n/// If you have an entity that does not require any [CollisionBehavior]s of its\n/// own, you can just add the hitbox directly to the entity's children.\n/// Any other entity that has a [CollisionBehavior] for that entity attached\n/// will then be able to collide with it.\n///\n/// **Note**: This behavior can also be used for collisions between entities\n/// and non-entity components, by passing the component's type as the\n/// `Collider` to the [CollisionBehavior].\n///\n/// The parent to which this behavior is added should be a [PositionComponent]\n/// that uses the [EntityMixin]. Flame behaviors comes with the\n/// [PositionedEntity] which does exactly that but any kind of position\n/// component will work.\n/// {@endtemplate}\nclass PropagatingCollisionBehavior<Parent extends EntityMixin>\n    extends Behavior<Parent>\n    with CollisionCallbacks {\n  /// {@macro propagating_collision_behavior}\n  PropagatingCollisionBehavior(this._hitbox, {super.priority, super.key})\n    : super(children: [_hitbox]);\n\n  final ShapeHitbox _hitbox;\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    assert(parent is PositionComponent, 'parent must be a PositionComponent');\n    super.onMount();\n  }\n\n  @override\n  void onLoad() {\n    _hitbox\n      ..onCollisionCallback = onCollision\n      ..onCollisionStartCallback = onCollisionStart\n      ..onCollisionEndCallback = onCollisionEnd;\n    parent.children.register<CollisionBehavior>();\n    _propagateToBehaviors = parent.children.query<CollisionBehavior>();\n  }\n\n  /// List of [CollisionBehavior]s to which it can propagate to.\n  Iterable<CollisionBehavior> _propagateToBehaviors = [];\n\n  /// Tries to find the entity that is colliding with the given entity.\n  ///\n  /// It will check if the parent is either a [PropagatingCollisionBehavior]\n  /// or a [Entity]. If it is neither, it will return [other] or null if [other]\n  /// is not mounted.\n  Component? findEntity(PositionComponent other) {\n    final parent = other.parent;\n    if (!other.isMounted) {\n      return null;\n    }\n\n    if (parent is! PropagatingCollisionBehavior && parent is! Entity) {\n      if (other is ShapeHitbox) {\n        return other.parent;\n      }\n      return other;\n    }\n\n    return parent is Entity\n        ? parent\n        : (parent as PropagatingCollisionBehavior?)?.parent;\n  }\n\n  @override\n  void onCollisionStart(\n    Set<Vector2> intersectionPoints,\n    PositionComponent other,\n  ) {\n    activeCollisions.add(other);\n    final otherEntity = findEntity(other);\n    if (otherEntity == null) {\n      return;\n    }\n\n    for (final behavior in _propagateToBehaviors) {\n      if (behavior.isValid(otherEntity)) {\n        behavior.onCollisionStart(intersectionPoints, otherEntity);\n      }\n    }\n    return super.onCollisionStart(intersectionPoints, other);\n  }\n\n  @override\n  @mustCallSuper\n  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {\n    final otherEntity = findEntity(other);\n    if (otherEntity == null) {\n      return;\n    }\n\n    for (final behavior in _propagateToBehaviors) {\n      if (behavior.isValid(otherEntity)) {\n        behavior.onCollision(intersectionPoints, otherEntity);\n      }\n    }\n    super.onCollision(intersectionPoints, other);\n  }\n\n  @override\n  void onCollisionEnd(PositionComponent other) {\n    activeCollisions.remove(other);\n    final otherEntity = findEntity(other);\n    if (otherEntity == null) {\n      return;\n    }\n\n    for (final behavior in _propagateToBehaviors) {\n      if (behavior.isValid(otherEntity)) {\n        behavior.onCollisionEnd(otherEntity);\n      }\n    }\n    return super.onCollisionEnd(other);\n  }\n}\n"
  },
  {
    "path": "packages/flame_behaviors/lib/src/entity.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\n\n/// A mixin that adds behavioral functionality to any kind of [Component].\n///\n/// Use this mixin when you can't extend [PositionedEntity] or [Entity] and\n/// still want to use behaviors. For example, if you want to add behaviors to\n/// a [SpriteComponent], [TextComponent], or even a [FlameGame] itself.\nmixin EntityMixin on Component {\n  Iterable<Behavior>? _behaviors;\n\n  /// Returns a list of behaviors with the given type, that are attached to\n  /// this entity.\n  ///\n  /// This will only return behaviors that have a completed lifecycle, aka they\n  /// are fully mounted.\n  Iterable<T> findBehaviors<T extends Behavior>() {\n    if (_behaviors == null) {\n      children.register<Behavior>();\n      _behaviors ??= children.query<Behavior>();\n    }\n    return _behaviors!.whereType<T>();\n  }\n\n  /// Returns the first found behavior with the given type, that is attached\n  /// to this entity.\n  ///\n  /// This will only return a behavior that has a completed lifecycle, aka it\n  /// is fully mounted. If no behavior is found, it will throw a [StateError].\n  T findBehavior<T extends Behavior>() {\n    final it = findBehaviors<T>().iterator;\n    if (!it.moveNext()) {\n      throw StateError('No behavior of type $T found.');\n    }\n    return it.current;\n  }\n\n  /// Checks if this entity has at least one behavior with the given type.\n  ///\n  ///\n  /// This will only return true if the behavior with the type [T] has a\n  /// completed lifecycle, aka it is fully mounted.\n  bool hasBehavior<T extends Behavior>() {\n    try {\n      findBehavior<T>();\n      return true;\n    } catch (e) {\n      if (e is StateError && e.message.startsWith('No behavior of type')) {\n        return false;\n      }\n      rethrow;\n    }\n  }\n}\n\n/// {@template entity}\n/// The entity is the building block of a game. It represents a game object\n/// that can hold multiple `Behavior`s, which in turn define how the\n/// entity behaves.\n///\n/// The visualization of the entity is defined by the [Component]s that are\n/// attached to it.\n/// {@endtemplate}\nabstract class Entity extends Component with EntityMixin {\n  /// {@macro entity}\n  Entity({\n    super.children,\n    super.priority,\n    super.key,\n    Iterable<Behavior>? behaviors,\n  }) : assert(\n         children?.whereType<Behavior>().isEmpty ?? true,\n         'Behaviors cannot be added to as a child directly.',\n       ) {\n    if (behaviors != null) {\n      addAll(behaviors);\n    }\n  }\n}\n\n/// {@template positioned_entity}\n/// {@macro entity}\n///\n/// This entity is based on the [PositionComponent] and can be positioned\n/// on the screen.\n/// {@endtemplate}\nabstract class PositionedEntity extends PositionComponent with EntityMixin {\n  /// {@macro positioned_entity}\n  PositionedEntity({\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.nativeAngle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.key,\n    Iterable<Behavior>? behaviors,\n  }) : assert(\n         children?.whereType<Behavior>().isEmpty ?? true,\n         'Behaviors cannot be added to as a child directly.',\n       ) {\n    if (behaviors != null) {\n      addAll(behaviors);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_behaviors/pubspec.yaml",
    "content": "name: flame_behaviors\nresolution: workspace\ndescription: Flame Behaviors applies separation of concerns to game logic in the form of Entities and Behaviors, originally built by Very Good Ventures.\nversion: 1.3.4\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_behaviors\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flame_test: ^2.2.3\n  flutter_test:\n    sdk: flutter\n  mocktail: ^1.0.4"
  },
  {
    "path": "packages/flame_behaviors/test/helpers/helpers.dart",
    "content": "export 'throws_behavior_not_found_for.dart';\n"
  },
  {
    "path": "packages/flame_behaviors/test/helpers/throws_behavior_not_found_for.dart",
    "content": "import 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\n/// A matcher for [StateError]s thrown when a certain behavior is not found.\nMatcher throwsBehaviorNotFoundFor<T extends Behavior>() => throwsA(\n  isStateError.having(\n    (e) => e.message,\n    'message',\n    equals('No behavior of type $T found.'),\n  ),\n);\n"
  },
  {
    "path": "packages/flame_behaviors/test/src/behaviors/behavior_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _TestEntity extends PositionedEntity {\n  _TestEntity({super.behaviors}) : super(size: Vector2.all(32));\n}\n\nclass _TestBehavior extends Behavior<_TestEntity> {}\n\nvoid main() {\n  group('Behavior', () {\n    flameGame.testGameWidget(\n      'can be added to an Entity',\n      setUp: (game, tester) async {\n        final behavior = _TestBehavior();\n        final entity = _TestEntity(behaviors: [behavior]);\n        await game.ensureAdd(entity);\n      },\n      verify: (game, tester) async {\n        final entity = game.firstChild<_TestEntity>()!;\n\n        expect(game.descendants().whereType<_TestBehavior>().length, equals(1));\n        expect(entity.children.whereType<_TestBehavior>().length, equals(1));\n      },\n    );\n\n    flameGame.testGameWidget(\n      'contains point is relative to parent',\n      setUp: (game, tester) async {\n        final behavior = _TestBehavior();\n        final entity = _TestEntity(behaviors: [behavior]);\n        await game.ensureAdd(entity);\n      },\n      verify: (game, tester) async {\n        final entity = game.firstChild<_TestEntity>()!;\n        final behavior = entity.firstChild<_TestBehavior>()!;\n\n        expect(behavior.containsLocalPoint(Vector2.zero()), isTrue);\n        expect(behavior.containsLocalPoint(Vector2(31, 31)), isTrue);\n        expect(behavior.containsLocalPoint(Vector2(32, 32)), isFalse);\n      },\n    );\n\n    flameGame.testGameWidget(\n      'contains local point is relative to parent',\n      setUp: (game, tester) async {\n        final behavior = _TestBehavior();\n        final entity = _TestEntity(behaviors: [behavior]);\n        await game.ensureAdd(entity);\n      },\n      verify: (game, tester) async {\n        final entity = game.firstChild<_TestEntity>()!;\n        final behavior = entity.firstChild<_TestBehavior>()!;\n\n        expect(behavior.containsLocalPoint(Vector2.zero()), isTrue);\n        expect(behavior.containsLocalPoint(Vector2(31, 31)), isTrue);\n        expect(behavior.containsLocalPoint(Vector2(32, 32)), isFalse);\n      },\n    );\n\n    flameGame.testGameWidget(\n      'debugMode is provided by the parent',\n      setUp: (game, tester) async {\n        final behavior = _TestBehavior();\n        final entity = _TestEntity(behaviors: [behavior]);\n        await game.ensureAdd(entity);\n      },\n      verify: (game, tester) async {\n        final entity = game.firstChild<_TestEntity>()!;\n        final behavior = entity.firstChild<_TestBehavior>()!;\n\n        expect(behavior.debugMode, isFalse);\n        entity.debugMode = true;\n        expect(behavior.debugMode, isTrue);\n      },\n    );\n\n    group('children', () {\n      late _TestBehavior testBehavior;\n      late _TestEntity testEntity;\n\n      setUp(() {\n        testBehavior = _TestBehavior();\n        testEntity = _TestEntity(behaviors: [testBehavior]);\n      });\n\n      flameGame.testGameWidget(\n        'can have its own children',\n        setUp: (game, tester) async {\n          await game.ensureAdd(testEntity);\n        },\n        verify: (game, tester) async {\n          expect(() => testBehavior.add(Component()), returnsNormally);\n        },\n      );\n\n      flameGame.testGameWidget(\n        'can not have behaviors as children',\n        setUp: (game, tester) async {\n          await game.ensureAdd(testEntity);\n        },\n        verify: (game, tester) async {\n          expect(\n            () => testBehavior.add(_TestBehavior()),\n            failsAssert('Behaviors cannot have behaviors.'),\n          );\n        },\n      );\n\n      flameGame.testGameWidget(\n        'can not have entities as children',\n        setUp: (game, tester) async {\n          await game.ensureAdd(testEntity);\n        },\n        verify: (game, tester) async {\n          expect(\n            () => testBehavior.add(_TestEntity()),\n            failsAssert('Behaviors cannot have entities.'),\n          );\n        },\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_behaviors/test/src/behaviors/events/draggable_behavior_test.dart",
    "content": "import 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _TestBehavior extends DraggableBehavior {}\n\nvoid main() {\n  group('$DraggableBehavior', () {\n    test('can be instantiated', () {\n      expect(_TestBehavior(), isNotNull);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_behaviors/test/src/behaviors/events/hoverable_behavior_test.dart",
    "content": "import 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _TestBehavior extends HoverableBehavior {}\n\nvoid main() {\n  group('$HoverableBehavior', () {\n    test('can be instantiated', () {\n      expect(_TestBehavior(), isNotNull);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_behaviors/test/src/behaviors/events/tappable_behavior_test.dart",
    "content": "import 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _TestBehavior extends TappableBehavior {}\n\nvoid main() {\n  group('$TappableBehavior', () {\n    test('can be instantiated', () {\n      expect(_TestBehavior(), isNotNull);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_behaviors/test/src/behaviors/propagating_collision_behavior_test.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _EntityA extends PositionedEntity {\n  _EntityA({super.behaviors}) : super(size: Vector2.all(16));\n}\n\nclass _EntityB extends PositionedEntity {\n  _EntityB({super.behaviors}) : super(size: Vector2.all(16));\n}\n\nclass _EntityC extends PositionedEntity {\n  _EntityC() : super(size: Vector2.all(16));\n}\n\nclass _EntityD extends Entity {\n  _EntityD();\n}\n\nabstract class _CollisionBehavior<\n  A extends Component,\n  B extends PositionedEntity\n>\n    extends CollisionBehavior<A, B> {\n  bool onCollisionStartCalled = false;\n  bool onCollisionCalled = false;\n  bool onCollisionEndCalled = false;\n\n  @override\n  void onCollisionStart(Set<Vector2> intersectionPoints, A other) {\n    super.onCollisionStart(intersectionPoints, other);\n    onCollisionStartCalled = true;\n  }\n\n  @override\n  void onCollision(Set<Vector2> intersectionPoints, A other) {\n    super.onCollision(intersectionPoints, other);\n    onCollisionCalled = true;\n  }\n\n  @override\n  void onCollisionEnd(A other) {\n    super.onCollisionEnd(other);\n    onCollisionEndCalled = true;\n  }\n}\n\nclass _CollisionBehaviorAtoB extends _CollisionBehavior<_EntityB, _EntityA> {}\n\nclass _CollisionBehaviorAtoC extends _CollisionBehavior<_EntityC, _EntityA> {}\n\nclass _CollisionBehaviorAtoComponent\n    extends _CollisionBehavior<PositionComponent, _EntityA> {}\n\nclass _TestGame extends FlameGame with HasCollisionDetection {}\n\nvoid main() {\n  final flameTester = FlameTester(_TestGame.new);\n\n  group('$CollisionBehavior', () {\n    flameTester.testGameWidget(\n      'isColliding returns true if it is current colliding with the Collider',\n      setUp: (game, tester) async {\n        final collisionBehaviorAtoB = _CollisionBehaviorAtoB();\n        final entityA = _EntityA(\n          behaviors: [\n            PropagatingCollisionBehavior(RectangleHitbox()),\n            collisionBehaviorAtoB,\n          ],\n        );\n\n        final entityB = _EntityB(\n          behaviors: [PropagatingCollisionBehavior(RectangleHitbox())],\n        );\n\n        await game.ensureAdd(entityA);\n        await game.ensureAdd(entityB);\n      },\n      verify: (game, tester) async {\n        final entityA = game.firstChild<_EntityA>()!;\n        final collisionBehaviorAtoB = entityA\n            .firstChild<_CollisionBehaviorAtoB>()!;\n\n        game.update(0);\n\n        expect(collisionBehaviorAtoB.isColliding, isTrue);\n      },\n    );\n\n    flameTester.testGameWidget(\n      'isColliding returns false if it is not colliding with any Colliders',\n      setUp: (game, tester) async {\n        final collisionBehaviorAtoC = _CollisionBehaviorAtoC();\n        final entityA = _EntityA(\n          behaviors: [\n            PropagatingCollisionBehavior(RectangleHitbox()),\n            collisionBehaviorAtoC,\n          ],\n        );\n\n        final entityB = _EntityB(\n          behaviors: [PropagatingCollisionBehavior(RectangleHitbox())],\n        );\n\n        await game.ensureAdd(entityA);\n        await game.ensureAdd(entityB);\n      },\n      verify: (game, tester) async {\n        final entityA = game.firstChild<_EntityA>()!;\n        final collisionBehaviorAtoC = entityA\n            .firstChild<_CollisionBehaviorAtoC>()!;\n\n        game.update(0);\n\n        expect(collisionBehaviorAtoC.isColliding, isFalse);\n      },\n    );\n  });\n\n  group('$PropagatingCollisionBehavior', () {\n    flameTester.testGameWidget(\n      'can be added to an Entity',\n      setUp: (game, tester) async {\n        final propagatingCollisionBehavior = PropagatingCollisionBehavior(\n          RectangleHitbox(),\n        );\n        final entityA = _EntityA(behaviors: [propagatingCollisionBehavior]);\n\n        await game.ensureAdd(entityA);\n      },\n      verify: (game, tester) async {\n        final entityA = game.firstChild<_EntityA>()!;\n        final propagatingCollisionBehavior = entityA\n            .firstChild<PropagatingCollisionBehavior>()!;\n\n        expect(\n          entityA.findBehavior<PropagatingCollisionBehavior>(),\n          equals(propagatingCollisionBehavior),\n        );\n        expect(\n          propagatingCollisionBehavior.children\n              .whereType<RectangleHitbox>()\n              .length,\n          equals(1),\n        );\n      },\n    );\n\n    flameTester.testGameWidget(\n      'throws assertion exception if parent is not a positioned component',\n      setUp: (game, tester) async {\n        await game.ensureAdd(_EntityD());\n        return game.pauseEngine(); // Pausing engine to trigger it manually\n      },\n      verify: (game, tester) async {\n        final entity = game.firstChild<_EntityD>()!;\n        final propagatingCollisionBehavior = PropagatingCollisionBehavior(\n          RectangleHitbox(),\n        );\n\n        await expectLater(() async {\n          await entity.add(propagatingCollisionBehavior);\n          game.update(0);\n        }, failsAssert('parent must be a PositionComponent'));\n      },\n    );\n\n    flameTester.testGameWidget(\n      '_findEntity() returns proper values during lifecycle',\n      setUp: (game, tester) async {\n        final collisionBehaviorAtoB = _CollisionBehaviorAtoB();\n        final entityA = _EntityA(\n          behaviors: [\n            PropagatingCollisionBehavior(RectangleHitbox()),\n            collisionBehaviorAtoB,\n          ],\n        );\n        final entityB = _EntityB(\n          behaviors: [PropagatingCollisionBehavior(RectangleHitbox())],\n        );\n        await game.ensureAdd(entityA);\n        await game.ensureAdd(entityB);\n        return game.pauseEngine(); // Pausing engine to trigger it manually\n      },\n      verify: (game, tester) async {\n        final entityA = game.firstChild<_EntityA>()!;\n        final entityB = game.firstChild<_EntityB>()!;\n        final collisionBehaviorAtoB = entityA\n            .firstChild<_CollisionBehaviorAtoB>()!;\n\n        final collisionPropagatingBehavior = entityA\n            .findBehavior<PropagatingCollisionBehavior>();\n\n        game.update(0);\n\n        expect(collisionBehaviorAtoB.onCollisionStartCalled, isTrue);\n        expect(collisionPropagatingBehavior.findEntity(entityB), isNotNull);\n\n        entityB.removeFromParent();\n\n        game.update(0);\n\n        expect(entityB.isMounted, isFalse);\n\n        expect(collisionPropagatingBehavior.findEntity(entityB), isNull);\n      },\n    );\n\n    group('propagates collision', () {\n      flameTester.testGameWidget(\n        'on start to the correct collision behavior',\n        setUp: (game, tester) async {\n          final collisionBehaviorAtoB = _CollisionBehaviorAtoB();\n          final collisionBehaviorAtoC = _CollisionBehaviorAtoC();\n          final collisionBehaviorAtoComponent =\n              _CollisionBehaviorAtoComponent();\n          final entityA = _EntityA(\n            behaviors: [\n              PropagatingCollisionBehavior(RectangleHitbox()),\n              collisionBehaviorAtoB,\n              collisionBehaviorAtoC,\n              collisionBehaviorAtoComponent,\n            ],\n          );\n\n          final entityB = _EntityB(\n            behaviors: [PropagatingCollisionBehavior(RectangleHitbox())],\n          );\n\n          final positionComponent = PositionComponent(\n            size: Vector2.all(16),\n            children: [RectangleHitbox()],\n          );\n\n          await game.ensureAdd(entityA);\n          await game.ensureAdd(entityB);\n          await game.ensureAdd(positionComponent);\n        },\n        verify: (game, tester) async {\n          final entityA = game.firstChild<_EntityA>()!;\n          final collisionBehaviorAtoB = entityA\n              .firstChild<_CollisionBehaviorAtoB>()!;\n          final collisionBehaviorAtoC = entityA\n              .firstChild<_CollisionBehaviorAtoC>()!;\n          final collisionBehaviorAtoComponent = entityA\n              .firstChild<_CollisionBehaviorAtoComponent>()!;\n\n          game.update(0);\n\n          expect(collisionBehaviorAtoB.onCollisionStartCalled, isTrue);\n          expect(collisionBehaviorAtoC.onCollisionStartCalled, isFalse);\n          expect(collisionBehaviorAtoComponent.onCollisionStartCalled, isTrue);\n        },\n      );\n\n      flameTester.testGameWidget(\n        'on collision to the correct collision behavior',\n        setUp: (game, tester) async {\n          final collisionBehaviorAtoB = _CollisionBehaviorAtoB();\n          final entityA = _EntityA(\n            behaviors: [\n              PropagatingCollisionBehavior(RectangleHitbox()),\n              collisionBehaviorAtoB,\n            ],\n          );\n\n          final entityB = _EntityB(\n            behaviors: [PropagatingCollisionBehavior(RectangleHitbox())],\n          );\n\n          await game.ensureAdd(entityA);\n          await game.ensureAdd(entityB);\n        },\n        verify: (game, tester) async {\n          final entityA = game.firstChild<_EntityA>()!;\n          final collisionBehaviorAtoB = entityA\n              .firstChild<_CollisionBehaviorAtoB>()!;\n\n          game.update(0);\n\n          expect(collisionBehaviorAtoB.onCollisionCalled, isTrue);\n        },\n      );\n\n      flameTester.testGameWidget(\n        'on end to the correct collision behavior',\n        setUp: (game, tester) async {\n          final collisionBehaviorAtoB = _CollisionBehaviorAtoB();\n          final collisionBehaviorAtoC = _CollisionBehaviorAtoC();\n          final collisionBehaviorAtoComponent =\n              _CollisionBehaviorAtoComponent();\n          final entityA = _EntityA(\n            behaviors: [\n              PropagatingCollisionBehavior(RectangleHitbox()),\n              collisionBehaviorAtoB,\n              collisionBehaviorAtoC,\n              collisionBehaviorAtoComponent,\n            ],\n          );\n\n          final entityB = _EntityB(\n            behaviors: [PropagatingCollisionBehavior(RectangleHitbox())],\n          );\n\n          final positionComponent = PositionComponent(\n            size: Vector2.all(16),\n            children: [RectangleHitbox()],\n          );\n\n          await game.ensureAdd(positionComponent);\n          await game.ensureAdd(entityA);\n          await game.ensureAdd(entityB);\n        },\n        verify: (game, tester) async {\n          final entityA = game.firstChild<_EntityA>()!;\n          final collisionBehaviorAtoB = entityA\n              .firstChild<_CollisionBehaviorAtoB>()!;\n          final collisionBehaviorAtoC = entityA\n              .firstChild<_CollisionBehaviorAtoC>()!;\n          final collisionBehaviorAtoComponent = entityA\n              .firstChild<_CollisionBehaviorAtoComponent>()!;\n\n          final entityB = game.firstChild<_EntityB>()!;\n          final positionComponent = game.firstChild<PositionComponent>()!;\n\n          game.update(0);\n\n          expect(collisionBehaviorAtoB.onCollisionEndCalled, isFalse);\n          expect(collisionBehaviorAtoC.onCollisionEndCalled, isFalse);\n          expect(collisionBehaviorAtoComponent.onCollisionEndCalled, isFalse);\n\n          entityB.position += Vector2.all(50);\n          positionComponent.position += Vector2.all(50);\n\n          game.update(0);\n\n          expect(collisionBehaviorAtoB.onCollisionEndCalled, isTrue);\n          expect(collisionBehaviorAtoC.onCollisionEndCalled, isFalse);\n          expect(collisionBehaviorAtoComponent.onCollisionEndCalled, isTrue);\n        },\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_behaviors/test/src/entity_test.dart",
    "content": "import 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../helpers/helpers.dart';\n\nclass _TestEntity extends Entity {\n  _TestEntity({super.behaviors});\n}\n\nclass _TestBehavior extends Behavior<_TestEntity> {}\n\nvoid main() {\n  group('Entity', () {\n    flameGame.testGameWidget(\n      'adds behaviors directly to itself',\n      setUp: (game, tester) async {\n        final behavior = _TestBehavior();\n        final entity = _TestEntity(behaviors: [behavior]);\n        await game.ensureAdd(entity);\n      },\n      verify: (game, tester) async {\n        final entity = game.firstChild<_TestEntity>()!;\n\n        expect(game.descendants().whereType<_TestBehavior>().length, equals(1));\n        expect(entity.children.whereType<_TestBehavior>().length, equals(1));\n      },\n    );\n\n    flameGame.testGameWidget(\n      'adds behaviors to itself',\n      setUp: (game, tester) async {\n        final behavior = _TestBehavior();\n        final entity = _TestEntity();\n        await entity.add(behavior);\n        await game.ensureAdd(entity);\n      },\n      verify: (game, tester) async {\n        final entity = game.firstChild<_TestEntity>()!;\n\n        expect(game.descendants().whereType<_TestBehavior>().length, equals(1));\n        expect(entity.children.whereType<_TestBehavior>().length, equals(1));\n      },\n    );\n\n    flameGame.testGameWidget(\n      'behavior can be removed from entity and the internal cache',\n      setUp: (game, tester) async {\n        final entity = _TestEntity(behaviors: []);\n        await game.ensureAdd(entity);\n\n        final behavior = _TestBehavior();\n        expect(\n          entity.findBehavior<_TestBehavior>,\n          throwsBehaviorNotFoundFor<_TestBehavior>(),\n        );\n        await entity.ensureAdd(behavior);\n        expect(entity.findBehavior<_TestBehavior>(), isNotNull);\n        behavior.removeFromParent();\n      },\n      verify: (game, tester) async {\n        final entity = game.firstChild<_TestEntity>()!;\n\n        expect(\n          entity.findBehavior<_TestBehavior>,\n          throwsBehaviorNotFoundFor<_TestBehavior>(),\n        );\n      },\n    );\n\n    flameGame.testGameWidget(\n      'can correctly confirm if it has a behavior',\n      setUp: (game, tester) async {\n        final entity = _TestEntity(behaviors: []);\n        final behavior = _TestBehavior();\n        await game.ensureAdd(entity);\n\n        expect(entity.hasBehavior<_TestBehavior>(), isFalse);\n        await entity.ensureAdd(behavior);\n        expect(entity.hasBehavior<_TestBehavior>(), isTrue);\n\n        behavior.removeFromParent();\n      },\n      verify: (game, tester) async {\n        final entity = game.firstChild<_TestEntity>()!;\n\n        expect(entity.hasBehavior<_TestBehavior>(), isFalse);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_bloc/.min_coverage",
    "content": "100.0\n"
  },
  {
    "path": "packages/flame_bloc/CHANGELOG.md",
    "content": "## 1.12.22\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 1.12.21\n\n - Update a dependency to the latest release.\n\n## 1.12.20\n\n - Update a dependency to the latest release.\n\n## 1.12.19\n\n - Update a dependency to the latest release.\n\n## 1.12.18\n\n - Update a dependency to the latest release.\n\n## 1.12.17\n\n - Update a dependency to the latest release.\n\n## 1.12.16\n\n - Update a dependency to the latest release.\n\n## 1.12.15\n\n - Update a dependency to the latest release.\n\n## 1.12.14\n\n - Update a dependency to the latest release.\n\n## 1.12.13\n\n - Update a dependency to the latest release.\n\n## 1.12.12\n\n - Update a dependency to the latest release.\n\n## 1.12.11\n\n - Update a dependency to the latest release.\n\n## 1.12.10\n\n - Update a dependency to the latest release.\n\n## 1.12.9\n\n - Update a dependency to the latest release.\n\n## 1.12.8\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 1.12.7\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 1.12.6\n\n - Update a dependency to the latest release.\n\n## 1.12.5\n\n - Update a dependency to the latest release.\n\n## 1.12.4\n\n - Update a dependency to the latest release.\n\n## 1.12.3\n\n - Update a dependency to the latest release.\n\n## 1.12.2\n\n - Update a dependency to the latest release.\n\n## 1.12.1\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 1.12.0\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n - **FIX**: Call `super.onLoad` from `FlameBlockReader` ([#3175](https://github.com/flame-engine/flame/issues/3175)). ([349f7bd7](https://github.com/flame-engine/flame/commit/349f7bd71437abad666d05f973b6983970ccd0c6))\n - **FEAT**: Expand flame_lint to respect required pub.dev checks ([#3139](https://github.com/flame-engine/flame/issues/3139)). ([6e80bf5e](https://github.com/flame-engine/flame/commit/6e80bf5e679d1cdeeb9362d4103690b0b381161d))\n\n## 1.11.1\n\n - Update a dependency to the latest release.\n\n## 1.11.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 1.10.10\n\n - Update a dependency to the latest release.\n\n## 1.10.9\n\n - Update a dependency to the latest release.\n\n## 1.10.8\n\n - Update a dependency to the latest release.\n\n## 1.10.7\n\n - Update a dependency to the latest release.\n\n## 1.10.6\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n## 1.10.5\n\n - Update a dependency to the latest release.\n\n## 1.10.4\n\n - **FIX**: Remove deprecations for 1.10.0 ([#2809](https://github.com/flame-engine/flame/issues/2809)). ([5b67b8f1](https://github.com/flame-engine/flame/commit/5b67b8f14ad4fdb38a249d0a41ecba49ba2fcc44))\n\n## 1.10.3\n\n - Update a dependency to the latest release.\n\n## 1.10.2\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **DOCS**: Enable CSpell on tests ([#2723](https://github.com/flame-engine/flame/issues/2723)). ([e051298c](https://github.com/flame-engine/flame/commit/e051298cba76550229780438b1a589557c7b488d))\n\n## 1.10.1\n\n - Update a dependency to the latest release.\n\n## 1.10.0\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FEAT**: ComponentKey API ([#2566](https://github.com/flame-engine/flame/issues/2566)). ([b3efb612](https://github.com/flame-engine/flame/commit/b3efb612cb3cb77f69bc030e9ba71516348035d2))\n - **FEAT**: Add onInitialState to FlameBlocListener ([#2565](https://github.com/flame-engine/flame/issues/2565)). ([f440bbf5](https://github.com/flame-engine/flame/commit/f440bbf5db207d454b4abba75a62e0ff2ff5b408))\n\n## 1.9.0\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n - **FEAT**: Add listener for initial state on flame_bloc ([#2382](https://github.com/flame-engine/flame/issues/2382)). ([01121c22](https://github.com/flame-engine/flame/commit/01121c220bec391e0242dfa9afc3d4a03bb3358b))\n - **FEAT**: Accept `CollisionType` in hitbox constructor ([#2509](https://github.com/flame-engine/flame/issues/2509)). ([89926227](https://github.com/flame-engine/flame/commit/89926227c5132455b971dece6ed313634d7ac873))\n\n## 1.8.4\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n## 1.8.3\n\n - **REFACTOR**: Remove unused event \"ScoreEventCleared\" from flame_block example ([#2380](https://github.com/flame-engine/flame/issues/2380)). ([a9db3f4c](https://github.com/flame-engine/flame/commit/a9db3f4ce5c7c11ddca511826bdf9ab72eb19dfe))\n - **FIX**: Override `remove()` method to fix the functionality issue in the `FlameMultiBlocProvider` ([#2280](https://github.com/flame-engine/flame/issues/2280)). ([6a818464](https://github.com/flame-engine/flame/commit/6a818464f5f942ce25c3c3c59839b6bddaada386))\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n - **DOCS**: Fix actual typos that made into our dictionary ([#2305](https://github.com/flame-engine/flame/issues/2305)). ([343b8452](https://github.com/flame-engine/flame/commit/343b84529d8f06c0d020b97a40c082b71f0de770))\n\n## 1.8.2\n\n - **FIX**: Depend on test: any for flame_test ([#2207](https://github.com/flame-engine/flame/issues/2207)). ([acfd418d](https://github.com/flame-engine/flame/commit/acfd418d882ee6872f3aa9961c39680ec123c2e6))\n\n## 1.8.1\n\n - **FIX**: flame-bloc : Remove final keyword from subscription in FlameBlocListenable ([#2098](https://github.com/flame-engine/flame/issues/2098)). ([8a136c99](https://github.com/flame-engine/flame/commit/8a136c9985d7878940f2103484b90e1ffb202a03))\n\n## 1.8.0\n\n - **FEAT**: Add avoid_final_parameters, depend_on_referenced_packages, unnecessary_to_list_in_spreads ([#1927](https://github.com/flame-engine/flame/issues/1927)). ([deccb434](https://github.com/flame-engine/flame/commit/deccb4349d38b6a91ccf5bdf229980b2a3296ce5))\n - **FEAT**: Add `removeWhere` to `Component` ([#1878](https://github.com/flame-engine/flame/issues/1878)). ([abd28f28](https://github.com/flame-engine/flame/commit/abd28f28a627799ea4602026d91f52bc97feb91e))\n\n## 1.7.0\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Adding bloc getter to FlameBlocListenable mixin ([#1732](https://github.com/flame-engine/flame/issues/1732)). ([3d19caa3](https://github.com/flame-engine/flame/commit/3d19caa36dcb470b306b841ef9c03647a2f307d7))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **DOCS**: Fixing typo in `FlameMultiBlocProvider` dartdoc. ([67be6ab8](https://github.com/flame-engine/flame/commit/67be6ab86264f6def4b1b3b0e4ba00763c7dab4e))\n - **DOCS**: updating README to the new flame bloc version ([#1737](https://github.com/flame-engine/flame/issues/1737)). ([6a2356aa](https://github.com/flame-engine/flame/commit/6a2356aa5eba1696caa6f88ecfe8143c4ffdb507))\n\n## 1.6.0\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Adding bloc getter to FlameBlocListenable mixin ([#1732](https://github.com/flame-engine/flame/issues/1732)). ([3d19caa3](https://github.com/flame-engine/flame/commit/3d19caa36dcb470b306b841ef9c03647a2f307d7))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **DOCS**: updating README to the new flame bloc version ([#1737](https://github.com/flame-engine/flame/issues/1737)). ([6a2356aa](https://github.com/flame-engine/flame/commit/6a2356aa5eba1696caa6f88ecfe8143c4ffdb507))\n\n## 1.5.0\n\n - **REFACTOR**: Update and guarantee consistency on mocktail dev dependency version across repo ([#1701](https://github.com/flame-engine/flame/issues/1701)). ([f4a98878](https://github.com/flame-engine/flame/commit/f4a98878062dbd4fe8238a8b014e6be3e528c5d8))\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n - **FEAT**: new flame bloc API ([#1538](https://github.com/flame-engine/flame/issues/1538)). ([f98970a9](https://github.com/flame-engine/flame/commit/f98970a91f91fe70e4a38834d7b69bfcb438d197))\n\n## 1.4.0\n\n - **FEAT**: new flame bloc API (#1538). ([f98970a9](https://github.com/flame-engine/flame/commit/f98970a91f91fe70e4a38834d7b69bfcb438d197))\n - **FEAT**: flame tests can now generate golden tests (#1501). ([316a0b3b](https://github.com/flame-engine/flame/commit/316a0b3bb0996ed20a3b93175102524b38bfa3e2))\n\n## 1.3.0\n\n - **FEAT**: flame tests can now generate golden tests (#1501). ([316a0b3b](https://github.com/flame-engine/flame/commit/316a0b3bb0996ed20a3b93175102524b38bfa3e2))\n\n## 1.2.0\n\n - Graduate package to a stable release. See pre-releases prior to this version for changelog entries.\n\n## 1.2.0-releasecandidate.6\n\n - **FEAT**: Possibility to mark gesture events as handled (#1465). ([4c3960c3](https://github.com/flame-engine/flame/commit/4c3960c3418f8ff4d557c1764c6793468238a8da))\n\n## 1.2.0-releasecandidate.5\n\n - Update a dependency to the latest release.\n\n## 1.2.0-releasecandidate.4\n\n - Update a dependency to the latest release.\n\n## 1.2.0-releasecandidate.3\n\n - **REFACTOR**: Parent change and component removal logic (#1385). ([8b9fa352](https://github.com/flame-engine/flame/commit/8b9fa3521cc44f7696c5ce0b396e3007c2ae7e8c))\n\n## 1.2.0-releasecandidate.2\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Remove Loadable, optional onLoads (#1333). ([05f7a4c3](https://github.com/flame-engine/flame/commit/05f7a4c3d6b1e3b67575c4ec920cf270691bbab4))\n - **REFACTOR**: Loadable mixin no longer declares onMount and onRemove (#1243). ([b1f6a34c](https://github.com/flame-engine/flame/commit/b1f6a34c198a732d51471bf0b79a71a4f3e60973))\n - **FIX**: Fix collision detection comments and typo (#1422). ([dfeafdd6](https://github.com/flame-engine/flame/commit/dfeafdd6f3e962d6f5148340ab461a9e805652b7))\n - **FEAT**: adding FlameBloc mixin to allow its usage with enhanced FlameGame classes (#1399). ([78aab426](https://github.com/flame-engine/flame/commit/78aab42694c66c8b9ea749ac11187f1ed1789a4c))\n - **FEAT**: Components are now always added in the correct order (#1337). ([c753fc46](https://github.com/flame-engine/flame/commit/c753fc4636d337d850a5a5cc684be8155f08b214))\n - **FEAT**: Optional Camera argument in FlameBlocGame (#1331). ([bcb27f70](https://github.com/flame-engine/flame/commit/bcb27f706f3afcecfe417e065d6c16a6edb1463f))\n - **FEAT**: publish flame bloc (#1319). ([4d5adcb0](https://github.com/flame-engine/flame/commit/4d5adcb0d01d374ca807c71f2b8d963d0781a976))\n - **DOCS**: Upgrade documentation site (#1365). ([12cf8f70](https://github.com/flame-engine/flame/commit/12cf8f70963dc25b4e12182d0c7d80fe7d5a00e0))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n - **DOCS**: Fix typo in flame_bloc readme (#1332). ([9bff96bf](https://github.com/flame-engine/flame/commit/9bff96bf3a668fc107c0712aadc6b095ebd50788))\n - **BREAKING** **FEAT**: Use a broadphase to make collision detection more efficient (#1252). ([29dd09ca](https://github.com/flame-engine/flame/commit/29dd09ca925e934f3ca4e266a8a0cdb8ad62ef3b))\n - **BREAKING** **FEAT**: updating flame_bloc to bloc 8 (#1311). ([574e0ab5](https://github.com/flame-engine/flame/commit/574e0ab58baa14680cb0d0eded642b4729b062e7))\n\n## 1.2.0-releasecandidate.1\n\n## 1.1.0\n\n - Bump \"flame_bloc\" to `1.1.0`.\n\n# CHANGELOG\n\n## [1.0.0]\n - Bumped to 1.0.0 of Flame\n\n## [1.0.0-releasecandidate.15]\n - Initial release\n"
  },
  {
    "path": "packages/flame_bloc/DEPRECATED_README.md",
    "content": "# Flame Bloc 🔥🧱\n\nThis is documentation for the deprecated version of the Flame Bloc. This API will live until the\nnext major version is released.\n\n`flame_bloc` provides easy access to blocs/cubits that are available on the widget tree to your Flame\ngame and makes it possible for Flame components to listen to state changes to those blocs/cubits.\n\n\n## How to use\n\nLets assume we have a bloc that handles player inventory and it is available on the widget tree via\na `BlocProvider` like this:\n\n```dart\nBlocProvider<ExampleGame>(\n  create: (_) => InventoryBloc(),\n  child: GameWidget(game: ExampleGame()),\n)\n```\n\nTo enable the features of `flame_bloc` in your game, you can make your game class inherit from\n`FlameBlocGame`, or if you are already using an enhanced `FlameGame` class (like for example a\n`Forge2DGame`), the `FlameBloc` mixin can be used instead.\n\n\nTo access the bloc from inside your game, the `read` method can be used.\n\n```dart\nclass ExampleGame extends FlameBlocGame {\n  void selectWeapon() {\n    read<InventoryBloc>.add(WeaponSelected('axe'));\n  }\n}\n```\n\nTo have your components listen to state change, the `BlocComponent` mixin can be used.\n\n\n```dart\nclass PlayerComponent with BlocComponent<InventoryBloc, InventoryState> {\n\n  // onNewState can be overridden to so the component\n  // can be notified on state changes\n  @override\n  void onNewState(InventoryState state) {\n    print(state.weapon);\n  }\n\n  @override\n  void update(double dt) {\n    // the `state` getter can also be used to have\n    // direct access to the current state\n    print(state.weapon);\n  }\n}\n```\n\nFor a full example, check the [example folder](./example)\n"
  },
  {
    "path": "packages/flame_bloc/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_bloc/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nOffers a simple and natural way to use <a href=\"https://github.com/felangel/bloc\">flutter_bloc</a> inside <a href=\"https://github.com/flame-engine/flame\">Flame</a>.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_bloc\" ><img src=\"https://img.shields.io/pub/v/flame_bloc.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n# flame_bloc 🔥🧱\n\n`flame_bloc` offers a simple and natural (as in similar to `flutter_bloc`) way to use blocs and\ncubits inside a `FlameGame`.\n\nFor a migration guide from the previous API to the current one,\n[check this article](https://verygood.ventures/blog/flame-bloc-new-api).\n\n\n## How to use\n\nLets assume we have a bloc that handles player inventory, first we need to make it available to our\ncomponents.\n\nWe can do that by using `FlameBlocProvider` component:\n\n```dart\nclass MyGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    await add(\n      FlameBlocProvider<PlayerInventoryBloc, PlayerInventoryState>(\n        create: () => PlayerInventoryBloc(),\n        children: [\n          Player(),\n          // ...\n        ],\n      ),\n    );\n  }\n}\n```\n\nWith the above changes, the `Player` component will now have access to our bloc.\n\nIf more than one bloc needs to be provided, `FlameMultiBlocProvider` can be used in a similar fashion:\n\n```dart\nclass MyGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    await add(\n      FlameMultiBlocProvider(\n        providers: [\n          FlameBlocProvider<PlayerInventoryBloc, PlayerInventoryState>(\n            create: () => PlayerInventoryBloc(),\n          ),\n          FlameBlocProvider<PlayerStatsBloc, PlayerStatsState>(\n            create: () => PlayerStatsBloc(),\n          ),\n        ],\n        children: [\n          Player(),\n          // ...\n        ],\n      ),\n    );\n  }\n}\n```\n\nListening to states changes at the component level can be done with two approaches:\n\nBy using `FlameBlocListener` component:\n\n```dart\nclass Player extends PositionComponent {\n  @override\n  Future<void> onLoad() async {\n    await add(\n      FlameBlocListener<PlayerInventoryBloc, PlayerInventoryState>(\n        listener: (state) {\n          updateGear(state);\n        },\n      ),\n    );\n  }\n}\n```\n\nOr by using `FlameBlocListenable` mixin:\n\n```dart\nclass Player extends PositionComponent\n  with FlameBlocListenable<PlayerInventoryBloc, PlayerInventoryState> {\n\n  @override\n  void onNewState(state) {\n    updateGear(state);\n  }\n}\n```\n\nIf all your component need is to simply access a bloc, the `FlameBlocReader` mixin can be applied\nto a component:\n\n\n```dart\nclass Player extends PositionComponent\n  with FlameBlocReader<PlayerStatsBloc, PlayerStatsState> {\n\n  void takeHit() {\n    bloc.add(const PlayerDamaged());\n  }\n}\n```\n\nNote that one limitation of the mixin is that it can access only a single bloc.\n\nFor a full example, check the [example folder](./example)\n"
  },
  {
    "path": "packages/flame_bloc/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    - public_member_api_docs\n"
  },
  {
    "path": "packages/flame_bloc/example/README.md",
    "content": "# example\n\nExample game using `flame_bloc` package.\n\n"
  },
  {
    "path": "packages/flame_bloc/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    public_member_api_docs: false\n"
  },
  {
    "path": "packages/flame_bloc/example/lib/main.dart",
    "content": "import 'package:flame_bloc_example/src/game.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(const MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return const MaterialApp(\n      home: GamePage(),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/example/lib/src/game/components/bullet.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_bloc_example/src/game/components/enemy.dart';\nimport 'package:flame_bloc_example/src/game/game.dart';\nimport 'package:flame_bloc_example/src/inventory/bloc/inventory_bloc.dart';\n\nclass BulletComponent extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame>, CollisionCallbacks {\n  static const bulletSpeed = -500;\n\n  bool destroyed = false;\n\n  double xDirection;\n\n  final Weapon weapon;\n\n  BulletComponent(\n    double x,\n    double y,\n    this.weapon, {\n    this.xDirection = 0.0,\n  }) : super(position: Vector2(x, y)) {\n    size = Vector2(_mapWidth(), 20);\n\n    add(RectangleHitbox());\n  }\n\n  double _mapWidth() {\n    return switch (weapon) {\n      Weapon.bullet => 10,\n      Weapon.laser || Weapon.plasma => 5,\n    };\n  }\n\n  String _mapSpritePath() {\n    return switch (weapon) {\n      Weapon.bullet => 'bullet.png',\n      Weapon.laser => 'laser.png',\n      Weapon.plasma => 'plasma.png',\n    };\n  }\n\n  double _mapSpriteWidth() {\n    return switch (weapon) {\n      Weapon.bullet => 8,\n      Weapon.laser || Weapon.plasma => 4,\n    };\n  }\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    animation = await game.loadSpriteAnimation(\n      _mapSpritePath(),\n      SpriteAnimationData.sequenced(\n        stepTime: 0.2,\n        amount: 4,\n        textureSize: Vector2(_mapSpriteWidth(), 16),\n      ),\n    );\n  }\n\n  @override\n  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {\n    super.onCollision(intersectionPoints, other);\n    if (other is EnemyComponent) {\n      destroyed = true;\n      other.takeHit();\n    }\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    y += bulletSpeed * dt;\n    if (xDirection != 0) {\n      x += bulletSpeed * dt * xDirection;\n    }\n\n    if (destroyed || toRect().bottom <= 0) {\n      removeFromParent();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/example/lib/src/game/components/enemy.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\n\nimport 'package:flame_bloc_example/src/game/components/explosion.dart';\nimport 'package:flame_bloc_example/src/game/game.dart';\n\nclass EnemyComponent extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame>, CollisionCallbacks {\n  static const enemySpeed = 50;\n\n  bool destroyed = false;\n\n  EnemyComponent(double x, double y)\n    : super(position: Vector2(x, y), size: Vector2.all(25)) {\n    add(RectangleHitbox(collisionType: CollisionType.passive));\n  }\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    animation = await game.loadSpriteAnimation(\n      'enemy.png',\n      SpriteAnimationData.sequenced(\n        stepTime: 0.2,\n        amount: 4,\n        textureSize: Vector2.all(16),\n      ),\n    );\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    y += enemySpeed * dt;\n    if (destroyed || y >= game.size.y) {\n      removeFromParent();\n    }\n  }\n\n  void takeHit() {\n    destroyed = true;\n\n    game.add(ExplosionComponent(x - 25, y - 25));\n    game.increaseScore();\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/example/lib/src/game/components/enemy_creator.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\n\nimport 'package:flame_bloc_example/src/game/components/enemy.dart';\nimport 'package:flame_bloc_example/src/game/game.dart';\n\nclass EnemyCreator extends TimerComponent\n    with HasGameReference<SpaceShooterGame> {\n  Random random = Random();\n\n  EnemyCreator() : super(period: 1, repeat: true);\n\n  @override\n  void onTick() {\n    game.add(\n      EnemyComponent(\n        (game.size.x - 25) * random.nextDouble(),\n        0,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/example/lib/src/game/components/explosion.dart",
    "content": "import 'package:flame/components.dart';\n\nimport 'package:flame_bloc_example/src/game/game.dart';\n\nclass ExplosionComponent extends SpriteAnimationComponent\n    with HasGameReference<SpaceShooterGame> {\n  ExplosionComponent(double x, double y)\n    : super(\n        position: Vector2(x, y),\n        size: Vector2.all(50),\n        removeOnFinish: true,\n      );\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    animation = await game.loadSpriteAnimation(\n      'explosion.png',\n      SpriteAnimationData.sequenced(\n        stepTime: 0.1,\n        amount: 6,\n        loop: false,\n        textureSize: Vector2.all(32),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/example/lib/src/game/components/player.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_bloc/flame_bloc.dart';\nimport 'package:flame_bloc_example/src/game/components/bullet.dart';\nimport 'package:flame_bloc_example/src/game/components/enemy.dart';\nimport 'package:flame_bloc_example/src/game/components/explosion.dart';\nimport 'package:flame_bloc_example/src/game/game.dart';\nimport 'package:flame_bloc_example/src/game_stats/bloc/game_stats_bloc.dart';\nimport 'package:flame_bloc_example/src/inventory/bloc/inventory_bloc.dart';\nimport 'package:flutter/services.dart';\n\nclass PlayerController extends Component\n    with\n        HasGameReference<SpaceShooterGame>,\n        FlameBlocListenable<GameStatsBloc, GameStatsState> {\n  @override\n  bool listenWhen(GameStatsState previousState, GameStatsState newState) {\n    return previousState.status != newState.status;\n  }\n\n  @override\n  void onNewState(GameStatsState state) {\n    if (state.status == GameStatus.respawn ||\n        state.status == GameStatus.initial) {\n      game.statsBloc.add(const PlayerRespawned());\n      parent?.add(game.player = PlayerComponent());\n    }\n  }\n}\n\nclass PlayerComponent extends SpriteAnimationComponent\n    with\n        HasGameReference<SpaceShooterGame>,\n        CollisionCallbacks,\n        KeyboardHandler,\n        FlameBlocListenable<InventoryBloc, InventoryState> {\n  bool destroyed = false;\n  late Timer bulletCreator;\n\n  PlayerComponent()\n    : super(size: Vector2(50, 75), position: Vector2(100, 500)) {\n    bulletCreator = Timer(0.5, repeat: true, onTick: _createBullet);\n\n    add(RectangleHitbox());\n  }\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    animation = await game.loadSpriteAnimation(\n      'player.png',\n      SpriteAnimationData.sequenced(\n        stepTime: 0.2,\n        amount: 4,\n        textureSize: Vector2(32, 48),\n      ),\n    );\n  }\n\n  InventoryState? state;\n\n  @override\n  void onNewState(InventoryState state) {\n    this.state = state;\n  }\n\n  void _createBullet() {\n    final bulletX = x + 20;\n    final bulletY = y + 20;\n\n    game.add(\n      BulletComponent(\n        bulletX,\n        bulletY,\n        state?.weapon ?? Weapon.bullet,\n      ),\n    );\n  }\n\n  void beginFire() {\n    bulletCreator.start();\n  }\n\n  void stopFire() {\n    bulletCreator.stop();\n  }\n\n  void move(double deltaX, double deltaY) {\n    x += deltaX;\n    y += deltaY;\n  }\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    bulletCreator.update(dt);\n    if (destroyed) {\n      removeFromParent();\n    }\n  }\n\n  void takeHit() {\n    game.add(ExplosionComponent(x, y));\n    removeFromParent();\n    game.statsBloc.add(const PlayerDied());\n  }\n\n  @override\n  bool onKeyEvent(\n    KeyEvent event,\n    Set<LogicalKeyboardKey> keysPressed,\n  ) {\n    if (keysPressed.contains(LogicalKeyboardKey.tab)) {\n      game.inventoryBloc.add(const NextWeaponEquipped());\n      return true;\n    }\n    return false;\n  }\n\n  @override\n  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {\n    super.onCollision(intersectionPoints, other);\n    if (other is EnemyComponent) {\n      takeHit();\n      other.takeHit();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/example/lib/src/game/game.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_bloc/flame_bloc.dart';\nimport 'package:flame_bloc_example/src/game/components/enemy.dart';\nimport 'package:flame_bloc_example/src/game/components/enemy_creator.dart';\nimport 'package:flame_bloc_example/src/game/components/player.dart';\nimport 'package:flame_bloc_example/src/game_stats/bloc/game_stats_bloc.dart';\nimport 'package:flame_bloc_example/src/inventory/bloc/inventory_bloc.dart';\n\nclass GameStatsController extends Component\n    with HasGameReference<SpaceShooterGame> {\n  @override\n  Future<void>? onLoad() async {\n    add(\n      FlameBlocListener<GameStatsBloc, GameStatsState>(\n        listenWhen: (previousState, newState) {\n          return previousState.status != newState.status &&\n              newState.status == GameStatus.initial;\n        },\n        onNewState: (state) {\n          game.removeWhere((element) => element is EnemyComponent);\n        },\n      ),\n    );\n  }\n}\n\nclass SpaceShooterGame extends FlameGame\n    with PanDetector, HasCollisionDetection, HasKeyboardHandlerComponents {\n  late PlayerComponent player;\n\n  final GameStatsBloc statsBloc;\n  final InventoryBloc inventoryBloc;\n\n  SpaceShooterGame({\n    required this.statsBloc,\n    required this.inventoryBloc,\n  });\n\n  @override\n  Future<void> onLoad() async {\n    await add(\n      FlameMultiBlocProvider(\n        providers: [\n          FlameBlocProvider<InventoryBloc, InventoryState>.value(\n            value: inventoryBloc,\n          ),\n          FlameBlocProvider<GameStatsBloc, GameStatsState>.value(\n            value: statsBloc,\n          ),\n        ],\n        children: [\n          player = PlayerComponent(),\n          PlayerController(),\n          GameStatsController(),\n        ],\n      ),\n    );\n\n    add(EnemyCreator());\n  }\n\n  @override\n  void onPanStart(_) {\n    player.beginFire();\n  }\n\n  @override\n  void onPanEnd(_) {\n    player.stopFire();\n  }\n\n  @override\n  void onPanCancel() {\n    player.stopFire();\n  }\n\n  @override\n  void onPanUpdate(DragUpdateInfo info) {\n    player.move(info.delta.global.x, info.delta.global.y);\n  }\n\n  void increaseScore() {\n    statsBloc.add(const ScoreEventAdded(100));\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/example/lib/src/game.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame_bloc_example/src/game/game.dart';\nimport 'package:flame_bloc_example/src/game_stats/bloc/game_stats_bloc.dart';\nimport 'package:flame_bloc_example/src/game_stats/view/game_stat.dart';\nimport 'package:flame_bloc_example/src/inventory/bloc/inventory_bloc.dart';\nimport 'package:flame_bloc_example/src/inventory/view/inventory.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass GamePage extends StatelessWidget {\n  const GamePage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: MultiBlocProvider(\n        providers: [\n          BlocProvider<GameStatsBloc>(create: (_) => GameStatsBloc()),\n          BlocProvider<InventoryBloc>(create: (_) => InventoryBloc()),\n        ],\n        child: const GameView(),\n      ),\n    );\n  }\n}\n\nclass GameView extends StatelessWidget {\n  const GameView({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return const Column(\n      children: [\n        GameStat(),\n        Expanded(\n          child: Stack(\n            children: [\n              Positioned.fill(child: Game()),\n              Positioned(top: 50, right: 10, child: Inventory()),\n            ],\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass Game extends StatelessWidget {\n  const Game({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return GameWidget(\n      game: SpaceShooterGame(\n        statsBloc: context.read<GameStatsBloc>(),\n        inventoryBloc: context.read<InventoryBloc>(),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/example/lib/src/game_stats/bloc/game_stats_bloc.dart",
    "content": "import 'package:equatable/equatable.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\npart 'game_stats_event.dart';\npart 'game_stats_state.dart';\n\nclass GameStatsBloc extends Bloc<GameStatsEvent, GameStatsState> {\n  GameStatsBloc() : super(const GameStatsState.empty()) {\n    on<ScoreEventAdded>(\n      (event, emit) => emit(\n        state.copyWith(score: state.score + event.score),\n      ),\n    );\n\n    on<PlayerRespawned>(\n      (event, emit) => emit(\n        state.copyWith(status: GameStatus.respawned),\n      ),\n    );\n\n    on<PlayerDied>((event, emit) {\n      if (state.lives > 1) {\n        emit(\n          state.copyWith(\n            lives: state.lives - 1,\n            status: GameStatus.respawn,\n          ),\n        );\n      } else {\n        emit(\n          state.copyWith(\n            lives: 0,\n            status: GameStatus.gameOver,\n          ),\n        );\n      }\n    });\n\n    on<GameReset>(\n      (event, emit) => emit(\n        const GameStatsState.empty(),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/example/lib/src/game_stats/bloc/game_stats_event.dart",
    "content": "part of 'game_stats_bloc.dart';\n\nabstract class GameStatsEvent extends Equatable {\n  const GameStatsEvent();\n}\n\nclass ScoreEventAdded extends GameStatsEvent {\n  const ScoreEventAdded(this.score);\n\n  final int score;\n\n  @override\n  List<Object?> get props => [score];\n}\n\nclass PlayerDied extends GameStatsEvent {\n  const PlayerDied();\n\n  @override\n  List<Object?> get props => [];\n}\n\nclass PlayerRespawned extends GameStatsEvent {\n  const PlayerRespawned();\n\n  @override\n  List<Object?> get props => [];\n}\n\nclass GameReset extends GameStatsEvent {\n  const GameReset();\n\n  @override\n  List<Object?> get props => [];\n}\n"
  },
  {
    "path": "packages/flame_bloc/example/lib/src/game_stats/bloc/game_stats_state.dart",
    "content": "part of 'game_stats_bloc.dart';\n\nenum GameStatus {\n  initial,\n  respawn,\n  respawned,\n  gameOver,\n}\n\nclass GameStatsState extends Equatable {\n  final int score;\n  final int lives;\n  final GameStatus status;\n\n  const GameStatsState({\n    required this.score,\n    required this.lives,\n    required this.status,\n  });\n\n  const GameStatsState.empty()\n    : this(\n        score: 0,\n        lives: 3,\n        status: GameStatus.initial,\n      );\n\n  GameStatsState copyWith({\n    int? score,\n    int? lives,\n    GameStatus? status,\n  }) {\n    return GameStatsState(\n      score: score ?? this.score,\n      lives: lives ?? this.lives,\n      status: status ?? this.status,\n    );\n  }\n\n  @override\n  List<Object?> get props => [score, lives, status];\n}\n"
  },
  {
    "path": "packages/flame_bloc/example/lib/src/game_stats/view/game_stat.dart",
    "content": "import 'package:flame_bloc_example/src/game_stats/bloc/game_stats_bloc.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass GameStat extends StatelessWidget {\n  const GameStat({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocConsumer<GameStatsBloc, GameStatsState>(\n      builder: (context, state) {\n        return Container(\n          height: 50,\n          color: Colors.white,\n          child: Row(\n            mainAxisAlignment: MainAxisAlignment.spaceAround,\n            children: [\n              Text('Score: ${state.score}'),\n              Text('Lives: ${state.lives}'),\n            ],\n          ),\n        );\n      },\n      listenWhen: (previous, current) =>\n          previous.status != current.status &&\n          current.status == GameStatus.gameOver,\n      listener: (context, state) {\n        final bloc = context.read<GameStatsBloc>();\n        showDialog<void>(\n          context: context,\n          builder: (context) {\n            return Dialog(\n              child: Container(\n                height: 200,\n                padding: const EdgeInsets.all(32),\n                child: Center(\n                  child: Column(\n                    children: [\n                      Text(\n                        'Game Over',\n                        style: Theme.of(context).textTheme.displayMedium,\n                      ),\n                      ElevatedButton(\n                        onPressed: () {\n                          bloc.add(const GameReset());\n                          Navigator.of(context).pop();\n                        },\n                        child: const Text('Reset'),\n                      ),\n                    ],\n                  ),\n                ),\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/example/lib/src/inventory/bloc/inventory_bloc.dart",
    "content": "import 'package:equatable/equatable.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\npart 'inventory_event.dart';\npart 'inventory_state.dart';\n\nclass InventoryBloc extends Bloc<InventoryEvent, InventoryState> {\n  InventoryBloc() : super(const InventoryState.empty()) {\n    on<WeaponEquipped>(\n      (event, emit) => emit(\n        state.copyWith(weapon: event.weapon),\n      ),\n    );\n\n    on<NextWeaponEquipped>((event, emit) {\n      const values = Weapon.values;\n      final i = values.indexOf(state.weapon);\n      if (i == values.length - 1) {\n        emit(state.copyWith(weapon: Weapon.bullet));\n      } else {\n        emit(state.copyWith(weapon: values[i + 1]));\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/example/lib/src/inventory/bloc/inventory_event.dart",
    "content": "part of 'inventory_bloc.dart';\n\nabstract class InventoryEvent extends Equatable {\n  const InventoryEvent();\n}\n\nclass WeaponEquipped extends InventoryEvent {\n  final Weapon weapon;\n\n  const WeaponEquipped(this.weapon);\n\n  @override\n  List<Object?> get props => [weapon];\n}\n\nclass NextWeaponEquipped extends InventoryEvent {\n  const NextWeaponEquipped();\n\n  @override\n  List<Object?> get props => [];\n}\n"
  },
  {
    "path": "packages/flame_bloc/example/lib/src/inventory/bloc/inventory_state.dart",
    "content": "part of 'inventory_bloc.dart';\n\nenum Weapon {\n  bullet,\n  laser,\n  plasma,\n}\n\nclass InventoryState extends Equatable {\n  final Weapon weapon;\n\n  const InventoryState({\n    required this.weapon,\n  });\n\n  const InventoryState.empty() : this(weapon: Weapon.bullet);\n\n  InventoryState copyWith({\n    Weapon? weapon,\n  }) {\n    return InventoryState(weapon: weapon ?? this.weapon);\n  }\n\n  @override\n  List<Object?> get props => [weapon];\n}\n"
  },
  {
    "path": "packages/flame_bloc/example/lib/src/inventory/view/inventory.dart",
    "content": "import 'package:flame_bloc_example/src/inventory/bloc/inventory_bloc.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass Inventory extends StatelessWidget {\n  const Inventory({super.key});\n\n  Color _mapWeaponColor(Weapon weapon) {\n    return switch (weapon) {\n      Weapon.bullet => Colors.orange,\n      Weapon.laser => Colors.red,\n      Weapon.plasma => Colors.blue,\n    };\n  }\n\n  String _mapWeaponName(Weapon weapon) {\n    return switch (weapon) {\n      Weapon.bullet => 'Kinetic',\n      Weapon.laser => 'Laser',\n      Weapon.plasma => 'Plasma',\n    };\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final state = context.watch<InventoryBloc>().state;\n\n    return Column(\n      children: [\n        for (final weapon in Weapon.values)\n          GestureDetector(\n            onTap: () {\n              if (weapon != state.weapon) {\n                context.read<InventoryBloc>().add(WeaponEquipped(weapon));\n              }\n            },\n            child: Opacity(\n              opacity: weapon == state.weapon ? 0.8 : 0.6,\n              child: Container(\n                color: Colors.white,\n                width: 50,\n                height: 50,\n                child: Center(\n                  child: Text(\n                    _mapWeaponName(weapon),\n                    style: TextStyle(color: _mapWeaponColor(weapon)),\n                  ),\n                ),\n              ),\n            ),\n          ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/example/pubspec.yaml",
    "content": "name: flame_bloc_example\nresolution: workspace\ndescription: Example game using the flame_bloc package\n\npublish_to: 'none'\n\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  equatable: ^2.0.5\n  flame: ^1.36.0\n  flame_bloc: ^1.12.22\n  flutter:\n    sdk: flutter\n  flutter_bloc: ^8.1.2\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n\nflutter:\n  uses-material-design: true\n  assets:\n    - assets/images/\n"
  },
  {
    "path": "packages/flame_bloc/lib/flame_bloc.dart",
    "content": "export 'src/flame_bloc_listenable.dart';\nexport 'src/flame_bloc_listener.dart';\nexport 'src/flame_bloc_provider.dart';\nexport 'src/flame_bloc_reader.dart';\nexport 'src/flame_multi_bloc_provider.dart';\n"
  },
  {
    "path": "packages/flame_bloc/lib/src/flame_bloc_listenable.dart",
    "content": "import 'dart:async';\n\nimport 'package:bloc/bloc.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_bloc/flame_bloc.dart';\nimport 'package:meta/meta.dart';\n\n/// Adds [Bloc] access and listening to a [Component]\nmixin FlameBlocListenable<B extends BlocBase<S>, S> on Component {\n  late S _state;\n  late StreamSubscription<S> _subscription;\n  late B _bloc;\n  B? _blocOverride;\n\n  /// Returns the bloc that this component is reading from once the component\n  /// has been mounted.\n  B get bloc {\n    assert(\n      isMounted,\n      'Cannot access the bloc instance before it has been mounted.',\n    );\n    return _bloc;\n  }\n\n  /// Explicitly set the bloc instance which the component will be listening to.\n  /// This is useful in cases where the bloc being listened to\n  /// has not be provided via [FlameBlocProvider].\n  set bloc(B bloc) {\n    assert(\n      _blocOverride == null,\n      'Cannot update the bloc instance once it has been set.',\n    );\n    _blocOverride = bloc;\n  }\n\n  @override\n  @mustCallSuper\n  void onMount() {\n    super.onMount();\n    var bloc = _blocOverride;\n    if (bloc == null) {\n      final providers = ancestors().whereType<FlameBlocProvider<B, S>>();\n      assert(\n        providers.isNotEmpty,\n        'No FlameBlocProvider<$B, $S> available on the component tree',\n      );\n\n      final provider = providers.first;\n      _blocOverride = bloc = provider.bloc;\n    }\n    _bloc = bloc;\n    _state = bloc.state;\n    onInitialState(_state);\n    _subscription = bloc.stream.listen((newState) {\n      if (_state != newState) {\n        final callNewState = listenWhen(_state, newState);\n        _state = newState;\n\n        if (callNewState) {\n          onNewState(newState);\n        }\n      }\n    });\n  }\n\n  /// Override this to make [onNewState] be called only when\n  /// a certain state change happens.\n  ///\n  /// Default implementation returns true.\n  bool listenWhen(S previousState, S newState) => true;\n\n  /// Listener called every time a new state is emitted to this component.\n  ///\n  /// Default implementation is a no-op.\n  void onNewState(S state) {}\n\n  /// Called only once with the initial state provided.\n  ///\n  /// Default implementation is a no-op.\n  void onInitialState(S state) {}\n\n  @override\n  @mustCallSuper\n  void onRemove() {\n    super.onRemove();\n    _subscription.cancel();\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/lib/src/flame_bloc_listener.dart",
    "content": "import 'package:bloc/bloc.dart';\nimport 'package:flame/components.dart';\n\nimport 'package:flame_bloc/flame_bloc.dart';\n\n/// {@template flame_bloc_listener}\n/// A [Component] which can listen to changes in a [Bloc] state.\n/// {@endtemplate}\nclass FlameBlocListener<B extends BlocBase<S>, S> extends Component\n    with FlameBlocListenable<B, S> {\n  /// {@macro flame_bloc_listener}\n  FlameBlocListener({\n    required void Function(S state) onNewState,\n    void Function(S state)? onInitialState,\n    B? bloc,\n    bool Function(S previousState, S newState)? listenWhen,\n    super.key,\n  }) : _onNewState = onNewState,\n       _onInitialState = onInitialState,\n       _listenWhen = listenWhen {\n    if (bloc != null) {\n      this.bloc = bloc;\n    }\n  }\n\n  final void Function(S state) _onNewState;\n  final void Function(S state)? _onInitialState;\n  final bool Function(S previousState, S newState)? _listenWhen;\n\n  @override\n  void onNewState(S state) => _onNewState(state);\n\n  @override\n  void onInitialState(S state) => _onInitialState?.call(state);\n\n  @override\n  bool listenWhen(S previousState, S newState) {\n    return _listenWhen?.call(previousState, newState) ?? true;\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/lib/src/flame_bloc_provider.dart",
    "content": "import 'package:bloc/bloc.dart';\nimport 'package:flame/components.dart';\nimport 'package:flutter/material.dart';\n\n/// {@template flame_bloc_provider}\n/// A [Component] that provides a [Bloc] to its children\n/// {@endtemplate}\nclass FlameBlocProvider<B extends BlocBase<S>, S> extends Component {\n  /// {@macro flame_bloc_provider}\n  ///\n  /// Will provide the [Bloc] returned by the [create] function,\n  /// when this constructor is used, the bloc will only live while\n  /// this component is alive.\n  FlameBlocProvider({\n    required B Function() create,\n    List<Component>? children,\n    super.key,\n  }) : _bloc = create(),\n       _created = true {\n    _addChildren(children);\n  }\n\n  /// {@macro flame_bloc_provider}\n  ///\n  /// Will provide the given [value] to its children, when this constructor is\n  /// used, the user is responsible for disposing of the bloc.\n  FlameBlocProvider.value({\n    required B value,\n    List<Component>? children,\n  }) : _bloc = value,\n       _created = false {\n    _addChildren(children);\n  }\n\n  final B _bloc;\n  final bool _created;\n\n  void _addChildren(List<Component>? children) {\n    if (children != null) {\n      children.forEach(add);\n    }\n  }\n\n  /// Returns the [Bloc] provided by this [FlameBlocProvider]\n  B get bloc => _bloc;\n\n  @override\n  @mustCallSuper\n  void onRemove() {\n    super.onRemove();\n\n    if (_created) {\n      _bloc.close();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/lib/src/flame_bloc_reader.dart",
    "content": "import 'dart:async';\n\nimport 'package:bloc/bloc.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_bloc/flame_bloc.dart';\nimport 'package:flutter/material.dart';\n\n/// Adds [Bloc] access to a [Component].\n///\n/// Useful for components that needs to only read\n/// a bloc current state or to trigger an event on it\nmixin FlameBlocReader<B extends BlocBase<S>, S> on Component {\n  late B _bloc;\n\n  /// Returns the bloc that this component is reading from\n  B get bloc => _bloc;\n\n  @override\n  @mustCallSuper\n  Future<void> onLoad() async {\n    super.onLoad();\n    final providers = ancestors().whereType<FlameBlocProvider<B, S>>();\n    assert(\n      providers.isNotEmpty,\n      'No FlameBlocProvider<$B, $S> available on the component tree',\n    );\n\n    final provider = providers.first;\n    _bloc = provider.bloc;\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/lib/src/flame_multi_bloc_provider.dart",
    "content": "import 'package:flame/components.dart';\n\nimport 'package:flame_bloc/flame_bloc.dart';\n\n/// {@template flame_multi_bloc_provider}\n/// Similar to [FlameBlocProvider], but provides multiples blocs down\n/// to the component tree\n/// {@endtemplate}\nclass FlameMultiBlocProvider extends Component {\n  /// {@macro flame_multi_bloc_provider}\n  FlameMultiBlocProvider({\n    required List<FlameBlocProvider> providers,\n    List<Component>? children,\n    super.key,\n  }) : _providers = providers,\n       _initialChildren = children,\n       assert(providers.isNotEmpty, 'At least one provider must be given') {\n    _addProviders();\n  }\n\n  final List<FlameBlocProvider> _providers;\n  final List<Component>? _initialChildren;\n  FlameBlocProvider? _lastProvider;\n\n  Future<void> _addProviders() async {\n    final list = [..._providers];\n\n    var current = list.removeAt(0);\n    while (list.isNotEmpty) {\n      final provider = list.removeAt(0);\n      await current.add(provider);\n      current = provider;\n    }\n\n    await add(_providers.first);\n    _lastProvider = current;\n\n    _initialChildren?.forEach(add);\n  }\n\n  @override\n  Future<void> add(Component component) async {\n    if (_lastProvider == null) {\n      await super.add(component);\n    }\n    await _lastProvider?.add(component);\n  }\n\n  @override\n  void remove(Component component) {\n    if (_lastProvider == null) {\n      super.remove(component);\n    }\n    _lastProvider?.remove(component);\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/pubspec.yaml",
    "content": "name: flame_bloc\nresolution: workspace\ndescription: Integration for the Bloc state management library to Flame games.\nversion: 1.12.22\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_bloc\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - state-management\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  bloc: \">=8.1.1 <10.0.0\"\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  flutter_bloc: \">=8.1.2 <10.0.0\"\n  meta: ^1.12.0\n\ndev_dependencies:\n  dartdoc: ^9.0.0\n  flame_lint: ^1.4.3\n  flame_test: ^2.2.3\n  flutter_test:\n    sdk: flutter\n  mocktail: ^1.0.4\n"
  },
  {
    "path": "packages/flame_bloc/test/inventory_cubit.dart",
    "content": "import 'package:bloc/bloc.dart';\n\nenum InventoryState {\n  sword,\n  bow,\n}\n\nclass InventoryCubit extends Cubit<InventoryState> {\n  InventoryCubit() : super(InventoryState.sword);\n\n  void selectBow() {\n    emit(InventoryState.bow);\n  }\n\n  void selectSword() {\n    emit(InventoryState.sword);\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/test/player_cubit.dart",
    "content": "import 'package:bloc/bloc.dart';\n\nenum PlayerState { alive, dead, sad }\n\nclass PlayerCubit extends Cubit<PlayerState> {\n  PlayerCubit() : super(PlayerState.alive);\n\n  void kill() {\n    emit(PlayerState.dead);\n  }\n\n  void makeSad() {\n    emit(PlayerState.sad);\n  }\n\n  void riseFromTheDead() {\n    emit(PlayerState.alive);\n  }\n}\n"
  },
  {
    "path": "packages/flame_bloc/test/src/flame_bloc_listenable_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_bloc/flame_bloc.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../inventory_cubit.dart';\nimport '../player_cubit.dart';\n\nclass _PlayerListener extends Component\n    with FlameBlocListenable<PlayerCubit, PlayerState> {\n  PlayerState? last;\n  @override\n  void onNewState(PlayerState state) {\n    super.onNewState(state);\n\n    last = state;\n  }\n\n  @override\n  void onInitialState(PlayerState state) {\n    super.onInitialState(state);\n\n    last ??= state;\n  }\n}\n\nclass _SadPlayerListener extends Component\n    with FlameBlocListenable<PlayerCubit, PlayerState> {\n  PlayerState? last;\n\n  @override\n  bool listenWhen(PlayerState previousState, PlayerState newState) {\n    return newState == PlayerState.sad;\n  }\n\n  @override\n  void onNewState(PlayerState state) {\n    super.onNewState(state);\n\n    last = state;\n  }\n\n  @override\n  void onInitialState(PlayerState state) {\n    super.onInitialState(state);\n\n    last ??= state;\n  }\n}\n\nvoid main() {\n  group('FlameBlocListenable', () {\n    testWithFlameGame(\n      'throws assertion error when the bloc is not provided',\n      (game) async {\n        final bloc = InventoryCubit();\n        final provider =\n            FlameBlocProvider<InventoryCubit, InventoryState>.value(\n              value: bloc,\n            );\n        await game.ensureAdd(provider);\n\n        final component = _PlayerListener();\n        expect(() => provider.ensureAdd(component), throwsAssertionError);\n      },\n    );\n\n    testWithFlameGame(\n      'throws assertion error when the bloc set multiple times',\n      (game) async {\n        final bloc = PlayerCubit();\n        final component = _PlayerListener()..bloc = bloc;\n        expect(() => component.bloc = bloc, throwsAssertionError);\n      },\n    );\n\n    testWithFlameGame(\n      'closes the subscription when it is removed',\n      (game) async {\n        final bloc = PlayerCubit();\n        final provider = FlameBlocProvider<PlayerCubit, PlayerState>.value(\n          value: bloc,\n        );\n        await game.ensureAdd(provider);\n\n        final component = _PlayerListener();\n        await provider.ensureAdd(component);\n\n        component.removeFromParent();\n        await game.ready();\n\n        bloc.makeSad();\n        await Future.microtask(() {});\n        expect(component.last, equals(PlayerState.alive));\n      },\n    );\n\n    testWithFlameGame(\n      'listen only listenWhen returns true',\n      (game) async {\n        final bloc = PlayerCubit();\n        final provider = FlameBlocProvider<PlayerCubit, PlayerState>.value(\n          value: bloc,\n        );\n        await game.ensureAdd(provider);\n\n        final component = _SadPlayerListener();\n        await provider.ensureAdd(component);\n\n        bloc.kill();\n        await Future.microtask(() {});\n        expect(component.last, equals(PlayerState.alive));\n      },\n    );\n\n    testWithFlameGame(\n      'successfully retrieve the bloc via getter',\n      (game) async {\n        final bloc = PlayerCubit();\n        final provider = FlameBlocProvider<PlayerCubit, PlayerState>.value(\n          value: bloc,\n        );\n        await game.ensureAdd(provider);\n\n        final component = _PlayerListener();\n        await provider.ensureAdd(component);\n\n        expect(component.bloc, bloc);\n      },\n    );\n\n    testWithFlameGame(\n      'successfully revisit previously visited route with bloc listener',\n      (game) async {\n        var playerPushCalled = 0;\n        var playerPopCalled = 0;\n        var sadPushCalled = 0;\n        var sadPopCalled = 0;\n        final router = RouterComponent(\n          routes: {\n            'start': Route(Component.new),\n            'playerRoute': _CustomBlocRoute(\n              onPush: (self, prevRoute) {\n                playerPushCalled++;\n              },\n              onPop: (self, prevRoute) {\n                playerPopCalled++;\n              },\n              build: (self) => _PlayerListener(),\n            ),\n            'sadRoute': _CustomBlocRoute(\n              onPush: (self, prevRoute) {\n                sadPushCalled++;\n              },\n              onPop: (self, prevRoute) {\n                sadPopCalled++;\n              },\n              build: (self) => _SadPlayerListener(),\n            ),\n          },\n          initialRoute: 'start',\n        );\n\n        final bloc = PlayerCubit();\n        final provider = FlameBlocProvider<PlayerCubit, PlayerState>.value(\n          value: bloc,\n          children: [router],\n        );\n\n        await game.ensureAdd(provider);\n\n        //Visit routes first time\n        router.pushNamed('playerRoute');\n        await game.ready();\n        expect(router.currentRoute.name, 'playerRoute');\n        expect(playerPushCalled, 1);\n        router.pop();\n        await game.ready();\n        expect(playerPushCalled, 1);\n        expect(playerPopCalled, 1);\n        router.pushNamed('sadRoute');\n        await game.ready();\n        expect(sadPushCalled, 1);\n        expect(router.currentRoute.name, 'sadRoute');\n        router.pop();\n\n        //Revisit playerRoute\n        await game.ready();\n        expect(sadPushCalled, 1);\n        expect(sadPopCalled, 1);\n        router.pushNamed('playerRoute');\n\n        await game.ready();\n        expect(playerPushCalled, 2);\n        router.pop();\n\n        //Revisit sadRoute\n        await game.ready();\n        router.pushNamed('sadRoute');\n        await game.ready();\n        expect(sadPushCalled, 2);\n      },\n    );\n  });\n}\n\nclass _CustomBlocRoute extends Route {\n  _CustomBlocRoute({\n    Component Function()? builder,\n    void Function(Route, Route?)? onPush,\n    void Function(Route, Route)? onPop,\n    Component Function(Route)? build,\n  }) : _onPush = onPush,\n       _onPop = onPop,\n       _build = build,\n       super(builder);\n\n  final void Function(Route, Route?)? _onPush;\n  final void Function(Route, Route)? _onPop;\n  final Component Function(Route)? _build;\n\n  @override\n  void onPush(Route? route) => _onPush?.call(this, route);\n\n  @override\n  void onPop(Route route) => _onPop?.call(this, route);\n\n  @override\n  Component build() => _build?.call(this) ?? super.build();\n}\n"
  },
  {
    "path": "packages/flame_bloc/test/src/flame_bloc_listener_test.dart",
    "content": "import 'package:flame_bloc/flame_bloc.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../player_cubit.dart';\n\nvoid main() {\n  group('FlameBlocListener', () {\n    testWithFlameGame(\n      'onNewsState is called when state changes',\n      (game) async {\n        final bloc = PlayerCubit();\n        final provider = FlameBlocProvider<PlayerCubit, PlayerState>.value(\n          value: bloc,\n        );\n        final states = <PlayerState>[];\n        await game.ensureAdd(provider);\n\n        final component = FlameBlocListener<PlayerCubit, PlayerState>(\n          onNewState: states.add,\n        );\n        await provider.ensureAdd(component);\n\n        bloc.kill();\n        await Future.microtask(() {});\n        expect(states, equals([PlayerState.dead]));\n      },\n    );\n\n    testWithFlameGame(\n      'onInitialState is called when the initial state is received',\n      (game) async {\n        final bloc = PlayerCubit();\n        final provider = FlameBlocProvider<PlayerCubit, PlayerState>.value(\n          value: bloc,\n        );\n        final states = <PlayerState>[];\n        await game.ensureAdd(provider);\n\n        final component = FlameBlocListener<PlayerCubit, PlayerState>(\n          onNewState: (_) {},\n          onInitialState: states.add,\n        );\n        await provider.ensureAdd(component);\n\n        bloc.kill();\n        await Future.microtask(() {});\n        expect(states, equals([PlayerState.alive]));\n      },\n    );\n\n    testWithFlameGame(\n      'onNewsState is not called when listenWhen returns false',\n      (game) async {\n        final bloc = PlayerCubit();\n        final provider = FlameBlocProvider<PlayerCubit, PlayerState>.value(\n          value: bloc,\n        );\n        final states = <PlayerState>[];\n        await game.ensureAdd(provider);\n\n        final component = FlameBlocListener<PlayerCubit, PlayerState>(\n          onNewState: states.add,\n          listenWhen: (_, __) => false,\n        );\n        await provider.ensureAdd(component);\n\n        bloc.kill();\n        await Future.microtask(() {});\n        expect(states, isEmpty);\n      },\n    );\n\n    testWithFlameGame(\n      'a bloc can be explicitly passed',\n      (game) async {\n        final bloc = PlayerCubit();\n        final states = <PlayerState>[];\n        final component = FlameBlocListener<PlayerCubit, PlayerState>(\n          bloc: bloc,\n          onNewState: states.add,\n        );\n        await game.ensureAdd(component);\n\n        bloc.kill();\n        await Future.microtask(() {});\n        expect(states, equals([PlayerState.dead]));\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_bloc/test/src/flame_bloc_provider_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_bloc/flame_bloc.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../inventory_cubit.dart';\n\nclass _InventoryReader extends Component\n    with FlameBlocReader<InventoryCubit, InventoryState> {}\n\nclass _InventoryListener extends Component\n    with FlameBlocListenable<InventoryCubit, InventoryState> {\n  InventoryState? lastState;\n\n  @override\n  void onNewState(InventoryState state) {\n    super.onNewState(state);\n    lastState = state;\n  }\n\n  @override\n  void onInitialState(InventoryState state) {\n    super.onInitialState(state);\n\n    lastState ??= state;\n  }\n}\n\nvoid main() {\n  group('FlameBlocProvider', () {\n    testWithFlameGame('Provides a bloc down on the tree', (game) async {\n      final bloc = InventoryCubit();\n      final provider = FlameBlocProvider<InventoryCubit, InventoryState>.value(\n        value: bloc,\n      );\n      await game.ensureAdd(provider);\n\n      final component = _InventoryReader();\n      await provider.ensureAdd(component);\n\n      expect(component.bloc, bloc);\n    });\n\n    testWithFlameGame('can listen to new state changes', (game) async {\n      final bloc = InventoryCubit();\n      final provider = FlameBlocProvider<InventoryCubit, InventoryState>.value(\n        value: bloc,\n      );\n      await game.ensureAdd(provider);\n\n      final component = _InventoryListener();\n      await provider.ensureAdd(component);\n\n      bloc.selectBow();\n      await Future<void>.microtask(() {});\n      expect(component.lastState, equals(InventoryState.bow));\n    });\n\n    group('when using children constructor argument', () {\n      testWithFlameGame('Provides a bloc down on the tree', (game) async {\n        final bloc = InventoryCubit();\n\n        late _InventoryReader component;\n        final provider =\n            FlameBlocProvider<InventoryCubit, InventoryState>.value(\n              value: bloc,\n              children: [\n                component = _InventoryReader(),\n              ],\n            );\n        await game.ensureAdd(provider);\n\n        expect(component.bloc, bloc);\n      });\n\n      testWithFlameGame(\n        'initial state is used to properly track last state',\n        (game) async {\n          final bloc = InventoryCubit();\n          late _InventoryListener component;\n          final provider =\n              FlameBlocProvider<InventoryCubit, InventoryState>.value(\n                value: bloc,\n                children: [\n                  component = _InventoryListener(),\n                ],\n              );\n          await game.ensureAdd(provider);\n          expect(component.lastState, equals(InventoryState.sword));\n        },\n      );\n      testWithFlameGame('can listen to new state changes', (game) async {\n        final bloc = InventoryCubit();\n        late _InventoryListener component;\n        final provider =\n            FlameBlocProvider<InventoryCubit, InventoryState>.value(\n              value: bloc,\n              children: [\n                component = _InventoryListener(),\n              ],\n            );\n        await game.ensureAdd(provider);\n\n        bloc.selectBow();\n        await Future<void>.microtask(() {});\n\n        bloc.selectSword();\n        await Future<void>.microtask(() {});\n\n        expect(component.lastState, equals(InventoryState.sword));\n      });\n    });\n\n    group('onRemove', () {\n      testWithFlameGame('dispose created blocs', (game) async {\n        final provider = FlameBlocProvider<InventoryCubit, InventoryState>(\n          create: InventoryCubit.new,\n        );\n        await game.ensureAdd(provider);\n        expect(provider.bloc.isClosed, isFalse);\n\n        provider.removeFromParent();\n        await game.ready();\n        expect(provider.bloc.isClosed, isTrue);\n      });\n\n      testWithFlameGame(\"don't dispose value bloc\", (game) async {\n        final bloc = InventoryCubit();\n        final provider =\n            FlameBlocProvider<InventoryCubit, InventoryState>.value(\n              value: bloc,\n            );\n        await game.ensureAdd(provider);\n        expect(provider.bloc.isClosed, isFalse);\n\n        provider.removeFromParent();\n        await game.ready();\n        expect(provider.bloc.isClosed, isFalse);\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_bloc/test/src/flame_bloc_reader_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_bloc/flame_bloc.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../inventory_cubit.dart';\nimport '../player_cubit.dart';\n\nclass _PlayerReader extends Component\n    with FlameBlocReader<PlayerCubit, PlayerState> {}\n\nvoid main() {\n  group('FlameBlocReader', () {\n    testWithFlameGame(\n      'throws assertion error when the bloc is not provided',\n      (game) async {\n        final bloc = InventoryCubit();\n        final provider =\n            FlameBlocProvider<InventoryCubit, InventoryState>.value(\n              value: bloc,\n            );\n        await game.ensureAdd(provider);\n\n        final component = _PlayerReader();\n        expect(() => provider.ensureAdd(component), throwsAssertionError);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_bloc/test/src/flame_multi_bloc_provider_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_bloc/flame_bloc.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../inventory_cubit.dart';\nimport '../player_cubit.dart';\n\nclass _InventoryReader extends Component\n    with FlameBlocReader<InventoryCubit, InventoryState> {}\n\nclass _InventoryListener extends Component\n    with FlameBlocListenable<InventoryCubit, InventoryState> {\n  InventoryState? lastState;\n\n  @override\n  void onNewState(InventoryState state) {\n    lastState = state;\n  }\n}\n\nclass _PlayerReader extends Component\n    with FlameBlocReader<PlayerCubit, PlayerState> {}\n\nclass _PlayerListener extends Component\n    with FlameBlocListenable<PlayerCubit, PlayerState> {\n  PlayerState? lastState;\n\n  @override\n  void onNewState(PlayerState state) {\n    lastState = state;\n  }\n}\n\nvoid main() {\n  group('FlameMultiBlocProvider', () {\n    testWithFlameGame('Provides multiple blocs down on the tree', (game) async {\n      final inventoryCubit = InventoryCubit();\n      final playerCubit = PlayerCubit();\n\n      final provider = FlameMultiBlocProvider(\n        providers: [\n          FlameBlocProvider<InventoryCubit, InventoryState>.value(\n            value: inventoryCubit,\n          ),\n          FlameBlocProvider<PlayerCubit, PlayerState>.value(\n            value: playerCubit,\n          ),\n        ],\n      );\n      await game.ensureAdd(provider);\n\n      final inventory = _InventoryReader();\n      final player = _PlayerReader();\n\n      await provider.ensureAdd(inventory);\n      await provider.ensureAdd(player);\n\n      expect(inventory.bloc, equals(inventoryCubit));\n      expect(player.bloc, equals(playerCubit));\n    });\n\n    testWithFlameGame('can listen to new state changes', (game) async {\n      final inventoryCubit = InventoryCubit();\n      final playerCubit = PlayerCubit();\n\n      final provider = FlameMultiBlocProvider(\n        providers: [\n          FlameBlocProvider<InventoryCubit, InventoryState>.value(\n            value: inventoryCubit,\n          ),\n          FlameBlocProvider<PlayerCubit, PlayerState>.value(\n            value: playerCubit,\n          ),\n        ],\n      );\n      await game.ensureAdd(provider);\n\n      final inventory = _InventoryListener();\n      final player = _PlayerListener();\n\n      await provider.ensureAdd(inventory);\n      await provider.ensureAdd(player);\n\n      playerCubit.makeSad();\n      inventoryCubit.selectBow();\n      await Future<void>.microtask(() {});\n\n      expect(player.lastState, equals(PlayerState.sad));\n      expect(inventory.lastState, equals(InventoryState.bow));\n    });\n\n    testWithFlameGame('Add and remove a child with two providers', (\n      game,\n    ) async {\n      final inventoryCubit = InventoryCubit();\n      final playerCubit = PlayerCubit();\n\n      late FlameBlocProvider inventoryCubitProvider;\n      late FlameBlocProvider playerCubitProvider;\n\n      final provider = FlameMultiBlocProvider(\n        providers: [\n          inventoryCubitProvider =\n              FlameBlocProvider<InventoryCubit, InventoryState>.value(\n                value: inventoryCubit,\n              ),\n          playerCubitProvider =\n              FlameBlocProvider<PlayerCubit, PlayerState>.value(\n                value: playerCubit,\n              ),\n        ],\n      );\n      await game.ensureAdd(provider);\n\n      final myTestComponent = PositionComponent(position: Vector2.all(10));\n\n      await provider.ensureAdd(myTestComponent);\n      expect(inventoryCubitProvider.children.length, 1);\n      expect(inventoryCubitProvider.firstChild(), playerCubitProvider);\n      expect(myTestComponent.parent, equals(playerCubitProvider));\n      expect(playerCubitProvider.firstChild(), equals(myTestComponent));\n      await provider.ensureRemove(myTestComponent);\n      expect(myTestComponent.parent, null);\n      expect(playerCubitProvider.children.length, 0);\n    });\n\n    group('when using children on constructor', () {\n      testWithFlameGame('Provides multiple blocs down on the tree', (\n        game,\n      ) async {\n        final inventoryCubit = InventoryCubit();\n        final playerCubit = PlayerCubit();\n\n        late _InventoryReader inventory;\n        late _PlayerReader player;\n\n        final provider = FlameMultiBlocProvider(\n          providers: [\n            FlameBlocProvider<InventoryCubit, InventoryState>.value(\n              value: inventoryCubit,\n            ),\n            FlameBlocProvider<PlayerCubit, PlayerState>.value(\n              value: playerCubit,\n            ),\n          ],\n          children: [\n            inventory = _InventoryReader(),\n            player = _PlayerReader(),\n          ],\n        );\n        await game.ensureAdd(provider);\n\n        expect(inventory.bloc, equals(inventoryCubit));\n        expect(player.bloc, equals(playerCubit));\n      });\n\n      testWithFlameGame('can listen to new state changes', (game) async {\n        final inventoryCubit = InventoryCubit();\n        final playerCubit = PlayerCubit();\n\n        late _InventoryListener inventory;\n        late _PlayerListener player;\n\n        final provider = FlameMultiBlocProvider(\n          providers: [\n            FlameBlocProvider<InventoryCubit, InventoryState>.value(\n              value: inventoryCubit,\n            ),\n            FlameBlocProvider<PlayerCubit, PlayerState>.value(\n              value: playerCubit,\n            ),\n          ],\n          children: [\n            inventory = _InventoryListener(),\n            player = _PlayerListener(),\n          ],\n        );\n        await game.ensureAdd(provider);\n\n        playerCubit.makeSad();\n        inventoryCubit.selectBow();\n        await Future<void>.microtask(() {});\n\n        expect(player.lastState, equals(PlayerState.sad));\n        expect(inventory.lastState, equals(InventoryState.bow));\n      });\n\n      testWithFlameGame(\n        'can listen to multiple subsequent state changes',\n        (game) async {\n          final playerCubit = PlayerCubit();\n          late _PlayerListener player;\n\n          final provider = FlameMultiBlocProvider(\n            providers: [\n              FlameBlocProvider<PlayerCubit, PlayerState>.value(\n                value: playerCubit,\n              ),\n            ],\n            children: [\n              player = _PlayerListener(),\n            ],\n          );\n          await game.ensureAdd(provider);\n\n          playerCubit.kill();\n          await Future<void>.microtask(() {});\n          expect(player.lastState, equals(PlayerState.dead));\n\n          playerCubit.riseFromTheDead();\n          await Future<void>.microtask(() {});\n          expect(player.lastState, equals(PlayerState.alive));\n        },\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_console/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\nmigrate_working_dir/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.\n/pubspec.lock\n**/doc/api/\n.dart_tool/\nbuild/\n"
  },
  {
    "path": "packages/flame_console/CHANGELOG.md",
    "content": "## 0.1.2+17\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 0.1.2+16\n\n - Update a dependency to the latest release.\n\n## 0.1.2+15\n\n - Update a dependency to the latest release.\n\n## 0.1.2+14\n\n - Update a dependency to the latest release.\n\n## 0.1.2+13\n\n - **DOCS**: Add console with backtick on flame_console example ([#3743](https://github.com/flame-engine/flame/issues/3743)). ([8534a557](https://github.com/flame-engine/flame/commit/8534a5574083ba3478979a1a619bf67820062bf1))\n\n## 0.1.2+12\n\n - Update a dependency to the latest release.\n\n## 0.1.2+11\n\n - Update a dependency to the latest release.\n\n## 0.1.2+10\n\n - Update a dependency to the latest release.\n\n## 0.1.2+9\n\n - Update a dependency to the latest release.\n\n## 0.1.2+8\n\n - Update a dependency to the latest release.\n\n## 0.1.2+7\n\n - **FIX**: Export necessary classes to build custom commands, update docs [flame_console] ([#3579](https://github.com/flame-engine/flame/issues/3579)). ([b05d55bc](https://github.com/flame-engine/flame/commit/b05d55bcc5f331ac8a8d82619b6df3a546848e10))\n\n## 0.1.2+6\n\n - Update a dependency to the latest release.\n\n## 0.1.2+5\n\n - Update a dependency to the latest release.\n\n## 0.1.2+4\n\n - Update a dependency to the latest release.\n\n## 0.1.2+3\n\n - Update a dependency to the latest release.\n\n## 0.1.2+2\n\n - Update a dependency to the latest release.\n\n## 0.1.2+1\n\n - Update a dependency to the latest release.\n\n## 0.1.2\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n - **FEAT**: Refactoring flame_console to use terminui ([#3388](https://github.com/flame-engine/flame/issues/3388)). ([de74a93b](https://github.com/flame-engine/flame/commit/de74a93b44f442341f816a2988c854f40902ff7e))\n\n## 0.1.1\n\n - **FIX**(flame_console): MemoryRepository can't be const ([#3362](https://github.com/flame-engine/flame/issues/3362)). ([e977bd49](https://github.com/flame-engine/flame/commit/e977bd495b196368582eda4e7d8019adc6c268f4))\n - **FEAT**: Adding FlameConsole ([#3329](https://github.com/flame-engine/flame/issues/3329)). ([cf5358cd](https://github.com/flame-engine/flame/commit/cf5358cd9069dab9e327e766553bd65e151f1540))\n\n## 0.1.0\n\n* First release\n"
  },
  {
    "path": "packages/flame_console/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_console/README.md",
    "content": "# Flame Console 💻\n\nTerminal overlay for Flame games which allows developers to debug and interact with their running games.\n\nCheck out the documentation\n[here](https://docs.flame-engine.org/latest/bridge_packages/flame_console/flame_console.html) for\nmore information.\n"
  },
  {
    "path": "packages/flame_console/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_console/example/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.build/\n.buildlog/\n.history\n.svn/\n.swiftpm/\nmigrate_working_dir/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.pub-cache/\n.pub/\n/build/\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Android Studio will place build artifacts here\n/android/app/debug\n/android/app/profile\n/android/app/release\n\ndevtools_options.yaml\n"
  },
  {
    "path": "packages/flame_console/example/README.md",
    "content": "# flame_console example\n\nExample of using the flame console package\n"
  },
  {
    "path": "packages/flame_console/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_console/example/lib/commands/clear_effects_command.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame_console/flame_console.dart';\nimport 'package:flame_console_example/game.dart';\n\nclass ClearEffectsCommand extends FlameConsoleCommand<MyGame> {\n  @override\n  String get name => 'clear_effects';\n\n  @override\n  String get description => 'Clear all effects on all components';\n\n  @override\n  (String?, String) execute(MyGame game, ArgResults args) {\n    var total = 0;\n    for (final child in game.children) {\n      total += _removeEffects(child);\n    }\n    return (null, 'Removed $total effects');\n  }\n\n  int _removeEffects(Component component) {\n    var total = 0;\n    for (final child in component.children) {\n      if (child is Effect) {\n        child.removeFromParent();\n        total++;\n      } else {\n        total += _removeEffects(child);\n      }\n    }\n    return total;\n  }\n}\n"
  },
  {
    "path": "packages/flame_console/example/lib/commands/commands.dart",
    "content": "import 'package:flame_console_example/commands/clear_effects_command.dart';\n\nconst customCommandsProvider = [\n  ClearEffectsCommand.new,\n];\n"
  },
  {
    "path": "packages/flame_console/example/lib/game.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/palette.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter/widgets.dart';\n\nclass MyGame extends FlameGame with HasKeyboardHandlerComponents {\n  @override\n  FutureOr<void> onLoad() async {\n    await super.onLoad();\n\n    world.addAll([\n      RectangleComponent(\n        position: Vector2(100, 0),\n        size: Vector2(100, 100),\n        paint: BasicPalette.white.paint(),\n        children: [\n          RectangleHitbox.relative(\n            Vector2.all(0.8),\n            parentSize: Vector2(100, 100),\n          ),\n          SequenceEffect(\n            [\n              MoveEffect.by(\n                Vector2(-200, 0),\n                LinearEffectController(1),\n              ),\n              MoveEffect.by(\n                Vector2(200, 0),\n                LinearEffectController(1),\n              ),\n            ],\n            infinite: true,\n          ),\n        ],\n      ),\n      RectangleComponent(\n        position: Vector2(200, 100),\n        size: Vector2(100, 100),\n        paint: BasicPalette.white.paint(),\n        children: [\n          RectangleHitbox.relative(\n            Vector2.all(0.4),\n            parentSize: Vector2(100, 100),\n          ),\n          SequenceEffect(\n            [\n              MoveEffect.by(\n                Vector2(-200, 0),\n                LinearEffectController(1),\n              ),\n              MoveEffect.by(\n                Vector2(200, 0),\n                LinearEffectController(1),\n              ),\n            ],\n            infinite: true,\n          ),\n        ],\n      ),\n      RectangleComponent(\n        position: Vector2(300, 200),\n        size: Vector2(100, 100),\n        paint: BasicPalette.white.paint(),\n        children: [\n          RectangleHitbox.relative(\n            Vector2.all(0.2),\n            parentSize: Vector2(100, 100),\n          ),\n          SequenceEffect(\n            [\n              MoveEffect.by(\n                Vector2(-200, 0),\n                LinearEffectController(1),\n              ),\n              MoveEffect.by(\n                Vector2(200, 0),\n                LinearEffectController(1),\n              ),\n            ],\n            infinite: true,\n          ),\n        ],\n      ),\n    ]);\n  }\n\n  @override\n  KeyEventResult onKeyEvent(\n    KeyEvent event,\n    Set<LogicalKeyboardKey> keysPressed,\n  ) {\n    if (!overlays.isActive('console')) {\n      if (event is KeyDownEvent) {\n        final key = event.logicalKey;\n        if (key == LogicalKeyboardKey.backquote) {\n          overlays.add('console');\n          return KeyEventResult.handled;\n        }\n      }\n    }\n    return super.onKeyEvent(event, keysPressed);\n  }\n}\n"
  },
  {
    "path": "packages/flame_console/example/lib/main.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame_console/flame_console.dart';\nimport 'package:flame_console_example/commands/commands.dart';\nimport 'package:flame_console_example/game.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(const MaterialApp(home: MyGameApp()));\n}\n\nclass MyGameApp extends StatefulWidget {\n  const MyGameApp({super.key});\n\n  @override\n  State<MyGameApp> createState() => _MyGameAppState();\n}\n\nclass _MyGameAppState extends State<MyGameApp> {\n  late final MyGame _game;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _game = MyGame();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: GameWidget(\n        game: _game,\n        overlayBuilderMap: {\n          'console': (BuildContext context, MyGame game) => FlameConsoleView(\n            game: game,\n            customCommands: customCommandsProvider.map((it) => it()).toList(),\n            onClose: () {\n              _game.overlays.remove('console');\n            },\n          ),\n        },\n      ),\n      floatingActionButton: FloatingActionButton(\n        heroTag: 'console_button',\n        onPressed: () {\n          _game.overlays.add('console');\n        },\n        child: const Icon(Icons.developer_mode),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_console/example/pubspec.yaml",
    "content": "name: flame_console_example\nresolution: workspace\ndescription: \"Example of using the flame console package\"\n\npublish_to: 'none'\n\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_console: ^0.1.2+17\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n\nflutter:\n  uses-material-design: true\n"
  },
  {
    "path": "packages/flame_console/lib/flame_console.dart",
    "content": "export 'src/flame_console.dart';\n"
  },
  {
    "path": "packages/flame_console/lib/src/commands/commands.dart",
    "content": "import 'package:args/args.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_console/src/commands/commands.dart';\nimport 'package:flame_console/src/commands/pause_command.dart';\nimport 'package:flame_console/src/commands/resume_command.dart';\nimport 'package:terminui/terminui.dart';\n\nexport 'package:args/args.dart' show ArgResults;\n\nexport 'debug_command.dart';\nexport 'ls_command.dart';\nexport 'remove_command.dart';\n\nabstract class FlameConsoleCommand<T extends FlameGame>\n    extends TerminuiCommand<T> {\n  List<Component> listAllChildren(Component component) {\n    return [\n      for (final child in component.children) ...[\n        child,\n        ...listAllChildren(child),\n      ],\n    ];\n  }\n\n  void onChildMatch(\n    void Function(Component) onChild, {\n    required Component rootComponent,\n    List<String> ids = const [],\n    List<String> types = const [],\n    int? limit,\n  }) {\n    final components = listAllChildren(rootComponent);\n\n    var count = 0;\n\n    for (final element in components) {\n      if (limit != null && count >= limit) {\n        break;\n      }\n\n      final isIdMatch =\n          ids.isEmpty || ids.contains(element.hashCode.toString());\n      final isTypeMatch =\n          types.isEmpty || types.contains(element.runtimeType.toString());\n\n      if (isIdMatch && isTypeMatch) {\n        count++;\n        onChild(element);\n      }\n    }\n  }\n}\n\nabstract class QueryCommand<G extends FlameGame>\n    extends FlameConsoleCommand<G> {\n  (String?, String) processChildren(List<Component> children);\n\n  @override\n  (String?, String) execute(G game, ArgResults results) {\n    final children = <Component>[];\n\n    onChildMatch(\n      children.add,\n      rootComponent: game,\n      ids: results['id'] as List<String>? ?? [],\n      types: results['type'] as List<String>? ?? [],\n      limit: optionalIntResult('limit', results),\n    );\n\n    return processChildren(children);\n  }\n\n  @override\n  ArgParser get parser => ArgParser()\n    ..addMultiOption(\n      'id',\n      abbr: 'i',\n    )\n    ..addMultiOption(\n      'type',\n      abbr: 't',\n    )\n    ..addOption(\n      'limit',\n      abbr: 'l',\n    );\n}\n\nclass FlameConsoleCommands {\n  static List<FlameConsoleCommand> commands = [\n    LsConsoleCommand(),\n    RemoveConsoleCommand(),\n    DebugConsoleCommand(),\n    PauseConsoleCommand(),\n    ResumeConsoleCommand(),\n  ];\n}\n"
  },
  {
    "path": "packages/flame_console/lib/src/commands/debug_command.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_console/src/commands/commands.dart';\n\nclass DebugConsoleCommand<G extends FlameGame> extends QueryCommand<G> {\n  @override\n  (String?, String) processChildren(List<Component> children) {\n    for (final child in children) {\n      child.debugMode = !child.debugMode;\n    }\n    return (null, '');\n  }\n\n  @override\n  String get name => 'debug';\n\n  @override\n  String get description => 'Toggle debug mode on the matched components.';\n}\n"
  },
  {
    "path": "packages/flame_console/lib/src/commands/ls_command.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_console/flame_console.dart';\n\nclass LsConsoleCommand<G extends FlameGame> extends QueryCommand<G> {\n  @override\n  (String?, String) processChildren(List<Component> children) {\n    final out = StringBuffer();\n    for (final component in children) {\n      final componentType = component.runtimeType.toString();\n      out.writeln('${component.hashCode}@$componentType');\n    }\n\n    return (null, out.toString());\n  }\n\n  @override\n  String get name => 'ls';\n\n  @override\n  String get description => 'List components that match the query arguments.';\n}\n"
  },
  {
    "path": "packages/flame_console/lib/src/commands/pause_command.dart",
    "content": "import 'package:args/args.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_console/flame_console.dart';\n\nclass PauseConsoleCommand<G extends FlameGame> extends FlameConsoleCommand<G> {\n  @override\n  (String?, String) execute(G game, ArgResults results) {\n    if (game.paused) {\n      return (\n        'Game is already paused, use the resume command start it again',\n        '',\n      );\n    } else {\n      game.pauseEngine();\n      return (null, '');\n    }\n  }\n\n  @override\n  ArgParser get parser => ArgParser();\n\n  @override\n  String get name => 'pause';\n\n  @override\n  String get description => 'Pauses the game loop.';\n}\n"
  },
  {
    "path": "packages/flame_console/lib/src/commands/remove_command.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_console/flame_console.dart';\n\nclass RemoveConsoleCommand<G extends FlameGame> extends QueryCommand<G> {\n  @override\n  (String?, String) processChildren(List<Component> children) {\n    for (final component in children) {\n      component.removeFromParent();\n    }\n    return (null, '');\n  }\n\n  @override\n  String get name => 'rm';\n\n  @override\n  String get description =>\n      'Removes components that match the query arguments.';\n}\n"
  },
  {
    "path": "packages/flame_console/lib/src/commands/resume_command.dart",
    "content": "import 'package:args/args.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_console/flame_console.dart';\n\nclass ResumeConsoleCommand<G extends FlameGame> extends FlameConsoleCommand<G> {\n  @override\n  (String?, String) execute(G game, ArgResults results) {\n    if (!game.paused) {\n      return ('Game is not paused, use the pause command to pause it', '');\n    } else {\n      game.resumeEngine();\n      return (null, '');\n    }\n  }\n\n  @override\n  ArgParser get parser => ArgParser();\n\n  @override\n  String get name => 'resume';\n\n  @override\n  String get description => 'Resumes the game loop.';\n}\n"
  },
  {
    "path": "packages/flame_console/lib/src/flame_console.dart",
    "content": "export 'commands/commands.dart';\nexport 'view/view.dart';\n"
  },
  {
    "path": "packages/flame_console/lib/src/view/console_view.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_console/flame_console.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:terminui/terminui.dart';\n\n/// A Console like view that can be used to interact with a game.\n///\n/// It should be registered as an overlay in the game widget\n/// of the game you want to interact with.\n///\n/// Example:\n///\n/// ```dart\n/// GameWidget(\n///   game: _game,\n///   overlayBuilderMap: {\n///     'console': (BuildContext context, MyGame game) => ConsoleView(\n///       game: game,\n///       onClose: () {\n///         _game.overlays.remove('console');\n///       },\n///     ),\n///   },\n/// )\nclass FlameConsoleView<G extends FlameGame> extends StatefulWidget {\n  const FlameConsoleView({\n    required this.game,\n    required this.onClose,\n    this.customCommands,\n    this.repository,\n    this.containerBuilder,\n    this.cursorBuilder,\n    this.cursorColor,\n    this.historyBuilder,\n    this.textStyle,\n    super.key,\n  });\n\n  final G game;\n  final List<FlameConsoleCommand<G>>? customCommands;\n  final VoidCallback onClose;\n  final TerminuiRepository? repository;\n\n  final ContainerBuilder? containerBuilder;\n  final WidgetBuilder? cursorBuilder;\n  final HistoryBuilder? historyBuilder;\n\n  final Color? cursorColor;\n  final TextStyle? textStyle;\n\n  @override\n  State<FlameConsoleView> createState() => _ConsoleViewState();\n}\n\nclass _ConsoleKeyboardHandler extends Component with KeyboardHandler {\n  _ConsoleKeyboardHandler(this._onKeyEvent);\n\n  final KeyEventResult Function(KeyEvent, Set<LogicalKeyboardKey>) _onKeyEvent;\n\n  @override\n  bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {\n    _onKeyEvent(event, keysPressed);\n    return false;\n  }\n}\n\nclass _ConsoleViewState extends State<FlameConsoleView> {\n  late final List<FlameConsoleCommand> _commandList = [\n    ...FlameConsoleCommands.commands,\n    if (widget.customCommands != null) ...widget.customCommands!,\n  ];\n\n  late final repository = widget.repository ?? MemoryTerminuiRepository();\n\n  late final _keyboardEventEmitter = KeyboardEventEmitter();\n\n  late final KeyboardHandler _keyboardHandler;\n\n  @override\n  void initState() {\n    super.initState();\n\n    widget.game.add(\n      _keyboardHandler = _ConsoleKeyboardHandler(\n        _keyboardEventEmitter.emit,\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    _keyboardHandler.removeFromParent();\n    _keyboardEventEmitter.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return TerminuiView(\n      onClose: widget.onClose,\n      commands: _commandList,\n      subject: widget.game,\n      keyboardEventEmitter: _keyboardEventEmitter,\n      containerBuilder: widget.containerBuilder,\n      cursorBuilder: widget.cursorBuilder,\n      cursorColor: widget.cursorColor,\n      historyBuilder: widget.historyBuilder,\n      textStyle: widget.textStyle,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_console/lib/src/view/container_builder.dart",
    "content": "import 'package:flutter/material.dart';\n\nWidget defaultContainerBuilder(BuildContext context, Widget child) {\n  return DecoratedBox(\n    decoration: BoxDecoration(\n      color: Colors.black.withValues(alpha: 0.8),\n      border: Border.all(color: Colors.white),\n    ),\n    child: Padding(\n      padding: const EdgeInsets.all(8.0),\n      child: child,\n    ),\n  );\n}\n"
  },
  {
    "path": "packages/flame_console/lib/src/view/view.dart",
    "content": "export 'console_view.dart';\n"
  },
  {
    "path": "packages/flame_console/pubspec.yaml",
    "content": "name: flame_console\nresolution: workspace\ndescription: \"An extensible and customizable console to help debug Flame games.\"\nversion: 0.1.2+17\nrepository: https://github.com/flame-engine/flame/tree/main/packages/flame_console\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - games\n  - utils\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  args: ^2.5.0\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  terminui: ^0.3.0\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flame_test: ^2.2.3\n  flutter_test:\n    sdk: flutter\n"
  },
  {
    "path": "packages/flame_console/test/src/commands_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_console/flame_console.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _NoopCommand extends FlameConsoleCommand {\n  @override\n  String get description => '';\n\n  @override\n  String get name => '';\n\n  @override\n  (String?, String) execute(FlameGame<World> game, ArgResults results) {\n    return (null, '');\n  }\n}\n\nvoid main() {\n  group('Commands', () {\n    testWithGame(\n      'listAllChildren crawls on all children',\n      FlameGame.new,\n      (game) async {\n        await game.world.add(\n          RectangleComponent(\n            children: [\n              PositionComponent(),\n            ],\n          ),\n        );\n\n        await game.ready();\n\n        final command = _NoopCommand();\n        final components = command.listAllChildren(game.world);\n\n        expect(components, hasLength(2));\n        expect(components[0], isA<RectangleComponent>());\n        expect(components[1], isA<PositionComponent>());\n      },\n    );\n\n    group('onChildMatch', () {\n      testWithGame(\n        'match children with the given types',\n        FlameGame.new,\n        (game) async {\n          await game.world.addAll([\n            RectangleComponent(\n              children: [\n                PositionComponent(),\n              ],\n            ),\n            PositionComponent(),\n          ]);\n\n          await game.ready();\n\n          final command = _NoopCommand();\n          final components = <Component>[];\n          command.onChildMatch(\n            components.add,\n            rootComponent: game.world,\n            types: ['PositionComponent'],\n          );\n\n          expect(components, hasLength(2));\n          expect(components[0], isA<PositionComponent>());\n          expect(components[1], isA<PositionComponent>());\n        },\n      );\n\n      testWithGame(\n        'match children with the given types and limit',\n        FlameGame.new,\n        (game) async {\n          await game.world.addAll([\n            RectangleComponent(\n              children: [\n                PositionComponent(),\n              ],\n            ),\n            PositionComponent(),\n          ]);\n\n          await game.ready();\n\n          final command = _NoopCommand();\n          final components = <Component>[];\n          command.onChildMatch(\n            components.add,\n            rootComponent: game.world,\n            types: ['PositionComponent'],\n            limit: 1,\n          );\n\n          expect(components, hasLength(1));\n          expect(components[0], isA<PositionComponent>());\n        },\n      );\n\n      testWithGame(\n        'match children with the given id',\n        FlameGame.new,\n        (game) async {\n          late Component target;\n          await game.world.addAll([\n            target = RectangleComponent(\n              children: [\n                PositionComponent(),\n              ],\n            ),\n            PositionComponent(),\n          ]);\n\n          await game.ready();\n\n          final command = _NoopCommand();\n          final components = <Component>[];\n          command.onChildMatch(\n            components.add,\n            rootComponent: game.world,\n            ids: [target.hashCode.toString()],\n          );\n\n          expect(components, hasLength(1));\n          expect(components[0], isA<PositionComponent>());\n        },\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_console/test/src/debug_command_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_console/flame_console.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Debug Command', () {\n    final components = [\n      RectangleComponent(),\n      PositionComponent(),\n    ];\n    testWithGame(\n      'toggle debug mode on components',\n      FlameGame.new,\n      (game) async {\n        await game.world.addAll(components);\n\n        await game.ready();\n\n        final command = DebugConsoleCommand();\n        command.execute(game, command.parser.parse([]));\n\n        for (final component in components) {\n          expect(component.debugMode, isTrue);\n        }\n\n        command.execute(game, command.parser.parse([]));\n        for (final component in components) {\n          expect(component.debugMode, isFalse);\n        }\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_console/test/src/pause_command_test.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame_console/src/commands/pause_command.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Pause Command', () {\n    testWithGame(\n      'pauses the game',\n      FlameGame.new,\n      (game) async {\n        expect(game.paused, isFalse);\n        final command = PauseConsoleCommand();\n        command.execute(game, command.parser.parse([]));\n        expect(game.paused, isTrue);\n      },\n    );\n\n    group('when the game is already paused', () {\n      testWithGame(\n        'returns error',\n        FlameGame.new,\n        (game) async {\n          game.pauseEngine();\n          expect(game.paused, isTrue);\n          final command = PauseConsoleCommand();\n          final result = command.execute(game, command.parser.parse([]));\n          expect(game.paused, isTrue);\n\n          expect(\n            result.$1,\n            'Game is already paused, use the resume command start it again',\n          );\n        },\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_console/test/src/resume_command_test.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame_console/src/commands/resume_command.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Resume Command', () {\n    testWithGame(\n      'resumes the game',\n      FlameGame.new,\n      (game) async {\n        game.pauseEngine();\n        expect(game.paused, isTrue);\n        final command = ResumeConsoleCommand();\n        command.execute(game, command.parser.parse([]));\n        expect(game.paused, isFalse);\n      },\n    );\n\n    group('when the game is not paused', () {\n      testWithGame(\n        'returns error',\n        FlameGame.new,\n        (game) async {\n          expect(game.paused, isFalse);\n          final command = ResumeConsoleCommand();\n          final result = command.execute(game, command.parser.parse([]));\n          expect(game.paused, isFalse);\n\n          expect(\n            result.$1,\n            'Game is not paused, use the pause command to pause it',\n          );\n        },\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_devtools/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nA Flutter-based game engine.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame\"><img src=\"https://img.shields.io/pub/v/flame.svg?style=popout\"/></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n# flame_devtools\n\nA DevTools extension for Flame games. To use it you just have to run your\nFlame game in debug mode and open the DevTools in your browser, and it should\nask you if you want to add the Flame DevTools extension as a separate tab.\n\n\n## Development\n\nTo run it locally, make sure to run `melos devtools-build` to build the\nextension so that it can be loaded in the browser (the build files are not\ncommitted to the repository).\n\nAfter you have done any changes, make sure to run `melos devtools-build` to\nbuild and copy the changes to `packages/flame/extension/build`.\n\nTo develop things from the Flame side, create a new `DevToolsConnector` which\nregisters the new extension end points so that you can communicate with Flame\nfrom the devtools extension. Don't forget to add the new connector to the\nlist of connectors in the `DevToolsService` class.\n\nIf you want to run with the devtools extension with the simulated mode for\nfaster development, you can use `melos devtools-simulator` to start the\nsimulated environment and run the devtools extension in the browser.\nRemember that you have to manually enter the Dart VM Service Connection URI\nin the simulated devtools environment.\n"
  },
  {
    "path": "packages/flame_devtools/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options.yaml\n"
  },
  {
    "path": "packages/flame_devtools/lib/main.dart",
    "content": "import 'package:devtools_extensions/devtools_extensions.dart';\nimport 'package:flame_devtools/widgets/component_tree.dart';\nimport 'package:flame_devtools/widgets/debug_mode_button.dart';\nimport 'package:flame_devtools/widgets/game_loop_controls.dart';\nimport 'package:flame_devtools/widgets/overlay_navigation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nvoid main() {\n  runApp(const FlameDevTools());\n}\n\nclass FlameDevTools extends StatelessWidget {\n  const FlameDevTools({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return DevToolsExtension(\n      child: ProviderScope(\n        child: Column(\n          children: [\n            Row(\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                const GameLoopControls(),\n                const DebugModeButton(),\n              ].withSpacing(),\n            ),\n            const Expanded(child: ComponentTree()),\n            const Flexible(child: OverlayNavigation()),\n          ].withSpacing(),\n        ),\n      ),\n    );\n  }\n}\n\nextension on List<Widget> {\n  List<Widget> withSpacing() {\n    return expand((item) sync* {\n      yield const SizedBox(width: 16, height: 16);\n      yield item;\n    }).skip(1).toList();\n  }\n}\n"
  },
  {
    "path": "packages/flame_devtools/lib/providers/position_component_attributes_provider.dart",
    "content": "import 'package:flame_devtools/repository.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nfinal positionComponentAttributesProvider = FutureProvider.autoDispose\n    .family<PositionComponentAttributes, int>(\n      (ref, id) async {\n        return Repository.getPositionComponentAttributes(id: id);\n      },\n    );\n"
  },
  {
    "path": "packages/flame_devtools/lib/repository.dart",
    "content": "import 'package:devtools_extensions/devtools_extensions.dart';\nimport 'package:flame/devtools.dart';\n\nsealed class Repository {\n  Repository._();\n\n  static Future<int> getComponentCount() async {\n    final componentCountResponse = await serviceManager\n        .callServiceExtensionOnMainIsolate(\n          'ext.flame_devtools.getComponentCount',\n        );\n    return componentCountResponse.json!['component_count'] as int;\n  }\n\n  static Future<ComponentTreeNode> getComponentTree() async {\n    final componentTreeResponse = await serviceManager\n        .callServiceExtensionOnMainIsolate(\n          'ext.flame_devtools.getComponentTree',\n        );\n    return ComponentTreeNode.fromJson(\n      componentTreeResponse.json!['component_tree'] as Map<String, dynamic>,\n    );\n  }\n\n  static Future<List<String>> getOverlays() async {\n    final overlaysResponse = await serviceManager\n        .callServiceExtensionOnMainIsolate(\n          'ext.flame_devtools.getOverlays',\n        );\n    return List<String>.from(overlaysResponse.json!['overlays'] as List);\n  }\n\n  static Future<void> navigateToOverlay(String overlay) async {\n    await serviceManager.callServiceExtensionOnMainIsolate(\n      'ext.flame_devtools.navigateToOverlay',\n      args: {'overlay': overlay},\n    );\n  }\n\n  static Future<bool> swapDebugMode({int? id}) async {\n    final nextDebugMode = !(await getDebugMode(id: id));\n    await serviceManager.callServiceExtensionOnMainIsolate(\n      'ext.flame_devtools.setDebugMode',\n      args: {\n        'debug_mode': nextDebugMode,\n        'id': id,\n      },\n    );\n    return nextDebugMode;\n  }\n\n  static Future<bool> getDebugMode({int? id}) async {\n    final debugModeResponse = await serviceManager\n        .callServiceExtensionOnMainIsolate(\n          'ext.flame_devtools.getDebugMode',\n          args: {'id': id},\n        );\n    return debugModeResponse.json!['debug_mode'] as bool;\n  }\n\n  // ignore: avoid_positional_boolean_parameters\n  static Future<bool> setPaused(bool shouldPause) async {\n    await serviceManager.callServiceExtensionOnMainIsolate(\n      'ext.flame_devtools.setPaused',\n      args: {'paused': shouldPause},\n    );\n    return shouldPause;\n  }\n\n  static Future<bool> getPaused() async {\n    final getPausedResponse = await serviceManager\n        .callServiceExtensionOnMainIsolate(\n          'ext.flame_devtools.getPaused',\n        );\n    return getPausedResponse.json!['paused'] as bool;\n  }\n\n  static Future<double> step({required double stepTime}) async {\n    final stepResponse = await serviceManager.callServiceExtensionOnMainIsolate(\n      'ext.flame_devtools.step',\n      args: {'step_time': stepTime.toString()},\n    );\n    return stepResponse.json!['step_time'] as double;\n  }\n\n  static Future<String?> snapshot({String? id}) async {\n    final snapshotResponse = await serviceManager\n        .callServiceExtensionOnMainIsolate(\n          'ext.flame_devtools.getComponentSnapshot',\n          args: {'id': id},\n        );\n    return snapshotResponse.json!['snapshot'] as String?;\n  }\n\n  static Future<PositionComponentAttributes> getPositionComponentAttributes({\n    int? id,\n  }) async {\n    final potentialPositionComponentResponse = await serviceManager\n        .callServiceExtensionOnMainIsolate(\n          'ext.flame_devtools.getPositionComponentAttributes',\n          args: {'id': id},\n        );\n\n    return PositionComponentAttributes.fromJson(\n      potentialPositionComponentResponse.json!,\n    );\n  }\n\n  static Future<void> setPositionComponentAttribute({\n    required String attribute,\n    required dynamic value,\n    int? id,\n  }) async {\n    await serviceManager.callServiceExtensionOnMainIsolate(\n      'ext.flame_devtools.setPositionComponentAttributes',\n      args: {\n        'id': id,\n        'attribute': attribute,\n        'value': value,\n      },\n    );\n  }\n}\n\nclass PositionComponentAttributes {\n  final double x;\n  final double y;\n  final double width;\n  final double height;\n  final double angle;\n  final double scaleX;\n  final double scaleY;\n\n  PositionComponentAttributes({\n    required this.x,\n    required this.y,\n    required this.width,\n    required this.height,\n    required this.angle,\n    required this.scaleX,\n    required this.scaleY,\n  });\n\n  factory PositionComponentAttributes.fromJson(Map<String, dynamic> json) {\n    return PositionComponentAttributes(\n      x: json['x'] as double,\n      y: json['y'] as double,\n      width: json['width'] as double,\n      height: json['height'] as double,\n      angle: json['angle'] as double,\n      scaleX: json['scaleX'] as double,\n      scaleY: json['scaleY'] as double,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_devtools/lib/widgets/component_counter.dart",
    "content": "import 'package:flame_devtools/repository.dart';\nimport 'package:flutter/material.dart';\n\nclass ComponentCounter extends StatefulWidget {\n  const ComponentCounter({super.key});\n\n  @override\n  State<ComponentCounter> createState() => _ComponentCounterState();\n}\n\nclass _ComponentCounterState extends State<ComponentCounter> {\n  Future<int>? _componentCount;\n\n  @override\n  void initState() {\n    _componentCount = Repository.getComponentCount();\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: _componentCount,\n      builder: (context, value) {\n        return Column(\n          children: [\n            Text(\n              value.hasData\n                  ? '${value.data} Components in the tree'\n                  : 'Loading...',\n            ),\n            ElevatedButton(\n              onPressed: () => setState(() {\n                _componentCount = Repository.getComponentCount();\n              }),\n              child: const Text('Update count'),\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_devtools/lib/widgets/component_snapshot.dart",
    "content": "import 'package:flame/flame.dart';\nimport 'package:flame/widgets.dart';\nimport 'package:flame_devtools/repository.dart';\nimport 'package:flutter/material.dart' hide Image;\n\nclass ComponentSnapshot extends StatefulWidget {\n  const ComponentSnapshot({\n    required this.id,\n    super.key,\n  });\n\n  final String id;\n\n  @override\n  State<ComponentSnapshot> createState() => _ComponentSnapshotState();\n}\n\nclass _ComponentSnapshotState extends State<ComponentSnapshot> {\n  late Future<String?> _snapshot;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _snapshot = Repository.snapshot(id: widget.id);\n  }\n\n  @override\n  void didUpdateWidget(ComponentSnapshot oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    if (oldWidget.id != widget.id) {\n      _snapshot = Repository.snapshot(id: widget.id);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder<String?>(\n      future: _snapshot,\n      builder: (context, snapshot) {\n        if (snapshot.connectionState == ConnectionState.done &&\n            snapshot.hasData) {\n          return Base64Image(\n            base64: snapshot.data!,\n            imageId: widget.id,\n          );\n        }\n        return const Text('Loading snapshot...');\n      },\n    );\n  }\n}\n\nclass Base64Image extends StatelessWidget {\n  const Base64Image({\n    required this.base64,\n    required this.imageId,\n    super.key,\n  });\n\n  final String base64;\n  final String imageId;\n\n  @override\n  Widget build(BuildContext context) {\n    final imageFuture = Flame.images.fromBase64(\n      imageId,\n      base64,\n    );\n    return FutureBuilder(\n      future: imageFuture,\n      builder: (context, snapshot) {\n        if (snapshot.connectionState == ConnectionState.done) {\n          return SizedBox(\n            width: 200,\n            height: 200,\n            child: SpriteWidget(\n              sprite: Sprite(\n                snapshot.data!,\n              ),\n            ),\n          );\n        }\n        return const Text('Loading image...');\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_devtools/lib/widgets/component_tree.dart",
    "content": "import 'package:animated_tree_view/animated_tree_view.dart';\nimport 'package:devtools_app_shared/ui.dart' as devtools_ui;\nimport 'package:flame_devtools/widgets/component_snapshot.dart';\nimport 'package:flame_devtools/widgets/component_tree_model.dart';\nimport 'package:flame_devtools/widgets/debug_mode_button.dart';\nimport 'package:flame_devtools/widgets/position_component_attributes_form.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nclass ComponentTree extends StatelessWidget {\n  const ComponentTree({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return devtools_ui.SplitPane(\n      axis: MediaQuery.of(context).size.width > 1000\n          ? Axis.horizontal\n          : Axis.vertical,\n      initialFractions: const [0.5, 0.5],\n      minSizes: const [300, 350],\n      children: const [\n        ComponentTreeSection(),\n        ComponentSection(),\n      ],\n    );\n  }\n}\n\nclass ComponentTreeSection extends ConsumerWidget {\n  const ComponentTreeSection({super.key});\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final theme = Theme.of(context);\n    final loader = ref.read(componentTreeLoaderProvider);\n    final loadedModel = ref.watch(loadedTreeModelProvider);\n    final selectedTreeNode = ref.watch(selectedTreeNodeProvider);\n    final componentCount = loadedModel.componentCount;\n\n    return devtools_ui.RoundedOutlinedBorder(\n      child: Column(\n        children: [\n          devtools_ui.AreaPaneHeader(\n            title: Row(\n              children: [\n                Text(\n                  'Component Tree ($componentCount components)',\n                  style: theme.textTheme.titleSmall,\n                ),\n                IconButton(\n                  icon: const Icon(Icons.refresh),\n                  iconSize: 18,\n                  alignment: Alignment.center,\n                  onPressed: loader.isLoading\n                      ? null\n                      : () => ref.refresh(componentTreeLoaderProvider),\n                ),\n              ],\n            ),\n          ),\n          Expanded(\n            child: SingleChildScrollView(\n              child: TreeView.simple(\n                showRootNode: false,\n                shrinkWrap: true,\n                indentation: const Indentation(\n                  color: Colors.blue,\n                  style: IndentStyle.roundJoint,\n                ),\n                onTreeReady: (controller) =>\n                    controller.expandAllChildren(controller.tree),\n                padding: const EdgeInsets.only(left: 20),\n                expansionIndicatorBuilder: (context, node) => node.isLeaf\n                    ? NoExpansionIndicator(tree: node)\n                    : ChevronIndicator.rightDown(\n                        tree: node,\n                        alignment: Alignment.centerLeft,\n                      ),\n                builder: (context, node) {\n                  return Padding(\n                    padding: node.isLeaf\n                        ? EdgeInsets.zero\n                        : const EdgeInsets.only(left: 20),\n                    child: ListTile(\n                      key: Key(\n                        node.data?.id.toString() ?? node.key,\n                      ),\n                      selected: node == selectedTreeNode,\n                      selectedColor: theme.colorScheme.primary,\n                      title: Text(node.data!.name),\n                      subtitle: Text(node.data!.id.toString()),\n                      onTap: () {\n                        ref.read(selectedTreeNodeProvider.notifier).state =\n                            node;\n                      },\n                    ),\n                  );\n                },\n                tree: loadedModel.treeRoot,\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass ComponentSection extends ConsumerWidget {\n  const ComponentSection({super.key});\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final node = ref.watch(selectedTreeNodeProvider)?.data;\n    final theme = Theme.of(context);\n    final textStyle = theme.textTheme.bodyLarge;\n\n    return devtools_ui.RoundedOutlinedBorder(\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          devtools_ui.AreaPaneHeader(\n            title: Row(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Text(\n                  'Selected Component',\n                  style: theme.textTheme.titleSmall,\n                ),\n              ],\n            ),\n          ),\n          Expanded(\n            child: Padding(\n              padding: const EdgeInsets.all(20),\n              child: node == null\n                  ? Text(\n                      'Select a component in the tree',\n                      style: textStyle,\n                    )\n                  : SingleChildScrollView(\n                      child: Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        children: [\n                          Row(\n                            children: [\n                              Text('Id: ${node.id}', style: textStyle),\n                              DebugModeButton(id: node.id),\n                            ].withSpacing(),\n                          ),\n                          Text('Type: ${node.name}', style: textStyle),\n                          Text(\n                            'Children: ${node.children.length}',\n                            style: textStyle,\n                          ),\n                          if (node.isPositionComponent)\n                            Padding(\n                              padding: const EdgeInsets.symmetric(vertical: 8),\n                              child: PositionComponentAttributesForm(\n                                componentId: node.id,\n                              ),\n                            ),\n                          Text(\n                            'toString:\\n${node.toStringText}',\n                            style: textStyle,\n                          ),\n                          ComponentSnapshot(id: node.id.toString()),\n                        ].withSpacing(),\n                      ),\n                    ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nextension on List<Widget> {\n  List<Widget> withSpacing() {\n    return expand((item) sync* {\n      yield const SizedBox(width: 10, height: 10);\n      yield item;\n    }).skip(1).toList();\n  }\n}\n"
  },
  {
    "path": "packages/flame_devtools/lib/widgets/component_tree_model.dart",
    "content": "import 'package:animated_tree_view/animated_tree_view.dart';\nimport 'package:flame/devtools.dart';\nimport 'package:flame_devtools/repository.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:flutter_riverpod/legacy.dart';\n\nfinal selectedTreeNodeProvider = StateProvider<TreeNode<ComponentTreeNode>?>(\n  (_) => null,\n);\n\n// This is in a separate provider due to the fact that the tree can't keep\n// track of whether it has been changed or not since the nodes don't implement\n// the == operator and hashCode properly.\nfinal loadedTreeModelProvider = StateProvider<ComponentTreeModel>(\n  (_) => ComponentTreeModel(),\n);\n\nfinal componentTreeLoaderProvider = FutureProvider<void>((ref) async {\n  final previousTreeModel = ref.watch(loadedTreeModelProvider);\n  final updatedModel = await ComponentTreeModel.refreshComponentTree(\n    previousTreeModel,\n  );\n  if (updatedModel != null) {\n    ref.read(loadedTreeModelProvider.notifier).state = updatedModel;\n  }\n});\n\n@immutable\nclass ComponentTreeModel {\n  ComponentTreeModel({\n    this.nodeHash = 0,\n    this.componentCount = 0,\n    TreeNode<ComponentTreeNode>? treeRoot,\n  }) : treeRoot = treeRoot ?? TreeNode<ComponentTreeNode>.root();\n\n  final TreeNode<ComponentTreeNode> treeRoot;\n  final int componentCount;\n  final int nodeHash;\n\n  static Future<ComponentTreeModel?> refreshComponentTree(\n    ComponentTreeModel previousModel,\n  ) {\n    final updatedComponentTree = Repository.getComponentTree();\n    return updatedComponentTree.then((node) {\n      final treeRoot = previousModel.treeRoot;\n      final componentRoot = TreeNode(key: node.id.toString(), data: node);\n      final (:count, :nodeHash) = _buildTree(node, componentRoot, isRoot: true);\n      if (previousModel.nodeHash != nodeHash) {\n        treeRoot.clear();\n        treeRoot.add(componentRoot);\n        return previousModel.copyWith(\n          componentCount: count,\n          nodeHash: nodeHash,\n        );\n      }\n      return null;\n    });\n  }\n\n  static ({int count, int nodeHash}) _buildTree(\n    ComponentTreeNode node,\n    TreeNode<ComponentTreeNode> parent, {\n    bool isRoot = false,\n  }) {\n    final current = isRoot\n        ? parent\n        : TreeNode(\n            key: node.id.toString(),\n            parent: parent,\n            data: node,\n          );\n    if (!isRoot) {\n      parent.add(current);\n    }\n    var componentCount = 1;\n    var computedHash = node.id;\n    for (final child in node.children) {\n      final (:count, :nodeHash) = _buildTree(child, current);\n      componentCount += count;\n      computedHash += nodeHash ^ (parent.data?.id ?? 0);\n    }\n    return (count: componentCount, nodeHash: computedHash);\n  }\n\n  ComponentTreeModel copyWith({\n    int? componentCount,\n    int? nodeHash,\n  }) {\n    return ComponentTreeModel(\n      treeRoot: treeRoot,\n      componentCount: componentCount ?? this.componentCount,\n      nodeHash: nodeHash ?? this.nodeHash,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_devtools/lib/widgets/debug_mode_button.dart",
    "content": "import 'package:flame_devtools/repository.dart';\nimport 'package:flutter/material.dart';\n\nclass DebugModeButton extends StatefulWidget {\n  const DebugModeButton({super.key, this.id});\n\n  final int? id;\n\n  @override\n  Key? get key => super.key ?? ValueKey(id);\n\n  @override\n  State<DebugModeButton> createState() => _DebugModeButtonState();\n}\n\nclass _DebugModeButtonState extends State<DebugModeButton> {\n  Future<bool>? _debugMode;\n\n  @override\n  void initState() {\n    _debugMode = Repository.getDebugMode(id: widget.id);\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: _debugMode,\n      builder: (context, value) {\n        final buttonPrefix = switch (value.data) {\n          null => 'Loading',\n          true => 'Disable',\n          false => 'Enable',\n        };\n\n        return ElevatedButton(\n          onPressed: value.data == null\n              ? null\n              : () {\n                  setState(\n                    () {\n                      _debugMode = Repository.swapDebugMode(id: widget.id);\n                    },\n                  );\n                },\n          child: Text('$buttonPrefix Debug Mode'),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_devtools/lib/widgets/game_loop_controls.dart",
    "content": "import 'package:flame_devtools/repository.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nclass GameLoopControls extends StatefulWidget {\n  const GameLoopControls({super.key});\n\n  @override\n  State<GameLoopControls> createState() => _GameLoopControlsState();\n}\n\nclass _GameLoopControlsState extends State<GameLoopControls> {\n  Future<bool>? _paused;\n  final _stepTimeController = TextEditingController();\n  double get stepTime => double.tryParse(_stepTimeController.text) ?? 0;\n\n  @override\n  void initState() {\n    _paused = Repository.getPaused();\n    _stepTimeController.text = (1 / 60).toStringAsFixed(3);\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: _paused,\n      builder: (context, value) {\n        final isPaused = value.data;\n        return Row(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Text('Step time:'),\n            Container(\n              width: 60,\n              padding: const EdgeInsets.symmetric(horizontal: 8),\n              child: TextField(\n                inputFormatters: [\n                  FilteringTextInputFormatter.allow(RegExp(r'^\\d+\\.?\\d*')),\n                ],\n                textAlign: TextAlign.end,\n                controller: _stepTimeController,\n              ),\n            ),\n            IconButton(\n              onPressed: (isPaused == null || !isPaused)\n                  ? null\n                  : () => Repository.step(stepTime: -stepTime),\n              icon: const Icon(Icons.skip_previous),\n            ),\n            IconButton(\n              onPressed: (isPaused == null || isPaused)\n                  ? null\n                  : () => _setPaused(true),\n              icon: const Icon(Icons.pause),\n            ),\n            IconButton(\n              onPressed: (isPaused == null || !isPaused)\n                  ? null\n                  : () => _setPaused(false),\n              icon: const Icon(Icons.play_arrow),\n            ),\n            IconButton(\n              onPressed: (isPaused == null || !isPaused)\n                  ? null\n                  : () => Repository.step(stepTime: stepTime),\n              icon: const Icon(Icons.skip_next),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  void _setPaused(bool paused) {\n    setState(() {\n      _paused = Repository.setPaused(paused);\n    });\n  }\n\n  @override\n  void dispose() {\n    _stepTimeController.dispose();\n    super.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/flame_devtools/lib/widgets/incremental_number_form_field.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass IncrementalNumberFormField<T extends num> extends StatefulWidget {\n  const IncrementalNumberFormField({\n    required this.initialValue,\n    required this.label,\n    this.onChanged,\n    super.key,\n  });\n\n  final String label;\n  final T initialValue;\n  final void Function(T)? onChanged;\n\n  @override\n  State<IncrementalNumberFormField<T>> createState() =>\n      _IncrementalNumberFormFieldState<T>();\n}\n\nclass _IncrementalNumberFormFieldState<T extends num>\n    extends State<IncrementalNumberFormField<T>> {\n  late final _controller = TextEditingController()\n    ..text = widget.initialValue.toString();\n\n  String? errorText;\n\n  @override\n  void didUpdateWidget(covariant IncrementalNumberFormField<T> oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    if (oldWidget.initialValue != widget.initialValue) {\n      setState(() {\n        _controller.text = widget.initialValue.toString();\n        errorText = null;\n      });\n    }\n  }\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    super.dispose();\n  }\n\n  T _parse() {\n    if (T == double) {\n      return double.parse(_controller.text) as T;\n    } else {\n      return int.parse(_controller.text) as T;\n    }\n  }\n\n  void _tryUpdate(String value) {\n    try {\n      final value = _parse();\n      _update(value);\n    } on Exception catch (_) {\n      setState(() {\n        errorText = 'Invalid number';\n      });\n    }\n  }\n\n  void _update(T v) {\n    setState(() {\n      errorText = null;\n    });\n\n    widget.onChanged?.call(v);\n  }\n\n  void _increment() {\n    final value = _parse() + 1 as T;\n    _update(value);\n    _controller.text = value.toString();\n  }\n\n  void _decrement() {\n    final value = _parse() - 1 as T;\n    _update(value);\n    _controller.text = value.toString();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        IconButton(\n          onPressed: _decrement,\n          icon: const Icon(Icons.remove),\n        ),\n        const SizedBox(width: 8),\n        SizedBox(\n          width: 100,\n          child: TextField(\n            decoration: InputDecoration(\n              labelText: widget.label,\n              errorText: errorText,\n            ),\n            controller: _controller,\n            onChanged: _tryUpdate,\n          ),\n        ),\n        const SizedBox(width: 8),\n        IconButton(\n          onPressed: _increment,\n          icon: const Icon(Icons.add),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_devtools/lib/widgets/overlay_navigation.dart",
    "content": "import 'package:devtools_app_shared/ui.dart' as devtools_ui;\nimport 'package:flame_devtools/repository.dart';\nimport 'package:flutter/material.dart';\n\nclass OverlayNavigation extends StatefulWidget {\n  const OverlayNavigation({super.key});\n\n  @override\n  State<OverlayNavigation> createState() => _OverlayNavigationState();\n}\n\nclass _OverlayNavigationState extends State<OverlayNavigation> {\n  Future<List<String>>? _overlays;\n\n  @override\n  void initState() {\n    _overlays = Repository.getOverlays();\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder<List<String>>(\n      future: _overlays,\n      builder: (context, snapshot) {\n        if (!snapshot.hasData || snapshot.data!.isEmpty) {\n          return const SizedBox();\n        }\n\n        return devtools_ui.RoundedOutlinedBorder(\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              const devtools_ui.AreaPaneHeader(title: Text('Overlays')),\n              for (final overlay in snapshot.data!)\n                ListTile(\n                  dense: true,\n                  leading: const Icon(Icons.layers, size: 20),\n                  title: Text(overlay),\n                  onTap: () => Repository.navigateToOverlay(overlay),\n                ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_devtools/lib/widgets/position_component_attributes_form.dart",
    "content": "import 'package:flame_devtools/providers/position_component_attributes_provider.dart';\nimport 'package:flame_devtools/repository.dart';\nimport 'package:flame_devtools/widgets/incremental_number_form_field.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nclass PositionComponentAttributesForm extends ConsumerWidget {\n  const PositionComponentAttributesForm({\n    required this.componentId,\n    super.key,\n  });\n\n  final int componentId;\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final attributesData = ref.watch(\n      positionComponentAttributesProvider(\n        componentId,\n      ),\n    );\n\n    return attributesData.when(\n      error: (e, s) => const Text(\n        'Error loading component attributes',\n      ),\n      loading: () => const Center(child: CircularProgressIndicator()),\n      data: (attributes) {\n        return Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Text(\n              'Position',\n              style: Theme.of(context).textTheme.labelLarge,\n            ),\n            Row(\n              children: [\n                IncrementalNumberFormField<double>(\n                  key: Key('x_field_$componentId'),\n                  label: 'X',\n                  initialValue: attributes.x,\n                  onChanged: (v) {\n                    Repository.setPositionComponentAttribute(\n                      id: componentId,\n                      attribute: 'x',\n                      value: v,\n                    );\n                  },\n                ),\n                const SizedBox(width: 8),\n                IncrementalNumberFormField<double>(\n                  key: Key('y_field_$componentId'),\n                  label: 'Y',\n                  initialValue: attributes.y,\n                  onChanged: (v) {\n                    Repository.setPositionComponentAttribute(\n                      id: componentId,\n                      attribute: 'y',\n                      value: v,\n                    );\n                  },\n                ),\n              ],\n            ),\n            const SizedBox(height: 8),\n            Text(\n              'Size',\n              style: Theme.of(context).textTheme.labelLarge,\n            ),\n            Row(\n              children: [\n                IncrementalNumberFormField<double>(\n                  key: Key('width_field_$componentId'),\n                  label: 'Width',\n                  initialValue: attributes.width,\n                  onChanged: (v) {\n                    Repository.setPositionComponentAttribute(\n                      id: componentId,\n                      attribute: 'width',\n                      value: v,\n                    );\n                  },\n                ),\n                const SizedBox(width: 8),\n                IncrementalNumberFormField<double>(\n                  key: Key('height_field_$componentId'),\n                  label: 'Height',\n                  initialValue: attributes.height,\n                  onChanged: (v) {\n                    Repository.setPositionComponentAttribute(\n                      id: componentId,\n                      attribute: 'height',\n                      value: v,\n                    );\n                  },\n                ),\n              ],\n            ),\n            const SizedBox(height: 8),\n            Text(\n              'Angle',\n              style: Theme.of(context).textTheme.labelLarge,\n            ),\n            IncrementalNumberFormField<double>(\n              key: Key('angle_field_$componentId'),\n              label: 'Radian',\n              initialValue: attributes.angle,\n              onChanged: (v) {\n                Repository.setPositionComponentAttribute(\n                  id: componentId,\n                  attribute: 'angle',\n                  value: v,\n                );\n              },\n            ),\n            const SizedBox(height: 8),\n            Text(\n              'Scale',\n              style: Theme.of(context).textTheme.labelLarge,\n            ),\n            Row(\n              children: [\n                IncrementalNumberFormField<double>(\n                  key: Key('scale_x_field_$componentId'),\n                  label: 'X',\n                  initialValue: attributes.scaleX,\n                  onChanged: (v) {\n                    Repository.setPositionComponentAttribute(\n                      id: componentId,\n                      attribute: 'scaleX',\n                      value: v,\n                    );\n                  },\n                ),\n                const SizedBox(width: 8),\n                IncrementalNumberFormField<double>(\n                  key: Key('scale_y_field_$componentId'),\n                  label: 'Y',\n                  initialValue: attributes.scaleY,\n                  onChanged: (v) {\n                    Repository.setPositionComponentAttribute(\n                      id: componentId,\n                      attribute: 'scaleY',\n                      value: v,\n                    );\n                  },\n                ),\n              ],\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_devtools/pubspec.yaml",
    "content": "name: flame_devtools\nresolution: workspace\ndescription: \"The DevTools extension for Flame\"\npublish_to: 'none'\nversion: 0.1.0\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  animated_tree_view: ^2.3.0\n  devtools_app_shared: ^0.3.1\n  devtools_extensions: ^0.3.1\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  flutter_riverpod: ^3.0.3\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n\nflutter:\n  uses-material-design: true\n"
  },
  {
    "path": "packages/flame_devtools/web/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <!--\n    If you are serving your web app in a path other than the root, change the\n    href value below to reflect the base path you are serving from.\n\n    The path provided below has to start and end with a slash \"/\" in order for\n    it to work correctly.\n\n    For more details:\n    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base\n\n    This is a placeholder for base href that will be replaced by the value of\n    the `--base-href` argument provided to `flutter build`.\n  -->\n  <base href=\"$FLUTTER_BASE_HREF\">\n\n  <meta charset=\"UTF-8\">\n  <meta content=\"IE=Edge\" http-equiv=\"X-UA-Compatible\">\n  <meta name=\"description\" content=\"The devtools extensions for Flame.\">\n\n  <!-- iOS meta tags & icons -->\n  <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n  <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\">\n  <meta name=\"apple-mobile-web-app-title\" content=\"flame_devtools\">\n\n  <title>flame_devtools</title>\n  <link rel=\"manifest\" href=\"manifest.json\">\n</head>\n<body>\n  <script src=\"flutter_bootstrap.js\" async></script>\n</body>\n</html>\n"
  },
  {
    "path": "packages/flame_devtools/web/manifest.json",
    "content": "{\n    \"name\": \"flame_devtools\",\n    \"short_name\": \"flame_devtools\",\n    \"start_url\": \".\",\n    \"display\": \"standalone\",\n    \"background_color\": \"#0175C2\",\n    \"theme_color\": \"#0175C2\",\n    \"description\": \"The devtools extensions for Flame.\",\n    \"orientation\": \"portrait-primary\",\n    \"prefer_related_applications\": false,\n    \"icons\": []\n}\n"
  },
  {
    "path": "packages/flame_fire_atlas/.min_coverage",
    "content": "100.0\n"
  },
  {
    "path": "packages/flame_fire_atlas/CHANGELOG.md",
    "content": "## 1.8.16\n\n - **FIX**: Fix warnings on flame_fire_atlas.dart ([#3818](https://github.com/flame-engine/flame/issues/3818)). ([458a79a9](https://github.com/flame-engine/flame/commit/458a79a97d63d128ab127fc15616192114dc9448))\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 1.8.15\n\n - Update a dependency to the latest release.\n\n## 1.8.14\n\n - Update a dependency to the latest release.\n\n## 1.8.13\n\n - Update a dependency to the latest release.\n\n## 1.8.12\n\n - **DOCS**: Deprecate TapDetector in favour of TapCallbacks ([#2886](https://github.com/flame-engine/flame/issues/2886)). ([b173697b](https://github.com/flame-engine/flame/commit/b173697bfb7ea61287251c43cd3c9d2fdb448fe3))\n\n## 1.8.11\n\n - Update a dependency to the latest release.\n\n## 1.8.10\n\n - Update a dependency to the latest release.\n\n## 1.8.9\n\n - Update a dependency to the latest release.\n\n## 1.8.8\n\n - Update a dependency to the latest release.\n\n## 1.8.7\n\n - Update a dependency to the latest release.\n\n## 1.8.6\n\n - Update a dependency to the latest release.\n\n## 1.8.5\n\n - Update a dependency to the latest release.\n\n## 1.8.4\n\n - Update a dependency to the latest release.\n\n## 1.8.3\n\n - Update a dependency to the latest release.\n\n## 1.8.2\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 1.8.1\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 1.8.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Bump tiled to 0.11.0 and add ColorData extension (#3473).\n\n## 1.7.1\n\n - Update a dependency to the latest release.\n\n## 1.7.0\n\n - **FEAT**: Adding getters and methods for easier manipulation of selections ([#3350](https://github.com/flame-engine/flame/issues/3350)). ([291af57d](https://github.com/flame-engine/flame/commit/291af57deb7d742a73438b026ca2f4fd1c6a3454))\n\n## 1.6.0\n\n - **FEAT**: Adding getter for the atlas image on flame fire atlas ([#3326](https://github.com/flame-engine/flame/issues/3326)). ([ae230ffa](https://github.com/flame-engine/flame/commit/ae230ffaaa588df7a99a3e2e8fa8980dc32104c0))\n\n## 1.5.5\n\n - Update a dependency to the latest release.\n\n## 1.5.4\n\n## 1.5.3\n\n - **FEAT**: Adding group to flame fire atlas ([#3245](https://github.com/flame-engine/flame/issues/3245)). ([0fab444c](https://github.com/flame-engine/flame/commit/0fab444c3dd9ad8faa1e0e9e702150b950dbf30f))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 1.5.2\n\n - Update a dependency to the latest release.\n\n## 1.5.1\n\n - Update a dependency to the latest release.\n\n## 1.5.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 1.4.8\n\n - Update a dependency to the latest release.\n\n## 1.4.7\n\n - Update a dependency to the latest release.\n\n## 1.4.6\n\n - Update a dependency to the latest release.\n\n## 1.4.5\n\n - Update a dependency to the latest release.\n\n## 1.4.4\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n## 1.4.3\n\n - Update a dependency to the latest release.\n\n## 1.4.2\n\n - **REFACTOR**: Remove unnecessary 'async' keyword across the codebase [DCM] ([#2803](https://github.com/flame-engine/flame/issues/2803)). ([2dfe0e5a](https://github.com/flame-engine/flame/commit/2dfe0e5a431213c7148ab6389e3e8c8dc49fbf3d))\n - **FIX**: Remove deprecations for 1.10.0 ([#2809](https://github.com/flame-engine/flame/issues/2809)). ([5b67b8f1](https://github.com/flame-engine/flame/commit/5b67b8f14ad4fdb38a249d0a41ecba49ba2fcc44))\n\n## 1.4.1\n\n - Update a dependency to the latest release.\n\n## 1.4.0\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **FEAT**(fire_atlas): Encoded option to load json instead of .fa ([#2649](https://github.com/flame-engine/flame/issues/2649)). ([5be6fc8c](https://github.com/flame-engine/flame/commit/5be6fc8caea138b577bf91244165c0a61659b4c5))\n\n## 1.3.8\n\n - Update a dependency to the latest release.\n\n## 1.3.7\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n\n## 1.3.6\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n## 1.3.5\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n## 1.3.4\n\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n - **DOCS**: Rename caveace asset to cave_ace in our examples ([#2304](https://github.com/flame-engine/flame/issues/2304)). ([e2399f91](https://github.com/flame-engine/flame/commit/e2399f91e3ce39da8db9ae2b9622c8a6050b94b9))\n\n## 1.3.3\n\n - Update a dependency to the latest release.\n\n## 1.3.2\n\n## 1.3.1\n\n - Update a dependency to the latest release.\n\n## 1.3.0\n\n - **PERF**: Avoid unnecessary copy in AssetsCache.readBinaryFile ([#1749](https://github.com/flame-engine/flame/issues/1749)). ([7e79638d](https://github.com/flame-engine/flame/commit/7e79638dd577cb07d55402d4e862de08ce832b85))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n## 1.2.0\n\n - **PERF**: Avoid unnecessary copy in AssetsCache.readBinaryFile ([#1749](https://github.com/flame-engine/flame/issues/1749)). ([7e79638d](https://github.com/flame-engine/flame/commit/7e79638dd577cb07d55402d4e862de08ce832b85))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n## 1.1.0\n\n - **REFACTOR**: Update and guarantee consistency on mocktail dev dependency version across repo ([#1701](https://github.com/flame-engine/flame/issues/1701)). ([f4a98878](https://github.com/flame-engine/flame/commit/f4a98878062dbd4fe8238a8b014e6be3e528c5d8))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Optional key for Images.load ([#1624](https://github.com/flame-engine/flame/issues/1624)). ([067c34b5](https://github.com/flame-engine/flame/commit/067c34b5f29e1a9bd51861d872092ae5ee0a551f))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n\n## 1.0.2\n\n - Update a dependency to the latest release.\n\n## 1.0.1\n\n - Graduate package to a stable release. See pre-releases prior to this version for changelog entries.\n\n## 1.0.1-releasecandidate.1\n\n# CHANGELOG\n\n## [1.0.0]\n\n * Update to Flame 1.0.0\n\n## [1.0.0-rc2]\n\n * Compatibility with Flame v1.0.0-rc9 and null safe support\n\n## [1.0.0-rc1]\n\n * Compatibility with Flame v1.0.0-rc5\n\n## [0.0.4]\n\n * Breaking tile size into width and height\n\n## [0.0.3]\n\n * Updating Flame version\n\n## [0.0.2]\n\n * Updating Flame version\n\n## [0.0.1]\n\n * Initial release\n"
  },
  {
    "path": "packages/flame_fire_atlas/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_fire_atlas/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nAdds support for <a href=\"https://github.com/flame-engine/fire-atlas\">fire_atlas</a>-based sprite sheets inside <a href=\"https://github.com/flame-engine/flame\">Flame</a>.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_fire_atlas\" ><img src=\"https://img.shields.io/pub/v/flame_fire_atlas.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n\n# flame_fire_atlas\n\nFlame Fire Atlas is a texture atlas lib for Flame. Atlases can be created using the  \n<a href=\"https://github.com/flame-engine/fire-atlas\">Fire Atlas Editor</a>.\n\n\n## How to use\n\nAdd the `flame_fire_atlas` dependency on your `pubspec.yaml`.\n\nThen, load the atlas from your assets:\n\n```dart\n// file at assets/atlas.fa\nfinal atlas = await FireAtlas.loadAsset('atlas.fa');\n```\n\nor when inside a game instance, the `loadFireAtlas` can be used:\n\n```dart\n// file at assets/atlas.fa\nfinal atlas = await loadFireAtlas('atlas.fa');\n```\n\nWith the instance loaded you can now get sprites and animations like:\n\n```dart\natlas.getAnimation('animation_name');\natlas.getSprite('sprite_name');\n```\n"
  },
  {
    "path": "packages/flame_fire_atlas/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    - public_member_api_docs\n"
  },
  {
    "path": "packages/flame_fire_atlas/example/README.md",
    "content": "# Flame Fire Atlas Example\n\nSimple app to showcase how to use the flame fire atlas package\n\n"
  },
  {
    "path": "packages/flame_fire_atlas/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    public_member_api_docs: false\n"
  },
  {
    "path": "packages/flame_fire_atlas/example/lib/main.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_fire_atlas/flame_fire_atlas.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  final game = ExampleGame();\n  runApp(GameWidget(game: game));\n}\n\nclass ExampleGame extends FlameGame with TapCallbacks {\n  late FireAtlas _atlas;\n\n  @override\n  Future<void> onLoad() async {\n    _atlas = await loadFireAtlas('cave_ace.fa');\n    add(\n      SpriteAnimationComponent(\n        size: Vector2(150, 100),\n        animation: _atlas.getAnimation('shooting_ptero'),\n      )..y = 50,\n    );\n\n    add(\n      SpriteAnimationComponent(\n          size: Vector2(150, 100),\n          animation: _atlas.getAnimation('bomb_ptero'),\n        )\n        ..y = 50\n        ..x = 200,\n    );\n\n    add(\n      SpriteComponent(size: Vector2(50, 50), sprite: _atlas.getSprite('bullet'))\n        ..y = 200,\n    );\n\n    add(\n      SpriteComponent(size: Vector2(50, 50), sprite: _atlas.getSprite('shield'))\n        ..x = 100\n        ..y = 200,\n    );\n\n    add(\n      SpriteComponent(size: Vector2(50, 50), sprite: _atlas.getSprite('ham'))\n        ..x = 200\n        ..y = 200,\n    );\n  }\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    add(\n      SpriteAnimationComponent(\n          size: Vector2(100, 100),\n          animation: _atlas.getAnimation('explosion'),\n          removeOnFinish: true,\n        )\n        ..anchor = Anchor.center\n        ..position = event.canvasPosition,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_fire_atlas/example/pubspec.yaml",
    "content": "name: flame_fire_atlas_example\nresolution: workspace\ndescription: Example app to showcase flame fire atlas package\n\npublish_to: 'none'\n\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_fire_atlas: ^1.8.16\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n\nflutter:\n  uses-material-design: true\n  assets:\n    - assets/cave_ace.fa\n"
  },
  {
    "path": "packages/flame_fire_atlas/lib/flame_fire_atlas.dart",
    "content": "library flame_fire_atlas;\n\nimport 'dart:convert';\n\nimport 'package:archive/archive.dart';\nimport 'package:flame/cache.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/sprite.dart';\n\n/// Adds FireAtlas loading methods to Flame [Game].\nextension FireAtlasExtensions on Game {\n  /// Load a [FireAtlas] instances from the given [asset]\n  Future<FireAtlas> loadFireAtlas(String asset) {\n    return FireAtlas.loadAsset(\n      asset,\n      assets: assets,\n      images: images,\n    );\n  }\n}\n\n/// {@template _selection}\n/// Represents the basic information of a selection inside a fire atlas file,\n/// containing its [id] and its coordinates and dimensions.\n/// {@endtemplate}\nclass Selection {\n  /// The id of the selection.\n  String id;\n\n  /// The horizontal coordinate of the selection.\n  int x;\n\n  /// The vertical coordinate of the selection.\n  int y;\n\n  /// The width of the selection.\n  int w;\n\n  /// The height of the selection.\n  int h;\n\n  /// {@macro _selection}\n  Selection({\n    required this.id,\n    required this.x,\n    required this.y,\n    required this.w,\n    required this.h,\n  });\n\n  /// Creates a [Selection] from [json].\n  factory Selection.fromJson(Map<String, dynamic> json) {\n    return Selection(\n      id: json['id'] as String,\n      x: json['x'] as int,\n      y: json['y'] as int,\n      w: json['w'] as int,\n      h: json['h'] as int,\n    );\n  }\n\n  /// Copies this instance with a new id.\n  Selection copyWith({\n    String? id,\n    int? x,\n    int? y,\n    int? w,\n    int? h,\n  }) {\n    return Selection(\n      id: id ?? this.id,\n      x: x ?? this.x,\n      y: y ?? this.y,\n      w: w ?? this.w,\n      h: h ?? this.h,\n    );\n  }\n}\n\n/// {@template _base_selection}\n/// Base class for all FireAtlas selections, containing information for a\n/// section of the image.\n///\n/// Check [SpriteSelection] for [Sprite] based selections, and\n/// [AnimationSelection] for [SpriteAnimation] based selections.\n/// {@endtemplate}\nabstract class BaseSelection {\n  final Selection _info;\n\n  /// {@macro _base_selection}\n  BaseSelection(this._info, {this.group});\n\n  /// The id of the selection.\n  String get id => _info.id;\n\n  /// The horizontal coordinate of the selection.\n  int get x => _info.x;\n\n  /// The vertical coordinate of the selection.\n  int get y => _info.y;\n\n  /// The width of the selection.\n  int get w => _info.w;\n\n  /// The height of the selection.\n  int get h => _info.h;\n\n  /// A group that this selection belongs to.\n  final String? group;\n\n  /// The selection information.\n  Selection get selection => _info;\n\n  /// Copies this instance with a new group.\n  BaseSelection copyWithGroup(String? group);\n\n  /// Copies this instance with a new selection info.\n  BaseSelection copyWithInfo(Selection info);\n\n  /// Returns this instance as a json.\n  Map<String, dynamic> toJson() {\n    final json = <String, dynamic>{}\n      ..['id'] = id\n      ..['x'] = x\n      ..['y'] = y\n      ..['w'] = w\n      ..['h'] = h\n      ..['group'] = group;\n\n    return json;\n  }\n}\n\n/// {@template _sprite_selection}\n/// Represents a specific selection of Flame [Sprite]s.\n/// {@endtemplate}\nclass SpriteSelection extends BaseSelection {\n  /// {@macro _sprite_selection}\n  SpriteSelection({\n    required Selection info,\n    super.group,\n  }) : super(info);\n\n  /// Creates a [SpriteSelection] from [json].\n  factory SpriteSelection.fromJson(Map<String, dynamic> json) {\n    final info = Selection.fromJson(json);\n    final group = json['group'] as String?;\n    return SpriteSelection(info: info, group: group);\n  }\n\n  /// Returns this instance as a json.\n  @override\n  Map<String, dynamic> toJson() {\n    return super.toJson()..['type'] = 'sprite';\n  }\n\n  /// Copies this instance with a new group.\n  @override\n  SpriteSelection copyWithGroup(String? group) {\n    return SpriteSelection(info: _info, group: group);\n  }\n\n  /// Copies this instance with a new info.\n  @override\n  SpriteSelection copyWithInfo(Selection info) {\n    return SpriteSelection(info: info, group: group);\n  }\n}\n\n/// {@template _animation_selection}\n/// Represents a specific selection of Flame [Sprite]s as an animation.\n/// {@endtemplate}\nclass AnimationSelection extends BaseSelection {\n  /// The number of frames of this animation.\n  int frameCount;\n\n  /// The time between each frame.\n  double stepTime;\n\n  /// If the animation is looping or not.\n  bool loop;\n\n  /// {@macro _animation_selection}\n  AnimationSelection({\n    required Selection info,\n    required this.frameCount,\n    required this.stepTime,\n    required this.loop,\n    super.group,\n  }) : super(info);\n\n  /// Creates a [AnimationSelection] from [json].\n  factory AnimationSelection.fromJson(Map<String, dynamic> json) {\n    final info = Selection.fromJson(json);\n    final group = json['group'] as String?;\n\n    return AnimationSelection(\n      info: info,\n      frameCount: json['frameCount'] as int,\n      stepTime: json['stepTime'] as double,\n      loop: json['loop'] as bool,\n      group: group,\n    );\n  }\n\n  /// Returns this instance as json.\n  @override\n  Map<String, dynamic> toJson() {\n    return super.toJson()\n      ..['frameCount'] = frameCount\n      ..['stepTime'] = stepTime\n      ..['loop'] = loop\n      ..['type'] = 'animation';\n  }\n\n  /// Copies this instance with a new group.\n  @override\n  AnimationSelection copyWithGroup(String? group) {\n    return AnimationSelection(\n      info: _info,\n      frameCount: frameCount,\n      stepTime: stepTime,\n      loop: loop,\n      group: group,\n    );\n  }\n\n  /// Copies this instance with a new info.\n  @override\n  AnimationSelection copyWithInfo(Selection info) {\n    return AnimationSelection(\n      info: info,\n      frameCount: frameCount,\n      stepTime: stepTime,\n      loop: loop,\n      group: group,\n    );\n  }\n}\n\n/// FireAtlas is a mapping file that can hold several [Sprite]s and\n/// [SpriteAnimation]s.\n///\n/// Use [getSprite] and [getAnimation] to retrieve mapped assets.\nclass FireAtlas {\n  /// Id of the FireAtlas, mainly used by the Fire Atlas Editor for file\n  /// identification.\n  String id;\n\n  /// The width of the tile.\n  double tileWidth;\n\n  /// The height of the tile.\n  double tileHeight;\n\n  /// Stores the asset data once this instance has loaded.\n  String? imageData;\n  Image? _image;\n\n  /// Creates a FireAtlas instance.\n  ///\n  /// This constructor is often used internally; to load a FireAtlas instance\n  /// into you game, check [loadAsset].\n  FireAtlas({\n    required this.id,\n    required this.tileWidth,\n    required this.tileHeight,\n    required this.imageData,\n  });\n\n  /// Holds all the selections of this file.\n  Map<String, BaseSelection> selections = {};\n\n  /// Loads the atlas image into memory so it can be used, this method\n  /// is used internally by [loadAsset], prefer that method unless\n  /// there is a very specific use case for it.\n  ///\n  /// [clearImageData] Can be set to false to avoid clearing the stored\n  /// information about the image on this object, this is true by default, its\n  /// use is intended to enable serializing this object\n  /// [images] The images cache to be used, falls back to [Flame.images] when\n  /// omitted.\n  ///\n  Future<void> loadImage({bool clearImageData = true, Images? images}) async {\n    if (imageData == null) {\n      throw 'Attempting on calling load on an already loaded Image';\n    }\n    final imagesCache = images ?? Flame.images;\n    _image = await imagesCache.fromBase64(id, imageData!);\n\n    // Clear memory\n    if (clearImageData) {\n      imageData = null;\n    }\n  }\n\n  /// Serializes the mappings on this Atlas in a json format.\n  Map<String, dynamic> toJson() {\n    final selectionsJson = <String, dynamic>{};\n    selections.entries.forEach((entry) {\n      selectionsJson[entry.key] = entry.value.toJson();\n    });\n\n    final json = <String, dynamic>{}\n      ..['id'] = id\n      ..['imageData'] = imageData\n      ..['selections'] = selectionsJson\n      ..['tileWidth'] = tileWidth\n      ..['tileHeight'] = tileHeight;\n\n    return json;\n  }\n\n  factory FireAtlas._fromJson(Map<String, dynamic> json) {\n    final tileHeight = json['tileHeight'] as num?;\n    final tileWidth = json['tileWidth'] as num?;\n    final tileSize = json['tileSize'] as num? ?? 0;\n\n    final atlas = FireAtlas(\n      id: json['id'] as String,\n      imageData: json['imageData'] as String?,\n      tileHeight: (tileHeight ?? tileSize).toDouble(),\n      tileWidth: (tileWidth ?? tileSize).toDouble(),\n    );\n\n    final selections = json['selections'] as Map<String, dynamic>;\n    selections.entries.forEach((entry) {\n      final value = entry.value as Map<String, dynamic>;\n      final selection = value['type'] == 'animation'\n          ? AnimationSelection.fromJson(value)\n          : SpriteSelection.fromJson(value);\n\n      atlas.selections[entry.key] = selection;\n    });\n\n    return atlas;\n  }\n\n  /// Loads the [FireAtlas] from an asset.\n  ///\n  /// Use [encoded] = false to load the asset from a json file.\n  static Future<FireAtlas> loadAsset(\n    String fileName, {\n    AssetsCache? assets,\n    Images? images,\n    bool encoded = true,\n  }) async {\n    final assetsCache = assets ?? Flame.assets;\n    final FireAtlas atlas;\n    if (encoded) {\n      final bytes = await assetsCache.readBinaryFile(fileName);\n      atlas = FireAtlas.deserializeBytes(bytes);\n    } else {\n      final json = await assetsCache.readJson(fileName);\n      atlas = FireAtlas.deserializeJson(json);\n    }\n    await atlas.loadImage(images: images);\n    return atlas;\n  }\n\n  /// Serializes this instances into a byte array.\n  ///\n  /// If [encoded] is set to true,\n  /// it will return a gzip compressed byte array,\n  /// otherwise it will return a string byte array.\n  List<int> serialize({bool encoded = true}) {\n    final raw = jsonEncode(toJson());\n\n    final stringBytes = utf8.encode(raw);\n    if (!encoded) {\n      return stringBytes;\n    }\n\n    return const GZipEncoder().encode(stringBytes);\n  }\n\n  /// Reads a [FireAtlas] instance from a json file.\n  factory FireAtlas.deserializeJson(Map<String, dynamic> rawJson) =>\n      FireAtlas._fromJson(rawJson);\n\n  /// Reads a [FireAtlas] instance from a byte array.\n  factory FireAtlas.deserializeBytes(List<int> bytes) {\n    final unzippedBytes = const GZipDecoder().decodeBytes(bytes);\n    final unzippedString = utf8.decode(unzippedBytes);\n    return FireAtlas.deserializeJson(\n      jsonDecode(unzippedString) as Map<String, dynamic>,\n    );\n  }\n\n  Image _assertImageLoaded() {\n    if (_image == null) {\n      throw Exception('Atlas is not loaded yet, call \"load\" before using it');\n    }\n\n    return _image!;\n  }\n\n  /// Returns a Sprite with the given [selectionId].\n  Sprite getSprite(String selectionId) {\n    final selection = selections[selectionId];\n\n    if (selection == null) {\n      throw 'There is no selection with the id \"$selectionId\" on this atlas';\n    }\n\n    if (selection is! SpriteSelection) {\n      throw 'Selection \"$selectionId\" is not a Sprite';\n    }\n\n    final image = _assertImageLoaded();\n\n    return Sprite(\n      image,\n      srcPosition: Vector2(\n        selection.x.toDouble() * tileWidth,\n        selection.y.toDouble() * tileHeight,\n      ),\n      srcSize: Vector2(\n        (1 + selection.w.toDouble()) * tileWidth,\n        (1 + selection.h.toDouble()) * tileHeight,\n      ),\n    );\n  }\n\n  /// Returns a SpriteAnimation with the given [selectionId].\n  SpriteAnimation getAnimation(String selectionId) {\n    final selection = selections[selectionId];\n\n    final image = _assertImageLoaded();\n\n    if (selection == null) {\n      throw 'There is no selection with the id \"$selectionId\" on this atlas';\n    }\n    if (selection is! AnimationSelection) {\n      throw 'Selection \"$selectionId\" is not an Animation';\n    }\n\n    final initialX = selection.x.toDouble();\n\n    final frameSize = (1 + selection.w.toDouble()) / selection.frameCount;\n\n    final width = frameSize * tileWidth;\n    final height = (1 + selection.h.toDouble()) * tileHeight;\n\n    final sprites = List.generate(selection.frameCount, (i) {\n      final x = (initialX + i) * frameSize;\n      return Sprite(\n        image,\n        srcPosition: Vector2(\n          x * tileWidth,\n          selection.y.toDouble() * tileHeight,\n        ),\n        srcSize: Vector2(width, height),\n      );\n    });\n\n    return SpriteAnimation.spriteList(\n      sprites,\n      stepTime: selection.stepTime,\n      loop: selection.loop,\n    );\n  }\n\n  /// Returns the atlas image.\n  ///\n  /// Throws if called before the image is loaded.\n  Image get image => _assertImageLoaded();\n}\n"
  },
  {
    "path": "packages/flame_fire_atlas/pubspec.yaml",
    "content": "name: flame_fire_atlas\nresolution: workspace\ndescription: Easy to use texture atlases for the flame engine created with the\n  fire atlas editor\nversion: 1.8.16\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_fire_atlas\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - tools\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  archive: ^4.0.2\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  dartdoc: ^9.0.0\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n  mocktail: ^1.0.4\n"
  },
  {
    "path": "packages/flame_fire_atlas/test/flame_fire_atlas_test.dart",
    "content": "import 'dart:io';\nimport 'dart:typed_data';\nimport 'dart:ui';\n\nimport 'package:flame/cache.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_fire_atlas/flame_fire_atlas.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass _AssetsCacheMock extends Mock implements AssetsCache {}\n\nclass _ImagesMock extends Mock implements Images {}\n\nclass _ImageMock extends Mock implements Image {}\n\nclass _MockedGame extends Mock implements FlameGame {\n  final _imagesMock = _ImagesMock();\n  @override\n  Images get images => _imagesMock;\n\n  final _assetsMock = _AssetsCacheMock();\n  @override\n  AssetsCache get assets => _assetsMock;\n}\n\nFuture<Uint8List> _readTestFile() async {\n  final exampleAtlas = File('./example/assets/cave_ace.fa');\n  final bytes = await exampleAtlas.readAsBytes();\n  return bytes;\n}\n\nFuture<FireAtlas> _readTestAtlas() async {\n  final assetsMock = _AssetsCacheMock();\n\n  when(() => assetsMock.readBinaryFile('cave.fa')).thenAnswer((_) async {\n    return _readTestFile();\n  });\n\n  final imagesMock = _ImagesMock();\n\n  when(() => imagesMock.fromBase64(any(), any())).thenAnswer((_) async {\n    return _ImageMock();\n  });\n\n  final atlas = await FireAtlas.loadAsset(\n    'cave.fa',\n    assets: assetsMock,\n    images: imagesMock,\n  );\n  return atlas;\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('FireAtlas', () {\n    test('can load the asset', () async {\n      final atlas = await _readTestAtlas();\n      expect(atlas.id, 'cave_ace');\n    });\n\n    test('image returns the loaded image', () async {\n      final atlas = await _readTestAtlas();\n      final image = atlas.image;\n      expect(image, isA<Image>());\n    });\n\n    test('image throws when the image is not loaded', () async {\n      final atlas = FireAtlas(\n        id: '',\n        tileWidth: 0,\n        tileHeight: 0,\n        imageData: '',\n      );\n\n      expect(\n        () => atlas.image,\n        throwsA(\n          isA<Exception>().having(\n            (e) => e.toString(),\n            'toString',\n            'Exception: Atlas is not loaded yet, call \"load\" before using it',\n          ),\n        ),\n      );\n    });\n\n    test('can load the asset using the global assets/images', () async {\n      final assetsMock = _AssetsCacheMock();\n\n      when(() => assetsMock.readBinaryFile('cave.fa')).thenAnswer((_) async {\n        return _readTestFile();\n      });\n\n      final imagesMock = _ImagesMock();\n\n      when(() => imagesMock.fromBase64(any(), any())).thenAnswer((_) async {\n        return _ImageMock();\n      });\n\n      Flame.images = imagesMock;\n      Flame.assets = assetsMock;\n\n      await FireAtlas.loadAsset('cave.fa');\n\n      verify(() => Flame.assets.readBinaryFile(any())).called(1);\n      verify(() => Flame.images.fromBase64(any(), any())).called(1);\n    });\n\n    test('returns a sprite', () async {\n      final atlas = await _readTestAtlas();\n\n      final bullet = atlas.getSprite('bullet');\n      expect(bullet, isNotNull);\n    });\n\n    test('throws when there is not sprite for that id', () async {\n      final atlas = await _readTestAtlas();\n\n      expect(\n        () => atlas.getSprite('bla'),\n        throwsA(\n          equals('There is no selection with the id \"bla\" on this atlas'),\n        ),\n      );\n    });\n\n    test('throws when getSprite is used for an animation selection', () async {\n      final atlas = await _readTestAtlas();\n\n      expect(\n        () => atlas.getSprite('bomb_ptero'),\n        throwsA(equals('Selection \"bomb_ptero\" is not a Sprite')),\n      );\n    });\n\n    test('returns an animation', () async {\n      final atlas = await _readTestAtlas();\n      final bombPtero = atlas.getAnimation('bomb_ptero');\n      expect(bombPtero, isNotNull);\n    });\n\n    test('throws when there is not an animation for that id', () async {\n      final atlas = await _readTestAtlas();\n\n      expect(\n        () => atlas.getAnimation('bla'),\n        throwsA(\n          equals('There is no selection with the id \"bla\" on this atlas'),\n        ),\n      );\n    });\n\n    test('throws when getAnimation is used for a sprite selection', () async {\n      final atlas = await _readTestAtlas();\n\n      expect(\n        () => atlas.getAnimation('bullet'),\n        throwsA(equals('Selection \"bullet\" is not an Animation')),\n      );\n    });\n\n    test('converts to json', () async {\n      final atlas = await _readTestAtlas();\n      final json = atlas.toJson();\n      expect(json['id'], 'cave_ace');\n    });\n\n    test('serialize/deserialize', () async {\n      final atlas = await _readTestAtlas();\n\n      final bytes = atlas.serialize();\n\n      final copy = FireAtlas.deserializeBytes(bytes);\n      expect(copy.id, atlas.id);\n    });\n\n    test(\n      'Uses the game images and assets when loading from the game',\n      () async {\n        final game = _MockedGame();\n\n        when(() => game.assets.readBinaryFile('cave.fa')).thenAnswer((_) async {\n          return _readTestFile();\n        });\n\n        when(() => game.images.fromBase64(any(), any())).thenAnswer((_) async {\n          return _ImageMock();\n        });\n\n        await game.loadFireAtlas('cave.fa');\n\n        verify(() => game.assets.readBinaryFile(any())).called(1);\n        verify(() => game.images.fromBase64(any(), any())).called(1);\n      },\n    );\n\n    group('SpriteSelection', () {\n      test('can be created from a json', () async {\n        final sprite = SpriteSelection.fromJson({\n          'id': 'kinetic_bullet',\n          'x': 0,\n          'y': 0,\n          'w': 16,\n          'h': 16,\n          'group': 'bullets',\n        });\n        expect(sprite.id, 'kinetic_bullet');\n        expect(sprite.x, 0);\n        expect(sprite.y, 0);\n        expect(sprite.w, 16);\n        expect(sprite.h, 16);\n        expect(sprite.group, 'bullets');\n      });\n\n      test('serializes to json', () async {\n        final sprite = SpriteSelection(\n          info: Selection(\n            id: 'kinetic_bullet',\n            x: 0,\n            y: 0,\n            w: 16,\n            h: 16,\n          ),\n          group: 'bullets',\n        );\n        final json = sprite.toJson();\n        expect(json['id'], 'kinetic_bullet');\n        expect(json['x'], 0);\n        expect(json['y'], 0);\n        expect(json['w'], 16);\n        expect(json['h'], 16);\n        expect(json['group'], 'bullets');\n      });\n\n      group('copyWithGroup', () {\n        test('creates a copy with a new group', () async {\n          final sprite = SpriteSelection(\n            info: Selection(\n              id: 'kinetic_bullet',\n              x: 0,\n              y: 0,\n              w: 16,\n              h: 16,\n            ),\n            group: 'bullets',\n          );\n\n          final copy = sprite.copyWithGroup('new_group');\n          expect(copy.group, 'new_group');\n          expect(copy.id, 'kinetic_bullet');\n          expect(copy.x, 0);\n          expect(copy.y, 0);\n          expect(copy.w, 16);\n          expect(copy.h, 16);\n        });\n      });\n    });\n\n    group('AnimationSelection', () {\n      test('can be created from a json', () async {\n        final animation = AnimationSelection.fromJson({\n          'id': 'bomb_ptero',\n          'frameCount': 1,\n          'stepTime': 0.2,\n          'loop': true,\n          'x': 0,\n          'y': 0,\n          'w': 16,\n          'h': 16,\n          'group': 'enemies',\n        });\n\n        expect(animation.id, 'bomb_ptero');\n        expect(animation.frameCount, 1);\n        expect(animation.stepTime, 0.2);\n        expect(animation.loop, isTrue);\n        expect(animation.x, 0);\n        expect(animation.y, 0);\n        expect(animation.w, 16);\n        expect(animation.h, 16);\n        expect(animation.group, 'enemies');\n      });\n\n      test('serializes to json', () async {\n        final animation = AnimationSelection(\n          info: Selection(\n            id: 'bomb_ptero',\n            x: 0,\n            y: 0,\n            w: 16,\n            h: 16,\n          ),\n          frameCount: 1,\n          stepTime: 0.2,\n          loop: true,\n          group: 'enemies',\n        );\n        final json = animation.toJson();\n        expect(json['id'], 'bomb_ptero');\n        expect(json['frameCount'], 1);\n        expect(json['stepTime'], 0.2);\n        expect(json['loop'], isTrue);\n        expect(json['x'], 0);\n        expect(json['y'], 0);\n        expect(json['w'], 16);\n        expect(json['h'], 16);\n        expect(json['group'], 'enemies');\n      });\n\n      group('copyWithGroup', () {\n        test('creates a copy with a new group', () async {\n          final animation = AnimationSelection(\n            info: Selection(\n              id: 'bomb_ptero',\n              x: 0,\n              y: 0,\n              w: 16,\n              h: 16,\n            ),\n            frameCount: 1,\n            stepTime: 0.2,\n            loop: true,\n            group: 'enemies',\n          );\n\n          final copy = animation.copyWithGroup('new_group');\n          expect(copy.group, 'new_group');\n          expect(copy.id, 'bomb_ptero');\n          expect(copy.frameCount, 1);\n          expect(copy.stepTime, 0.2);\n          expect(copy.loop, isTrue);\n          expect(copy.x, 0);\n          expect(copy.y, 0);\n          expect(copy.w, 16);\n          expect(copy.h, 16);\n        });\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_forge2d/CHANGELOG.md",
    "content": "## 0.19.2+5\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 0.19.2+4\n\n - Update a dependency to the latest release.\n\n## 0.19.2+3\n\n - Update a dependency to the latest release.\n\n## 0.19.2+2\n\n - Update a dependency to the latest release.\n\n## 0.19.2+1\n\n - Update a dependency to the latest release.\n\n## 0.19.2\n\n - **FEAT**: Allow bodies to not be destroyed when the world is removed from the component tree ([#3716](https://github.com/flame-engine/flame/issues/3716)). ([7d18fd3d](https://github.com/flame-engine/flame/commit/7d18fd3d1cf076bce7032eb60dbfd7777643539d))\n\n## 0.19.1\n\n - **FIX**: Use correct `Forge2DWorld` in `onRemove` ([#3713](https://github.com/flame-engine/flame/issues/3713)). ([140833d1](https://github.com/flame-engine/flame/commit/140833d1eacf6b756f81f0452ec237c6991f2ae0))\n\n## 0.19.0+6\n\n - Update a dependency to the latest release.\n\n## 0.19.0+5\n\n - Update a dependency to the latest release.\n\n## 0.19.0+4\n\n - Update a dependency to the latest release.\n\n## 0.19.0+3\n\n - Update a dependency to the latest release.\n\n## 0.19.0+2\n\n - Update a dependency to the latest release.\n\n## 0.19.0+1\n\n - Update a dependency to the latest release.\n\n## 0.19.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Use 32bit Vector2 in Flame to be compatible with shaders and forge2d ([#3515](https://github.com/flame-engine/flame/issues/3515)). ([19dcecf5](https://github.com/flame-engine/flame/commit/19dcecf5df85345fd4fac168e1f360cee4665658))\n\n## 0.18.3+1\n\n - **FIX**: Remove outdated deprecations ([#3541](https://github.com/flame-engine/flame/issues/3541)). ([b918e40d](https://github.com/flame-engine/flame/commit/b918e40d0ce14b89ba9b5c82aed8ff51d6f549c3))\n\n## 0.18.3\n\n - **FIX**: Expose event dispatcher classes ([#3532](https://github.com/flame-engine/flame/issues/3532)). ([db8e0b20](https://github.com/flame-engine/flame/commit/db8e0b20746dc96a221dc4e85b09f5a35ecc7339))\n\n## 0.18.2+7\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 0.18.2+6\n\n - **FIX**: Use correct matrix indices in BodyComponent ([#3491](https://github.com/flame-engine/flame/issues/3491)). ([d6c83fab](https://github.com/flame-engine/flame/commit/d6c83fab6c5cf56b047dcd22b9f1f0a075c26201))\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 0.18.2+5\n\n - Update a dependency to the latest release.\n\n## 0.18.2+4\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n\n## 0.18.2+3\n\n - Update a dependency to the latest release.\n\n## 0.18.2+2\n\n - Update a dependency to the latest release.\n\n## 0.18.2+1\n\n - Update a dependency to the latest release.\n\n## 0.18.2\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 0.18.1\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n\n## 0.18.0\n\n - **FIX**: Use camera argument name in Forge2DGame ([#3115](https://github.com/flame-engine/flame/issues/3115)). ([9d97b123](https://github.com/flame-engine/flame/commit/9d97b12348161b4b150ee4166ba552f28d5f9d8b))\n - **DOCS**: Upgrade dashbook version ([#3109](https://github.com/flame-engine/flame/issues/3109)). ([a717bcb4](https://github.com/flame-engine/flame/commit/a717bcb475a5604c5d8c66a3a5ac53f0dc173109))\n\n## 0.17.1\n\n - **FIX**: Null gravity override by Forge2dGame ([#3092](https://github.com/flame-engine/flame/issues/3092)). ([3c35d59b](https://github.com/flame-engine/flame/commit/3c35d59b4a4ec064106d24a17e748005a20d9fde))\n\n## 0.17.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: BodyComponent fixtures should test with global point ([#3042](https://github.com/flame-engine/flame/issues/3042)). ([7c3038be](https://github.com/flame-engine/flame/commit/7c3038becba91550eb47a033cbed7208d570e012))\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 0.16.0+5\n\n - Update a dependency to the latest release.\n\n## 0.16.0+4\n\n - **FIX**: Wake up bodies on gravity change ([#2954](https://github.com/flame-engine/flame/issues/2954)). ([4f58329c](https://github.com/flame-engine/flame/commit/4f58329ceacef84d91cd41019d72bc2351bc50cd))\n\n## 0.16.0+3\n\n - Update a dependency to the latest release.\n\n## 0.16.0+2\n\n - Update a dependency to the latest release.\n\n## 0.16.0+1\n\n - Update a dependency to the latest release.\n\n## 0.16.0\n - Updated to Forge2D 0.12.2\n\n### Migration instructions\n\nThe gravity and bullet are now field setters and getters, so if you before had `setGravity(Vector2(0, -10))` then you now do `gravity = Vector2(0, -10);` and if you previously had `body.setBullet(true);` you now do `body.isBullet = true;`.\n\n - Updated to Forge2D 0.12.2\n\n### Migration instructions\n\nThe gravity and bullet are now field setters and getters, so if you before had `setGravity(Vector2(0, -10))` then you now do `gravity = Vector2(0, -10);` and if you previously had `body.setBullet(true);` you now do `body.isBullet = true;`.\n\n## 0.15.1\n\n - **FEAT**: Allow for bodyDef and fixtureDefs to be prepared earlier ([#2768](https://github.com/flame-engine/flame/issues/2768)). ([21357bca](https://github.com/flame-engine/flame/commit/21357bcac1e7e1cebfa6f2a496ec4b627d62d0e7))\n\n## 0.15.0+1\n\n - Update a dependency to the latest release.\n\n## 0.15.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **FIX**: Absolute angle takes into account BodyComponent ancestors too ([#2678](https://github.com/flame-engine/flame/issues/2678)). ([75aee767](https://github.com/flame-engine/flame/commit/75aee767811ef440841956d9e467be157c4ab880))\n - **FIX**: Proper Flame dependency in flame_forge2d ([#2644](https://github.com/flame-engine/flame/issues/2644)). ([9bbecb88](https://github.com/flame-engine/flame/commit/9bbecb88d86aa051626267fd69e5bf71fdca66d6))\n - **DOCS**: Enable CSpell on tests ([#2723](https://github.com/flame-engine/flame/issues/2723)). ([e051298c](https://github.com/flame-engine/flame/commit/e051298cba76550229780438b1a589557c7b488d))\n - **BREAKING** **FEAT**: Add CameraComponent to FlameGame ([#2740](https://github.com/flame-engine/flame/issues/2740)). ([7c2f4000](https://github.com/flame-engine/flame/commit/7c2f4000761580dbabb5d73b27f64d5819b34e8d))\n - **BREAKING** **FEAT**: Move `Forge2DGame` to use `CameraComponent` ([#2728](https://github.com/flame-engine/flame/issues/2728)). ([7a3d5126](https://github.com/flame-engine/flame/commit/7a3d5126a54d23cdebde20953772a53ba1a53204))\n\n## 0.14.1+1\n\n - Update a dependency to the latest release.\n\n## 0.14.1\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FEAT**: ComponentKey API ([#2566](https://github.com/flame-engine/flame/issues/2566)). ([b3efb612](https://github.com/flame-engine/flame/commit/b3efb612cb3cb77f69bc030e9ba71516348035d2))\n - **DOCS**: Fix broken link on flame_forge2d readme ([#2588](https://github.com/flame-engine/flame/issues/2588)). ([45115bbf](https://github.com/flame-engine/flame/commit/45115bbff8539010f5d7bb7cf9479183b1a27cc8))\n\n## 0.14.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n - **BREAKING** **REFACTOR**: Move `CameraComponent` and events out of experimental ([#2505](https://github.com/flame-engine/flame/issues/2505)). ([87b8a067](https://github.com/flame-engine/flame/commit/87b8a067f3e0096cebff3db4f5767e68616928fd))\n\n## 0.13.0+1\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n## 0.13.0\n\n> Note: This release has breaking changes.\n\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n - **DOCS**: Added a page for Joints documentation + ConstantVolumeJoint doc and example ([#2362](https://github.com/flame-engine/flame/issues/2362)). ([957ad240](https://github.com/flame-engine/flame/commit/957ad2402af1c44aea500d77092d387ed463b7e0))\n - **BREAKING** **FEAT**: The `HasTappableComponents` mixin is no longer needed ([#2450](https://github.com/flame-engine/flame/issues/2450)). ([b5bdf4ec](https://github.com/flame-engine/flame/commit/b5bdf4ec173e87907a59a9f62fcdf35cc968af2a))\n\n## 0.12.5\n\n - **FIX**: Depend on test: any for flame_test ([#2207](https://github.com/flame-engine/flame/issues/2207)). ([acfd418d](https://github.com/flame-engine/flame/commit/acfd418d882ee6872f3aa9961c39680ec123c2e6))\n\n## 0.12.4\n\n - **FIX**: 🐛 unit test for `Forge2dGame` ([#2068](https://github.com/flame-engine/flame/issues/2068)). ([d659b85d](https://github.com/flame-engine/flame/commit/d659b85d090614ebb3df06fb68254c087f6f9dff))\n\n## 0.12.3\n\n - **FEAT**: Allow flame_forge2d's followBodyComponent to follow centre of mass ([#1947](https://github.com/flame-engine/flame/issues/1947)) ([#1948](https://github.com/flame-engine/flame/issues/1948)). ([c4fd2ba5](https://github.com/flame-engine/flame/commit/c4fd2ba5402f42d5a333270f401bb7208e050986))\n - **DOCS**: Fix broken link in forge2d readme ([#1853](https://github.com/flame-engine/flame/issues/1853)). ([31d39f86](https://github.com/flame-engine/flame/commit/31d39f86708295ef19624554e636e1ddd4846c4d))\n\n## 0.12.2\n\n - **FIX**: `renderChain` should allow open-ended chain drawing ([#1804](https://github.com/flame-engine/flame/issues/1804)). ([60daa196](https://github.com/flame-engine/flame/commit/60daa196a8b2f9d3b022bf4d25b0dc8af29f40b8))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n## 0.12.1\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n## 0.12.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **FIX**: flips back defaultGravity on y axis ([#1585](https://github.com/flame-engine/flame/issues/1585)). ([6b217ac4](https://github.com/flame-engine/flame/commit/6b217ac466f7522772cf1f974b39af1392f5a807))\n - **FIX**: MouseJoint gets less and less reactive ([#1562](https://github.com/flame-engine/flame/issues/1562)). ([90747bf4](https://github.com/flame-engine/flame/commit/90747bf4a52bb4c82611fa1e9c50f0f11e309baa))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: allow controlling when a fixture is rendered ([#1648](https://github.com/flame-engine/flame/issues/1648)). ([1b59d801](https://github.com/flame-engine/flame/commit/1b59d801c6c1bcc325948ac4e18dfa536baa5a9c))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n - **FEAT**: allowed specifying renderBody via constructor ([#1548](https://github.com/flame-engine/flame/issues/1548)). ([ceb72666](https://github.com/flame-engine/flame/commit/ceb726666e39e20cd12786be86da60ab9cc61c9a))\n - **DOCS**: Move flame_forge2d examples to main examples ([#1588](https://github.com/flame-engine/flame/issues/1588)). ([6dd0a970](https://github.com/flame-engine/flame/commit/6dd0a970e6f106d8927b542d688f3bc9231e1b69))\n - **BREAKING** **FEAT**: enhance ContactCallback process ([#1547](https://github.com/flame-engine/flame/issues/1547)). ([a50d4a1e](https://github.com/flame-engine/flame/commit/a50d4a1e7d9eaf66726ed1bb9894c9d495547d8f))\n\n## 0.11.0\n\n> Note: This release has breaking changes.\n\n - **FEAT**: Bump forg2d version and have flame_forge2d examples use latest syntax (#1535). ([4f7a12eb](https://github.com/flame-engine/flame/commit/4f7a12eb2c00d370fd093de4af6a3f9f740aa03a))\n - **FEAT**: Added children parameter to Component constructor (#1525). ([f0b31fcf](https://github.com/flame-engine/flame/commit/f0b31fcfc0fc6b0f8f96895ef6a68fd5a30a3159))\n - **DOCS**: Fix flame_forge2d readme links (#1540). ([c51bc6db](https://github.com/flame-engine/flame/commit/c51bc6db5dbd32283a7e441b450e0dc4636891c6))\n - **BREAKING** **FEAT**: Flip gravity in flame_forge2d to be able to mix Forge2D and Flame components (#1506). ([bdb360f1](https://github.com/flame-engine/flame/commit/bdb360f18128f9305baa0e6ca77ee6fcad496bc7))\n\n## 0.10.0\n\n - Bump \"flame_forge2d\" to `0.10.0`.\n\n## 0.9.0\n\n - Graduate package to a stable release. See pre-releases prior to this version for changelog entries.\n\n## 0.9.0-releasecandidate.6\n\n - **FEAT**: updating forge2d version (#1479). ([4678e21a](https://github.com/flame-engine/flame/commit/4678e21a0b714b8344ae2453b1ac6df68adfb4cd))\n - **FEAT**: Possibility to mark gesture events as handled (#1465). ([4c3960c3](https://github.com/flame-engine/flame/commit/4c3960c3418f8ff4d557c1764c6793468238a8da))\n\n## 0.9.0-releasecandidate.5\n\n - **FEAT**: `BodyComponent` can properly have normal Flame component children (#1442). ([7fe8b6de](https://github.com/flame-engine/flame/commit/7fe8b6deb18b3579fecc99cc44e0ffea73be5f02))\n\n## 0.9.0-releasecandidate.4\n\n - **FIX**: Don't use debug rendering by default in BodyComponent (#1439). ([33b725e8](https://github.com/flame-engine/flame/commit/33b725e8378d4060e726e99c0452b64f54ef8f67))\n\n## 0.9.0-releasecandidate.3\n\n - Update a dependency to the latest release.\n\n## 0.9.0-releasecandidate.2\n\n - **FIX**: PositionBodyComponent had an async onMount, without needing (#1424). ([7b0fd20a](https://github.com/flame-engine/flame/commit/7b0fd20a2c6d9f6cac0a88877c793608ab4d14c8))\n - **FEAT**: Make ContactCallback begin end methods optional overrides (#1415). ([29dd1891](https://github.com/flame-engine/flame/commit/29dd1891b6409ed71c54e05272100dbb180d18e7))\n\n## 0.9.0-releasecandidate.1\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Remove Loadable, optional onLoads (#1333). ([05f7a4c3](https://github.com/flame-engine/flame/commit/05f7a4c3d6b1e3b67575c4ec920cf270691bbab4))\n - **REFACTOR**: Add a few more rules to flame_lint, including use_key_in_widget_constructors (#1248). ([bac6c8a4](https://github.com/flame-engine/flame/commit/bac6c8a4469f2c5c2926335f2f589eec9b1a5b5b))\n - **FIX**: Clone input vector before projecting it (#1255). ([d1d6ad4d](https://github.com/flame-engine/flame/commit/d1d6ad4d8c07a5c6895e6120871e336d447ee5a8))\n - **FEAT**: improving generics on position body component (#1397). ([7edbb299](https://github.com/flame-engine/flame/commit/7edbb299855a3926693e846bc1f8e0cbc4272629))\n - **FEAT**: Add missing optional priority to SpriteBodyComponent (#1404). ([a000eb11](https://github.com/flame-engine/flame/commit/a000eb1172ae06ea397d6233c96f2b0ee1f0d93d))\n - **FEAT**: Components are now always added in the correct order (#1337). ([c753fc46](https://github.com/flame-engine/flame/commit/c753fc4636d337d850a5a5cc684be8155f08b214))\n - **FEAT**: Allow to pass a camera to Forge2D Game (#1364). ([9890e9ca](https://github.com/flame-engine/flame/commit/9890e9caada0abc9cd8942b840d72f98853e0cba))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n - **DOCS**: Add Raycast example for flame_forge2d (#1253). ([994f27d5](https://github.com/flame-engine/flame/commit/994f27d54ccfaeb1251dd5e95e566611fc967022))\n - **BREAKING** **FIX**: Remove pointerId from Draggable callbacks (#1313). ([27adda17](https://github.com/flame-engine/flame/commit/27adda17b7b4d8c229cca53799826c7b854eae95))\n\n# CHANGELOG\n\n## [0.8.3]\n - Update to Flame 1.0.0\n\n## [0.8.2-releasecandidate.17]\n - Arguments of `PositionBodyComponent` are now optional so that initialization can be done in `onLoad`\n\n## [0.8.2-releasecandidate.15]\n - Destroy body before calling `super.onRemove`\n\n## [0.8.1-releasecandidate.15]\n - The rendering of `BodyComponent` is now inline with the Flame coordinate system\n - Moved `BodyComponent` base from `BaseComponent` to `Component`\n - Pass `Forge2DCamera` to super class in `Forge2DGame`\n\n## [0.8.1-releasecandidate.13]\n - Added physics tied to widgets example\n - Added basic joint example\n - Add generics passed to HasGameRef from PositionBodyComponent and SpriteBodyComponent\n\n## [0.8.0-releasecandidate.13]\n - Update mechanism by which `BodyComponent`'s are disposed to use the `onRemove` method\n - Flip y-axis after translation of body position, so that normal flame components can be children\n - Update to Forge2D 0.8.0\n - Update to Flame 1.0.0-releasecandidate.13\n - Rename `prepareCanvas` to `preRender`\n\n## [0.7.3-releasecandidate.12]\n - Fix prepareCanvas type error\n\n## [0.7.2-releasecandidate.12]\n - Update to Forge2D 0.7.2\n - Update to Flame 1.0.0-releasecandidate.12\n - Use `Camera` from Flame instead of the old internal viewport module\n\n## [0.7.1-rc8]\n - Take viewport yFlip into consideration on `cameraFollow`\n - Update to forge2d 0.7.1\n - Update to flame 1.0.0-rc8\n - Integrate Flame viewport and camera to replace own viewport\n\n## [0.6.4-rc4]\n - Add PositionBodyComponent, keeps a PositionComponent on top of a BodyComponent\n - Update to forge2d 0.6.4\n\n## [0.6.3-rc4]\n - Renamed the `prepare` method to `add` to be more inline with Flame Game naming.\n\n## [0.6.3-rc3]\n - Added an example for MouseJoint\n - Update to forge2d 0.6.3\n\n## [0.6.2-rc3]\n - BodyComponent should follow Forge2D game debug mode\n - Fix generics for BodyComponent\n - Align with Flame 1.0.0-rc6\n - Update to forge2d 0.6.2\n\n## [0.6.0-rc2]\n - Align with Flame 1.0.0-rc5\n\n## [0.6.0-rc1]\n - Align with Flame 1.0.0-rc4\n - Align with Forge2D 0.6.0\n\n## [0.5.0-rc1]\n - Initial move of box2d related files\n - Move over to refactored box2d\n\n## [0.0.1-rc1]\n - Empty release; in the future all flame box2d related code will live here.\n"
  },
  {
    "path": "packages/flame_forge2d/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_forge2d/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://github.com/flame-engine/forge2d\">\n    <img alt=\"flame\" width=\"300px\" src=\"https://raw.githubusercontent.com/flame-engine/forge2d/main/design/with-text.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nAdds support for <a href=\"https://github.com/flame-engine/forge2d\">Forge2d</a>, the Box2d-based physics engine, to your <a href=\"https://github.com/flame-engine/flame\">Flame</a> games.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_forge2d\" ><img src=\"https://img.shields.io/pub/v/flame_forge2d.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n\n# flame_forge2d\n\nThis library acts as a bridge between [Forge2D](https://github.com/flame-engine/forge2d) (our port\nof Box2D) and the Flame game engine.\n\n\n## Installation\n\nCheck [pub.dev](https://pub.dev/packages/flame_forge2d/install) for the latest version, and also\nremember to add the latest version of [Flame](https://pub.dev/packages/flame/install) to your\n`pubspec.yaml` file.\n\n\n## Examples\n\nIn the example folder of this directory you can find some\n[examples](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/bridge_libraries/flame_forge2d),\nand you can also find some examples in the\n[Forge2D repository](https://github.com/flame-engine/forge2d/tree/main/packages/forge2d/example).\n\n\n## Documentation\n\nSome more documentation can be found\n[here](https://docs.flame-engine.org/main/bridge_packages/flame_forge2d/flame_forge2d.html).\n\n"
  },
  {
    "path": "packages/flame_forge2d/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_forge2d/example/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\n/build/\n\n# Web related\nlib/generated_plugin_registrant.dart\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Exceptions to above rules.\n!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages\n"
  },
  {
    "path": "packages/flame_forge2d/example/README.md",
    "content": "# Forge2D Samples\n\nA dashbook showcasing some samples of how to use Forge2D (box2d) together with Flame.\n"
  },
  {
    "path": "packages/flame_forge2d/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_forge2d/example/lib/main.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\nimport 'package:flutter/widgets.dart';\n\nvoid main() {\n  runApp(const GameWidget.controlled(gameFactory: Forge2DExample.new));\n}\n\nclass Forge2DExample extends Forge2DGame {\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n\n    camera.viewport.add(FpsTextComponent());\n    world.add(Ball());\n    world.addAll(createBoundaries());\n  }\n\n  List<Component> createBoundaries() {\n    final visibleRect = camera.visibleWorldRect;\n    final topLeft = visibleRect.topLeft.toVector2();\n    final topRight = visibleRect.topRight.toVector2();\n    final bottomRight = visibleRect.bottomRight.toVector2();\n    final bottomLeft = visibleRect.bottomLeft.toVector2();\n\n    return [\n      Wall(topLeft, topRight),\n      Wall(topRight, bottomRight),\n      Wall(bottomLeft, bottomRight),\n      Wall(topLeft, bottomLeft),\n    ];\n  }\n}\n\nclass Ball extends BodyComponent with TapCallbacks {\n  Ball({Vector2? initialPosition})\n    : super(\n        fixtureDefs: [\n          FixtureDef(\n            CircleShape()..radius = 5,\n            restitution: 0.8,\n            friction: 0.4,\n          ),\n        ],\n        bodyDef: BodyDef(\n          angularDamping: 0.8,\n          position: initialPosition ?? Vector2.zero(),\n          type: BodyType.dynamic,\n        ),\n      );\n\n  @override\n  void onTapDown(_) {\n    body.applyLinearImpulse(Vector2.random() * 5000);\n  }\n}\n\nclass Wall extends BodyComponent {\n  final Vector2 _start;\n  final Vector2 _end;\n\n  Wall(this._start, this._end);\n\n  @override\n  Body createBody() {\n    final shape = EdgeShape()..set(_start, _end);\n    final fixtureDef = FixtureDef(shape, friction: 0.3);\n    final bodyDef = BodyDef(\n      position: Vector2.zero(),\n    );\n\n    return world.createBody(bodyDef)..createFixture(fixtureDef);\n  }\n}\n"
  },
  {
    "path": "packages/flame_forge2d/example/pubspec.yaml",
    "content": "name: flame_forge2d_example\nresolution: workspace\ndescription: Flame Forge2D (box2d) samples\n\npublish_to: 'none'\n\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_forge2d: ^0.19.2+5\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n\nflutter:\n  uses-material-design: true\n"
  },
  {
    "path": "packages/flame_forge2d/lib/body_component.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart' hide World;\nimport 'package:flame/effects.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\nimport 'package:flutter/foundation.dart';\n\n/// Since a pure BodyComponent doesn't have anything drawn on top of it,\n/// it is a good idea to turn on [debugMode] for it so that the bodies can be\n/// seen\n///\n/// You can use the optional [bodyDef] and [fixtureDefs] arguments to create\n/// the [BodyComponent]'s body without having to create the definitions within\n/// the component.\nclass BodyComponent<T extends Forge2DGame> extends Component\n    with HasGameReference<T>, HasPaint\n    implements\n        CoordinateTransform,\n        ReadOnlyPositionProvider,\n        ReadOnlyAngleProvider {\n  BodyComponent({\n    Paint? paint,\n    super.children,\n    super.priority,\n    this.renderBody = true,\n    this.bodyDef,\n    this.fixtureDefs,\n    super.key,\n  }) {\n    this.paint = paint ?? (Paint()..color = defaultColor);\n  }\n\n  static const defaultColor = Color.fromARGB(255, 255, 255, 255);\n  late Body body;\n\n  /// The default implementation of [createBody] will use this value to create\n  /// the [Body], if it is provided.\n  ///\n  /// If you do not provide a [BodyDef] here, you must override [createBody].\n  BodyDef? bodyDef;\n\n  /// The default implementation of [createBody] will add these fixtures to the\n  /// [Body] that it creates from [bodyDef].\n  List<FixtureDef>? fixtureDefs;\n\n  @override\n  Vector2 get position => body.position;\n\n  /// Specifies if the body's fixtures should be rendered.\n  ///\n  /// [renderBody] is true by default for [BodyComponent], if set to false\n  /// the body's fixtures wont be rendered.\n  ///\n  /// If you render something on top of the [BodyComponent], or doesn't want it\n  /// to be seen, you probably want to set it to false.\n  bool renderBody;\n\n  /// You should create the Forge2D [Body] in this method when you extend\n  /// the BodyComponent.\n  Body createBody() {\n    assert(\n      bodyDef != null,\n      'Ensure this.bodyDef is not null or override createBody',\n    );\n    final body = world.createBody(bodyDef!);\n    fixtureDefs?.forEach(body.createFixture);\n    return body;\n  }\n\n  @mustCallSuper\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    world = game.world;\n    body = createBody();\n  }\n\n  @override\n  void onMount() {\n    super.onMount();\n    world = game.world;\n  }\n\n  late Forge2DWorld world;\n  CameraComponent get camera => game.camera;\n  Vector2 get center => body.worldCenter;\n\n  @override\n  double get angle => body.angle;\n\n  /// The matrix used for preparing the canvas\n  final Transform2D _transform = Transform2D();\n  Matrix4 get _transformMatrix => _transform.transformMatrix;\n  double? _lastAngle;\n\n  @mustCallSuper\n  @override\n  void renderTree(Canvas canvas) {\n    final matrix = _transformMatrix;\n    if (matrix.m41 != body.position.x ||\n        matrix.m42 != body.position.y ||\n        _lastAngle != angle) {\n      matrix.setIdentity();\n      matrix.translateByDouble(body.position.x, body.position.y, 0.0, 1.0);\n      matrix.rotateZ(angle);\n      _lastAngle = angle;\n    }\n    canvas.save();\n    canvas.transform32(matrix.storage);\n    super.renderTree(canvas);\n    canvas.restore();\n  }\n\n  @override\n  void render(Canvas canvas) {\n    if (renderBody) {\n      for (final fixture in body.fixtures) {\n        renderFixture(canvas, fixture);\n      }\n    }\n  }\n\n  @override\n  void renderDebugMode(Canvas canvas) {\n    body.fixtures.forEach(\n      (fixture) => renderFixture(canvas, fixture),\n    );\n  }\n\n  /// Renders a [Fixture] in a [Canvas].\n  ///\n  /// Called for each fixture in [body] when [render]ing. Override this method\n  /// to customize how fixtures are rendered. For example, you can filter out\n  /// fixtures that you don't want to render.\n  ///\n  /// **NOTE**: If [renderBody] is false, no fixtures will be rendered. Hence,\n  /// [renderFixture] is not called when [render]ing.\n  void renderFixture(Canvas canvas, Fixture fixture) {\n    canvas.save();\n    switch (fixture.type) {\n      case ShapeType.chain:\n        _renderChain(canvas, fixture);\n      case ShapeType.circle:\n        _renderCircle(canvas, fixture);\n      case ShapeType.edge:\n        _renderEdge(canvas, fixture);\n      case ShapeType.polygon:\n        _renderPolygon(canvas, fixture);\n    }\n    canvas.restore();\n  }\n\n  void _renderChain(Canvas canvas, Fixture fixture) {\n    final chainShape = fixture.shape as ChainShape;\n    renderChain(\n      canvas,\n      chainShape.vertices.map((v) => v.toOffset()).toList(growable: false),\n    );\n  }\n\n  void renderChain(Canvas canvas, List<Offset> points) {\n    canvas.drawPoints(PointMode.polygon, points, paint);\n  }\n\n  void _renderCircle(Canvas canvas, Fixture fixture) {\n    final circle = fixture.shape as CircleShape;\n    renderCircle(canvas, circle.position.toOffset(), circle.radius);\n  }\n\n  void renderCircle(Canvas canvas, Offset center, double radius) {\n    canvas.drawCircle(center, radius, paint);\n  }\n\n  void _renderPolygon(Canvas canvas, Fixture fixture) {\n    final polygon = fixture.shape as PolygonShape;\n    renderPolygon(\n      canvas,\n      polygon.vertices.map((v) => v.toOffset()).toList(growable: false),\n    );\n  }\n\n  late final Path _path = Path();\n\n  void renderPolygon(Canvas canvas, List<Offset> points) {\n    final path = _path\n      ..reset()\n      ..addPolygon(points, true);\n    // TODO(Spydon): Use drawVertices instead.\n    canvas.drawPath(path, paint);\n  }\n\n  void _renderEdge(Canvas canvas, Fixture fixture) {\n    final edge = fixture.shape as EdgeShape;\n    renderEdge(canvas, edge.vertex1.toOffset(), edge.vertex2.toOffset());\n  }\n\n  void renderEdge(Canvas canvas, Offset p1, Offset p2) {\n    canvas.drawLine(p1, p2, paint);\n  }\n\n  @override\n  Vector2 parentToLocal(Vector2 point) => _transform.globalToLocal(point);\n\n  @override\n  Vector2 localToParent(Vector2 point) => _transform.localToGlobal(point);\n\n  late final Vector2 _hitTestPoint = Vector2.zero();\n\n  @override\n  bool containsLocalPoint(Vector2 point) {\n    _transform.localToGlobal(point, output: _hitTestPoint);\n    return body.fixtures.any((fixture) => fixture.testPoint(_hitTestPoint));\n  }\n\n  @override\n  bool containsPoint(Vector2 point) {\n    return body.fixtures.any((fixture) => fixture.testPoint(point));\n  }\n\n  @override\n  void onRemove() {\n    if (!world.isRemoving || world.destroyBodiesOnRemove) {\n      world.destroyBody(body);\n    }\n    super.onRemove();\n  }\n}\n"
  },
  {
    "path": "packages/flame_forge2d/lib/contact_callbacks.dart",
    "content": "import 'package:flame_forge2d/flame_forge2d.dart';\n\n/// Used to listen to a [BodyComponent]'s contact events.\n///\n/// Contact events occur whenever two [Fixture] meet each other.\n///\n/// To react to contacts you should assign [ContactCallbacks] to a [Body]'s\n/// userData or/and to a [Fixture]'s userData.\n/// {@macro flame_forge2d.world_contact_listener.algorithm}\nmixin class ContactCallbacks {\n  /// Called when two [Fixture]s start being in contact.\n  ///\n  /// It is called for sensors and non-sensors.\n  void beginContact(Object other, Contact contact) {\n    onBeginContact?.call(other, contact);\n  }\n\n  /// Called when two [Fixture]s cease being in contact.\n  ///\n  /// It is called for sensors and non-sensors.\n  void endContact(Object other, Contact contact) {\n    onEndContact?.call(other, contact);\n  }\n\n  /// Called after collision detection, but before collision resolution.\n  ///\n  /// This gives you a chance to disable the [Contact] based on the current\n  /// configuration.\n  /// Sensors do not create [Manifold]s.\n  void preSolve(Object other, Contact contact, Manifold oldManifold) {\n    onPreSolve?.call(other, contact, oldManifold);\n  }\n\n  /// Called after collision resolution.\n  ///\n  /// Usually defined to gather collision impulse results.\n  /// If one of the colliding objects is a sensor, this will not be called.\n  void postSolve(Object other, Contact contact, ContactImpulse impulse) {\n    onPostSolve?.call(other, contact, impulse);\n  }\n\n  void Function(Object other, Contact contact)? onBeginContact;\n\n  void Function(Object other, Contact contact)? onEndContact;\n\n  void Function(Object other, Contact contact, Manifold oldManifold)?\n  onPreSolve;\n\n  void Function(Object other, Contact contact, ContactImpulse impulse)?\n  onPostSolve;\n}\n"
  },
  {
    "path": "packages/flame_forge2d/lib/flame_forge2d.dart",
    "content": "library flame_forge2d;\n\nexport 'package:forge2d/forge2d.dart';\n\nexport 'body_component.dart';\nexport 'contact_callbacks.dart';\nexport 'forge2d_game.dart';\nexport 'forge2d_world.dart';\nexport 'world_contact_listener.dart';\n"
  },
  {
    "path": "packages/flame_forge2d/lib/forge2d_game.dart",
    "content": "import 'package:flame/camera.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_forge2d/forge2d_world.dart';\nimport 'package:forge2d/forge2d.dart';\n\n/// The base game class for creating games that uses the Forge2D physics engine.\nclass Forge2DGame<T extends Forge2DWorld> extends FlameGame<T> {\n  Forge2DGame({\n    Forge2DWorld? world,\n    CameraComponent? camera,\n    Vector2? gravity,\n    ContactListener? contactListener,\n    double zoom = 10,\n  }) : super(\n         world:\n             ((world?..gravity = gravity ?? world.gravity) ??\n                     Forge2DWorld(\n                       gravity: gravity,\n                       contactListener: contactListener,\n                     ))\n                 as T,\n         camera: (camera ?? CameraComponent())..viewfinder.zoom = zoom,\n       );\n\n  /// Takes a point in world coordinates and returns it in screen coordinates.\n  Vector2 worldToScreen(Vector2 position) {\n    return camera.localToGlobal(position);\n  }\n\n  /// Takes a point in screen coordinates and returns it in world coordinates.\n  ///\n  /// Remember that if you are using this for your events you can most of the\n  /// time just use `event.localPosition` directly instead.\n  Vector2 screenToWorld(Vector2 position) {\n    return camera.globalToLocal(position);\n  }\n}\n"
  },
  {
    "path": "packages/flame_forge2d/lib/forge2d_world.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart' hide World;\nimport 'package:forge2d/forge2d.dart' as forge2d;\n\n/// The root component when using [Forge2DGame], can handle both\n/// [BodyComponent]s and normal Flame components.\n///\n/// Wraps the world class that comes from Forge2D ([forge2d.World]).\nclass Forge2DWorld extends World {\n  Forge2DWorld({\n    Vector2? gravity,\n    forge2d.ContactListener? contactListener,\n    super.children,\n  }) : physicsWorld = forge2d.World(gravity ?? defaultGravity)\n         ..setContactListener(contactListener ?? WorldContactListener());\n\n  static final Vector2 defaultGravity = Vector2(0, 10.0);\n\n  final forge2d.World physicsWorld;\n\n  /// If true, all bodies will be destroyed when the world is removed from\n  /// the component tree.\n  /// Set this to false if you want to keep the bodies state for later, if\n  /// you for example plan to add the world back to the component tree.\n  bool destroyBodiesOnRemove = true;\n\n  @override\n  void update(double dt) {\n    physicsWorld.stepDt(dt);\n  }\n\n  Body createBody(BodyDef def) {\n    return physicsWorld.createBody(def);\n  }\n\n  void destroyBody(Body body) {\n    physicsWorld.destroyBody(body);\n  }\n\n  void createJoint(forge2d.Joint joint) {\n    physicsWorld.createJoint(joint);\n  }\n\n  void destroyJoint(forge2d.Joint joint) {\n    physicsWorld.destroyJoint(joint);\n  }\n\n  void raycast(RayCastCallback callback, Vector2 point1, Vector2 point2) {\n    physicsWorld.raycast(callback, point1, point2);\n  }\n\n  void clearForces() {\n    physicsWorld.clearForces();\n  }\n\n  void queryAABB(forge2d.QueryCallback callback, AABB aabb) {\n    physicsWorld.queryAABB(callback, aabb);\n  }\n\n  void raycastParticle(\n    forge2d.ParticleRaycastCallback callback,\n    Vector2 point1,\n    Vector2 point2,\n  ) {\n    physicsWorld.particleSystem.raycast(callback, point1, point2);\n  }\n\n  /// Don't change the gravity object directly, use the setter instead.\n  Vector2 get gravity => physicsWorld.gravity;\n\n  /// Sets the gravity of the world and wakes up all bodies.\n  set gravity(Vector2? gravity) {\n    physicsWorld.gravity = gravity ?? defaultGravity;\n    for (final body in physicsWorld.bodies) {\n      body.setAwake(true);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_forge2d/lib/world_contact_listener.dart",
    "content": "import 'package:flame_forge2d/flame_forge2d.dart';\n\n/// Listens to the entire [World]'s contact events.\n///\n/// It propagates the contact events ([beginContact], [endContact], [preSolve],\n/// [postSolve]) to [ContactCallbacks]s when a [Body] or at least one of its\n// fixtures' `userData` is set to a [ContactCallbacks].\n///\n/// {@template flame_forge2d.world_contact_listener.algorithm}\n/// If the [Body] `userData` is set to a [ContactCallbacks] the contact events\n/// of this will be called when any [Body]'s fixture contacts another [Fixture].\n///\n/// If instead you wish to be more specific and only trigger contact events\n/// when a specific [Body]'s fixture contacts another [Fixture], you can\n/// set the fixture `userData` to a [ContactCallbacks].\n///\n/// If the colliding [Fixture] `userData` and [Body] `userData` are `null`, then\n/// the contact events are not called.\n///\n/// The described behavior is a simple out of the box solution to propagate\n/// contact events. If you wish to implement your own logic you can subclass\n/// [ContactListener] and provide it to your [Forge2DGame].\n/// {@endtemplate}\nclass WorldContactListener extends ContactListener {\n  void _callback(\n    Contact contact,\n    void Function(ContactCallbacks contactCallback, Object other) callback,\n  ) {\n    final userData = {\n      contact.bodyA.userData,\n      contact.fixtureA.userData,\n      contact.bodyB.userData,\n      contact.fixtureB.userData,\n    }.whereType<Object>();\n\n    for (final contactCallback in userData.whereType<ContactCallbacks>()) {\n      for (final object in userData) {\n        if (object != contactCallback) {\n          callback(contactCallback, object);\n        }\n      }\n    }\n  }\n\n  @override\n  void beginContact(Contact contact) {\n    _callback(\n      contact,\n      (contactCallback, other) => contactCallback.beginContact(other, contact),\n    );\n  }\n\n  @override\n  void endContact(Contact contact) {\n    _callback(\n      contact,\n      (contactCallback, other) => contactCallback.endContact(other, contact),\n    );\n  }\n\n  @override\n  void preSolve(Contact contact, Manifold oldManifold) {\n    _callback(\n      contact,\n      (contactCallback, other) =>\n          contactCallback.preSolve(other, contact, oldManifold),\n    );\n  }\n\n  @override\n  void postSolve(Contact contact, ContactImpulse impulse) {\n    _callback(\n      contact,\n      (contactCallback, other) =>\n          contactCallback.postSolve(other, contact, impulse),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_forge2d/pubspec.yaml",
    "content": "name: flame_forge2d\nresolution: workspace\ndescription: Forge2D (Box2D) support for the Flame game engine. This uses the forge2d package and provides wrappers and components to be used inside Flame.\nversion: 0.19.2+5\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_forge2d\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - forge2d\n  - physics\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  forge2d: ^0.14.2\n\ndev_dependencies:\n  dartdoc: ^9.0.0\n  flame_lint: ^1.4.3\n  flame_test: ^2.2.3\n  flutter_test:\n    sdk: flutter\n  mocktail: ^1.0.4\n  test: any\n"
  },
  {
    "path": "packages/flame_forge2d/test/body_component_test.dart",
    "content": "// ignore_for_file: invalid_use_of_internal_member\n\nimport 'dart:math';\n\nimport 'package:flame/components.dart'\n    show Anchor, ComponentKey, PositionComponent;\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame_forge2d/flame_forge2d.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nimport 'helpers/mocks.dart';\n\nclass _TestBodyComponent extends BodyComponent with TapCallbacks {\n  int tapCount = 0;\n\n  @override\n  Body createBody() => body;\n\n  @override\n  void noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);\n\n  @override\n  void onTapDown(TapDownEvent _) {\n    tapCount++;\n  }\n}\n\nclass _MockCanvas extends Mock implements Canvas {}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('BodyComponent', () {\n    group('renderBody', () {\n      test('is true by default', () {\n        final component = _TestBodyComponent();\n        expect(component.renderBody, isTrue);\n      });\n\n      test('sets and gets', () {\n        final component = _TestBodyComponent()..renderBody = false;\n        expect(component.renderBody, isFalse);\n      });\n    });\n\n    group('render', () {\n      group('draws correctly', () {\n        String goldenPath(String name) => 'goldens/body_component/$name.png';\n\n        final flameTester = FlameTester(Forge2DGame.new);\n        final testPaint = Paint()..color = const Color(0xffff0000);\n\n        flameTester.testGameWidget(\n          'a CircleShape',\n          setUp: (game, tester) async {\n            final body = game.world.createBody(BodyDef());\n            final shape = CircleShape()..radius = 5;\n            body.createFixture(FixtureDef(shape));\n\n            final component = _TestBodyComponent()\n              ..body = body\n              ..paint = testPaint;\n            await game.world.add(component);\n\n            game.camera.follow(component);\n          },\n        );\n\n        flameTester.testGameWidget(\n          'an EdgeShape',\n          setUp: (game, tester) async {\n            final body = game.world.createBody(BodyDef());\n            final shape = EdgeShape()\n              ..set(\n                Vector2.zero(),\n                Vector2.all(10),\n              );\n            body.createFixture(FixtureDef(shape));\n\n            final component = _TestBodyComponent()\n              ..body = body\n              ..paint = testPaint;\n            await game.world.add(component);\n\n            game.camera.follow(component);\n          },\n          verify: (game, tester) async {\n            await expectLater(\n              find.byGame<Forge2DGame>(),\n              matchesGoldenFile(goldenPath('edge_shape')),\n            );\n          },\n        );\n\n        flameTester.testGameWidget(\n          'a PolygonShape',\n          setUp: (game, tester) async {\n            final body = game.world.createBody(BodyDef());\n            final shape = PolygonShape()\n              ..set(\n                [\n                  Vector2.zero(),\n                  Vector2.all(10),\n                  Vector2(0, 10),\n                ],\n              );\n            body.createFixture(FixtureDef(shape));\n\n            final component = _TestBodyComponent()\n              ..body = body\n              ..paint = testPaint;\n            await game.world.add(component);\n\n            game.camera.follow(component);\n\n            // a PolygonShape contains point\n            expect(component.containsPoint(Vector2.all(10)), isTrue);\n          },\n          verify: (game, tester) async {\n            await expectLater(\n              find.byGame<Forge2DGame>(),\n              matchesGoldenFile(goldenPath('polygon_shape')),\n            );\n          },\n        );\n\n        flameTester.testGameWidget(\n          'an open ChainShape',\n          setUp: (game, tester) async {\n            final body = game.world.createBody(BodyDef());\n            final shape = ChainShape()\n              ..createChain(\n                [\n                  Vector2.zero(),\n                  Vector2.all(10),\n                  Vector2(10, 0),\n                ],\n              );\n            body.createFixture(FixtureDef(shape));\n\n            final component = _TestBodyComponent()\n              ..body = body\n              ..paint = testPaint;\n            await game.world.add(component);\n\n            game.camera.follow(component);\n          },\n          verify: (game, tester) async {\n            await expectLater(\n              find.byGame<Forge2DGame>(),\n              matchesGoldenFile(goldenPath('chain_shape_open')),\n            );\n          },\n        );\n\n        flameTester.testGameWidget(\n          'a closed ChainShape',\n          setUp: (game, tester) async {\n            final body = game.world.createBody(BodyDef());\n            final shape = ChainShape()\n              ..createLoop(\n                [\n                  Vector2.zero(),\n                  Vector2.all(10),\n                  Vector2(10, 0),\n                ],\n              );\n            body.createFixture(FixtureDef(shape));\n\n            final component = _TestBodyComponent()\n              ..body = body\n              ..paint = testPaint;\n            await game.world.add(component);\n\n            game.camera.follow(component);\n          },\n          verify: (game, tester) async {\n            await expectLater(\n              find.byGame<Forge2DGame>(),\n              matchesGoldenFile(goldenPath('chain_shape_closed')),\n            );\n          },\n        );\n      });\n    });\n\n    group('renderFixture', () {\n      group('returns normally', () {\n        late Canvas canvas;\n        late Body body;\n\n        setUp(() {\n          canvas = _MockCanvas();\n          final world = World();\n          body = world.createBody(BodyDef());\n        });\n\n        test('when rendering a CircleShape', () {\n          final component = _TestBodyComponent();\n          final shape = CircleShape()..radius = 5;\n          final fixture = body.createFixture(\n            FixtureDef(shape),\n          );\n\n          expect(\n            () => component.renderFixture(canvas, fixture),\n            returnsNormally,\n          );\n        });\n\n        test('when rendering an EdgeShape', () {\n          final component = _TestBodyComponent();\n          final shape = EdgeShape()\n            ..set(\n              Vector2.zero(),\n              Vector2.all(10),\n            );\n          final fixture = body.createFixture(\n            FixtureDef(shape),\n          );\n\n          expect(\n            () => component.renderFixture(canvas, fixture),\n            returnsNormally,\n          );\n        });\n\n        test('when rendering a PolygonShape', () {\n          final component = _TestBodyComponent();\n          final shape = PolygonShape()\n            ..set(\n              [\n                Vector2.zero(),\n                Vector2.all(10),\n                Vector2(0, 10),\n              ],\n            );\n          final fixture = body.createFixture(\n            FixtureDef(shape),\n          );\n\n          expect(\n            () => component.renderFixture(canvas, fixture),\n            returnsNormally,\n          );\n        });\n\n        test('when rendering a ChainShape', () {\n          final component = _TestBodyComponent();\n          final shape = ChainShape()\n            ..createChain(\n              [\n                Vector2.zero(),\n                Vector2.all(10),\n                Vector2(10, 0),\n              ],\n            );\n          final fixture = body.createFixture(\n            FixtureDef(shape),\n          );\n\n          expect(\n            () => component.renderFixture(canvas, fixture),\n            returnsNormally,\n          );\n        });\n      });\n    });\n\n    group('Add component to parent', () {\n      final flameTester = FlameTester(Forge2DGame.new);\n      final testPaint = Paint()..color = const Color(0xffff0000);\n\n      flameTester.testGameWidget(\n        'add and remove child to BodyComponent',\n        setUp: (game, tester) async {\n          final bodyDef = BodyDef();\n          final body = game.world.createBody(bodyDef);\n          final shape = PolygonShape()\n            ..set(\n              [\n                Vector2.zero(),\n                Vector2.all(10),\n                Vector2(0, 10),\n              ],\n            );\n          body.createFixture(FixtureDef(shape));\n\n          final component = _TestBodyComponent()\n            ..body = body\n            ..paint = testPaint;\n\n          game.world.add(component);\n          await game.ready();\n\n          expect(game.world.contains(component), true);\n          expect(component.isMounted, true);\n          expect(game.world.children.length, 1);\n          component.removeFromParent();\n          await game.ready();\n\n          expect(component.isMounted, false);\n          expect(component.isLoaded, true);\n          expect(game.world.children.length, 0);\n        },\n      );\n    });\n\n    group('BodyComponent contact events', () {\n      test('beginContact called', () {\n        final contactCallback = MockContactCallback();\n        final contact = MockContact();\n        final bodyA = MockBody()..angularDamping = 1.0;\n        final fixtureA = MockFixture();\n        when(() => bodyA.userData).thenReturn(contactCallback);\n        when(() => fixtureA.userData).thenReturn(Object());\n        contactCallback.beginContact(fixtureA.userData!, contact);\n\n        verify(\n          () => contactCallback.beginContact(fixtureA.userData!, contact),\n        ).called(1);\n      });\n    });\n\n    group('PositionComponent parented by BodyComponent', () {\n      final flameTester = FlameTester(Forge2DGame.new);\n\n      flameTester.testGameWidget(\n        'absoluteAngle',\n        setUp: (game, tester) async {\n          // Creates a body with an angle of 2 radians\n          final body = game.world.createBody(BodyDef(angle: 2.0));\n          final shape = EdgeShape()\n            ..set(\n              Vector2.zero(),\n              Vector2.all(10),\n            );\n          body.createFixture(FixtureDef(shape));\n          final bodyComponent = _TestBodyComponent()..body = body;\n\n          // Creates a positional component with an angle of 1 radians\n          final positionComponent = PositionComponent(angle: 1.0);\n\n          // Creates a hierarchy: game > bodyComponent > positionComponent\n          game.world.add(bodyComponent);\n          bodyComponent.add(positionComponent);\n\n          await game.ready();\n\n          // Checks the hierarchy\n          expect(game.world.contains(bodyComponent), true);\n          expect(bodyComponent.contains(positionComponent), true);\n          expect(game.world.children.length, 1);\n          expect(bodyComponent.children.length, 1);\n          expect(positionComponent.children.length, 0);\n\n          // Expects the absolute angle to be (2 + 1) radians\n          expect(positionComponent.absoluteAngle, 3.0);\n        },\n      );\n    });\n\n    group('createBody', () {\n      test('should throw an error if bodyDef is null', () {\n        final bodyComponent = BodyComponent();\n        expect(bodyComponent.createBody, throwsAssertionError);\n      });\n\n      group('should create body', () {\n        final flameTester = FlameTester(Forge2DGame.new);\n\n        flameTester.testGameWidget(\n          'with no fixtures',\n          setUp: (game, tester) async {\n            final bodyComponent = BodyComponent(\n              bodyDef: BodyDef(position: Vector2(33, 44)),\n              key: ComponentKey.named('tested'),\n            );\n            game.world.add(bodyComponent);\n          },\n          verify: (game, tester) async {\n            expect(\n              game.findByKeyName<BodyComponent>('tested')!.body.position,\n              Vector2(33, 44),\n            );\n          },\n        );\n\n        flameTester.testGameWidget(\n          'with a set of fixtures',\n          setUp: (game, tester) async {\n            final bodyComponent = BodyComponent(\n              bodyDef: BodyDef(),\n              fixtureDefs: [\n                FixtureDef(CircleShape()..radius = 10),\n                FixtureDef(CircleShape()..radius = 20),\n                FixtureDef(CircleShape()..radius = 30),\n              ],\n              key: ComponentKey.named('tested'),\n            );\n            game.world.add(bodyComponent);\n          },\n          verify: (game, tester) async {\n            final bodyComponent = game.findByKeyName<BodyComponent>('tested')!;\n            expect(bodyComponent.body.fixtures[0].shape.radius, 10);\n            expect(bodyComponent.body.fixtures[1].shape.radius, 20);\n            expect(bodyComponent.body.fixtures[2].shape.radius, 30);\n          },\n        );\n      });\n    });\n\n    group('containsLocalPoint', () {\n      testWithGame('with rotation', Forge2DGame.new, (game) async {\n        game.camera.viewfinder.anchor = Anchor.topLeft;\n        final zoom = game.camera.viewfinder.zoom;\n        final position = Vector2.all(10);\n        final body = game.world.createBody(\n          BodyDef(position: position, angle: pi / 2),\n        );\n\n        body.createFixtureFromShape(\n          CircleShape()\n            ..radius = 1\n            ..position.setFrom(Vector2(3, 0)),\n        );\n        body.createFixtureFromShape(\n          CircleShape()\n            ..radius = 1\n            ..position.setFrom(Vector2(-3, 0)),\n        );\n        final component = _TestBodyComponent()..body = body;\n\n        await game.world.ensureAdd(component);\n        game.update(0);\n        final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;\n\n        tapDispatcher.handleTapDown(\n          1,\n          TapDownDetails(\n            globalPosition: position.toOffset() * zoom + Offset(0, 3 * zoom),\n          ),\n        );\n        expect(component.tapCount, 1);\n\n        tapDispatcher.handleTapDown(\n          1,\n          TapDownDetails(\n            globalPosition: position.toOffset() * zoom + Offset(3 * zoom, 0),\n          ),\n        );\n        expect(component.tapCount, 1);\n\n        tapDispatcher.handleTapDown(\n          1,\n          TapDownDetails(\n            globalPosition: position.toOffset() * zoom + Offset(0, -3 * zoom),\n          ),\n        );\n        expect(component.tapCount, 2);\n\n        tapDispatcher.handleTapDown(\n          1,\n          TapDownDetails(\n            globalPosition: position.toOffset() * zoom + Offset(-3 * zoom, 0),\n          ),\n        );\n        expect(component.tapCount, 2);\n\n        tapDispatcher.handleTapDown(\n          1,\n          TapDownDetails(\n            globalPosition: position.toOffset() * zoom,\n          ),\n        );\n        expect(component.tapCount, 2);\n      });\n    });\n\n    testWithGame(\n      'BodyComponent.world consistency in onRemove',\n      Forge2DGame.new,\n      (game) async {\n        final bodyDef = BodyDef();\n        final body = game.world.createBody(bodyDef);\n        final shape = CircleShape()..radius = 5;\n        body.createFixture(FixtureDef(shape));\n\n        final component = _ConsistentBodyComponent(bodyDef: bodyDef);\n        await game.world.add(component);\n        await game.ready();\n        component.removeFromParent();\n        await game.ready();\n\n        // Verify that the world is the same in onMount and onRemove\n        expect(component.onMountWorld, equals(component.onRemoveWorld));\n      },\n    );\n\n    testWithGame(\n      'BodyComponent.world consistency in onRemove with world change',\n      Forge2DGame.new,\n      (game) async {\n        final bodyDef = BodyDef();\n        final body = game.world.createBody(bodyDef);\n        final shape = CircleShape()..radius = 5;\n        body.createFixture(FixtureDef(shape));\n\n        final component = _ConsistentBodyComponent(bodyDef: bodyDef);\n        await game.world.add(component);\n        await game.ready();\n        game.world = Forge2DWorld();\n        await game.ready();\n\n        // Verify that the world is the same in onMount and onRemove\n        expect(component.onMountWorld, equals(component.onRemoveWorld));\n      },\n    );\n  });\n}\n\nclass _ConsistentBodyComponent extends BodyComponent {\n  _ConsistentBodyComponent({super.bodyDef});\n\n  Forge2DWorld? onMountWorld;\n  Forge2DWorld? onRemoveWorld;\n\n  @override\n  void onMount() {\n    super.onMount();\n    onMountWorld = world;\n  }\n\n  @override\n  void onRemove() {\n    super.onRemove();\n    onRemoveWorld = world;\n  }\n}\n"
  },
  {
    "path": "packages/flame_forge2d/test/contact_callbacks_test.dart",
    "content": "import 'package:flame_forge2d/flame_forge2d.dart';\nimport 'package:test/expect.dart';\nimport 'package:test/scaffolding.dart';\n\nimport 'helpers/helpers.dart';\n\nvoid main() {\n  group('ContactCallbacks', () {\n    late Object other;\n    late Contact contact;\n    late Manifold manifold;\n    late ContactImpulse contactImpulse;\n\n    setUp(() {\n      other = Object();\n      contact = MockContact();\n      manifold = MockManifold();\n      contactImpulse = MockContactImpulse();\n    });\n\n    test('beginContact calls onBeginContact', () {\n      final contactCallbacks = ContactCallbacks();\n      var called = 0;\n      contactCallbacks.onBeginContact = (_, __) => called++;\n\n      contactCallbacks.beginContact(other, contact);\n\n      expect(called, equals(1));\n    });\n\n    test('endContact calls onEndContact', () {\n      final contactCallbacks = ContactCallbacks();\n      var called = 0;\n      contactCallbacks.onEndContact = (_, __) => called++;\n\n      contactCallbacks.endContact(other, contact);\n\n      expect(called, equals(1));\n    });\n\n    test('preSolve calls onPreSolve', () {\n      final contactCallbacks = ContactCallbacks();\n      var called = 0;\n      contactCallbacks.onPreSolve = (_, __, ___) => called++;\n\n      contactCallbacks.preSolve(other, contact, manifold);\n\n      expect(called, equals(1));\n    });\n\n    test('postSolve calls on postSolve', () {\n      final contactCallbacks = ContactCallbacks();\n      var called = 0;\n      contactCallbacks.onPostSolve = (_, __, ___) => called++;\n\n      contactCallbacks.postSolve(other, contact, contactImpulse);\n\n      expect(called, equals(1));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_forge2d/test/forge2d_game_test.dart",
    "content": "import 'package:flame_forge2d/flame_forge2d.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nclass _TestForge2dGame extends Forge2DGame {\n  _TestForge2dGame() : super(zoom: 4.0, gravity: Vector2(0, -10.0));\n}\n\nvoid main() {\n  group(\n    'Test corresponding position on screen and in the Forge2D world',\n    () {\n      testWithGame(\n        'Center positioned camera should be zero in world',\n        _TestForge2dGame.new,\n        (game) async {\n          final size = Vector2.all(100);\n          game.update(0);\n          game.onGameResize(size);\n          expect(\n            game.screenToWorld(size / 2),\n            Vector2.zero(),\n          );\n        },\n      );\n\n      testWithGame(\n        'Top left position should be converted correctly to world',\n        _TestForge2dGame.new,\n        (game) async {\n          final size = Vector2.all(100);\n          game.onGameResize(size);\n          expect(\n            game.screenToWorld(Vector2.zero()),\n            -(size / 2) / game.camera.viewfinder.zoom,\n          );\n        },\n      );\n\n      testWithGame(\n        'Non-zero position should be converted correctly to world',\n        _TestForge2dGame.new,\n        (game) async {\n          final size = Vector2.all(100);\n          final screenPosition = Vector2(10, 20);\n          game.onGameResize(size);\n          expect(\n            game.screenToWorld(screenPosition),\n            (-size / 2 + screenPosition) / game.camera.viewfinder.zoom,\n          );\n        },\n      );\n\n      testWithGame(\n        'Converts a vector in the world space to the screen space',\n        _TestForge2dGame.new,\n        (game) async {\n          final size = Vector2.all(100);\n          game.onGameResize(size);\n          expect(\n            game.worldToScreen(Vector2.zero()),\n            size / 2,\n          );\n        },\n      );\n\n      testWithGame(\n        'Converts a non-zero vector in the world space to the screen space',\n        _TestForge2dGame.new,\n        (game) async {\n          final size = Vector2.all(100);\n          final worldPosition = Vector2.all(10);\n          game.onGameResize(size);\n          expect(\n            game.worldToScreen(worldPosition),\n            (size / 2) + worldPosition * game.camera.viewfinder.zoom,\n          );\n        },\n      );\n\n      testWithGame(\n        'Converts worldToScreen correctly with moved viewfinder',\n        _TestForge2dGame.new,\n        (game) async {\n          final size = Vector2.all(100);\n          final worldPosition = Vector2(10, 30);\n          final viewfinderPosition = Vector2(20, 10);\n          game.onGameResize(size);\n          game.camera.viewfinder.position = viewfinderPosition;\n          expect(\n            game.worldToScreen(worldPosition),\n            (size / 2) +\n                (worldPosition - viewfinderPosition) *\n                    game.camera.viewfinder.zoom,\n          );\n        },\n      );\n\n      test(\"Game does not override World's gravity with null\", () {\n        final game = Forge2DGame(world: Forge2DWorld(gravity: Vector2(10, 0)));\n        expect(game.world.gravity.x, 10);\n        expect(game.world.gravity.y, 0);\n      });\n    },\n  );\n}\n"
  },
  {
    "path": "packages/flame_forge2d/test/forge2d_world_test.dart",
    "content": "import 'package:flame_forge2d/flame_forge2d.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nclass _TestForge2DWorld extends Forge2DWorld {\n  _TestForge2DWorld() : super(gravity: Vector2(0, 0));\n}\n\nvoid main() {\n  testWithGame(\n    'Bodies are destroyed after world is removed when destroyBodiesOnRemove is '\n    'true',\n    () => Forge2DGame(world: _TestForge2DWorld()),\n    (game) async {\n      await game.ready();\n      final bodyDef = BodyDef()..type = BodyType.dynamic;\n      final component = BodyComponent(bodyDef: bodyDef);\n      await game.world.ensureAdd(component);\n      final body = component.body;\n\n      game.world.removeFromParent();\n      await game.ready();\n      expect(game.world.physicsWorld.bodies, isNot(contains(body)));\n    },\n  );\n\n  testWithGame(\n    'Bodies are not destroyed after world is removed when '\n    'destroyBodiesOnRemove is false',\n    () => Forge2DGame(\n      world: _TestForge2DWorld()..destroyBodiesOnRemove = false,\n    ),\n    (game) async {\n      await game.ready();\n      final bodyDef = BodyDef()..type = BodyType.dynamic;\n      final component = BodyComponent(bodyDef: bodyDef);\n      await game.world.ensureAdd(component);\n      final body = component.body;\n\n      game.world.removeFromParent();\n      await game.ready();\n      expect(game.world.physicsWorld.bodies, contains(body));\n    },\n  );\n}\n"
  },
  {
    "path": "packages/flame_forge2d/test/helpers/helpers.dart",
    "content": "export 'mocks.dart';\n"
  },
  {
    "path": "packages/flame_forge2d/test/helpers/mocks.dart",
    "content": "import 'package:flame_forge2d/flame_forge2d.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass MockContactCallback extends Mock implements ContactCallbacks {}\n\nclass MockContact extends Mock implements Contact {}\n\nclass MockBody extends Mock implements Body {}\n\nclass MockFixture extends Mock implements Fixture {}\n\nclass MockManifold extends Mock implements Manifold {}\n\nclass MockContactImpulse extends Mock implements ContactImpulse {}\n"
  },
  {
    "path": "packages/flame_forge2d/test/world_contact_listener_test.dart",
    "content": "import 'package:flame_forge2d/flame_forge2d.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/scaffolding.dart';\n\nimport 'helpers/helpers.dart';\n\nvoid main() {\n  group(\n    'WorldContactListener',\n    () {\n      late ContactCallbacks contactCallback;\n      late Contact contact;\n      late Body bodyA;\n      late Body bodyB;\n      late Fixture fixtureA;\n      late Fixture fixtureB;\n\n      setUp(() {\n        contactCallback = MockContactCallback();\n        contact = MockContact();\n        bodyA = MockBody();\n        bodyB = MockBody();\n        fixtureA = MockFixture();\n        fixtureB = MockFixture();\n\n        when(() => contact.bodyA).thenReturn(bodyA);\n        when(() => contact.bodyB).thenReturn(bodyB);\n        when(() => contact.fixtureA).thenReturn(fixtureA);\n        when(() => contact.fixtureB).thenReturn(fixtureB);\n      });\n\n      setUpAll(() {\n        registerFallbackValue(Object());\n      });\n\n      group(\n        'beginContact',\n        () {\n          test(\n            \"doesn't callback if userData are null\",\n            () {\n              final contactListener = WorldContactListener();\n\n              when(() => bodyA.userData).thenReturn(contactCallback);\n              when(() => bodyB.userData).thenReturn(null);\n              when(() => fixtureA.userData).thenReturn(null);\n              when(() => fixtureB.userData).thenReturn(null);\n\n              contactListener.beginContact(contact);\n\n              verifyNever(\n                () => contactCallback.beginContact(any(), contact),\n              );\n            },\n          );\n\n          test(\n            'callbacks for userData when not null',\n            () {\n              final contactListener = WorldContactListener();\n\n              when(() => bodyA.userData).thenReturn(contactCallback);\n              when(() => bodyB.userData).thenReturn(Object());\n              when(() => fixtureA.userData).thenReturn(Object());\n              when(() => fixtureB.userData).thenReturn(Object());\n\n              contactListener.beginContact(contact);\n\n              verify(\n                () => contactCallback.beginContact(bodyB.userData!, contact),\n              ).called(1);\n              verify(\n                () => contactCallback.beginContact(fixtureA.userData!, contact),\n              ).called(1);\n              verify(\n                () => contactCallback.beginContact(fixtureB.userData!, contact),\n              ).called(1);\n              verify(\n                () => contactCallback.beginContact(any(), contact),\n              ).called(3);\n            },\n          );\n\n          test(\n            \"doesn't callback itself\",\n            () {\n              final contactListener = WorldContactListener();\n\n              when(() => bodyA.userData).thenReturn(contactCallback);\n              when(() => bodyB.userData).thenReturn(contactCallback);\n              when(() => fixtureA.userData).thenReturn(null);\n              when(() => fixtureB.userData).thenReturn(null);\n\n              contactListener.beginContact(contact);\n\n              verifyNever(\n                () => contactCallback.beginContact(any(), contact),\n              );\n            },\n          );\n        },\n      );\n\n      group(\n        'endContact',\n        () {\n          test(\n            \"doesn't callback if userData are null\",\n            () {\n              final contactListener = WorldContactListener();\n\n              when(() => bodyA.userData).thenReturn(contactCallback);\n              when(() => bodyB.userData).thenReturn(null);\n              when(() => fixtureA.userData).thenReturn(null);\n              when(() => fixtureB.userData).thenReturn(null);\n\n              contactListener.endContact(contact);\n\n              verifyNever(\n                () => contactCallback.endContact(any(), contact),\n              );\n            },\n          );\n\n          test(\n            'callbacks for userData when not null',\n            () {\n              final contactListener = WorldContactListener();\n\n              when(() => bodyA.userData).thenReturn(contactCallback);\n              when(() => bodyB.userData).thenReturn(Object());\n              when(() => fixtureA.userData).thenReturn(Object());\n              when(() => fixtureB.userData).thenReturn(Object());\n\n              contactListener.endContact(contact);\n\n              verify(\n                () => contactCallback.endContact(bodyB.userData!, contact),\n              ).called(1);\n              verify(\n                () => contactCallback.endContact(fixtureA.userData!, contact),\n              ).called(1);\n              verify(\n                () => contactCallback.endContact(fixtureB.userData!, contact),\n              ).called(1);\n              verify(\n                () => contactCallback.endContact(any(), contact),\n              ).called(3);\n            },\n          );\n\n          test(\n            \"doesn't callback itself\",\n            () {\n              final contactListener = WorldContactListener();\n\n              when(() => bodyA.userData).thenReturn(contactCallback);\n              when(() => bodyB.userData).thenReturn(contactCallback);\n              when(() => fixtureA.userData).thenReturn(null);\n              when(() => fixtureB.userData).thenReturn(null);\n\n              contactListener.endContact(contact);\n\n              verifyNever(\n                () => contactCallback.endContact(any(), contact),\n              );\n            },\n          );\n        },\n      );\n\n      group(\n        'preSolve',\n        () {\n          late Manifold manifold;\n\n          setUp(() {\n            manifold = MockManifold();\n          });\n\n          test(\n            \"doesn't callback if userData are null\",\n            () {\n              final contactListener = WorldContactListener();\n\n              when(() => bodyA.userData).thenReturn(contactCallback);\n              when(() => bodyB.userData).thenReturn(null);\n              when(() => fixtureA.userData).thenReturn(null);\n              when(() => fixtureB.userData).thenReturn(null);\n\n              contactListener.preSolve(contact, manifold);\n\n              verifyNever(\n                () => contactCallback.preSolve(any(), contact, manifold),\n              );\n            },\n          );\n\n          test(\n            'callbacks for userData when not null',\n            () {\n              final contactListener = WorldContactListener();\n\n              when(() => bodyA.userData).thenReturn(contactCallback);\n              when(() => bodyB.userData).thenReturn(Object());\n              when(() => fixtureA.userData).thenReturn(Object());\n              when(() => fixtureB.userData).thenReturn(Object());\n\n              contactListener.preSolve(contact, manifold);\n\n              verify(\n                () => contactCallback.preSolve(\n                  bodyB.userData!,\n                  contact,\n                  manifold,\n                ),\n              ).called(1);\n              verify(\n                () => contactCallback.preSolve(\n                  fixtureA.userData!,\n                  contact,\n                  manifold,\n                ),\n              ).called(1);\n              verify(\n                () => contactCallback.preSolve(\n                  fixtureB.userData!,\n                  contact,\n                  manifold,\n                ),\n              ).called(1);\n              verify(\n                () => contactCallback.preSolve(\n                  any(),\n                  contact,\n                  manifold,\n                ),\n              ).called(3);\n            },\n          );\n\n          test(\n            \"doesn't callback itself\",\n            () {\n              final contactListener = WorldContactListener();\n\n              when(() => bodyA.userData).thenReturn(contactCallback);\n              when(() => bodyB.userData).thenReturn(contactCallback);\n              when(() => fixtureA.userData).thenReturn(null);\n              when(() => fixtureB.userData).thenReturn(null);\n\n              contactListener.preSolve(contact, manifold);\n\n              verifyNever(\n                () => contactCallback.preSolve(any(), contact, manifold),\n              );\n            },\n          );\n        },\n      );\n\n      group(\n        'postSolve',\n        () {\n          late ContactImpulse contactImpulse;\n\n          setUp(() {\n            contactImpulse = MockContactImpulse();\n          });\n\n          test(\n            \"doesn't callback if userData are null\",\n            () {\n              final contactListener = WorldContactListener();\n\n              when(() => bodyA.userData).thenReturn(contactCallback);\n              when(() => bodyB.userData).thenReturn(null);\n              when(() => fixtureA.userData).thenReturn(null);\n              when(() => fixtureB.userData).thenReturn(null);\n\n              contactListener.postSolve(contact, contactImpulse);\n\n              verifyNever(\n                () => contactCallback.postSolve(any(), contact, contactImpulse),\n              );\n            },\n          );\n\n          test(\n            'callbacks for userData when not null',\n            () {\n              final contactListener = WorldContactListener();\n\n              when(() => bodyA.userData).thenReturn(contactCallback);\n              when(() => bodyB.userData).thenReturn(Object());\n              when(() => fixtureA.userData).thenReturn(Object());\n              when(() => fixtureB.userData).thenReturn(Object());\n\n              contactListener.postSolve(contact, contactImpulse);\n\n              verify(\n                () => contactCallback.postSolve(\n                  bodyB.userData!,\n                  contact,\n                  contactImpulse,\n                ),\n              ).called(1);\n              verify(\n                () => contactCallback.postSolve(\n                  fixtureA.userData!,\n                  contact,\n                  contactImpulse,\n                ),\n              ).called(1);\n              verify(\n                () => contactCallback.postSolve(\n                  fixtureB.userData!,\n                  contact,\n                  contactImpulse,\n                ),\n              ).called(1);\n              verify(\n                () => contactCallback.postSolve(\n                  any(),\n                  contact,\n                  contactImpulse,\n                ),\n              ).called(3);\n            },\n          );\n\n          test(\n            \"doesn't callback itself\",\n            () {\n              final contactListener = WorldContactListener();\n\n              when(() => bodyA.userData).thenReturn(contactCallback);\n              when(() => bodyB.userData).thenReturn(contactCallback);\n              when(() => fixtureA.userData).thenReturn(null);\n              when(() => fixtureB.userData).thenReturn(null);\n\n              contactListener.postSolve(contact, contactImpulse);\n\n              verifyNever(\n                () => contactCallback.postSolve(any(), contact, contactImpulse),\n              );\n            },\n          );\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "packages/flame_isolate/CHANGELOG.md",
    "content": "## 0.6.2+21\n\n - **FIX**: Use scaledRadius for CircleHitbox collision detection ([#3808](https://github.com/flame-engine/flame/issues/3808)). ([3498c1e5](https://github.com/flame-engine/flame/commit/3498c1e565985ce7d12212e6a625f538da98c362))\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 0.6.2+20\n\n - Update a dependency to the latest release.\n\n## 0.6.2+19\n\n - Update a dependency to the latest release.\n\n## 0.6.2+18\n\n - Update a dependency to the latest release.\n\n## 0.6.2+17\n\n - Update a dependency to the latest release.\n\n## 0.6.2+16\n\n - Update a dependency to the latest release.\n\n## 0.6.2+15\n\n - Update a dependency to the latest release.\n\n## 0.6.2+14\n\n - Update a dependency to the latest release.\n\n## 0.6.2+13\n\n - Update a dependency to the latest release.\n\n## 0.6.2+12\n\n - Update a dependency to the latest release.\n\n## 0.6.2+11\n\n - Update a dependency to the latest release.\n\n## 0.6.2+10\n\n - Update a dependency to the latest release.\n\n## 0.6.2+9\n\n - **FIX**: Remove outdated deprecations ([#3541](https://github.com/flame-engine/flame/issues/3541)). ([b918e40d](https://github.com/flame-engine/flame/commit/b918e40d0ce14b89ba9b5c82aed8ff51d6f549c3))\n\n## 0.6.2+8\n\n - Update a dependency to the latest release.\n\n## 0.6.2+7\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 0.6.2+6\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 0.6.2+5\n\n - Update a dependency to the latest release.\n\n## 0.6.2+4\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n\n## 0.6.2+3\n\n - Update a dependency to the latest release.\n\n## 0.6.2+2\n\n - Update a dependency to the latest release.\n\n## 0.6.2+1\n\n - Update a dependency to the latest release.\n\n## 0.6.2\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 0.6.1\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n\n## 0.6.0+1\n\n - Update a dependency to the latest release.\n\n## 0.6.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 0.5.1\n\n - **FEAT**: Bumped integral_isolates package for flame_isolate ([#2994](https://github.com/flame-engine/flame/issues/2994)). ([3c38ee60](https://github.com/flame-engine/flame/commit/3c38ee6058e7c8b7546c3fcdb1b08e3e40ba138b))\n\n## 0.5.0+7\n\n - Update a dependency to the latest release.\n\n## 0.5.0+6\n\n - Update a dependency to the latest release.\n\n## 0.5.0+5\n\n - Update a dependency to the latest release.\n\n## 0.5.0+4\n\n - **DOCS**: Update flame_isolate to point at repository ([#2880](https://github.com/flame-engine/flame/issues/2880)). ([06fdeac6](https://github.com/flame-engine/flame/commit/06fdeac684b2be26206d50282e6a7f2cbac4264c))\n\n## 0.5.0+3\n\n - Update a dependency to the latest release.\n\n## 0.5.0+2\n\n - **REFACTOR**: Mark semantically final variables as final (or const) proper [DCM] ([#2783](https://github.com/flame-engine/flame/issues/2783)). ([71f7b475](https://github.com/flame-engine/flame/commit/71f7b475e33dd6fa7224c4a3ab29ffdb89702c34))\n\n## 0.5.0+1\n\n - Update a dependency to the latest release.\n\n## 0.5.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **DOCS**: Enable CSpell on tests ([#2723](https://github.com/flame-engine/flame/issues/2723)). ([e051298c](https://github.com/flame-engine/flame/commit/e051298cba76550229780438b1a589557c7b488d))\n - **BREAKING** **FEAT**: Add CameraComponent to FlameGame ([#2740](https://github.com/flame-engine/flame/issues/2740)). ([7c2f4000](https://github.com/flame-engine/flame/commit/7c2f4000761580dbabb5d73b27f64d5819b34e8d))\n\n## 0.4.0+2\n\n - Update a dependency to the latest release.\n\n## 0.4.0+1\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n\n## 0.4.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n - **BREAKING** **REFACTOR**: Move `CameraComponent` and events out of experimental ([#2505](https://github.com/flame-engine/flame/issues/2505)). ([87b8a067](https://github.com/flame-engine/flame/commit/87b8a067f3e0096cebff3db4f5767e68616928fd))\n\n## 0.3.0+1\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n## 0.3.0\n\n> Note: This release has breaking changes.\n\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n - **BREAKING** **FEAT**: The `HasTappableComponents` mixin is no longer needed ([#2450](https://github.com/flame-engine/flame/issues/2450)). ([b5bdf4ec](https://github.com/flame-engine/flame/commit/b5bdf4ec173e87907a59a9f62fcdf35cc968af2a))\n\n## 0.2.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Depend on test: any for flame_test ([#2207](https://github.com/flame-engine/flame/issues/2207)). ([acfd418d](https://github.com/flame-engine/flame/commit/acfd418d882ee6872f3aa9961c39680ec123c2e6))\n - **BREAKING** **REFACTOR**: The method `onLoad()` now returns `FutureOr<void>` ([#2228](https://github.com/flame-engine/flame/issues/2228)). ([d898b539](https://github.com/flame-engine/flame/commit/d898b539f734d3e14c47990ef0727043a0e32efb))\n\n## 0.1.1\n\n## 0.1.0\n\n - **FEAT**: FlameIsolate - a neat way of handling threads ([#1909](https://github.com/flame-engine/flame/issues/1909)). ([b25b9356](https://github.com/flame-engine/flame/commit/b25b935644e258c37145bd6abfe0962d8e872801))\n\n## 0.1.0\n\n- Initial version.\n"
  },
  {
    "path": "packages/flame_isolate/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_isolate/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nAdds support for <a href=\"https://pub.dev/packages/integral_isolates\">integral_isolates</a> to your <a href=\"https://github.com/flame-engine/flame\">Flame</a> games.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_isolate\" ><img src=\"https://img.shields.io/pub/v/flame_isolate.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n\n# flame_isolate\n\nThe power of [integral_isolates](https://pub.dev/packages/integral_isolates) in your\n[Flame](https://pub.dev/packages/flame) game.\n\n\n## Usage\n\nJust add the mixin `FlameIsolate` to your component and start utilizing the power of an isolate as\nsimple as running the [compute](https://api.flutter.dev/flutter/foundation/compute-constant.html)\nfunction.\n\nExample:\n\n```dart\nclass MyGame extends FlameGame with FlameIsolate {\n  ...\n  @override\n  void update(double dt) {\n    if (shouldRecalculate) {\n      isolate(recalculateWorld, worldData).then(updateWorld);\n    }\n    ...\n  }\n  ...\n}\n```\n\n\n### Performance note\n\nKeep in mind that every component with `FlameIsolate` mixin that you create and add to your game\nwill create a new isolate. This means you will probably want to create a manager component to\nmanage a lot of \"dumber\" components. Think of it like ants, where the queen controls the worker\nants. If every individual worker ant got it's own isolate, it would be a total waste of power,\nhence you would put it on the queen, which in turn tells all the worker ants what to do.\n\nA simple example of this can be found in the example application for the FlameIsolate package.\n\n\n### Backpressure Strategies\n\nBackpressure strategies is a way to cope with the job queue when job items are produced more rapidly\nthan the isolate can handle them. This presents the problem of what to do with such a growing\nbacklog of unhandled jobs. To mitigate this problem this library funnels all jobs through a job\nqueue handler. Also known as `BackpressureStrategy`.\n\nThe ones currently supported are:\n\n- `NoBackPressureStrategy` that basically does not handle back pressure. It uses a FIFO stack for\n  storing a backlog of unhandled jobs.\n-`ReplaceBackpressureStrategy` that has a job queue with size one, and discards the queue upon\n  adding a new job.\n-`DiscardNewBackPressureStrategy` that has a job queue with size one, and as long as the queue is\n  populated a new job will not be added.\n\nYou can specify a backpressure strategy by overriding the `backpressureStrategy` field. This will\ncreate the isolate with the specified strategy when component is mounted.\n\n```dart\nclass MyGame extends FlameGame with FlameIsolate {\n  @override\n  BackpressureStrategy get backpressureStrategy => ReplaceBackpressureStrategy();\n  ...\n}\n```\n\n\n### Additional information\n\nYou could expect this API to be *mostly* stable, but implementation of the underlying package\n(integral_isolates) is not fully finalized yet, and there is more features coming before both\npackages can count as stable.\n"
  },
  {
    "path": "packages/flame_isolate/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    - public_member_api_docs\n"
  },
  {
    "path": "packages/flame_isolate/example/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_isolate/example/README.md",
    "content": "# flutter_colonists\n\nA tiny example where jobs and workers are paired together by running a path finding algorithm in\nan isolate using flame_isolate.\n\nYou can toggle between compute types by tapping the HUD element in the top right corner.\n\nKeyboard bindings for the game are:\n\n- `WASD` for moving the camera.\n- `Num+ Num-` for zooming in and out.\n\n\nThank you Wyrmsun for the wonderful\n[Germanic Worker](https://opengameart.org/content/germanic-worker) asset. As this asset is licensed\nunder GPLv2, this example project is as well.\n"
  },
  {
    "path": "packages/flame_isolate/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_isolate/example/lib/brains/path_finder.dart",
    "content": "import 'dart:io';\n\nimport 'package:collection/collection.dart';\nimport 'package:flame_isolate_example/objects/colonists_object.dart';\nimport 'package:flame_isolate_example/standard/int_vector2.dart';\nimport 'package:flame_isolate_example/standard/pair.dart';\nimport 'package:flame_isolate_example/terrain/terrain.dart';\n\n/// Synchronous implementation of finding a path between start and destination.\nIterable<IntVector2>? findPath({\n  required IntVector2 start,\n  required IntVector2 destination,\n  required PathFinderData pathFinderData,\n}) {\n  // I know, I know, this is a thread sleep... But it's emulating an expensive\n  // path finding calculation\n  sleep(const Duration(milliseconds: 20));\n  return _findPathAStar(\n    start: start,\n    destination: destination,\n    pathFinderData: pathFinderData,\n  );\n}\n\nclass PathFinderData {\n  final Map<IntVector2, double> terrain;\n  final Set<IntVector2> unWalkableTiles;\n\n  const PathFinderData._({\n    required this.terrain,\n    required this.unWalkableTiles,\n  });\n\n  factory PathFinderData.fromWorld({\n    required Map<IntVector2, Terrain> terrain,\n    required List<ColonistsObject> worldObjects,\n  }) {\n    return PathFinderData._(\n      terrain: terrain.map((key, value) => MapEntry(key, value.difficulty)),\n      unWalkableTiles: worldObjects.map((e) => e.tilePosition).toSet(),\n    );\n  }\n\n  bool _complies(IntVector2 position, IntVector2 destination) {\n    return position == destination ||\n        terrain.containsKey(position) && !unWalkableTiles.contains(position);\n  }\n\n  /// Returns an iterable of the possible neighbors to move to\n  /// Unreachable terrain will not be sent here\n  Iterable<IntVector2> neighbors(\n    IntVector2 current,\n    IntVector2 destination,\n  ) sync* {\n    // + shape\n    final upOne = current.add(y: -1);\n    if (_complies(upOne, destination)) {\n      yield upOne;\n    }\n    final leftOne = current.add(x: -1);\n    if (_complies(leftOne, destination)) {\n      yield leftOne;\n    }\n    final downOne = current.add(y: 1);\n    if (_complies(downOne, destination)) {\n      yield downOne;\n    }\n    final rightOne = current.add(x: 1);\n    if (_complies(rightOne, destination)) {\n      yield rightOne;\n    }\n\n    // Diagonal\n    final upLeft = current.add(x: -1, y: -1);\n    if (_complies(upLeft, destination)) {\n      yield upLeft;\n    }\n    final downLeft = current.add(x: -1, y: 1);\n    if (_complies(downLeft, destination)) {\n      yield downLeft;\n    }\n    final upRight = current.add(x: 1, y: -1);\n    if (_complies(upRight, destination)) {\n      yield upRight;\n    }\n    final downRight = current.add(x: 1, y: 1);\n    if (_complies(downRight, destination)) {\n      yield downRight;\n    }\n  }\n\n  double cost(IntVector2 current, IntVector2 next) {\n    return terrain[next]! * current.distanceTo(next);\n  }\n}\n\n/// A* pathfinding algorithm, inspired by\n/// https://www.redblobgames.com/pathfinding/a-star/introduction.html\n/// https://www.redblobgames.com/pathfinding/a-star/implementation.html\nIterable<IntVector2>? _findPathAStar({\n  required IntVector2 start,\n  required IntVector2 destination,\n  required PathFinderData pathFinderData,\n}) {\n  final frontier = PriorityQueue<Pair<IntVector2, double>>(\n    (first, second) => first.second.compareTo(second.second),\n  );\n  frontier.add(Pair(start, 0));\n\n  final cameFrom = <IntVector2, IntVector2>{\n    start: start,\n  };\n  final costSoFar = <IntVector2, double>{\n    start: 0,\n  };\n\n  while (frontier.isNotEmpty) {\n    final current = frontier.removeFirst().first;\n\n    if (current == destination) {\n      break;\n    }\n\n    for (final next in pathFinderData.neighbors(current, destination)) {\n      final newCost = costSoFar[current]! + pathFinderData.cost(current, next);\n      if (!costSoFar.containsKey(next) || newCost < costSoFar[next]!) {\n        costSoFar[next] = newCost;\n        final distanceTo = destination.distanceTo(next);\n        final priority = newCost + distanceTo;\n        frontier.add(Pair(next, priority));\n        cameFrom[next] = current;\n      }\n    }\n  }\n\n  var current = destination;\n  final path = <IntVector2>[];\n  while (current != start) {\n    path.add(current);\n    final innerCurrent = cameFrom[current];\n    if (innerCurrent == null) {\n      return null;\n    }\n    current = innerCurrent;\n  }\n  path.add(start);\n\n  return path.reversed;\n}\n"
  },
  {
    "path": "packages/flame_isolate/example/lib/brains/worker_overmind.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame_isolate/flame_isolate.dart';\nimport 'package:flame_isolate_example/brains/path_finder.dart';\nimport 'package:flame_isolate_example/brains/worker_overmind_hud.dart';\nimport 'package:flame_isolate_example/colonists_game.dart';\nimport 'package:flame_isolate_example/objects/bread.dart';\nimport 'package:flame_isolate_example/objects/colonists_object.dart';\nimport 'package:flame_isolate_example/standard/int_vector2.dart';\nimport 'package:flame_isolate_example/standard/pair.dart';\nimport 'package:flame_isolate_example/units/worker.dart';\nimport 'package:flutter/foundation.dart';\n\nclass WorkerOvermind extends Component\n    with HasGameReference<ColonistsGame>, FlameIsolate {\n  final List<Pair<StaticColonistsObject, Vector2>> _queuedTasks = [];\n  late Timer _assignTaskInterval;\n  late WorkerOvermindHud isolateHud;\n\n  @override\n  Future<void> onLoad() async {\n    game.camera.viewport.add(isolateHud = WorkerOvermindHud());\n    super.onLoad();\n  }\n\n  @override\n  Future<void> onMount() {\n    calculateTasks();\n    _assignTaskInterval = Timer(0.2, repeat: true, onTick: _assignTasks)\n      ..start();\n    return super.onMount();\n  }\n\n  // Note: This would, in reality, also be moved to isolate\n  void calculateTasks() {\n    game.worldObjects.whereType<Bread>().forEach((bread) {\n      moveObject(bread, Vector2(8, 2));\n    });\n  }\n\n  void moveObject(StaticColonistsObject objectToMove, Vector2 destination) =>\n      _queuedTasks.add(Pair(objectToMove, destination));\n\n  @override\n  void update(double dt) {\n    _assignTaskInterval.update(dt);\n    super.update(dt);\n  }\n\n  /// Set that keeps track of what workers are currently in queue to get a job.\n  ///\n  /// As long as a worker is in this queue it should be prevented to be\n  /// calculated for a job.\n  final _workersBeingCalculated = <Worker>{};\n\n  /// Function that pairs a job and a worker.\n  ///\n  /// Using an isolate for the actual calculation.\n  Future<void> _assignTasks() async {\n    if (_queuedTasks.isEmpty) {\n      return;\n    }\n\n    final idleWorkers = game.workers\n        .where(\n          (worker) =>\n              worker.isIdle && !_workersBeingCalculated.contains(worker),\n        )\n        .take(10)\n        .toList(growable: false);\n\n    _workersBeingCalculated.addAll(idleWorkers);\n\n    final shortestQueue = min(idleWorkers.length, _queuedTasks.length);\n\n    // I know this is not proper handling of lists, but this is just for demo\n    final localQueue =\n        _queuedTasks\n            .map((task) => task.first.tilePosition)\n            .toList(growable: false)\n          ..shuffle();\n    final subQueue = localQueue\n        .getRange(0, shortestQueue)\n        .toList(growable: false);\n\n    // Commented out since I want to keep jobs in queue for demo\n    // _queuedTasks.removeRange(0, shortestQueue);\n\n    try {\n      if (idleWorkers.isNotEmpty && _queuedTasks.isNotEmpty) {\n        final calculateWorkData = _CalculateWorkData(\n          idleWorkerPositions: idleWorkers\n              .map((worker) => worker.tilePosition)\n              .toList(growable: false),\n          destinations: subQueue,\n          pathFinderData: game.pathFinderData,\n        );\n\n        final paths = switch (isolateHud.computeType) {\n          ComputeType.isolate => await isolateCompute(\n            _calculateWork,\n            calculateWorkData,\n          ),\n          ComputeType.synchronous => _calculateWork(\n            calculateWorkData,\n          ),\n          ComputeType.compute => await compute(\n            _calculateWork,\n            calculateWorkData,\n          ),\n        };\n\n        for (var i = 0; i < paths.length; i++) {\n          idleWorkers[i].issueWork(\n            _queuedTasks[i].first,\n            paths[i],\n          );\n        }\n      }\n    } on DropException catch (_) {\n      debugPrint('Dropped');\n    } finally {\n      idleWorkers.forEach(_workersBeingCalculated.remove);\n    }\n  }\n\n  static List<List<IntVector2>> _calculateWork(_CalculateWorkData data) {\n    final workers = data.idleWorkerPositions.iterator;\n    final destinations = data.destinations.iterator;\n\n    final workerPaths = <List<IntVector2>>[];\n\n    while (workers.moveNext() && destinations.moveNext()) {\n      final path = findPath(\n        start: workers.current,\n        destination: destinations.current,\n        pathFinderData: data.pathFinderData,\n      );\n      if (path != null) {\n        workerPaths.add(path.toList());\n      }\n    }\n\n    return workerPaths;\n  }\n\n  @override\n  BackpressureStrategy get backpressureStrategy =>\n      ReplaceBackpressureStrategy();\n}\n\n@immutable\nclass _CalculateWorkData {\n  final List<IntVector2> idleWorkerPositions;\n  final List<IntVector2> destinations;\n  final PathFinderData pathFinderData;\n\n  const _CalculateWorkData({\n    required this.idleWorkerPositions,\n    required this.destinations,\n    required this.pathFinderData,\n  });\n}\n"
  },
  {
    "path": "packages/flame_isolate/example/lib/brains/worker_overmind_hud.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flutter/rendering.dart';\n\nenum ComputeType {\n  isolate('Running in isolate'),\n  compute('Running in compute function'),\n  synchronous('Running synchronously')\n  ;\n\n  final String description;\n\n  const ComputeType(this.description);\n}\n\nclass WorkerOvermindHud extends PositionComponent with TapCallbacks {\n  ComputeType computeType = ComputeType.isolate;\n\n  @override\n  void onLoad() {\n    x = 10;\n    y = 10;\n    width = 210;\n    height = 80;\n  }\n\n  @override\n  void onTapDown(_) {\n    computeType =\n        ComputeType.values[(computeType.index + 1) % ComputeType.values.length];\n  }\n\n  final _paint = Paint()..color = const Color(0xa98d560d);\n\n  final textPaint = TextPaint(\n    style: const TextStyle(\n      fontSize: 15,\n    ),\n  );\n\n  late final rect = toRect();\n  late final centerVector = rect.center.toVector2();\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(rect, _paint);\n\n    textPaint.render(\n      canvas,\n      computeType.description,\n      centerVector,\n      anchor: Anchor.center,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_isolate/example/lib/colonists_game.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/input.dart';\nimport 'package:flame_isolate_example/brains/path_finder.dart';\nimport 'package:flame_isolate_example/brains/worker_overmind.dart';\nimport 'package:flame_isolate_example/constants.dart';\nimport 'package:flame_isolate_example/game_map/game_map.dart';\nimport 'package:flame_isolate_example/objects/colonists_object.dart';\nimport 'package:flame_isolate_example/terrain/terrain.dart';\nimport 'package:flame_isolate_example/units/worker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nclass ColonistsGame extends FlameGame with KeyboardEvents {\n  final PositionComponent _cameraPosition = PositionComponent();\n  late final GameMap _currentMap;\n  ColonistsGame()\n    : super(\n        camera: CameraComponent.withFixedResolution(\n          width: 400,\n          height: 600,\n        ),\n      );\n\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n    camera.follow(_cameraPosition);\n    camera.viewfinder.zoom = 0.4;\n\n    await Flame.images.load('bread.png');\n    await Flame.images.load('ant_walk.png');\n    await Flame.images.load('cheese.png');\n\n    world.add(_currentMap = GameMap());\n\n    _cameraPosition.position = Vector2(\n      GameMap.mapSizeX * Constants.tileSize / 2,\n      GameMap.mapSizeY * Constants.tileSize / 2,\n    );\n\n    add(WorkerOvermind());\n  }\n\n  Terrain tileAtPosition(int x, int y) {\n    return _currentMap.tileAtPosition(x, y);\n  }\n\n  static const double cameraSpeed = 200;\n\n  double _downForce = 0;\n  double _upForce = 0;\n  double _rightForce = 0;\n  double _leftForce = 0;\n\n  @override\n  KeyEventResult onKeyEvent(\n    KeyEvent event,\n    Set<LogicalKeyboardKey> keysPressed,\n  ) {\n    var howMuch = 0.0;\n    if (event is KeyDownEvent) {\n      howMuch = 1;\n    } else if (event is KeyUpEvent) {\n      howMuch = 0;\n    }\n\n    if (event.logicalKey == LogicalKeyboardKey.keyS) {\n      _downForce = howMuch;\n    } else if (event.logicalKey == LogicalKeyboardKey.keyW) {\n      _upForce = howMuch;\n    } else if (event.logicalKey == LogicalKeyboardKey.keyD) {\n      _rightForce = howMuch;\n    } else if (event.logicalKey == LogicalKeyboardKey.keyA) {\n      _leftForce = howMuch;\n    } else if (event.logicalKey == LogicalKeyboardKey.numpadAdd &&\n        event is KeyDownEvent) {\n      camera.viewfinder.zoom = min(\n        camera.viewfinder.zoom + 0.1,\n        5,\n      );\n    } else if (event.logicalKey == LogicalKeyboardKey.numpadSubtract &&\n        event is KeyDownEvent) {\n      camera.viewfinder.zoom = max(\n        camera.viewfinder.zoom - 0.1,\n        0.1,\n      );\n    }\n    return super.onKeyEvent(event, keysPressed);\n  }\n\n  final direction = Vector2(0, 0);\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n    direction.setValues(_rightForce - _leftForce, _downForce - _upForce);\n    final step = direction..scale(cameraSpeed * dt * 4);\n    _cameraPosition.position += step;\n  }\n\n  PathFinderData get pathFinderData => _currentMap.pathFinderData;\n\n  Set<Worker> get workers => _currentMap.workers;\n\n  List<ColonistsObject> get worldObjects => _currentMap.worldObjects;\n}\n"
  },
  {
    "path": "packages/flame_isolate/example/lib/constants.dart",
    "content": "class Constants {\n  static const double tileSize = 50;\n}\n"
  },
  {
    "path": "packages/flame_isolate/example/lib/extensions/range_extensions.dart",
    "content": "extension IntRangeExtensions on int {\n  /// Generates an Iterable of ints from this int to [to] (exclusive).\n  Iterable<int> to(int to) sync* {\n    for (var i = this; i < to; i++) {\n      yield i;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_isolate/example/lib/game_map/game_map.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame_isolate_example/brains/path_finder.dart';\nimport 'package:flame_isolate_example/colonists_game.dart';\nimport 'package:flame_isolate_example/constants.dart';\nimport 'package:flame_isolate_example/extensions/range_extensions.dart';\nimport 'package:flame_isolate_example/objects/bread.dart';\nimport 'package:flame_isolate_example/objects/cheese.dart';\nimport 'package:flame_isolate_example/objects/colonists_object.dart';\nimport 'package:flame_isolate_example/standard/int_vector2.dart';\nimport 'package:flame_isolate_example/terrain/grass.dart';\nimport 'package:flame_isolate_example/terrain/terrain.dart';\nimport 'package:flame_isolate_example/units/worker.dart';\n\nclass GameMap extends Component with HasGameReference<ColonistsGame> {\n  static const mapSizeX = 50;\n  static const mapSizeY = 50;\n  static const totalPositions = mapSizeX * mapSizeY;\n\n  static final int cheeseSpread = (0.03 * totalPositions).toInt();\n  static final int breadSpread = (0.05 * totalPositions).toInt();\n  static final int workerSpread = (0.1 * totalPositions).toInt();\n\n  static const double workerMinSpeed = 25;\n  static const double workerMaxSpeed = 75;\n\n  @override\n  Future<void> onLoad() async {\n    for (var x = 0; x < mapSizeX; x++) {\n      for (var y = 0; y < mapSizeY; y++) {\n        addTerrain(IntVector2(x, y), Grass());\n      }\n    }\n\n    final mapPositions = List.generate(totalPositions, (index) => index)\n      ..shuffle();\n\n    worldObjects = [\n      for (final _ in 0.to(cheeseSpread))\n        Cheese(\n          mapPositions[0] ~/ mapSizeX,\n          mapPositions.removeAt(0) % mapSizeY,\n        ),\n      for (final _ in 0.to(breadSpread))\n        Bread(\n          mapPositions[0] ~/ mapSizeX,\n          mapPositions.removeAt(0) % mapSizeY,\n        ),\n      for (final _ in 0.to(workerSpread))\n        Worker(\n          mapPositions[0] ~/ mapSizeX,\n          mapPositions.removeAt(0) % mapSizeY,\n          speed:\n              Random().nextDouble() * (workerMaxSpeed - workerMinSpeed) +\n              workerMinSpeed,\n        ),\n    ];\n\n    worldObjects.forEach(addObject);\n\n    if (workers.isNotEmpty) {\n      game.camera.follow(workers.first);\n    }\n  }\n\n  final Set<Worker> workers = {};\n  final Map<IntVector2, Terrain> _terrain = {};\n  late final List<ColonistsObject> worldObjects;\n\n  void addTerrain(IntVector2 position, Terrain terrain) {\n    _terrain[position] = terrain;\n    add(\n      terrain\n        ..x = position.x * Constants.tileSize\n        ..y = position.y * Constants.tileSize\n        ..width = Constants.tileSize\n        ..height = Constants.tileSize,\n    );\n  }\n\n  void addObject(ColonistsObject object) {\n    if (object is Worker) {\n      workers.add(object);\n    }\n\n    if (object is StaticColonistsObject) {\n      (_terrain[object.tilePosition]! as Grass).difficulty = object.difficulty;\n    }\n\n    add(object);\n  }\n\n  PathFinderData get pathFinderData => PathFinderData.fromWorld(\n    terrain: _terrain,\n    worldObjects: game.worldObjects,\n  );\n\n  Terrain tileAtPosition(int x, int y) {\n    return _terrain[IntVector2(x, y)]!;\n  }\n}\n"
  },
  {
    "path": "packages/flame_isolate/example/lib/main.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame_isolate_example/colonists_game.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(GameWidget(game: ColonistsGame()));\n}\n"
  },
  {
    "path": "packages/flame_isolate/example/lib/objects/bread.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame_isolate_example/constants.dart';\nimport 'package:flame_isolate_example/objects/colonists_object.dart';\nimport 'package:flame_isolate_example/standard/int_vector2.dart';\n\nclass Bread extends StaticColonistsObject {\n  @override\n  Sprite objectSprite = Sprite(Flame.images.fromCache('bread.png'));\n\n  @override\n  IntVector2 tileSize = const IntVector2(1, 1);\n\n  Bread(super.x, super.y);\n\n  @override\n  String toString() {\n    return 'Bread(${x / Constants.tileSize.toInt()}, '\n        '${y / Constants.tileSize.toInt()})';\n  }\n\n  @override\n  double difficulty = 11.3;\n}\n"
  },
  {
    "path": "packages/flame_isolate/example/lib/objects/cheese.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame_isolate_example/objects/colonists_object.dart';\nimport 'package:flame_isolate_example/standard/int_vector2.dart';\n\nclass Cheese extends StaticColonistsObject {\n  @override\n  final Sprite objectSprite = Sprite(Flame.images.fromCache('cheese.png'));\n\n  @override\n  final IntVector2 tileSize = const IntVector2(1, 1);\n\n  Cheese(super.x, super.y);\n\n  @override\n  double difficulty = 8.6;\n}\n"
  },
  {
    "path": "packages/flame_isolate/example/lib/objects/colonists_object.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_isolate_example/constants.dart';\nimport 'package:flame_isolate_example/standard/int_vector2.dart';\n\nmixin ColonistsObject on PositionComponent {\n  IntVector2 get tileSize;\n\n  IntVector2 get tilePosition => IntVector2(\n    x ~/ Constants.tileSize,\n    y ~/ Constants.tileSize,\n  );\n}\n\nabstract class StaticColonistsObject extends SpriteComponent\n    with ColonistsObject {\n  Sprite get objectSprite;\n\n  double get difficulty;\n\n  @override\n  IntVector2 get tileSize;\n\n  @override\n  IntVector2 get tilePosition => IntVector2(\n    x ~/ Constants.tileSize,\n    y ~/ Constants.tileSize,\n  );\n\n  StaticColonistsObject(int x, int y) {\n    sprite = objectSprite;\n    width = tileSize.x * Constants.tileSize;\n    height = tileSize.y * Constants.tileSize;\n    super.y = y * Constants.tileSize;\n    super.x = x * Constants.tileSize;\n  }\n}\n"
  },
  {
    "path": "packages/flame_isolate/example/lib/standard/int_vector2.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:quiver/core.dart';\n\n@immutable\nclass IntVector2 {\n  final int x;\n  final int y;\n\n  const IntVector2(this.x, this.y);\n\n  /// Manhattan distance on a square grid\n  int distanceTo(IntVector2 b) {\n    return (x - b.x).abs() + (y - b.y).abs();\n  }\n\n  @override\n  bool operator ==(Object other) =>\n      other is IntVector2 && x == other.x && y == other.y;\n\n  @override\n  int get hashCode => hash2(x, y);\n\n  @override\n  String toString() {\n    return '$x : $y';\n  }\n\n  IntVector2 add({int x = 0, int y = 0}) => IntVector2(this.x + x, this.y + y);\n\n  Vector2 toPosition() => Vector2(x.toDouble(), y.toDouble());\n}\n"
  },
  {
    "path": "packages/flame_isolate/example/lib/standard/pair.dart",
    "content": "class Pair<T, E> {\n  final T first;\n  final E second;\n\n  Pair(this.first, this.second);\n}\n"
  },
  {
    "path": "packages/flame_isolate/example/lib/terrain/grass.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_isolate_example/terrain/terrain.dart';\nimport 'package:flutter/material.dart';\n\nclass Grass extends PositionComponent with Terrain {\n  static final _color = Paint()..color = const Color(0xff567d46);\n  static final _debugColor = Paint()\n    ..color = Colors.black.withValues(alpha: 0.5);\n\n  late final _rect = size.toRect();\n  late final _rect2 = Rect.fromCenter(\n    center: _rect.center,\n    height: 10,\n    width: 10,\n  );\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawRect(_rect, _color);\n    canvas.drawRect(_rect2, _debugColor);\n  }\n\n  @override\n  double difficulty = 1.0;\n}\n"
  },
  {
    "path": "packages/flame_isolate/example/lib/terrain/terrain.dart",
    "content": "import 'package:flame/components.dart';\n\nmixin Terrain on PositionComponent {\n  double get difficulty;\n}\n"
  },
  {
    "path": "packages/flame_isolate/example/lib/units/actions/movable.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame_isolate_example/colonists_game.dart';\nimport 'package:flame_isolate_example/standard/int_vector2.dart';\nimport 'package:flutter/material.dart';\n\nenum MoveDirection {\n  idle(isLeft: false), // 0\n  up(isLeft: false), // 1\n  down(isLeft: false), // 2\n  upRight(isLeft: false), // 3\n  right(isLeft: false), // 4\n  downLeft(isLeft: true), // 5\n  upLeft(isLeft: true), // 6\n  left(isLeft: true), // 7\n  downRight(isLeft: false)\n  ; // 8\n\n  final bool isLeft;\n\n  const MoveDirection({\n    required this.isLeft,\n  });\n\n  /// Returns the horizontally mirrored direction\n  MoveDirection get mirrored {\n    if (index >= 3 && index <= 5) {\n      return MoveDirection.values[index + 3];\n    }\n    if (index >= 6 && index <= 8) {\n      return MoveDirection.values[index - 3];\n    }\n    // ignore: avoid_returning_this\n    return this;\n  }\n}\n\nmixin Movable on PositionComponent, HasGameReference<ColonistsGame> {\n  double get speed;\n\n  void reachedDestination();\n\n  @override\n  @mustCallSuper\n  FutureOr<void> onLoad() {\n    anchor = Anchor.center;\n    return super.onLoad();\n  }\n\n  PathLine? pathLine;\n\n  void _removePathLine() {\n    pathLine?.getGone();\n  }\n\n  void _walkAlongPath(List<Vector2> path) {\n    if (path.isEmpty) {\n      setCurrentDirection(MoveDirection.idle);\n      _removePathLine();\n      // Reached last position, make available again\n      reachedDestination();\n      return;\n    }\n\n    final nextPoint = path.removeAt(0);\n\n    final normalizedDirection = (nextPoint - position).normalized();\n    normalizedDirection.round();\n    setCurrentDirection(directions[normalizedDirection] ?? MoveDirection.idle);\n\n    add(\n      MoveToEffect(\n        nextPoint,\n        EffectController(speed: speed),\n        onComplete: () => _walkAlongPath(path),\n      ),\n    );\n  }\n\n  void walkPath(List<IntVector2> path) {\n    final absolutePath = path.map((e) {\n      return game.tileAtPosition(e.x, e.y).positionOfAnchor(Anchor.center);\n    }).toList();\n\n    _walkAlongPath(absolutePath);\n\n    if (path.length > 2) {\n      game.world.add(pathLine = PathLine(absolutePath));\n    }\n  }\n\n  void setCurrentDirection(MoveDirection direction);\n\n  final Map<Vector2, MoveDirection> directions = {\n    Vector2(0, 0): MoveDirection.idle,\n    Vector2(0, -1): MoveDirection.up,\n    Vector2(1, -1): MoveDirection.upRight,\n    Vector2(1, 0): MoveDirection.right,\n    Vector2(1, 1): MoveDirection.downRight,\n    Vector2(0, 1): MoveDirection.down,\n    Vector2(-1, 1): MoveDirection.downLeft,\n    Vector2(-1, 0): MoveDirection.left,\n    Vector2(-1, -1): MoveDirection.upLeft,\n  };\n}\n\nclass PathLine extends ShapeComponent {\n  final Path path;\n\n  PathLine(List<Vector2> path) : path = _toPath(path) {\n    paint = Paint()\n      ..color = const Color(0x30ffffff)\n      ..style = PaintingStyle.stroke;\n  }\n\n  static Path _toPath(List<Vector2> points) {\n    return Path()..addPolygon(\n      points.map((p) => p.toOffset()).toList(growable: false),\n      false,\n    );\n  }\n\n  void getGone() {\n    parent?.remove(this);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.drawPath(path, paint);\n  }\n}\n"
  },
  {
    "path": "packages/flame_isolate/example/lib/units/worker.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame_isolate_example/colonists_game.dart';\nimport 'package:flame_isolate_example/constants.dart';\nimport 'package:flame_isolate_example/objects/colonists_object.dart';\nimport 'package:flame_isolate_example/standard/int_vector2.dart';\nimport 'package:flame_isolate_example/standard/pair.dart';\nimport 'package:flame_isolate_example/units/actions/movable.dart';\n\nclass Worker extends SpriteAnimationGroupComponent<MoveDirection>\n    with ColonistsObject, HasGameReference<ColonistsGame>, Movable {\n  @override\n  final double speed;\n\n  Worker(num x, num y, {this.speed = 50}) {\n    super.y = y * Constants.tileSize;\n    super.x = x * Constants.tileSize;\n    height = Constants.tileSize;\n    width = Constants.tileSize;\n    current = MoveDirection.idle;\n    anchor = Anchor.center;\n\n    final downRightAnimation = getSpriteAnimation(5);\n    animations = {\n      MoveDirection.idle: SpriteAnimation.spriteList(\n        // Use the second frame from down-right animation\n        [downRightAnimation.frames[1].sprite],\n        stepTime: 1,\n      ),\n      MoveDirection.up: getSpriteAnimation(0),\n      MoveDirection.upRight: getSpriteAnimation(7),\n      MoveDirection.right: getSpriteAnimation(6),\n      MoveDirection.downRight: downRightAnimation,\n      MoveDirection.down: getSpriteAnimation(4),\n      MoveDirection.upLeft: getSpriteAnimation(1),\n      MoveDirection.left: getSpriteAnimation(2),\n      MoveDirection.downLeft: getSpriteAnimation(3),\n    };\n  }\n\n  SpriteAnimation getSpriteAnimation(int row) {\n    return SpriteAnimation.fromFrameData(\n      Flame.images.fromCache('ant_walk.png'),\n      SpriteAnimationData.sequenced(\n        amount: 4,\n        stepTime: 0.1,\n        textureSize: Vector2(64, 64),\n        amountPerRow: 4,\n        texturePosition: Vector2(0, 64.0 * row),\n      ),\n    );\n  }\n\n  @override\n  void setCurrentDirection(MoveDirection direction) {\n    current = direction;\n  }\n\n  Pair<StaticColonistsObject, List<IntVector2>>? _currentTask;\n\n  bool get isIdle => _currentTask == null;\n\n  @override\n  void reachedDestination() => _currentTask = null;\n\n  void issueWork(StaticColonistsObject objectToMove, List<IntVector2> path) {\n    walkPath(path);\n    _currentTask = Pair(objectToMove, path);\n  }\n\n  @override\n  IntVector2 tileSize = const IntVector2(1, 1);\n}\n"
  },
  {
    "path": "packages/flame_isolate/example/pubspec.yaml",
    "content": "name: flame_isolate_example\nresolution: workspace\ndescription: Flame game example for running threaded workloads\n\npublish_to: 'none'\n\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  collection: ^1.18.0\n  flame: ^1.36.0\n  flame_isolate: ^0.6.2+21\n  flutter:\n    sdk: flutter\n  quiver: ^3.2.1\n\ndev_dependencies:\n  flame_lint: ^1.4.3\nflutter:\n  assets:\n    - assets/images/\n"
  },
  {
    "path": "packages/flame_isolate/lib/flame_isolate.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:integral_isolates/integral_isolates.dart';\n\nexport 'package:integral_isolates/integral_isolates.dart';\n\n/// Mixin on [Component] that holds an instance of a long running isolate using\n/// the library integral_isolates.\n///\n/// Using the isolate is done by just running [isolateCompute] function the same\n/// way you would run Flutter's compute function or [isolateComputeStream] if\n/// your work returns a [Stream].\n///\n/// Keep in mind that every component you create and add with this mixin to the\n/// game will create and hold it's own isolate. This makes it easy to\n/// accidentally create a lot of isolates if attached to every instance of an\n/// NPC for example. What you´probably want to do is to instead create a manager\n/// component that does all the calculations and controls the NPCs.\n///\n/// The following is an example of running a world update cycle when enough time\n/// has passed.\n///\n/// ```dart\n/// class MyGame extends FlameGame with FlameIsolate {\n///   @override\n///   void update(double dt) {\n///     if (shouldRecalculate) {\n///       compute(recalculateWorld, worldData).then(updateWorld);\n///     }\n///     return super.update(dt);\n///   }\n/// }\n/// ```\nmixin FlameIsolate on Component {\n  StatefulIsolate? _isolate;\n\n  /// The backpressureStrategy to use.\n  ///\n  /// Override this to change strategy.\n  BackpressureStrategy get backpressureStrategy => NoBackPressureStrategy();\n\n  @override\n  Future<void> onMount() async {\n    _isolate = StatefulIsolate(backpressureStrategy: backpressureStrategy);\n    _isolate?.init();\n    return super.onMount();\n  }\n\n  @override\n  void onRemove() {\n    _isolate?.dispose();\n    _isolate = null;\n  }\n\n  /// A function that runs the provided [callback] on the long running isolate\n  /// and (eventually) returns the value returned.\n  ///\n  /// Same footprint as the function compute from flutter, but runs on the\n  /// long running thread.\n  Future<R> isolateCompute<Q, R>(\n    IsolateCallback<Q, R> callback,\n    Q message, {\n    String? debugLabel,\n  }) {\n    return _isolate!.compute(callback, message, debugLabel: debugLabel);\n  }\n\n  /// A computation function that returns a [Stream] of responses from the\n  /// long lived isolate.\n  ///\n  /// Very similar to the [isolateCompute] function, but instead of returning a\n  /// [Future], a [Stream] is returned to allow for a response in multiple\n  /// parts. Every stream event will be sent individually through from the\n  /// isolate.\n  Stream<R> isolateComputeStream<Q, R>(\n    IsolateStream<Q, R> callback,\n    Q message, {\n    String? debugLabel,\n  }) {\n    // ignore: experimental_member_use\n    return _isolate!.computeStream(callback, message, debugLabel: debugLabel);\n  }\n}\n"
  },
  {
    "path": "packages/flame_isolate/lib/flame_tailored_isolate.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:integral_isolates/integral_isolates.dart';\n\nexport 'package:integral_isolates/integral_isolates.dart';\n\n/// Mixin on [Component] that holds an instance of a long running tailored\n/// isolate using the library integral_isolates.\n///\n/// Using the isolate is done by just running [isolateCompute] function the same\n/// way you would run Flutter's compute function or [isolateComputeStream] if\n/// your work returns a [Stream].\n///\n/// Keep in mind that every component you create and add with this mixin to the\n/// game will create and hold it's own isolate. This makes it easy to\n/// accidentally create a lot of isolates if attached to every instance of an\n/// NPC for example. What you´probably want to do is to instead create a manager\n/// component that does all the calculations and controls the NPCs.\n///\n/// The following is an example of running a world update cycle when enough time\n/// has passed.\n///\n/// ```dart\n/// class MyGame extends FlameGame with FlameTailoredIsolate<Maze, WalkPath> {\n///   @override\n///   void update(double dt) {\n///     if (shouldRecalculate) {\n///       compute(recalculatePath, worldData).then(updateWorld);\n///     }\n///     return super.update(dt);\n///   }\n/// }\n/// ```\nmixin FlameTailoredIsolate<Q, R> on Component {\n  TailoredStatefulIsolate<Q, R>? _isolate;\n\n  /// The backpressureStrategy to use.\n  ///\n  /// Override this to change strategy.\n  BackpressureStrategy<Q, R> get backpressureStrategy =>\n      NoBackPressureStrategy();\n\n  @override\n  Future<void> onMount() async {\n    _isolate = TailoredStatefulIsolate<Q, R>(\n      backpressureStrategy: backpressureStrategy,\n    );\n    _isolate?.init();\n    return super.onMount();\n  }\n\n  @override\n  void onRemove() {\n    _isolate?.dispose();\n    _isolate = null;\n  }\n\n  /// A function that runs the provided [callback] on the long running isolate\n  /// and (eventually) returns the value returned.\n  ///\n  /// Same footprint as the function compute from flutter, but runs on the\n  /// long running thread.\n  Future<R> isolateCompute(\n    IsolateCallback<Q, R> callback,\n    Q message, {\n    String? debugLabel,\n  }) {\n    return _isolate!.compute(callback, message, debugLabel: debugLabel);\n  }\n\n  /// A computation function that returns a [Stream] of responses from the\n  /// long lived isolate.\n  ///\n  /// Very similar to the [isolateCompute] function, but instead of returning a\n  /// [Future], a [Stream] is returned to allow for a response in multiple\n  /// parts. Every stream event will be sent individually through from the\n  /// isolate.\n  Stream<R> isolateComputeStream(\n    IsolateStream<Q, R> callback,\n    Q message, {\n    String? debugLabel,\n  }) {\n    // ignore: experimental_member_use\n    return _isolate!.computeStream(callback, message, debugLabel: debugLabel);\n  }\n}\n"
  },
  {
    "path": "packages/flame_isolate/pubspec.yaml",
    "content": "name: flame_isolate\nresolution: workspace\ndescription: Flame wrapper for integral_isolates making multi-threading easy in Flame\nversion: 0.6.2+21\nrepository: https://github.com/flame-engine/flame/blob/main/packages/flame_isolate\ntopics:\n  - flame\n  - isolate\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  integral_isolates: ^0.5.1\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flame_test: ^2.2.3\n  flutter_test:\n    sdk: flutter\n  test: any\n\n"
  },
  {
    "path": "packages/flame_isolate/test/flame_isolate_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_isolate/flame_isolate.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nclass _TestGame extends FlameGame with FlameIsolate {}\n\nclass _IsolateComponent extends Component with FlameIsolate {}\n\nvoid main() {\n  testWithGame<_TestGame>(\n    'Test running isolateCompute on game',\n    _TestGame.new,\n    (game) async {\n      final result = game.isolateCompute(_pow, 10);\n      await expectLater(result, completion(100));\n    },\n  );\n  testWithGame<_TestGame>(\n    'Test running isolateComputeStream on game',\n    _TestGame.new,\n    (game) async {\n      final result = game.isolateComputeStream(_messages, 10);\n      await expectLater(result, emitsInOrder([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]));\n    },\n  );\n\n  group('Test isolate in sub-component', () {\n    testWithFlameGame(\n      'Running isolateCompute in sub-component',\n      (game) async {\n        final isolateComponent = _IsolateComponent();\n        await game.add(isolateComponent);\n        await game.ready();\n        final result = isolateComponent.isolateCompute(_pow, 4);\n        await expectLater(result, completion(16));\n      },\n    );\n\n    testWithFlameGame(\n      'Running isolateComputeStream in sub-component',\n      (game) async {\n        final isolateComponent = _IsolateComponent();\n        await game.add(isolateComponent);\n        await game.ready();\n        final result = isolateComponent.isolateComputeStream(_messages, 4);\n        await expectLater(result, emitsInOrder([1, 2, 3, 4]));\n      },\n    );\n\n    testWithFlameGame(\n      'Running isolateCompute or isolateComputeStream after remove gives error',\n      (game) async {\n        final isolateComponent = _IsolateComponent();\n        await game.add(isolateComponent);\n        await game.ready();\n\n        final result = isolateComponent.isolateCompute(_pow, 4);\n        await expectLater(result, completion(16));\n\n        game.remove(isolateComponent);\n        await game.ready();\n\n        expect(\n          () => isolateComponent.isolateCompute(_pow, 4),\n          throwsA(\n            isA<TypeError>().having(\n              (error) => error.toString(),\n              'Explicit non null assertion',\n              'Null check operator used on a null value',\n            ),\n          ),\n        );\n\n        expect(\n          () => isolateComponent.isolateComputeStream(_messages, 4),\n          throwsA(\n            isA<TypeError>().having(\n              (error) => error.toString(),\n              'Explicit non null assertion',\n              'Null check operator used on a null value',\n            ),\n          ),\n        );\n      },\n    );\n  });\n}\n\nint _pow(int message) {\n  return message * message;\n}\n\nStream<int> _messages(int amount) async* {\n  for (var i = 1; i <= amount; i++) {\n    yield i;\n  }\n}\n"
  },
  {
    "path": "packages/flame_isolate/test/flame_tailored_isolate_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_isolate/flame_tailored_isolate.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nclass _TestGame extends FlameGame with FlameTailoredIsolate<double, int> {}\n\nclass _IsolateComponent extends Component\n    with FlameTailoredIsolate<double, int> {}\n\nvoid main() {\n  testWithGame<_TestGame>(\n    'Test running isolateCompute on game',\n    _TestGame.new,\n    (game) async {\n      final result = game.isolateCompute(_pow, 10);\n      await expectLater(result, completion(100));\n    },\n  );\n  testWithGame<_TestGame>(\n    'Test running isolateComputeStream on game',\n    _TestGame.new,\n    (game) async {\n      final result = game.isolateComputeStream(_messages, 10);\n      await expectLater(result, emitsInOrder([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]));\n    },\n  );\n\n  group('Test isolate in sub-component', () {\n    testWithFlameGame(\n      'Running isolateCompute in sub-component',\n      (game) async {\n        final isolateComponent = _IsolateComponent();\n        await game.add(isolateComponent);\n        await game.ready();\n        final result = isolateComponent.isolateCompute(_pow, 4);\n        await expectLater(result, completion(16));\n      },\n    );\n\n    testWithFlameGame(\n      'Running isolateComputeStream in sub-component',\n      (game) async {\n        final isolateComponent = _IsolateComponent();\n        await game.add(isolateComponent);\n        await game.ready();\n        final result = isolateComponent.isolateComputeStream(_messages, 4);\n        await expectLater(result, emitsInOrder([1, 2, 3, 4]));\n      },\n    );\n\n    testWithFlameGame(\n      'Running isolateCompute or isolateComputeStream after remove gives error',\n      (game) async {\n        final isolateComponent = _IsolateComponent();\n        await game.add(isolateComponent);\n        await game.ready();\n\n        final result = isolateComponent.isolateCompute(_pow, 4);\n        await expectLater(result, completion(16));\n\n        game.remove(isolateComponent);\n        await game.ready();\n\n        expect(\n          () => isolateComponent.isolateCompute(_pow, 4),\n          throwsA(\n            isA<TypeError>().having(\n              (error) => error.toString(),\n              'Explicit non null assertion',\n              'Null check operator used on a null value',\n            ),\n          ),\n        );\n\n        expect(\n          () => isolateComponent.isolateComputeStream(_messages, 4),\n          throwsA(\n            isA<TypeError>().having(\n              (error) => error.toString(),\n              'Explicit non null assertion',\n              'Null check operator used on a null value',\n            ),\n          ),\n        );\n      },\n    );\n  });\n}\n\nint _pow(double message) {\n  return (message * message).toInt();\n}\n\nStream<int> _messages(double amount) async* {\n  for (var i = 1; i <= amount; i++) {\n    yield i;\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/CHANGELOG.md",
    "content": "## 1.0.0\n\n - **FIX**: Jenny now always stringifies whole numbers without .0 ([#2265](https://github.com/flame-engine/flame/issues/2265)). ([f262b7ee](https://github.com/flame-engine/flame/commit/f262b7ee39a270f5bfbf3bf2be89d85549d16cd1))\n - **FIX**: Remove whitespace before a command in dialogue option ([#2187](https://github.com/flame-engine/flame/issues/2187)). ([00f0e330](https://github.com/flame-engine/flame/commit/00f0e330b429f5f7ae87742ff5814f44924cb202))\n - **FIX**: Remove flutter from jenny ([#2162](https://github.com/flame-engine/flame/issues/2162)). ([29db304d](https://github.com/flame-engine/flame/commit/29db304d36fdf791f6c9df4c69b95511190b3057))\n - **FEAT**: Added the <<character>> command to Jenny ([#2274](https://github.com/flame-engine/flame/issues/2274)). ([6548e9cb](https://github.com/flame-engine/flame/commit/6548e9cb0a91353489812e211c2aa098fbd04f55))\n - **FEAT**: Added if() built-in function in Jenny ([#2259](https://github.com/flame-engine/flame/issues/2259)). ([087229ed](https://github.com/flame-engine/flame/commit/087229ede545644026eb6c303a037a93a792eaf2))\n - **FEAT**: Added command <<visit>> ([#2233](https://github.com/flame-engine/flame/issues/2233)). ([a90f90ef](https://github.com/flame-engine/flame/commit/a90f90efc5556f9697d409fd6a1e6558ae9e8236))\n - **FEAT**: OnDialogueChoice now returns null by default ([#2234](https://github.com/flame-engine/flame/issues/2234)). ([e2ab129e](https://github.com/flame-engine/flame/commit/e2ab129e5974485241223528fc50f3049ffecf8f))\n - **FEAT**: Added DialogueView.onNodeFinish event ([#2229](https://github.com/flame-engine/flame/issues/2229)). ([19a1f09a](https://github.com/flame-engine/flame/commit/19a1f09acc45199a4411c7026b8adf61a5a5a11f))\n - **FEAT**: Arguments of a UserDefinedCommand are now accessible ([#2224](https://github.com/flame-engine/flame/issues/2224)). ([0a9eaf38](https://github.com/flame-engine/flame/commit/0a9eaf380194e93c89cb8b2f5677d476a33eb83b))\n - **FEAT**: Added escape sequence \\- in yarn language ([#2220](https://github.com/flame-engine/flame/issues/2220)). ([43eacdd1](https://github.com/flame-engine/flame/commit/43eacdd1f5e1419c310f5cd34d1476adf03eb4d6))\n - **FEAT**: Add support for user-defined functions in jenny ([#2194](https://github.com/flame-engine/flame/issues/2194)). ([9364a0dd](https://github.com/flame-engine/flame/commit/9364a0dd324a2ed57b1e9a8907108da796e59352))\n - **FEAT**: Support for builtin functions in jenny ([#2192](https://github.com/flame-engine/flame/issues/2192)). ([82d35b8a](https://github.com/flame-engine/flame/commit/82d35b8a5dc8a9378dfee348b3392d0afabf2bc8))\n - **FEAT**: Add command <<local>> ([#2185](https://github.com/flame-engine/flame/issues/2185)). ([9e677e7d](https://github.com/flame-engine/flame/commit/9e677e7dc74bbe15b8521ec945a5b92ce8a4180a))\n - **FEAT**: Added support for markup attributes ([#2183](https://github.com/flame-engine/flame/issues/2183)). ([f887545b](https://github.com/flame-engine/flame/commit/f887545b127b41412b29217c52f9ec6ea0d6c885))\n - **FEAT**: Support user-defined commands ([#2168](https://github.com/flame-engine/flame/issues/2168)). ([ffb36a89](https://github.com/flame-engine/flame/commit/ffb36a89efdcd976fe63c16f27741b77b08aa284))\n - **FEAT**: Added the <<set>> command ([#2155](https://github.com/flame-engine/flame/issues/2155)). ([2b306d9e](https://github.com/flame-engine/flame/commit/2b306d9ee9c92416fe82b42e9a4ee33b280af46f))\n - **FEAT**: Implement the <<declare>> command ([#2154](https://github.com/flame-engine/flame/issues/2154)). ([8d592f17](https://github.com/flame-engine/flame/commit/8d592f17411800a5239720687149122eaf7750f1))\n - **FEAT**: Trim whitespace at the end of dialogue lines ([#2149](https://github.com/flame-engine/flame/issues/2149)). ([9c25e631](https://github.com/flame-engine/flame/commit/9c25e631e2e5ed5c593dbca4f498105e2c8fff66))\n - **FEAT**: DialogueRunner for jenny ([#2113](https://github.com/flame-engine/flame/issues/2113)). ([5ba6ff21](https://github.com/flame-engine/flame/commit/5ba6ff21a633a9f80e15228faaa31c6f0a3df60c))\n - **FEAT**: Support commands outside of nodes ([#2145](https://github.com/flame-engine/flame/issues/2145)). ([b313d630](https://github.com/flame-engine/flame/commit/b313d6302d713bda7baee7e90ecdb2fef2a3d6fc))\n - **FEAT**: Parser for jenny ([#2103](https://github.com/flame-engine/flame/issues/2103)). ([4e4117c8](https://github.com/flame-engine/flame/commit/4e4117c8a25a24686d6f571a9a5a23e19d660282))\n - **DOCS**: Update readme for jenny ([#2266](https://github.com/flame-engine/flame/issues/2266)). ([79129e4a](https://github.com/flame-engine/flame/commit/79129e4a72cec7c5bcfd67c17f9718b7528ac08c))\n - **DOCS**: Documentation for markup in Jenny ([#2262](https://github.com/flame-engine/flame/issues/2262)). ([8b57eaa1](https://github.com/flame-engine/flame/commit/8b57eaa1abc88d154ff45fdab6932bd15fe6eef7))\n - **DOCS**: Documentation for built-in functions in Jenny ([#2258](https://github.com/flame-engine/flame/issues/2258)). ([2eac6f5a](https://github.com/flame-engine/flame/commit/2eac6f5aa9485458203df7f41dc8c3718973eb61))\n - **DOCS**: Added documentation for basic expressions in Jenny ([#2256](https://github.com/flame-engine/flame/issues/2256)). ([69c13568](https://github.com/flame-engine/flame/commit/69c13568e647225bd2a2994e24a45f8258af0d16))\n - **DOCS**: Description of jenny package ([#2102](https://github.com/flame-engine/flame/issues/2102)). ([a99c9303](https://github.com/flame-engine/flame/commit/a99c93038128f913b7df05a5ef3e041e607069b9))\n\n"
  },
  {
    "path": "packages/flame_jenny/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/flame_jenny/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nAdds support for <a href=\"https://pub.dev/packages/jenny\">jenny</a>, the Dart port of <a href=\"https://yarnspinner.dev/\">YarnSpinner</a>, to your <a href=\"https://github.com/flame-engine/flame\">Flame</a> games.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_jenny\" ><img src=\"https://img.shields.io/pub/v/flame_jenny.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n\n# flame_jenny\n\nThe **flame_jenny** library integrates `jenny` (a dialogue framework) into the Flame engine. It\noffers a number of components for displaying conversations within a game.\n\nThe `jenny` library, inspired by [YarnSpinner], is a framework for embedding dialogues, interactive\nconversations, narratives, or step-by-step tutorials.\n\n[YarnSpinner]: https://yarnspinner.dev/\n"
  },
  {
    "path": "packages/flame_jenny/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_jenny/jenny/CHANGELOG.md",
    "content": "## 1.5.1\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 1.5.0\n\n - **FEAT**: Expose additional flame_jenny Command classes ([#3641](https://github.com/flame-engine/flame/issues/3641)). ([8ef2587d](https://github.com/flame-engine/flame/commit/8ef2587de4a1bef1d745cc1f5a626a7e84c6230c))\n\n## 1.4.0\n\n - **FEAT**(Jenny): Add onCommandFinish lifecycle method to DialogueView ([#3600](https://github.com/flame-engine/flame/issues/3600)). ([bd5a4ca6](https://github.com/flame-engine/flame/commit/bd5a4ca68a46feb6734a70d5320bb7bf23b782d5))\n\n## 1.3.3\n\n - **FEAT**: Bump to new lint package ([#3545](https://github.com/flame-engine/flame/issues/3545)). ([bf6ee518](https://github.com/flame-engine/flame/commit/bf6ee51897591b7ad6e5f9da2193b1eeeaf026f4))\n\n## 1.3.2\n\n - **FIX**: Fix analyze issue on main ([#3265](https://github.com/flame-engine/flame/issues/3265)). ([f60b6e13](https://github.com/flame-engine/flame/commit/f60b6e134177495bcfd0f405a50f9e0e666b8b42))\n\n## 1.3.1\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n\n## 1.3.0\n\n - **FEAT**: Add new methods to CommandStorage to support more arguments ([#3035](https://github.com/flame-engine/flame/issues/3035)). ([21922620](https://github.com/flame-engine/flame/commit/219226201a8d0c6e301c388299277be95b585c0e))\n\n## 1.2.1\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n## 1.2.0\n\n - **FEAT**: Added lastline before choice ([#2822](https://github.com/flame-engine/flame/issues/2822)). ([3ef52524](https://github.com/flame-engine/flame/commit/3ef525246a0d3b1d02c470b5696164e677cdb6c4))\n\n## 1.1.1\n\n - **REFACTOR**: Remove unnecessary 'async' keyword across the codebase [DCM] ([#2803](https://github.com/flame-engine/flame/issues/2803)). ([2dfe0e5a](https://github.com/flame-engine/flame/commit/2dfe0e5a431213c7148ab6389e3e8c8dc49fbf3d))\n - **REFACTOR**: Avoid nested conditional expressions whenever possible [DCM] ([#2784](https://github.com/flame-engine/flame/issues/2784)). ([7b6a5712](https://github.com/flame-engine/flame/commit/7b6a571263ece3aa1a8267644d9118230a850830))\n - **DOCS**: Remove last broad cSpell bypass regex and fix all violations ([#2802](https://github.com/flame-engine/flame/issues/2802)). ([9b16b178](https://github.com/flame-engine/flame/commit/9b16b178048eb19b6596273fcf4daec83277c3b5))\n\n## 1.1.0\n\n - **REFACTOR**: Enable new DCM rule: avoid-cascade-after-if-null ([#2676](https://github.com/flame-engine/flame/issues/2676)). ([158fc34c](https://github.com/flame-engine/flame/commit/158fc34cae858cf8d0b5d3b5155763e02454779a))\n - **REFACTOR**: Remove unused variable on dialogue_view_test.dart ([#2673](https://github.com/flame-engine/flame/issues/2673)). ([b77802a5](https://github.com/flame-engine/flame/commit/b77802a5e3a3c2fa68650dfa5d5f2aaed0f9a147))\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **FIX**: Change DialogueView to a mixin class ([#2652](https://github.com/flame-engine/flame/issues/2652)). ([f3d4158b](https://github.com/flame-engine/flame/commit/f3d4158b83685b479b0e7373bfbc7f8a6c16d822))\n - **FEAT**(flame_jenny): Allow removal of functions and commands ([#2717](https://github.com/flame-engine/flame/issues/2717)). ([a097cc01](https://github.com/flame-engine/flame/commit/a097cc01bc2bf789684c23320b16018dfc9dc664))\n - **FEAT**(flame_jenny): Allow removal of variables ([#2716](https://github.com/flame-engine/flame/issues/2716)). ([eaa8c091](https://github.com/flame-engine/flame/commit/eaa8c091627e4dd743c88dda42d4da70dca40e8b))\n - **FEAT**(flame_jenny): Allow removal of characters ([#2715](https://github.com/flame-engine/flame/issues/2715)). ([3421f4f9](https://github.com/flame-engine/flame/commit/3421f4f944516d998460a5347125d8f100366c42))\n - **FEAT**(flame_jenny): Public access to variables to allow load/save ([#2689](https://github.com/flame-engine/flame/issues/2689)). ([1485f842](https://github.com/flame-engine/flame/commit/1485f8426ebb6dd1390a0062c5e83ed1c0461f21))\n - **DOCS**: Enable CSpell on tests ([#2723](https://github.com/flame-engine/flame/issues/2723)). ([e051298c](https://github.com/flame-engine/flame/commit/e051298cba76550229780438b1a589557c7b488d))\n - **DOCS**: Improved spellchecking ([#2722](https://github.com/flame-engine/flame/issues/2722)). ([2f973abe](https://github.com/flame-engine/flame/commit/2f973abe8b298a4f6f1164065783de560953d789))\n\n## 1.0.4\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n\n## 1.0.3\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n## 1.0.2\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n## 1.0.1\n\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Create \"dart\" domain extension ([#2278](https://github.com/flame-engine/flame/issues/2278)). ([3b87e838](https://github.com/flame-engine/flame/commit/3b87e838f6308867b52f7c0cec3fa07e5629f3dc))\n\n## 1.0.0\n\n - **FIX**: Jenny now always stringifies whole numbers without .0 ([#2265](https://github.com/flame-engine/flame/issues/2265)). ([f262b7ee](https://github.com/flame-engine/flame/commit/f262b7ee39a270f5bfbf3bf2be89d85549d16cd1))\n - **FIX**: Remove whitespace before a command in dialogue option ([#2187](https://github.com/flame-engine/flame/issues/2187)). ([00f0e330](https://github.com/flame-engine/flame/commit/00f0e330b429f5f7ae87742ff5814f44924cb202))\n - **FIX**: Remove flutter from jenny ([#2162](https://github.com/flame-engine/flame/issues/2162)). ([29db304d](https://github.com/flame-engine/flame/commit/29db304d36fdf791f6c9df4c69b95511190b3057))\n - **FEAT**: Added the <<character>> command to Jenny ([#2274](https://github.com/flame-engine/flame/issues/2274)). ([6548e9cb](https://github.com/flame-engine/flame/commit/6548e9cb0a91353489812e211c2aa098fbd04f55))\n - **FEAT**: Added if() built-in function in Jenny ([#2259](https://github.com/flame-engine/flame/issues/2259)). ([087229ed](https://github.com/flame-engine/flame/commit/087229ede545644026eb6c303a037a93a792eaf2))\n - **FEAT**: Added command <<visit>> ([#2233](https://github.com/flame-engine/flame/issues/2233)). ([a90f90ef](https://github.com/flame-engine/flame/commit/a90f90efc5556f9697d409fd6a1e6558ae9e8236))\n - **FEAT**: OnDialogueChoice now returns null by default ([#2234](https://github.com/flame-engine/flame/issues/2234)). ([e2ab129e](https://github.com/flame-engine/flame/commit/e2ab129e5974485241223528fc50f3049ffecf8f))\n - **FEAT**: Added DialogueView.onNodeFinish event ([#2229](https://github.com/flame-engine/flame/issues/2229)). ([19a1f09a](https://github.com/flame-engine/flame/commit/19a1f09acc45199a4411c7026b8adf61a5a5a11f))\n - **FEAT**: Arguments of a UserDefinedCommand are now accessible ([#2224](https://github.com/flame-engine/flame/issues/2224)). ([0a9eaf38](https://github.com/flame-engine/flame/commit/0a9eaf380194e93c89cb8b2f5677d476a33eb83b))\n - **FEAT**: Added escape sequence \\- in yarn language ([#2220](https://github.com/flame-engine/flame/issues/2220)). ([43eacdd1](https://github.com/flame-engine/flame/commit/43eacdd1f5e1419c310f5cd34d1476adf03eb4d6))\n - **FEAT**: Add support for user-defined functions in jenny ([#2194](https://github.com/flame-engine/flame/issues/2194)). ([9364a0dd](https://github.com/flame-engine/flame/commit/9364a0dd324a2ed57b1e9a8907108da796e59352))\n - **FEAT**: Support for builtin functions in jenny ([#2192](https://github.com/flame-engine/flame/issues/2192)). ([82d35b8a](https://github.com/flame-engine/flame/commit/82d35b8a5dc8a9378dfee348b3392d0afabf2bc8))\n - **FEAT**: Add command <<local>> ([#2185](https://github.com/flame-engine/flame/issues/2185)). ([9e677e7d](https://github.com/flame-engine/flame/commit/9e677e7dc74bbe15b8521ec945a5b92ce8a4180a))\n - **FEAT**: Added support for markup attributes ([#2183](https://github.com/flame-engine/flame/issues/2183)). ([f887545b](https://github.com/flame-engine/flame/commit/f887545b127b41412b29217c52f9ec6ea0d6c885))\n - **FEAT**: Support user-defined commands ([#2168](https://github.com/flame-engine/flame/issues/2168)). ([ffb36a89](https://github.com/flame-engine/flame/commit/ffb36a89efdcd976fe63c16f27741b77b08aa284))\n - **FEAT**: Added the <<set>> command ([#2155](https://github.com/flame-engine/flame/issues/2155)). ([2b306d9e](https://github.com/flame-engine/flame/commit/2b306d9ee9c92416fe82b42e9a4ee33b280af46f))\n - **FEAT**: Implement the <<declare>> command ([#2154](https://github.com/flame-engine/flame/issues/2154)). ([8d592f17](https://github.com/flame-engine/flame/commit/8d592f17411800a5239720687149122eaf7750f1))\n - **FEAT**: Trim whitespace at the end of dialogue lines ([#2149](https://github.com/flame-engine/flame/issues/2149)). ([9c25e631](https://github.com/flame-engine/flame/commit/9c25e631e2e5ed5c593dbca4f498105e2c8fff66))\n - **FEAT**: DialogueRunner for jenny ([#2113](https://github.com/flame-engine/flame/issues/2113)). ([5ba6ff21](https://github.com/flame-engine/flame/commit/5ba6ff21a633a9f80e15228faaa31c6f0a3df60c))\n - **FEAT**: Support commands outside of nodes ([#2145](https://github.com/flame-engine/flame/issues/2145)). ([b313d630](https://github.com/flame-engine/flame/commit/b313d6302d713bda7baee7e90ecdb2fef2a3d6fc))\n - **FEAT**: Parser for jenny ([#2103](https://github.com/flame-engine/flame/issues/2103)). ([4e4117c8](https://github.com/flame-engine/flame/commit/4e4117c8a25a24686d6f571a9a5a23e19d660282))\n - **DOCS**: Update readme for jenny ([#2266](https://github.com/flame-engine/flame/issues/2266)). ([79129e4a](https://github.com/flame-engine/flame/commit/79129e4a72cec7c5bcfd67c17f9718b7528ac08c))\n - **DOCS**: Documentation for markup in Jenny ([#2262](https://github.com/flame-engine/flame/issues/2262)). ([8b57eaa1](https://github.com/flame-engine/flame/commit/8b57eaa1abc88d154ff45fdab6932bd15fe6eef7))\n - **DOCS**: Documentation for built-in functions in Jenny ([#2258](https://github.com/flame-engine/flame/issues/2258)). ([2eac6f5a](https://github.com/flame-engine/flame/commit/2eac6f5aa9485458203df7f41dc8c3718973eb61))\n - **DOCS**: Added documentation for basic expressions in Jenny ([#2256](https://github.com/flame-engine/flame/issues/2256)). ([69c13568](https://github.com/flame-engine/flame/commit/69c13568e647225bd2a2994e24a45f8258af0d16))\n - **DOCS**: Description of jenny package ([#2102](https://github.com/flame-engine/flame/issues/2102)). ([a99c9303](https://github.com/flame-engine/flame/commit/a99c93038128f913b7df05a5ef3e041e607069b9))\n\n"
  },
  {
    "path": "packages/flame_jenny/jenny/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/flame_jenny/jenny/README.md",
    "content": "# Jenny\n\nThe **jenny** library supports building *dialogues*, interactive *conversations*, *narratives*,\nor non-linear *story-telling*. The dialogue itself is written in a simple text-based Yarn language.\nThe `jenny` library allows you to load these `yarn` files into a YarnProject and then present this\ndialogue to users within a game or an app.\n\nThe `jenny` library mimics the features and functionality of the [Yarn Spinner] library for Unity.\n\nSee Jenny's [official documentation site] for more information.\n\n\n[Yarn Spinner]: https://docs.yarnspinner.dev/\n[official documentation site]: https://docs.flame-engine.org/main/other_modules/jenny/index.html\n"
  },
  {
    "path": "packages/flame_jenny/jenny/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    avoid_equals_and_hash_code_on_mutable_classes: false\n    hash_and_equals: false\n    unawaited_futures: true\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/jenny.dart",
    "content": "export 'src/character.dart' show Character;\nexport 'src/character_storage.dart' show CharacterStorage;\nexport 'src/command_storage.dart' show CommandStorage;\nexport 'src/dialogue_runner.dart' show DialogueRunner;\nexport 'src/dialogue_view.dart' show DialogueView;\nexport 'src/errors.dart' show SyntaxError, NameError, TypeError, DialogueError;\nexport 'src/function_storage.dart' show FunctionStorage;\nexport 'src/localization.dart' show Localization, localizationInfo;\nexport 'src/structure/commands/command.dart' show Command;\nexport 'src/structure/commands/jump_command.dart' show JumpCommand;\nexport 'src/structure/commands/user_defined_command.dart'\n    show UserDefinedCommand;\nexport 'src/structure/commands/visit_command.dart' show VisitCommand;\nexport 'src/structure/dialogue_choice.dart' show DialogueChoice;\nexport 'src/structure/dialogue_line.dart' show DialogueLine;\nexport 'src/structure/dialogue_option.dart' show DialogueOption;\nexport 'src/structure/markup_attribute.dart' show MarkupAttribute;\nexport 'src/structure/node.dart' show Node;\nexport 'src/variable_storage.dart' show VariableStorage;\nexport 'src/yarn_project.dart' show YarnProject;\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/character.dart",
    "content": "import 'package:jenny/jenny.dart';\n\n/// A **Character** represents a person who is speaking a particular line in a\n/// dialogue. This object is available as the `.character` property of a\n/// [DialogueLine] delivered to your [DialogueView].\n///\n/// All characters must be declared with the help of the `<<character>>`\n/// command, unless [YarnProject]'s setting `strictCharacterNames` is set to\n/// false.\nclass Character {\n  Character(this.name, {List<String>? aliases}) : aliases = aliases ?? [];\n\n  Map<String, dynamic>? _data;\n\n  /// The canonical name of the character, which was the first argument in the\n  /// [<<character>>] command.\n  final String name;\n\n  /// Additional names (IDs) that may be used for this character in yarn\n  /// scripts.\n  final List<String> aliases;\n\n  /// Additional information associated with this character. This may include\n  /// their short bio, portrait, affiliation, color, etc. This information\n  /// must be stored for each character manually, and then it will be\n  /// accessible from [DialogueView]s.\n  ///\n  /// You can store any key-value pairs here that you want, or nothing at all.\n  Map<String, dynamic> get data => _data ??= <String, dynamic>{};\n\n  @override\n  String toString() => 'Character($name)';\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/character_storage.dart",
    "content": "import 'package:jenny/src/character.dart';\nimport 'package:meta/meta.dart';\n\n/// The **CharacterStorage** is a cache for all [Character]s defined in your\n/// yarn scripts. This container is populated from the `<<character>>`\n/// commands as the YarnProject parses the input scripts.\nclass CharacterStorage {\n  final Map<String, Character> _cache = {};\n\n  bool get isEmpty => _cache.isEmpty;\n  bool get isNotEmpty => _cache.isNotEmpty;\n\n  /// Returns `true` if a character with the given name or alias was defined.\n  bool contains(String name) => _cache.containsKey(name);\n\n  /// Retrieves the character with the given name/alias, or returns `null` if\n  /// such character was not present.\n  Character? operator [](String name) => _cache[name];\n\n  /// Adds a new [character] to the container.\n  ///\n  /// This is intended for internal use; in yarn scripts use command\n  /// `<<character>>` to declare characters.\n  @internal\n  void add(Character character) {\n    _cache[character.name] = character;\n    for (final alias in character.aliases) {\n      _cache[alias] = character;\n    }\n  }\n\n  /// Clear all characters from storage.\n  ///\n  /// This could be used between scenes in preparation for loading a new\n  /// set of characters. It would not generally be used while a dialog is\n  /// in progress.\n  void clear() {\n    _cache.clear();\n  }\n\n  /// Remove a character by name. Its aliases will also be removed.\n  ///\n  /// This could be used if you are certain a character is no longer required.\n  /// It would not generally be used while a dialog is in progress.\n  void remove(String name) {\n    final character = _cache[name];\n    if (character != null) {\n      for (final alias in character.aliases) {\n        _cache.remove(alias);\n      }\n      _cache.remove(character.name);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/command_storage.dart",
    "content": "// ignore_for_file: library_private_types_in_public_api\n\nimport 'dart:async';\n\nimport 'package:jenny/jenny.dart';\nimport 'package:jenny/src/parse/ascii.dart';\nimport 'package:meta/meta.dart';\n\n/// [CommandStorage] is the repository of user-defined commands known to the\n/// YarnProject.\n///\n/// This repository is populated by the user, using commands [addCommand0],\n/// [addCommand1], [addCommand2], [addCommand3], [addCommand4], [addCommand5],\n/// and [addOrphanedCommand], depending on the arity of the function\n/// that needs to be invoked. All user-defined commands need to be declared\n/// before parsing any Yarn scripts.\nclass CommandStorage {\n  CommandStorage() : _commands = {};\n\n  final Map<String, _Cmd?> _commands;\n\n  /// Number of commands that have been registered.\n  int get length => _commands.length;\n  bool get isEmpty => _commands.isEmpty;\n  bool get isNotEmpty => _commands.isNotEmpty;\n\n  /// Returns `true` if command with the given [name] has been registered.\n  bool hasCommand(String name) => _commands.containsKey(name);\n\n  /// Registers a no-arguments function [fn] as a custom yarn command [name].\n  void addCommand0(String name, FutureOr<void> Function() fn) {\n    _checkName(name);\n    _commands[name] = _Cmd(name, const <Type>[], (List args) => fn());\n  }\n\n  /// Registers a single-argument function [fn] as a custom yarn command [name].\n  void addCommand1<T1>(String name, FutureOr<void> Function(T1) fn) {\n    _checkName(name);\n    _commands[name] = _Cmd(name, [T1], (List args) => fn(args[0] as T1));\n  }\n\n  /// Registers a 2-arguments function [fn] as a custom yarn command [name].\n  void addCommand2<T1, T2>(String name, FutureOr<void> Function(T1, T2) fn) {\n    _checkName(name);\n    _commands[name] = _Cmd(name, [\n      T1,\n      T2,\n    ], (List args) => fn(args[0] as T1, args[1] as T2));\n  }\n\n  /// Registers a 3-arguments function [fn] as a custom yarn command [name].\n  void addCommand3<T1, T2, T3>(\n    String name,\n    FutureOr<void> Function(T1, T2, T3) fn,\n  ) {\n    _checkName(name);\n    _commands[name] = _Cmd(\n      name,\n      [T1, T2, T3],\n      (List args) => fn(args[0] as T1, args[1] as T2, args[2] as T3),\n    );\n  }\n\n  /// Registers a 4-arguments function [fn] as a custom yarn command [name].\n  void addCommand4<T1, T2, T3, T4>(\n    String name,\n    FutureOr<void> Function(T1, T2, T3, T4) fn,\n  ) {\n    _checkName(name);\n    _commands[name] = _Cmd(\n      name,\n      [T1, T2, T3, T4],\n      (List args) => fn(\n        args[0] as T1,\n        args[1] as T2,\n        args[2] as T3,\n        args[3] as T4,\n      ),\n    );\n  }\n\n  /// Registers a 5-arguments function [fn] as a custom yarn command [name].\n  void addCommand5<T1, T2, T3, T4, T5>(\n    String name,\n    FutureOr<void> Function(T1, T2, T3, T4, T5) fn,\n  ) {\n    _checkName(name);\n    _commands[name] = _Cmd(\n      name,\n      [T1, T2, T3, T4, T5],\n      (List args) => fn(\n        args[0] as T1,\n        args[1] as T2,\n        args[2] as T3,\n        args[3] as T4,\n        args[4] as T5,\n      ),\n    );\n  }\n\n  /// Registers a command [name] which is not backed by any Dart function.\n  /// Instead, this command will be delivered directly to the dialogue views.\n  void addOrphanedCommand(String name) {\n    _commands[name] = null;\n  }\n\n  /// Executes the user-defined [command], if it is defined, otherwise does\n  /// nothing.\n  @internal\n  FutureOr<void> runCommand(UserDefinedCommand command) {\n    command.argumentString = command.content.evaluate();\n    final cmd = _commands[command.name];\n    if (cmd != null) {\n      final stringArgs = ArgumentsLexer(command.argumentString).tokenize();\n      final typedArgs = cmd.unpackArguments(stringArgs);\n      command.arguments = typedArgs;\n      return cmd.run(typedArgs);\n    }\n  }\n\n  /// Sanity checks for whether it is valid to add a command [name].\n  void _checkName(String name) {\n    assert(!hasCommand(name), 'Command <<$name>> has already been defined');\n    assert(!_builtinCommands.contains(name), 'Command <<$name>> is built-in');\n    assert(\n      _rxId.firstMatch(name) != null,\n      'Command name \"$name\" is not an identifier',\n    );\n  }\n\n  /// Clear all commands from storage.\n  void clear() {\n    _commands.clear();\n  }\n\n  /// Remove a command by [name].\n  void remove(String name) {\n    _commands.remove(name);\n  }\n\n  static final _rxId = RegExp(r'^[a-zA-Z_]\\w*$');\n\n  /// The list of built-in commands in Jenny; the user is not allowed to\n  /// register a command with the same name. Some of the commands in the list\n  /// are reserved for future use.\n  static const List<String> _builtinCommands = [\n    'declare',\n    'else',\n    'elseif',\n    'endif',\n    'for',\n    'if',\n    'jump',\n    'local',\n    'set',\n    'stop',\n    'stop',\n    'visit',\n    'wait',\n    'while',\n  ];\n}\n\n/// A wrapper around Dart function, which allows that function to be invoked\n/// dynamically from the Yarn runtime.\nclass _Cmd {\n  _Cmd(this.name, List<Type> types, this._wrappedFn)\n    : _signature = _unpackTypes(types),\n      _arguments = List<dynamic>.filled(types.length, null) {\n    numTrailingBooleans = _signature.reversed\n        .takeWhile((type) => type == _Type.boolean)\n        .length;\n  }\n\n  final String name;\n  final List<_Type> _signature;\n  final FutureOr<void> Function(List<dynamic>) _wrappedFn;\n  final List<dynamic> _arguments;\n  late final int numTrailingBooleans;\n\n  FutureOr<void> run(List<dynamic> arguments) {\n    return _wrappedFn(arguments);\n  }\n\n  List<dynamic> unpackArguments(List<String> stringArguments) {\n    if (stringArguments.length > _arguments.length ||\n        stringArguments.length + numTrailingBooleans < _arguments.length) {\n      String plural(int num, String word) => '$num $word${num == 1 ? '' : 's'}';\n      throw TypeError(\n        'Command <<$name>> expects ${plural(_arguments.length, 'argument')} '\n        'but received ${plural(stringArguments.length, 'argument')}',\n      );\n    }\n    for (var i = 0; i < numTrailingBooleans; i++) {\n      _arguments[_arguments.length - i - 1] = false;\n    }\n    for (var i = 0; i < stringArguments.length; i++) {\n      final strValue = stringArguments[i];\n      switch (_signature[i]) {\n        case _Type.boolean:\n          if (YarnProject.falseValues.contains(strValue)) {\n            _arguments[i] = false;\n          } else if (YarnProject.trueValues.contains(strValue)) {\n            _arguments[i] = true;\n          } else {\n            throw TypeError(\n              'Argument ${i + 1} for command <<$name>> has value \"$strValue\", '\n              'which is not a boolean',\n            );\n          }\n        case _Type.integer:\n          final value = int.tryParse(strValue);\n          if (value == null) {\n            throw TypeError(\n              'Argument ${i + 1} for command <<$name>> has value \"$strValue\", '\n              'which is not integer',\n            );\n          }\n          _arguments[i] = value;\n        case _Type.double:\n          final value = double.tryParse(strValue);\n          if (value == null) {\n            throw TypeError(\n              'Argument ${i + 1} for command <<$name>> has value \"$strValue\", '\n              'which is not a floating-point value',\n            );\n          }\n          _arguments[i] = value;\n        case _Type.numeric:\n          final value = num.tryParse(strValue);\n          if (value == null) {\n            throw TypeError(\n              'Argument ${i + 1} for command <<$name>> has value \"$strValue\", '\n              'which is not numeric',\n            );\n          }\n          _arguments[i] = value;\n        case _Type.string:\n          _arguments[i] = strValue;\n      }\n    }\n    return _arguments;\n  }\n\n  static List<_Type> _unpackTypes(List<Type> types) {\n    final result = List.filled(types.length, _Type.string);\n    for (var i = 0; i < types.length; i++) {\n      final expressionType = _typeMap[types[i]];\n      assert(\n        expressionType != null,\n        'Unsupported type ${types[i]} of argument ${i + 1}',\n      );\n      result[i] = expressionType!;\n    }\n    return result;\n  }\n\n  static const Map<Type, _Type> _typeMap = {\n    bool: _Type.boolean,\n    int: _Type.integer,\n    double: _Type.double,\n    num: _Type.numeric,\n    String: _Type.string,\n  };\n}\n\n@visibleForTesting\nclass ArgumentsLexer {\n  ArgumentsLexer(this.text);\n\n  final String text;\n  int position = 0;\n  List<_ModeFn> modeStack = [];\n  List<String> tokens = [];\n  StringBuffer buffer = StringBuffer();\n\n  List<String> tokenize() {\n    pushMode(modeStartOfArgument);\n    while (!eof) {\n      final ok = modeStack.last();\n      assert(ok);\n    }\n    if (modeStack.last == modeTextArgument) {\n      if (buffer.isNotEmpty) {\n        finalizeArgument();\n      }\n    } else if (modeStack.last == modeQuotedArgument) {\n      throw DialogueError('Unterminated quoted string');\n    }\n    assert(modeStack.last == modeStartOfArgument);\n    return tokens;\n  }\n\n  bool get eof => position >= text.length;\n\n  int get currentCodeUnit =>\n      position < text.length ? text.codeUnitAt(position) : -1;\n\n  bool pushMode(_ModeFn mode) {\n    modeStack.add(mode);\n    return true;\n  }\n\n  //----------------------------------------------------------------------------\n\n  bool modeStartOfArgument() {\n    return eatWhitespace() ||\n        (eatQuote() && pushMode(modeQuotedArgument)) ||\n        pushMode(modeTextArgument);\n  }\n\n  bool modeTextArgument() {\n    return (eatWhitespace() && finalizeArgument()) || eatCharacter();\n  }\n\n  bool modeQuotedArgument() {\n    return (eatQuote() && finalizeArgument() && checkWhitespaceAfterQuote()) ||\n        eatEscapedCharacter() ||\n        eatCharacter();\n  }\n\n  /// Returns true if current character is a whitespace, and skips over it.\n  bool eatWhitespace() {\n    final ch = currentCodeUnit;\n    if (ch == $space || ch == $tab) {\n      position += 1;\n      return true;\n    }\n    return false;\n  }\n\n  /// Returns true if current character is `\"`, and skips over it.\n  bool eatQuote() {\n    if (currentCodeUnit == $doubleQuote) {\n      position += 1;\n      return true;\n    }\n    return false;\n  }\n\n  /// Consumes any character and writes it into the buffer.\n  bool eatCharacter() {\n    buffer.writeCharCode(currentCodeUnit);\n    position += 1;\n    return true;\n  }\n\n  /// Consumes an escape sequence `\\\\`, `\\\"`, or `\\n` and writes the\n  /// corresponding unescaped character into the buffer.\n  bool eatEscapedCharacter() {\n    if (currentCodeUnit == $backslash) {\n      position += 1;\n      final ch = currentCodeUnit;\n      if (ch == $backslash || ch == $doubleQuote) {\n        buffer.writeCharCode(ch);\n      } else if (ch == $lowercaseN) {\n        buffer.writeCharCode($lineFeed);\n      } else {\n        throw DialogueError(\n          'Unrecognized escape sequence \\\\${String.fromCharCode(ch)}',\n        );\n      }\n      position += 1;\n      return true;\n    }\n    return false;\n  }\n\n  bool finalizeArgument() {\n    tokens.add(buffer.toString());\n    buffer.clear();\n    modeStack.removeLast();\n    assert(modeStack.last == modeStartOfArgument);\n    return true;\n  }\n\n  /// Throws an error if there is no whitespace after a quoted argument.\n  bool checkWhitespaceAfterQuote() {\n    if (eof || eatWhitespace()) {\n      return true;\n    }\n    throw DialogueError('Whitespace is required after a quoted argument');\n  }\n}\n\ntypedef _ModeFn = bool Function();\n\n/// Similar to `ExpressionType`, but also allows `integer`.\nenum _Type {\n  boolean,\n  integer,\n  double,\n  numeric,\n  string,\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/dialogue_runner.dart",
    "content": "import 'dart:async';\n\nimport 'package:jenny/src/dialogue_view.dart';\nimport 'package:jenny/src/errors.dart';\nimport 'package:jenny/src/structure/block.dart';\nimport 'package:jenny/src/structure/commands/command.dart';\nimport 'package:jenny/src/structure/commands/user_defined_command.dart';\nimport 'package:jenny/src/structure/dialogue_choice.dart';\nimport 'package:jenny/src/structure/dialogue_line.dart';\nimport 'package:jenny/src/structure/node.dart';\nimport 'package:jenny/src/yarn_project.dart';\nimport 'package:meta/meta.dart';\n\n/// The **DialogueRunner** is the engine that executes Jenny's dialogue at\n/// runtime.\n///\n/// If you imagine [YarnProject] as a \"program\", consisting of multiple [Node]s\n/// as \"functions\", then `DialogueRunner` is a virtual machine, capable of\n/// executing a single \"function\" in that \"program\".\n///\n/// A single `DialogueRunner` may only execute one dialogue node at a time. It\n/// is an error to try to run another node before the first one concludes.\n/// However, it is possible to create multiple `DialogueRunner`s for the same\n/// [YarnProject], and then they would be able to execute multiple dialogues\n/// simultaneously (for example, in a crowded room there could be multiple\n/// dialogues occurring at once within different groups of people).\n///\n/// The job of a `DialogueRunner` is to fetch the dialogue lines in the correct\n/// order and at the appropriate pace, to execute the logic in dialogue\n/// scripts, and to branch according to user input in [DialogueChoice]s. The\n/// output of a `DialogueRunner`, therefore, is a stream of dialogue statements\n/// that need to be presented to the player. Such presentation, is handled by\n/// [DialogueView]s.\nclass DialogueRunner {\n  /// Creates a `DialogueRunner` for executing the [yarnProject]. The dialogue\n  /// will be delivered to all the provided [dialogueViews]. Each of these\n  /// dialogue views may only be assigned to a single `DialogueRunner` at a\n  /// time.\n  DialogueRunner({\n    required YarnProject yarnProject,\n    required List<DialogueView> dialogueViews,\n  }) : project = yarnProject,\n       _dialogueViews = dialogueViews;\n\n  final List<DialogueView> _dialogueViews;\n  _LineDeliveryPipeline? _linePipeline;\n  Node? _currentNode;\n  NodeIterator? _currentIterator;\n  String? _initialNodeName;\n  String? _nextNode;\n\n  /// The `YarnProject` that this dialogue runner is executing.\n  final YarnProject project;\n\n  /// Starts the dialogue with the node [nodeName], and returns a future that\n  /// completes once the dialogue finishes running. While this future is\n  /// pending, the `DialogueRunner` cannot start any other dialogue.\n  Future<void> startDialogue(String nodeName) async {\n    try {\n      if (_initialNodeName != null) {\n        throw DialogueError(\n          'Cannot run node \"$nodeName\" because another node is '\n          'currently running: \"$_initialNodeName\"',\n        );\n      }\n      _initialNodeName = nodeName;\n      _dialogueViews.forEach((view) {\n        if (view.dialogueRunner != null) {\n          throw DialogueError(\n            'DialogueView is currently attached to another DialogueRunner',\n          );\n        }\n        view.dialogueRunner = this;\n      });\n      await _event((view) => view.onDialogueStart());\n      await _runNode(nodeName);\n      await _event((view) => view.onDialogueFinish());\n    } finally {\n      _dialogueViews.forEach((dv) => dv.dialogueRunner = null);\n      _initialNodeName = null;\n      _nextNode = null;\n      _currentIterator = null;\n      _currentNode = null;\n    }\n  }\n\n  /// Delivers the given [signal] to all dialogue views, in the form of a\n  /// [DialogueView] method `onLineSignal(line, signal)`. This can be used, for\n  /// example, as a means of communication between the dialogue views.\n  ///\n  /// The [signal] object here is completely arbitrary, and it is up to the\n  /// implementations to decide which signals to send and to receive.\n  /// Implementations should ignore any signals they do not understand.\n  void sendSignal(dynamic signal) {\n    assert(_linePipeline != null);\n    final line = _linePipeline!.line;\n    for (final view in _dialogueViews) {\n      view.onLineSignal(line, signal);\n    }\n  }\n\n  /// Requests (via `onLineStop()`) that the presentation of the current line\n  /// be finished as quickly as possible. The dialogue will then proceed\n  /// normally to the next line.\n  void stopLine() {\n    _linePipeline?.stop();\n  }\n\n  Future<void> _runNode(String nodeName) async {\n    _nextNode = nodeName;\n    while (_nextNode != null) {\n      final node = project.nodes[_nextNode!];\n      if (node == null) {\n        throw NameError('Node \"$_nextNode\" could not be found');\n      }\n\n      _nextNode = null;\n      _currentNode = node;\n      _currentIterator = node.iterator;\n\n      await _event((view) => view.onNodeStart(node));\n      while (_currentIterator?.moveNext() ?? false) {\n        final entry = _currentIterator!.current;\n        await entry.processInDialogueRunner(this);\n      }\n      _incrementNodeVisitCount();\n      await _event((view) => view.onNodeFinish(node));\n\n      _currentNode = null;\n      _currentIterator = null;\n    }\n  }\n\n  void _incrementNodeVisitCount() {\n    final nodeVariable = '@${_currentNode!.title}';\n    project.variables.setVariable(\n      nodeVariable,\n      project.variables.getNumericValue(nodeVariable) + 1,\n    );\n  }\n\n  @internal\n  Future<void> deliverLine(DialogueLine line) async {\n    final pipeline = _LineDeliveryPipeline(line, _dialogueViews);\n    _linePipeline = pipeline;\n    pipeline.start();\n    await pipeline.future;\n    _linePipeline = null;\n  }\n\n  @internal\n  Future<void> deliverChoices(DialogueChoice choice) async {\n    final futures = <Future<int?>>[];\n    final choices = <int>[];\n    for (final view in _dialogueViews) {\n      final futureOrResult = view.onChoiceStart(choice);\n      if (futureOrResult != null) {\n        if (futureOrResult is int) {\n          choices.add(futureOrResult);\n        } else {\n          // ignore: cast_nullable_to_non_nullable\n          futures.add(futureOrResult as Future<int?>);\n        }\n      }\n    }\n    for (final future in futures) {\n      final choice = await future;\n      if (choice != null) {\n        choices.add(choice);\n      }\n    }\n\n    if (choices.isEmpty) {\n      throw DialogueError('No option selected in a DialogueChoice');\n    }\n    final chosenIndex = choices.first;\n    if (chosenIndex < 0 || chosenIndex >= choice.options.length) {\n      throw DialogueError(\n        'Invalid option index chosen in a dialogue: $chosenIndex',\n      );\n    }\n    final chosenOption = choice.options[chosenIndex];\n    if (!chosenOption.isAvailable) {\n      throw DialogueError(\n        'A dialogue view selected a disabled option: $chosenOption',\n      );\n    }\n    await _event((view) => view.onChoiceFinish(chosenOption));\n    enterBlock(chosenOption.block);\n  }\n\n  @internal\n  Future<void> deliverCommand(Command command) async {\n    await _combineFutures([\n      // Start execution of commands\n      command.execute(this),\n      // Call [onCommand] for all the views registered with this runner\n      // so they can be notified that execution of the command has started.\n      if (command is UserDefinedCommand)\n        for (final view in _dialogueViews) view.onCommand(command),\n    ]);\n    // The thing we actually want to wait for is the result of\n    // [command.execute(this)]. It also makes sense to wait until all\n    // [onCommand] invocations are complete because conceptually,\n    // [onCommand] should always precede [onCommandFinish].\n    if (command is UserDefinedCommand) {\n      await _combineFutures([\n        for (final view in _dialogueViews) view.onCommandFinish(command),\n      ]);\n    }\n  }\n\n  @internal\n  void enterBlock(Block block) {\n    _currentIterator!.diveInto(block);\n  }\n\n  /// Stops the current node, and then starts running [nodeName]. If [nodeName]\n  /// is null, then stops the dialogue completely.\n  ///\n  /// This command is synchronous, i.e. it does not wait for the node to\n  /// *actually* finish (which calls the `onNodeFinish` callback).\n  @internal\n  void jumpToNode(String? nodeName) {\n    _currentIterator = null;\n    _nextNode = nodeName;\n  }\n\n  @internal\n  Future<void> visitNode(String nodeName) async {\n    final node = _currentNode;\n    final iterator = _currentIterator;\n    await _runNode(nodeName);\n    _currentNode = node;\n    _currentIterator = iterator;\n  }\n\n  /// Similar to `Future.wait()`, but accepts `FutureOr`s.\n  FutureOr<void> _combineFutures(List<FutureOr<void>> maybeFutures) {\n    final futures = maybeFutures.whereType<Future<void>>().toList();\n    if (futures.isNotEmpty) {\n      if (futures.length == 1) {\n        return futures[0];\n      } else {\n        final Future<void> result = Future.wait(futures);\n        return result;\n      }\n    }\n  }\n\n  FutureOr<void> _event(FutureOr<void> Function(DialogueView) callback) {\n    return _combineFutures([for (final view in _dialogueViews) callback(view)]);\n  }\n}\n\nclass _LineDeliveryPipeline {\n  _LineDeliveryPipeline(this.line, this.views)\n    : _completer = Completer(),\n      _futures = List.generate(views.length, (i) => null, growable: false);\n\n  final DialogueLine line;\n  final List<DialogueView> views;\n  final List<FutureOr<void>> _futures;\n  final Completer<void> _completer;\n  int _numPendingFutures = 0;\n  bool _interrupted = false;\n\n  Future<void> get future => _completer.future;\n\n  void start() {\n    assert(_numPendingFutures == 0);\n    for (var i = 0; i < views.length; i++) {\n      final maybeFuture = views[i].onLineStart(line);\n      if (maybeFuture is Future) {\n        // ignore: cast_nullable_to_non_nullable\n        final future = maybeFuture as Future<bool>;\n        _futures[i] = future.then((_) => startCompleted(i));\n        _numPendingFutures++;\n      } else {\n        continue;\n      }\n    }\n    if (_numPendingFutures == 0) {\n      finish();\n    }\n  }\n\n  void stop() {\n    _interrupted = true;\n    for (var i = 0; i < views.length; i++) {\n      if (_futures[i] != null) {\n        final newFuture = views[i].onLineStop(line);\n        _futures[i] = newFuture;\n        if (newFuture == null) {\n          _numPendingFutures -= 1;\n        }\n      }\n    }\n    if (_numPendingFutures == 0) {\n      finish();\n    }\n  }\n\n  void finish() {\n    assert(_numPendingFutures == 0);\n    for (var i = 0; i < views.length; i++) {\n      final maybeFuture = views[i].onLineFinish(line);\n      if (maybeFuture is Future) {\n        // ignore: unnecessary_cast\n        final future = maybeFuture as Future<void>;\n        _futures[i] = future.then((_) => finishCompleted(i));\n        _numPendingFutures++;\n      } else {\n        continue;\n      }\n    }\n    if (_numPendingFutures == 0) {\n      _completer.complete();\n    }\n  }\n\n  void startCompleted(int i) {\n    if (!_interrupted) {\n      assert(_futures[i] != null);\n      assert(_numPendingFutures > 0);\n      _futures[i] = null;\n      _numPendingFutures -= 1;\n      if (_numPendingFutures == 0) {\n        finish();\n      }\n    }\n  }\n\n  void finishCompleted(int i) {\n    assert(_futures[i] != null);\n    assert(_numPendingFutures > 0);\n    _futures[i] = null;\n    _numPendingFutures -= 1;\n    if (_numPendingFutures == 0) {\n      _completer.complete();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/dialogue_view.dart",
    "content": "import 'dart:async';\n\nimport 'package:jenny/src/dialogue_runner.dart';\nimport 'package:jenny/src/structure/commands/user_defined_command.dart';\nimport 'package:jenny/src/structure/dialogue_choice.dart';\nimport 'package:jenny/src/structure/dialogue_line.dart';\nimport 'package:jenny/src/structure/dialogue_option.dart';\nimport 'package:jenny/src/structure/node.dart';\nimport 'package:jenny/src/yarn_project.dart';\nimport 'package:meta/meta.dart';\n\n/// The **DialogueView** class is the main mechanism for integrating Jenny with\n/// a game engine. This class describes how {ref}`line <DialogueLine>`s and\n/// {ref}`option <DialogueOption>`s are presented to the user.\n///\n/// There are two ways to use this class:\n///\n/// - Extending DialogueView\n/// - Adding DialogueView as a mixin\n///\n/// In both cases you will need to create concrete implementations of the\n/// abstract event handler methods in order to use Jenny's dialogue system.\n/// The concrete `DialogueView` objects will then be passed to a\n/// [DialogueRunner], which will orchestrate the dialogue's progression.\n///\n/// The class defines a number of \"event handler\" methods, which can be\n/// overridden in subclasses in order to respond to the corresponding event.\n/// Each method has a default no-op implementation, which means you only need\n/// to override those methods that you care about.\n///\n/// Most of the event handler methods return [FutureOr], which means they can\n/// be implemented either synchronously or asynchronously. In the latter case\n/// the dialogue runner will wait for the future to resolve before proceeding\n/// (futures from several dialogue views will be awaited simultaneously).\nabstract mixin class DialogueView {\n  DialogueRunner? _dialogueRunner;\n\n  /// The owner of this `DialogueView`. This property will be `null` when the\n  /// dialogue view hasn't been attached to any `DialogueRunner` yet.\n  ///\n  /// This property can be used in order to access the parent [YarnProject],\n  /// or to send signals into the sibling `DialogueView`s.\n  DialogueRunner? get dialogueRunner => _dialogueRunner;\n  @internal\n  set dialogueRunner(DialogueRunner? value) => _dialogueRunner = value;\n\n  /// Called before the start of a new dialogue, i.e. before any lines, options,\n  /// or commands are delivered.\n  ///\n  /// This method is a good place to prepare the game's UI, such as fading in/\n  /// animating dialogue panels, or loading resources. If this method returns a\n  /// future, then the dialogue will start running only after the future\n  /// completes.\n  FutureOr<void> onDialogueStart() {}\n\n  /// Called when the dialogue has ended.\n  ///\n  /// This method can be used to clean up any of the dialogue UI. The returned\n  /// future will be awaited before the dialogue runner considers its job\n  /// finished.\n  FutureOr<void> onDialogueFinish() {}\n\n  /// Called when the dialogue enters a new [node].\n  ///\n  /// This will be called immediately after the [onDialogueStart], and then\n  /// possibly several times more over the course of the dialogue if it jumps\n  /// to other nodes. This method is a good place to perform node-specific\n  /// initialization, for example by querying the [node]'s properties or\n  /// metadata.\n  ///\n  /// If this method returns a future, then the dialogue runner will wait for it\n  /// to complete before proceeding with the actual dialogue.\n  FutureOr<void> onNodeStart(Node node) {}\n\n  /// Called when the dialogue exits the [node].\n  ///\n  /// For example, during a [[<<jump>>]] this callback will be called with the\n  /// current node, and then [onNodeStart] will be called with the new node.\n  /// Similarly, the command [[<<stop>>]] will trigger this callback too. At the\n  /// same time, during [[<<visit>>]] this callback will not be invoked.\n  ///\n  /// This callback can be used to clean up any preparations that were\n  /// performed in [onNodeStart].\n  FutureOr<void> onNodeFinish(Node node) {}\n\n  /// Called when the next dialogue [line] should be presented to the user.\n  ///\n  /// The [DialogueView] may decide to present the [line] in whatever way it\n  /// wants, or to not present the line at all. For example, the dialogue view\n  /// may: augment the line object, render the line at a certain place on the\n  /// screen, render only the character's name, show the portrait of whoever is\n  /// speaking, show the text within a chat bubble, play a voice-over audio\n  /// file, store the text into the player's conversation log, move the camera\n  /// to show the speaker, etc.\n  ///\n  /// Some of these methods of delivery can be considered \"primary\", while\n  /// others are \"auxiliary\". A \"primary\" [DialogueView] should return `true`,\n  /// while all others `false` (especially if a dialogue view ignores the line\n  /// completely). This is used as a robustness check: if none of the dialogue\n  /// views return `true`, then a `DialogueError` will be thrown because the\n  /// line was not shown to the user in a meaningful way.\n  ///\n  /// If this method returns a future, then the dialogue runner will wait for\n  /// that future to complete before advancing to the next line. If multiple\n  /// [DialogueView]s return such futures, then the dialogue runner will wait\n  /// for all of them to complete before proceeding.\n  ///\n  /// Returning a future is quite common for non-trivial [DialogueView]s. After\n  /// all, if this method were to return immediately, the dialogue runner would\n  /// immediately advance to the next line, and the player wouldn't have time\n  /// to read the first one. A common scenario then is to reveal the line\n  /// gradually, and then wait some time before returning; or, alternatively,\n  /// return a [Completer]-based future that completes based on some user action\n  /// such as clicking a button or pressing a keyboard key.\n  ///\n  /// Note that this method is supposed to only *show* the line to the player,\n  /// so do not try to hide it at the end -- for that, there is a dedicated\n  /// method [onLineFinish].\n  ///\n  /// Also, given that this method may take a significant amount of time, there\n  /// are two additional methods that may attempt to interfere into this\n  /// process: [onLineSignal] and [onLineStop].\n  FutureOr<bool> onLineStart(DialogueLine line) => false;\n\n  /// Called when the dialogue runner sends a [signal] to all dialogue views.\n  ///\n  /// The signal will be sent to all views, regardless of whether they have\n  /// finished running [onLineStart] or not. The interpretation of the signal\n  /// and the appropriate response is up to each dialogue view.\n  ///\n  /// For example, one possible scenario would be to speed up a typewriter\n  /// effect and reveal the text immediately in response to the RUSH signal.\n  /// Or make some kind of an interjection in response to an OMG event. Or\n  /// pause presentation in response to a PAUSE signal. Or give a warning if\n  /// the player makes a hostile gesture such as drawing a weapon.\n  void onLineSignal(DialogueLine line, dynamic signal) {}\n\n  /// Called when the game demands that the [line] finished presenting as soon\n  /// as possible.\n  ///\n  /// By itself, the dialogue runner will never call this method. However, it\n  /// may be invoked as a result of an explicit request by the game (or by one\n  /// of the dialogue views). Examples when this could be appropriate: (a) the\n  /// player was hit while talking to an NPC -- better stop talking and fight\n  /// for your life, (b) the user has pressed a \"skip dialogue\" button, so we\n  /// should stop the current line and proceed to the next one ASAP.\n  ///\n  /// This method returns a future that will be awaited before continuing to\n  /// the next line of the dialogue. At the same time, any future that's still\n  /// pending from the [onLineStart] call will be discarded and will no longer\n  /// be awaited. The [onLineFinish] method will not be called either.\n  FutureOr<void> onLineStop(DialogueLine line) {}\n\n  /// Called when the [line] has finished presenting in all dialogue views.\n  ///\n  /// Some dialogue views may need to clear their display when this event\n  /// happens, or make some other preparations to receive the next dialogue\n  /// line. If this method returns a future, that future will be awaited\n  /// before proceeding to the next line in the dialogue.\n  FutureOr<void> onLineFinish(DialogueLine line) {}\n\n  /// Called when the dialogue arrives at an option set, and the player must now\n  /// make a choice on how to proceed. If a dialogue view presents this choice\n  /// to the player and allows them to make a selection, then it must return a\n  /// future that completes when the choice is made. If the dialogue view does\n  /// not display menu choice, then it should return `null` (possibly in a\n  /// `Future`).\n  ///\n  /// The future returned by this method should deliver an integer value of the\n  /// index of the option that was selected. This index must not exceed the\n  /// length of the [choice] list, and the indicated option must not be marked\n  /// as \"unavailable\". If these conditions are violated, an exception will be\n  /// thrown.\n  FutureOr<int?> onChoiceStart(DialogueChoice choice) => null;\n\n  /// Called when the choice has been made, and the [option] was selected.\n  ///\n  /// The [option] will be the one returned from the [onChoiceStart] method by\n  /// one of the dialogue views.\n  FutureOr<void> onChoiceFinish(DialogueOption option) {}\n\n  /// Called when the dialogue encounters a [[user-defined command]].\n  ///\n  /// This method is invoked immediately after the command itself is executed,\n  /// but before the result of the execution was awaited.\n  /// (See [onCommandFinish]) Thus, if the command's effect is asynchronous,\n  /// then it will be send to dialogue views and executed *at the same time*.\n  ///\n  /// In cases when the command's effect occurs within the game, implementing\n  /// this method may not be necessary. However, if you want to have a command\n  /// that affects the dialogue views themselves, then this method provides\n  /// a way of achieving that.\n  FutureOr<void> onCommand(UserDefinedCommand command) {}\n\n  /// Called after the result of the [command] has been awaited.\n  FutureOr<void> onCommandFinish(UserDefinedCommand command) {}\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/errors.dart",
    "content": "class SyntaxError implements Exception {\n  SyntaxError(this.message);\n\n  final String? message;\n\n  @override\n  String toString() => 'SyntaxError: $message';\n}\n\n/// This error is emitted when accessing an unknown name, such as: undefined\n/// variable name, unknown node title, unrecognized function, unspecified\n/// command, etc.\nclass NameError implements Exception {\n  NameError(this.message);\n\n  final String? message;\n\n  @override\n  String toString() => 'NameError: $message';\n}\n\nclass TypeError implements Exception {\n  TypeError(this.message);\n\n  final String? message;\n\n  @override\n  String toString() => 'TypeError: $message';\n}\n\nclass DialogueError implements Exception {\n  DialogueError(this.message);\n\n  final String? message;\n\n  @override\n  String toString() => 'DialogueError: $message';\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/function_storage.dart",
    "content": "import 'package:jenny/src/errors.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/structure/expressions/functions/user_defined_function.dart';\nimport 'package:jenny/src/yarn_project.dart';\nimport 'package:meta/meta.dart';\n\n/// [FunctionStorage] is the container for all user-defined functions in a yarn\n/// project.\n///\n/// This repository is populates by the user, with methods [addFunction0],\n/// [addFunction1], [addFunction2], [addFunction3], [addFunction4], depending\n/// on the number of arguments of the function.\n///\n/// A function can be registered as a user-defined function if it satisfies the\n/// following conditions:\n/// - its return type is one of `int`, `double`, `num`, `bool`, or `String`;\n/// - all its arguments have types `int`, `int?`, `double`, `double?`, `num`,\n///   `num?`, `bool`, `bool?`, `String`, or `String?`;\n/// - the nullable arguments must be after the non-nullable ones. These\n///   arguments become optional in Yarn script, and if not provided they will\n///   be passed as `null` values;\n/// - the first argument in a function can also be `YarnProject`. If such\n///   argument is present, then it will be passed automatically. For example,\n///   if you have a function `fn(YarnProject, int)`, then it can be invoked\n///   from the yarn script simply as `fn(1)`.\n/// - the name does not match any of the built-in functions\n///\n/// The functions must be added to the YarnProject before parsing the yarn\n/// scripts, since the parser would throw an error if it sees a function which\n/// it does not recognize.\nclass FunctionStorage {\n  FunctionStorage();\n\n  /// The central repository of all functions registered in this\n  /// function storage.\n  final Map<String, Udf> _functions = {};\n\n  /// Number of functions that have been registered.\n  int get length => _functions.length;\n  bool get isEmpty => _functions.isEmpty;\n  bool get isNotEmpty => _functions.isNotEmpty;\n\n  /// Returns `true` if function with the given [name] has been registered.\n  bool hasFunction(String name) => _functions.containsKey(name);\n\n  /// Registers a no-arguments function [fn] as a custom yarn function [name].\n  void addFunction0<T0>(String name, T0 Function() fn) {\n    _checkName(name);\n    _functions[name] = Udf(name, T0, [], (args) => fn());\n  }\n\n  /// Registers a single-argument function [fn] with the given [name].\n  void addFunction1<T0, T1>(String name, T0 Function(T1) fn) {\n    _checkName(name);\n    _functions[name] = Udf(name, T0, [T1], (args) => fn(args[0] as T1));\n  }\n\n  /// Registers a two-argument function [fn] with the given [name].\n  void addFunction2<T0, T1, T2>(String name, T0 Function(T1, T2) fn) {\n    _checkName(name);\n    _functions[name] = Udf(name, T0, [\n      T1,\n      T2,\n    ], (args) => fn(args[0] as T1, args[1] as T2));\n  }\n\n  /// Registers a three-argument function [fn] with the given [name].\n  void addFunction3<T0, T1, T2, T3>(String name, T0 Function(T1, T2, T3) fn) {\n    _checkName(name);\n    _functions[name] = Udf(\n      name,\n      T0,\n      [T1, T2, T3],\n      (args) => fn(args[0] as T1, args[1] as T2, args[2] as T3),\n    );\n  }\n\n  /// Registers a four-argument function [fn] with the given [name].\n  void addFunction4<T0, T1, T2, T3, T4>(\n    String name,\n    T0 Function(T1, T2, T3, T4) fn,\n  ) {\n    _checkName(name);\n    _functions[name] = Udf(\n      name,\n      T0,\n      [T1, T2, T3, T4],\n      (args) => fn(args[0] as T1, args[1] as T2, args[2] as T3, args[3] as T4),\n    );\n  }\n\n  /// Returns a builder capable of creating function expressions. This method\n  /// is used by parse.dart.\n  @internal\n  FunctionBuilder? builderForFunction(String name) {\n    if (!hasFunction(name)) {\n      return null;\n    }\n    final function = _functions[name]!;\n    return (List<FunctionArgument> args, YarnProject yarn, ErrorFn errorFn) {\n      final arguments = function.checkAndUnpackArguments(args, errorFn);\n      function.useYarnProject(yarn);\n      return switch (function.returnType) {\n        ExpressionType.boolean => BooleanUserDefinedFn(function, arguments),\n        ExpressionType.numeric => NumericUserDefinedFn(function, arguments),\n        ExpressionType.string => StringUserDefinedFn(function, arguments),\n        _ => throw AssertionError('Bad return type'), // coverage:ignore-line\n      };\n    };\n  }\n\n  /// Sanity checks for whether it is valid to add a function [name].\n  void _checkName(String name) {\n    assert(!hasFunction(name), 'Function $name() has already been defined');\n    assert(\n      !builtinFunctions.containsKey(name),\n      'Function $name() is built-in',\n    );\n    assert(\n      _rxId.firstMatch(name) != null,\n      'Function name \"$name\" is not an identifier',\n    );\n  }\n\n  /// Clear all functions from storage.\n  void clear() {\n    _functions.clear();\n  }\n\n  /// Remove a function by [name].\n  void remove(String name) {\n    _functions.remove(name);\n  }\n\n  /// Regular expression that matches a valid identifier.\n  static final _rxId = RegExp(r'^[a-zA-Z_]\\w*$');\n}\n\n/// Wrapper for a user-provided function.\n///\n/// This wrapper encapsulates the knowledge about the function signature, and\n/// is capable of executing the underlying function given a plain list of\n@internal\nclass Udf {\n  Udf(this.name, Type returnType, List<Type> types, this._wrappedFn)\n    : _returnType = _convertReturnType(returnType),\n      _argumentTypes = _convertArgumentTypes(types),\n      _nOptionalArguments = _countOptionalArguments(types),\n      _preparedArguments = List<dynamic>.filled(types.length, null);\n\n  final String name;\n  final ExpressionType _returnType;\n  final List<_Type> _argumentTypes;\n  final int _nOptionalArguments;\n  final dynamic Function(List<dynamic>) _wrappedFn;\n  final List<dynamic> _preparedArguments;\n\n  ExpressionType get returnType => _returnType;\n\n  bool get hasYarnProjectArgument =>\n      _argumentTypes.isNotEmpty && _argumentTypes[0] == _Type.yarn;\n\n  void useYarnProject(YarnProject yarn) {\n    if (hasYarnProjectArgument) {\n      _preparedArguments[0] = yarn;\n    }\n  }\n\n  List<Expression> checkAndUnpackArguments(\n    List<FunctionArgument> args,\n    ErrorFn errorFn,\n  ) {\n    final i0 = hasYarnProjectArgument ? 1 : 0;\n    final maxArgs = _argumentTypes.length - i0;\n    final minArgs = maxArgs - _nOptionalArguments;\n    if (args.length < minArgs) {\n      errorFn(\n        'Function $name() expects ${minArgs == maxArgs ? '' : 'at least '}'\n        '${_plural(minArgs, 'argument')}',\n      );\n    }\n    if (args.length > maxArgs) {\n      errorFn(\n        'Function $name() expects ${minArgs == maxArgs ? '' : 'at most '}'\n        '${_plural(maxArgs, 'argument')}',\n        args[maxArgs].position,\n      );\n    }\n    final out = <Expression>[];\n    for (var i = 0; i < args.length; i++) {\n      final argType = args[i].expression.type;\n      final expectedType = _argumentTypes[i + i0];\n      final typesAreCompatible =\n          false ||\n          (argType == ExpressionType.boolean &&\n              expectedType == _Type.boolean) ||\n          (argType == ExpressionType.numeric &&\n              (expectedType == _Type.integer ||\n                  expectedType == _Type.double ||\n                  expectedType == _Type.numeric)) ||\n          (argType == ExpressionType.string && expectedType == _Type.string);\n      if (!typesAreCompatible) {\n        errorFn(\n          'Invalid type for argument $i: expected ${expectedType.name} but '\n          'received ${argType.name}',\n          args[i].position,\n        );\n      }\n      out.add(args[i].expression);\n    }\n    return out;\n  }\n\n  dynamic run(List<Expression> argExpressions) {\n    final i0 = hasYarnProjectArgument ? 1 : 0;\n    for (var i = i0; i < _preparedArguments.length; i++) {\n      _preparedArguments[i] = null;\n    }\n    for (var i = 0; i < argExpressions.length; i++) {\n      dynamic argValue = argExpressions[i].value;\n      if (_argumentTypes[i] == _Type.integer) {\n        argValue = (argValue as num).toInt();\n      }\n      if (_argumentTypes[i] == _Type.double) {\n        argValue = (argValue as num).toDouble();\n      }\n      _preparedArguments[i + i0] = argValue;\n    }\n    final dynamic result = _wrappedFn(_preparedArguments);\n    return result;\n  }\n\n  static ExpressionType _convertReturnType(Type type) {\n    if (type == String) {\n      return ExpressionType.string;\n    } else if (type == bool) {\n      return ExpressionType.boolean;\n    } else if (type == int || type == double || type == num) {\n      return ExpressionType.numeric;\n    }\n    throw TypeError(\n      'Unsupported return type <$type>, expected one of: bool, int, double, '\n      'num, or String',\n    );\n  }\n\n  static Type _getType<T>() => T;\n  static final Type _maybeInt = _getType<int?>();\n  static final Type _maybeBool = _getType<bool?>();\n  static final Type _maybeDouble = _getType<double?>();\n  static final Type _maybeNum = _getType<num?>();\n  static final Type _maybeString = _getType<String?>();\n\n  static List<_Type> _convertArgumentTypes(List<Type> types) {\n    final outTypes = <_Type>[];\n    for (final type in types) {\n      if (type == YarnProject) {\n        if (outTypes.isNotEmpty) {\n          throw TypeError(\n            'Argument of type YarnProject must be the first in a function',\n          );\n        }\n        outTypes.add(_Type.yarn);\n      } else if (type == int || type == _maybeInt) {\n        outTypes.add(_Type.integer);\n      } else if (type == num || type == _maybeNum) {\n        outTypes.add(_Type.numeric);\n      } else if (type == double || type == _maybeDouble) {\n        outTypes.add(_Type.double);\n      } else if (type == bool || type == _maybeBool) {\n        outTypes.add(_Type.boolean);\n      } else if (type == String || type == _maybeString) {\n        outTypes.add(_Type.string);\n      } else {\n        throw TypeError(\n          'Unsupported type <$type> for argument at index ${outTypes.length}',\n        );\n      }\n    }\n    return outTypes;\n  }\n\n  static int _countOptionalArguments(List<Type> types) {\n    var nOptionalArguments = 0;\n    for (final type in types) {\n      final isOptional =\n          false ||\n          (type == _maybeInt) ||\n          (type == _maybeBool) ||\n          (type == _maybeDouble) ||\n          (type == _maybeNum) ||\n          (type == _maybeString);\n      if (isOptional) {\n        nOptionalArguments += 1;\n      } else if (nOptionalArguments > 0) {\n        throw TypeError('Required arguments must come before the optional');\n      }\n    }\n    return nOptionalArguments;\n  }\n\n  static String _plural(int num, String singular) {\n    return '$num $singular${num == 1 ? '' : 's'}';\n  }\n}\n\n/// Similar to `ExpressionType`, but also allows `integer` and `double`.\nenum _Type {\n  boolean,\n  integer,\n  double,\n  numeric,\n  string,\n  yarn,\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/localization.dart",
    "content": "class Localization {\n  const Localization(\n    this.pluralFunction,\n    this.pluralMinWordCount, [\n    int? pluralMaxWordCount,\n  ]) : pluralMaxWordCount = pluralMaxWordCount ?? pluralMinWordCount;\n\n  final PluralFnType pluralFunction;\n  final int pluralMinWordCount;\n  final int pluralMaxWordCount;\n}\n\ntypedef PluralFnType = String Function(num, List<String>);\n\nfinal Map<String, Localization> localizationInfo = {\n  'az': const Localization(_plural8, 2),\n  'bg': const Localization(_plural8, 2),\n  'de': const Localization(_plural4, 2),\n  'el': const Localization(_plural8, 2),\n  'en': const Localization(_pluralEn, 1, 2),\n  'es': const Localization(_plural8, 2),\n  'et': const Localization(_plural4, 2),\n  'fi': const Localization(_plural4, 2),\n  'fr': const Localization(_plural2, 2),\n  'haw': const Localization(_plural8, 2),\n  'hu': const Localization(_plural8, 2),\n  'id': const Localization(_plural0, 1),\n  'it': const Localization(_plural4, 2),\n  'ja': const Localization(_plural0, 1),\n  'ka': const Localization(_plural8, 2),\n  'kk': const Localization(_plural8, 2),\n  'ko': const Localization(_plural0, 1),\n  'ky': const Localization(_plural8, 2),\n  'mn': const Localization(_plural8, 2),\n  'nl': const Localization(_plural4, 2),\n  'no': const Localization(_plural8, 2),\n  'pl': const Localization(_plural25, 3),\n  'pt': const Localization(_plural2, 2),\n  'ru': const Localization(_plural29, 3),\n  'sv': const Localization(_plural4, 2),\n  'th': const Localization(_plural0, 1),\n  'tk': const Localization(_plural8, 2),\n  'tr': const Localization(_plural8, 2),\n  'uk': const Localization(_plural29, 3),\n  'uz': const Localization(_plural8, 2),\n  'vi': const Localization(_plural0, 1),\n  'yi': const Localization(_plural4, 2),\n  'zh': const Localization(_plural0, 1),\n};\n\n//------------------------------------------------------------------------------\n// Plural functions\n//------------------------------------------------------------------------------\nconst Set<String> _enVowels = {'a', 'e', 'i', 'o', 'u'};\n\n/// English (en)\n///\n/// Either 1 or 2 arguments are accepted. In the first case the argument is the\n/// singular form of the word, and we attempt to form a plural by adding either\n/// '-s' or '-es'. In cases when this produces a wrong plural, the 2-argument\n/// form should be used, where the second argument is the plural form.\nString _pluralEn(num number, List<String> words) {\n  assert(words.isNotEmpty);\n  if (number == 1.0) {\n    return words[0];\n  }\n  if (words.length == 2) {\n    return words[1];\n  }\n  final singular = words[0];\n  final ch1 = singular.isNotEmpty ? singular[singular.length - 1] : '';\n  final ch2 = singular.length >= 2 ? singular[singular.length - 2] : '';\n  if ((ch2 == 's' && ch1 == 's') ||\n      (ch2 == 'c' && ch1 == 'h') ||\n      (ch2 == 's' && ch1 == 'h') ||\n      ch1 == 'x') {\n    return '${singular}es';\n  }\n  if (ch1 == 'y' && !_enVowels.contains(ch2)) {\n    return '${singular.substring(0, singular.length - 1)}ies';\n  }\n  return '${singular}s';\n}\n\n/// Indonesian (id), Japanese (ja), Korean (ko), Thai (th), Vietnamese (vi),\n/// Chinese (zh)\nString _plural0(num number, List<String> words) {\n  return words[0];\n}\n\n/// French (fr), Portuguese (pt)\nString _plural2(num number, List<String> words) {\n  final i = number.toInt();\n  return (i == 0 || i == 1) ? words[0] : words[1];\n}\n\n/// German (de), Estonian (et), Finnish (fi), Italian (it), Dutch (nl),\n/// Swedish (sv), Yiddish (yi)\nString _plural4(num number, List<String> words) {\n  if (number == 1.0) {\n    return words[0];\n  }\n  return words[1];\n}\n\n/// Azerbaijani (az), Bulgarian (bg), Greek (el), Spanish (es), Hawaiian (haw),\n/// Hungarian (hu), Georgian (ka), Kazakh (kk), Kyrgyz (ky), Mongolian (mn),\n/// Norwegian (no), Turkmen (tk), Turkish (tr), Uzbek (uz)\nString _plural8(num number, List<String> words) {\n  final n = number.abs();\n  return (n == 1.0) ? words[0] : words[1];\n}\n\n/// Polish (pl)\nString _plural25(num number, List<String> words) {\n  final i = number.toInt();\n  final iMod10 = i % 10;\n  final iMod100 = i % 100;\n  if (i == 1) {\n    return words[0];\n  }\n  if (iMod10 >= 2 && iMod10 <= 4 && (iMod100 < 12 || iMod100 > 14)) {\n    return words[1];\n  }\n  return words[2];\n}\n\n// used as examples of Ukrainian words on the documentation below\n// cSpell:ignore рушниця, рушниці, рушниць\n\n/// Ukrainian (uk), Russian (ru)\n///\n/// The function requires 3 forms of the word: single-, few-, and many-. You\n/// can think of them as words needed for \"1 X\", \"2 X\", and \"10 X\". For example,\n/// the words for \"рушниця\" (rifle) would be:\n/// ```\n/// plural(n, [\"рушниця\", \"рушниці\", \"рушниць\"])\n/// ```\nString _plural29(num number, List<String> words) {\n  final i = number.toInt();\n  if (i == number) {\n    final iMod10 = i % 10;\n    final iMod100 = i % 100;\n    if (iMod10 == 1 && iMod100 != 11) {\n      return words[0];\n    }\n    if (iMod10 >= 2 && iMod10 <= 4 && (iMod100 < 12 || iMod100 > 14)) {\n      return words[1];\n    }\n  }\n  return words[2];\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/parse/ascii.dart",
    "content": "//\n// ASCII codes for symbols needed for lexing.\n//\nconst int $tab = 0x09; //             '\\t'\nconst int $lineFeed = 0x0A; //        '\\n'\nconst int $carriageReturn = 0x0D; //  '\\r'\nconst int $space = 0x20; //            ' '\nconst int $exclamation = 0x21; //      '!'\nconst int $doubleQuote = 0x22; //      '\"'\nconst int $hash = 0x23; //             '#'\nconst int $dollar = 0x24; //           '$'\nconst int $percent = 0x25; //          '%'\nconst int $ampersand = 0x26; //        '&'\nconst int $singleQuote = 0x27; //     '\\''\nconst int $leftParenthesis = 0x28; //  '('\nconst int $rightParenthesis = 0x29; // ')'\nconst int $asterisk = 0x2a; //         '*'\nconst int $plus = 0x2b; //             '+'\nconst int $comma = 0x2c; //            ','\nconst int $minus = 0x2d; //            '-'\nconst int $dot = 0x2e; //              '.'\nconst int $slash = 0x2f; //            '/'\nconst int $digit0 = 0x30; //           '0'\nconst int $digit9 = 0x39; //           '9'\nconst int $colon = 0x3a; //            ':'\nconst int $lessThan = 0x3c; //         '<'\nconst int $equals = 0x3d; //           '='\nconst int $greaterThan = 0x3e; //      '>'\nconst int $uppercaseA = 0x41; //       'A'\nconst int $uppercaseZ = 0x5a; //       'Z'\nconst int $leftBracket = 0x5b; //      '['\nconst int $backslash = 0x5c; //       '\\\\'\nconst int $rightBracket = 0x5d; //     ']'\nconst int $caret = 0x5e; //            '^'\nconst int $underscore = 0x5f; //       '_'\nconst int $lowercaseA = 0x61; //       'a'\nconst int $lowercaseN = 0x6e; //       'n'\nconst int $lowercaseZ = 0x7a; //       'z'\nconst int $leftBrace = 0x7b; //        '{'\nconst int $pipe = 0x7c; //             '|'\nconst int $rightBrace = 0x7d; //       '}'\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/parse/parse.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:jenny/src/parse/token.dart';\nimport 'package:jenny/src/parse/tokenize.dart';\nimport 'package:jenny/src/structure/block.dart';\nimport 'package:jenny/src/structure/commands/character_command.dart';\nimport 'package:jenny/src/structure/commands/declare_command.dart';\nimport 'package:jenny/src/structure/commands/if_command.dart';\nimport 'package:jenny/src/structure/commands/local_command.dart';\nimport 'package:jenny/src/structure/commands/set_command.dart';\nimport 'package:jenny/src/structure/commands/stop_command.dart';\nimport 'package:jenny/src/structure/commands/wait_command.dart';\nimport 'package:jenny/src/structure/dialogue_entry.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/structure/expressions/functions/string.dart';\nimport 'package:jenny/src/structure/expressions/literal.dart';\nimport 'package:jenny/src/structure/expressions/operators/_common.dart'\n    hide ErrorFn;\nimport 'package:jenny/src/structure/expressions/operators/negate.dart';\nimport 'package:jenny/src/structure/expressions/operators/not.dart';\nimport 'package:jenny/src/structure/line_content.dart';\nimport 'package:meta/meta.dart';\n\n@internal\nvoid parse(String text, YarnProject project) {\n  final tokens = tokenize(text);\n  _Parser(project, text, tokens).parseMain();\n}\n\nclass _Parser {\n  _Parser(this.project, this.text, this.tokens) : position = 0;\n\n  final YarnProject project;\n  final String text;\n  final List<Token> tokens;\n  VariableStorage? localVariables;\n\n  /// The index of the next token to parse.\n  int position;\n\n  void parseMain() {\n    while (position < tokens.length) {\n      final token = peekToken();\n      if (token == Token.startCommand) {\n        final position0 = position;\n        final command = parseCommand();\n        if (command is! DeclareCommand && command is! CharacterCommand) {\n          position = position0;\n          typeError('command <<${command.name}>> is only allowed inside nodes');\n        }\n      } else if (token == Token.startHeader) {\n        break;\n      } else if (token == Token.newline) {\n        position += 1;\n      } else {\n        syntaxError('unexpected token: $token'); // coverage:ignore-line\n      }\n    }\n    while (position < tokens.length) {\n      if (peekToken() == Token.newline) {\n        position += 1;\n        continue;\n      }\n      final header = parseNodeHeader();\n      final block = parseNodeBody();\n      final name = header.title!;\n      project.nodes[name] = Node(\n        title: name,\n        tags: header.tags,\n        content: block,\n        variables: localVariables,\n      );\n      project.variables.setVariable('@$name', 0);\n      localVariables = null;\n    }\n  }\n\n  _NodeHeader parseNodeHeader() {\n    String? title;\n    final tags = <String, String>{};\n    take(Token.startHeader);\n    while (peekToken() != Token.endHeader) {\n      if (peekToken() == Token.newline) {\n        position += 1;\n        continue;\n      }\n      if (takeId() && take(Token.colon) && takeText() && takeNewline()) {\n        final id = peekToken(-4);\n        final text = peekToken(-2);\n        if (id.content == 'title') {\n          if (title != null) {\n            position -= 4;\n            syntaxError('a node can only have one title');\n          }\n          title = text.content;\n          if (project.nodes.containsKey(title)) {\n            position -= 4;\n            nameError(\n              'node with title \"$title\" has already been defined',\n            );\n          }\n        } else {\n          tags[id.content] = text.content;\n        }\n      }\n    }\n    take(Token.endHeader);\n    if (title == null) {\n      position -= 1;\n      syntaxError('node does not have a title');\n    }\n    return _NodeHeader(title, tags.isEmpty ? null : tags);\n  }\n\n  Block parseNodeBody() {\n    take(Token.startBody);\n    if (peekToken() == Token.startIndent) {\n      syntaxError('unexpected indent');\n    }\n    final out = parseStatementList();\n    take(Token.endBody);\n    return out;\n  }\n\n  Block parseStatementList() {\n    final lines = <DialogueEntry>[];\n    while (true) {\n      final nextToken = peekToken();\n      if (nextToken == Token.arrow) {\n        final option = parseOption();\n        if (lines.isNotEmpty && lines.last is DialogueChoice) {\n          (lines.last as DialogueChoice).options.add(option);\n        } else {\n          if (lines.isNotEmpty && lines.last is DialogueLine) {\n            final lastLine = lines.removeLast() as DialogueLine;\n            lines.add(\n              DialogueLine(\n                content: lastLine.content!,\n                character: lastLine.character,\n                tags: [...lastLine.tags, '#lastline'],\n              ),\n            );\n          }\n          lines.add(DialogueChoice([option]));\n        }\n      } else if (nextToken == Token.startCommand) {\n        final position0 = position;\n        final command = parseCommand();\n        if (command is DeclareCommand || command is CharacterCommand) {\n          syntaxError(\n            '<<${command.name}>> command cannot be used inside a node',\n            position0,\n          );\n        }\n        lines.add(command);\n      } else if (nextToken.isText ||\n          nextToken.isPerson ||\n          nextToken == Token.startExpression ||\n          nextToken == Token.startMarkupTag) {\n        lines.add(parseDialogueLine());\n      } else if (nextToken == Token.newline) {\n        position += 1;\n      } else {\n        break;\n      }\n    }\n    return Block(lines);\n  }\n\n  //#region Line parsing\n\n  /// Consumes a regular line of text from the input, up to and including the\n  /// NEWLINE token.\n  DialogueLine parseDialogueLine() {\n    final person = maybeParseLinePerson();\n    final content = parseLineContent();\n    final tags = maybeParseHashtags();\n    if (peekToken() == Token.startCommand) {\n      syntaxError('commands are not allowed on a dialogue line');\n    }\n    takeNewline();\n    return DialogueLine(\n      character: person,\n      content: content,\n      tags: tags,\n    );\n  }\n\n  DialogueOption parseOption() {\n    take(Token.arrow);\n    final person = maybeParseLinePerson();\n    final content = parseLineContent();\n    final condition = maybeParseLineCondition();\n    final tags = maybeParseHashtags();\n    if (peekToken() == Token.startCommand) {\n      syntaxError('multiple commands are not allowed on an option line');\n    }\n    take(Token.newline);\n    var block = const Block.empty();\n    if (peekToken() == Token.startIndent) {\n      position += 1;\n      block = parseStatementList();\n      take(Token.endIndent);\n    }\n    return DialogueOption(\n      content: content,\n      character: person,\n      tags: tags,\n      condition: condition,\n      block: block,\n    );\n  }\n\n  Character? maybeParseLinePerson() {\n    final token = peekToken();\n    if (token.isPerson) {\n      takePerson();\n      take(Token.colon);\n      final name = token.content;\n      if (project.strictCharacterNames && !project.characters.contains(name)) {\n        nameError('unknown character \"$name\"', position - 2);\n      }\n      return project.characters[name] ?? Character(name);\n    }\n    return null;\n  }\n\n  LineContent parseLineContent() {\n    final stringBuilder = StringBuffer();\n    final expressions = <InlineExpression>[];\n    final attributes = <MarkupAttribute>[];\n    final markupStack = <_Markup>[];\n    var subIndex = 0;\n    while (true) {\n      final token = peekToken();\n      if (token.isText) {\n        subIndex = 0;\n        stringBuilder.write(token.content);\n        position += 1;\n      } else if (token == Token.startExpression) {\n        subIndex += 1;\n        take(Token.startExpression);\n        final expression = parseExpression();\n        take(Token.endExpression);\n        expressions.add(\n          InlineExpression(\n            stringBuilder.length,\n            expression.isString\n                ? expression as StringExpression\n                : StringFn(expression),\n          ),\n        );\n      } else if (token == Token.startMarkupTag) {\n        take(Token.startMarkupTag);\n        final position0 = position;\n        final markupTag = parseMarkupTag();\n        take(Token.endMarkupTag);\n        if (markupTag.closing) {\n          if (markupStack.isEmpty) {\n            position = position0;\n            syntaxError('unexpected closing markup tag');\n          }\n          // close-all tag\n          if (markupTag.name == null) {\n            while (markupStack.isNotEmpty) {\n              final tag = markupStack.removeLast();\n              tag.endTextPosition = stringBuilder.length;\n              tag.endSubIndex = subIndex;\n              attributes.add(tag.build());\n            }\n          } else {\n            final openTag = markupStack.removeLast();\n            if (openTag.name != markupTag.name) {\n              position = position0 + 1;\n              syntaxError('Expected closing tag for [${openTag.name}]');\n            }\n            openTag.endTextPosition = stringBuilder.length;\n            openTag.endSubIndex = subIndex;\n            attributes.add(openTag.build());\n          }\n        } else {\n          markupTag.startTextPosition = stringBuilder.length;\n          markupTag.startSubIndex = subIndex;\n          // TODO(stpasha): check that the name of the markup tag is known\n          if (markupTag.selfClosing) {\n            markupTag.endTextPosition = stringBuilder.length;\n            markupTag.endSubIndex = subIndex;\n            attributes.add(markupTag.build());\n          } else {\n            markupStack.add(markupTag);\n          }\n        }\n      } else {\n        break;\n      }\n    }\n    if (markupStack.isNotEmpty) {\n      syntaxError('markup tag [${markupStack.last.name}] was not closed');\n    }\n    return LineContent(\n      stringBuilder.toString(),\n      expressions.isEmpty ? null : expressions,\n      attributes.isEmpty ? null : attributes,\n    );\n  }\n\n  BoolExpression? maybeParseLineCondition() {\n    final token = peekToken();\n    if (token == Token.startCommand) {\n      position += 1;\n      if (peekToken() != Token.commandIf) {\n        syntaxError('only \"if\" command is allowed for an option');\n      }\n      position += 1;\n      take(Token.startExpression);\n      final position0 = position;\n      final expression = parseExpression();\n      take(Token.endExpression);\n      if (!expression.isBoolean) {\n        position = position0;\n        typeError('the condition in \"if\" should be boolean');\n      }\n      take(Token.endCommand);\n      return expression as BoolExpression;\n    }\n    return null;\n  }\n\n  List<String>? maybeParseHashtags() {\n    final out = <String>[];\n    while (true) {\n      final token = peekToken();\n      if (token.isHashtag) {\n        out.add(token.content);\n        position += 1;\n      } else {\n        break;\n      }\n    }\n    return out.isEmpty ? null : out;\n  }\n\n  _Markup parseMarkupTag() {\n    final result = _Markup();\n    if (peekToken() == Token.closeMarkupTag) {\n      position += 1;\n      result.closing = true;\n      final nextToken = peekToken();\n      if (nextToken.isId) {\n        result.name = nextToken.content;\n        position += 1;\n      } else if (nextToken != Token.endMarkupTag) {\n        syntaxError('a markup tag name is expected');\n      }\n    } else {\n      final nextToken = peekToken();\n      if (nextToken.isId) {\n        result.name = nextToken.content;\n        position += 1;\n      } else {\n        syntaxError('a markup tag name is expected');\n      }\n      while (peekToken().isId) {\n        final position0 = position;\n        final parameter = peekToken().content;\n        position += 1;\n        final Expression expression;\n        if (peekToken() == Token.operatorAssign) {\n          position += 1;\n          expression = parseExpression();\n        } else {\n          expression = constTrue;\n        }\n        if (result.parameters.containsKey(parameter)) {\n          position = position0;\n          syntaxError('duplicate parameter $parameter in a markup attribute');\n        }\n        result.parameters[parameter] = expression;\n      }\n      final lastToken = peekToken();\n      if (lastToken == Token.closeMarkupTag) {\n        result.selfClosing = true;\n        position += 1;\n      }\n    }\n    return result;\n  }\n\n  //#endregion\n\n  //#region Commands parsing\n  //----------------------------------------------------------------------------\n  // COMMANDS\n  //\n  // The commands are the control structures of Yarn. Each builtin command has\n  // its own syntax, and therefore has to be parsed separately. In addition,\n  // there are also user-defined commands.\n  //\n  // See https://github.com/YarnSpinnerTool/YarnSpinner/blob/main/Documentation/Yarn-Spec.md#commands\n  //----------------------------------------------------------------------------\n\n  /// Parses and returns any line or multi-line command. On the other hand, this\n  /// function should not be used to parse line conditionals (i.e. commands at\n  /// the end of the line).\n  Command parseCommand() {\n    assert(peekToken() == Token.startCommand);\n    final token = peekToken(1);\n    if (token == Token.commandIf) {\n      return parseCommandIf();\n    } else if (token == Token.commandJump || token == Token.commandVisit) {\n      return parseCommandJumpOrVisit();\n    } else if (token == Token.commandStop) {\n      return parseCommandStop();\n    } else if (token == Token.commandWait) {\n      return parseCommandWait();\n    } else if (token == Token.commandSet) {\n      return parseCommandSet();\n    } else if (token == Token.commandDeclare || token == Token.commandLocal) {\n      return parseCommandDeclareOrLocal();\n    } else if (token == Token.commandCharacter) {\n      return parseCommandCharacter();\n    } else if (token == Token.commandElseif ||\n        token == Token.commandElse ||\n        token == Token.commandEndif) {\n      position += 1;\n      syntaxError('this command is only allowed after an <<if>>');\n    } else {\n      assert(token.isCommand, 'unimplemented $token');\n      return parseUserDefinedCommand();\n    }\n  }\n\n  /// Parses the `<<if>>` command, and the subsequent `<<elseif>>`, `<<else>>`,\n  /// up to and including the `<<endif>>`.\n  Command parseCommandIf() {\n    final parts = <IfBlock>[];\n    parts.add(parseCommandIfBlock('if'));\n\n    var hasElse = false;\n    while (true) {\n      final command = peekToken(1);\n      if (command == Token.commandElseif) {\n        parts.add(parseCommandIfBlock('elseif'));\n      } else if (command == Token.commandElse) {\n        if (hasElse) {\n          syntaxError('only one <<else>> is allowed');\n        }\n        parts.add(parseCommandElseBlock());\n        hasElse = true;\n      } else if (command == Token.commandEndif) {\n        take(Token.startCommand);\n        take(Token.commandEndif);\n        take(Token.endCommand);\n        takeNewline();\n        break;\n      } else {\n        syntaxError('<<endif>> expected');\n      }\n    }\n    return IfCommand(parts);\n  }\n\n  IfBlock parseCommandIfBlock(String commandName) {\n    take(Token.startCommand);\n    take(commandName == 'if' ? Token.commandIf : Token.commandElseif);\n    take(Token.startExpression);\n    final position0 = position;\n    final expression = parseExpression();\n    if (!expression.isBoolean) {\n      position = position0;\n      typeError('expression in an <<$commandName>> command must be boolean');\n    }\n    take(Token.endExpression);\n    take(Token.endCommand);\n    take(Token.newline);\n    if (peekToken() == Token.startCommand) {\n      return IfBlock(expression as BoolExpression, const Block.empty());\n    }\n    if (peekToken() != Token.startIndent) {\n      syntaxError('the body of the <<$commandName>> command must be indented');\n    }\n    take(Token.startIndent);\n    final block = parseStatementList();\n    take(Token.endIndent);\n    return IfBlock(expression as BoolExpression, block);\n  }\n\n  IfBlock parseCommandElseBlock() {\n    take(Token.startCommand);\n    take(Token.commandElse);\n    take(Token.endCommand);\n    take(Token.newline);\n    if (peekToken() == Token.startCommand) {\n      return const IfBlock(constTrue, Block.empty());\n    }\n    if (peekToken() != Token.startIndent) {\n      syntaxError('the body of the <<else>> command must be indented');\n    }\n    take(Token.startIndent);\n    final statements = parseStatementList();\n    take(Token.endIndent);\n    return IfBlock(constTrue, statements);\n  }\n\n  Command parseCommandJumpOrVisit() {\n    take(Token.startCommand);\n    final isJump = peekToken() == Token.commandJump;\n    final isVisit = peekToken() == Token.commandVisit;\n    assert(isJump || isVisit);\n    position += 1;\n    final token = peekToken();\n    StringExpression target;\n    if (token.isId) {\n      // TODO(st-pasha): add verification for node existence at the end of\n      //                 project setup\n      target = StringLiteral(token.content);\n      position += 1;\n    } else {\n      take(Token.startExpression);\n      final position0 = position;\n      final expression = parseExpression();\n      take(Token.endExpression);\n      if (expression.isString) {\n        target = expression as StringExpression;\n      } else {\n        typeError('target of <<jump>> must be a string expression', position0);\n      }\n    }\n    take(Token.endCommand);\n    take(Token.newline);\n    return isJump ? JumpCommand(target) : VisitCommand(target);\n  }\n\n  Command parseCommandStop() {\n    take(Token.startCommand);\n    take(Token.commandStop);\n    take(Token.endCommand);\n    take(Token.newline);\n    return const StopCommand();\n  }\n\n  Command parseCommandWait() {\n    take(Token.startCommand);\n    take(Token.commandWait);\n    take(Token.startExpression);\n    final position0 = position;\n    final expression = parseExpression();\n    if (!expression.isNumeric) {\n      typeError('<<wait>> command expects a numeric argument', position0);\n    }\n    take(Token.endExpression);\n    take(Token.endCommand);\n    takeNewline();\n    return WaitCommand(expression as NumExpression);\n  }\n\n  Command parseCommandSet() {\n    take(Token.startCommand);\n    take(Token.commandSet);\n    take(Token.startExpression);\n    final variableToken = peekToken();\n    if (!variableToken.isVariable) {\n      syntaxError('variable expected');\n    }\n    final variableName = variableToken.content;\n    final VariableStorage variableStorage;\n    if (localVariables?.hasVariable(variableName) ?? false) {\n      variableStorage = localVariables!;\n    } else if (project.variables.hasVariable(variableName)) {\n      variableStorage = project.variables;\n    } else {\n      nameError('variable $variableName has not been declared');\n    }\n    final variableExpression = variableStorage.getVariableAsExpression(\n      variableName,\n    );\n    position += 1;\n\n    final assignmentToken = peekToken();\n    if (!(assignmentToken == Token.operatorAssign ||\n        assignmentTokensToOperators.containsKey(assignmentToken))) {\n      syntaxError('an assignment operator is expected');\n    }\n    position += 1;\n    final expressionStartPosition = position;\n    final expression = parseExpression();\n    if (variableExpression.type != expression.type) {\n      typeError(\n        'variable $variableName of type ${variableExpression.type.name} '\n        'cannot be assigned a value of type ${expression.type.name}',\n        expressionStartPosition,\n      );\n    }\n    final Expression assignmentExpression;\n    if (assignmentToken == Token.operatorAssign) {\n      assignmentExpression = expression;\n    } else {\n      assignmentExpression = makeBinaryOpExpression(\n        assignmentTokensToOperators[assignmentToken]!,\n        variableExpression,\n        expression,\n        expressionStartPosition,\n        typeError,\n      );\n    }\n    take(Token.endExpression);\n    take(Token.endCommand);\n    takeNewline();\n    return SetCommand(variableName, assignmentExpression, variableStorage);\n  }\n\n  Command parseCommandDeclareOrLocal() {\n    take(Token.startCommand);\n    final isDeclare = peekToken() == Token.commandDeclare;\n    final isLocal = peekToken() == Token.commandLocal;\n    assert(isDeclare || isLocal);\n    position += 1;\n    take(Token.startExpression);\n    if (isLocal) {\n      localVariables ??= VariableStorage();\n    }\n    final variableToken = peekToken();\n    if (!variableToken.isVariable) {\n      syntaxError('variable name expected');\n    }\n    final variableName = variableToken.content;\n    if (isLocal && localVariables!.hasVariable(variableName)) {\n      nameError('redeclaration of local variable $variableName');\n    }\n    if (project.variables.hasVariable(variableName)) {\n      nameError(\n        isLocal\n            ? 'variable $variableName shadows a global variable with the '\n                  'same name'\n            : 'variable $variableName has already been declared',\n      );\n    }\n    position += 1;\n    late final Expression expression;\n    if (peekToken() == Token.asType) {\n      if (isLocal) {\n        syntaxError('assignment operator is expected');\n      }\n      take(Token.asType);\n      final typeToken = peekToken();\n      final typeExpr = typesToDefaultValues[typeToken];\n      if (typeExpr == null) {\n        syntaxError('a type is expected');\n      }\n      expression = typeExpr;\n      take(typeToken);\n    } else if (peekToken() == Token.operatorAssign) {\n      take(Token.operatorAssign);\n      expression = parseExpression();\n      final nextToken = peekToken();\n      if (nextToken == Token.asType) {\n        take(Token.asType);\n        final typeToken = peekToken();\n        final typeExpr = typesToDefaultValues[typeToken];\n        if (typeExpr == null) {\n          syntaxError('a type is expected');\n        }\n        if (typeExpr.type != expression.type) {\n          typeError('the expression evaluates to ${expression.type.name} type');\n        }\n        take(typeToken);\n      }\n    } else {\n      syntaxError('expected `= value` or `as Type`');\n    }\n    take(Token.endExpression);\n    take(Token.endCommand);\n    takeNewline();\n    if (isLocal) {\n      final dynamic initialValue = typesToDefaultValues.values\n          .where((Expression v) => v.type == expression.type)\n          .first\n          .value;\n      localVariables!.setVariable(variableName, initialValue);\n      return LocalCommand(variableName, expression, localVariables!);\n    } else {\n      project.variables.setVariable(variableName, expression.value);\n      return const DeclareCommand();\n    }\n  }\n\n  Command parseCommandCharacter() {\n    take(Token.startCommand);\n    take(Token.commandCharacter);\n    take(Token.startExpression);\n    String? realName;\n    if (peekToken().isString) {\n      realName = peekToken().content;\n      position += 1;\n    }\n    final aliases = <String>[];\n    while (peekToken().isId) {\n      final alias = peekToken().content;\n      if (project.characters.contains(alias)) {\n        final char = project.characters[alias]!;\n        nameError('character \"$alias\" was already defined: $char');\n      }\n      aliases.add(alias);\n      position += 1;\n    }\n    take(Token.endExpression);\n    if (aliases.isEmpty) {\n      syntaxError('at least one character id is required');\n    }\n    if (realName == null) {\n      realName = aliases.first;\n      aliases.removeAt(0);\n    }\n    take(Token.endCommand);\n    takeNewline();\n    final character = Character(realName, aliases: aliases);\n    project.characters.add(character);\n    return const CharacterCommand();\n  }\n\n  Command parseUserDefinedCommand() {\n    take(Token.startCommand);\n    final commandToken = peekToken();\n    position += 1;\n    assert(commandToken.isCommand);\n    final commandName = commandToken.content;\n    if (!project.commands.hasCommand(commandName)) {\n      position -= 1;\n      nameError('Unknown user-defined command <<$commandName>>');\n    }\n    final arguments = parseLineContent();\n    take(Token.endCommand);\n    takeNewline();\n    return UserDefinedCommand(commandName, arguments);\n  }\n\n  //#endregion\n\n  //#region Expression parsing\n  //----------------------------------------------------------------------------\n  // EXPRESSIONS\n  //\n  // Expressions are the mathematical notation used within the commands and\n  // interpolated text in order to perform calculations at runtime.\n  //\n  // See https://github.com/YarnSpinnerTool/YarnSpinner/blob/main/Documentation/Yarn-Spec.md#expressions\n  //----------------------------------------------------------------------------\n\n  /// Parses an expression starting from the current [position], and up to the\n  /// point where we encounter a token that cannot be interpreted as part of an\n  /// expression. Note that the startExpression / endExpressions tokens are not\n  /// consumed (nor required) by this method. This makes it suitable to use\n  /// this method to both extract an interpolated text expression, and a command\n  /// expression.\n  ///\n  /// If an expression cannot be parsed at the current location at all, the\n  /// [constVoid] will be returned.\n  Expression parseExpression() {\n    // We're using the Operator-Precedence parsing algorithm here, see\n    // https://en.wikipedia.org/wiki/Operator-precedence_parser\n    final lhs = parsePrimary();\n    if (lhs == constVoid) {\n      return lhs;\n    }\n    return _parseExpressionImpl(lhs, 0);\n  }\n\n  /// This is an implementation function for the Operator-Precedence parsing\n  /// algorithm. It consumes expressions of the form `LHS op RHS op...`, where\n  /// the precedence of operators must be greater or equal to [minPrecedence].\n  /// The initial [lhs] sub-expression is provided, and the parsing position\n  /// should be at the start of the next operator.\n  Expression _parseExpressionImpl(Expression lhs, int minPrecedence) {\n    var result = lhs;\n    while ((precedences[peekToken()] ?? -1) >= minPrecedence) {\n      var position0 = position;\n      final op = peekToken();\n      final opPrecedence = precedences[op]!;\n      position += 1;\n      var rhs = parsePrimary();\n      if (rhs == constVoid) {\n        syntaxError('unexpected expression');\n      }\n      var token = peekToken();\n      while ((precedences[token] ?? -1) > opPrecedence) {\n        rhs = _parseExpressionImpl(rhs, opPrecedence + 1);\n        token = peekToken();\n        position0 = position;\n      }\n      result = makeBinaryOpExpression(op, result, rhs, position0, typeError);\n    }\n    return result;\n  }\n\n  Expression parsePrimary() {\n    final token = peekToken();\n    position += 1;\n    if (token == Token.startParenthesis) {\n      final expression = parseExpression();\n      take(Token.endParenthesis, 'missing closing \")\"');\n      return expression;\n    } else if (token == Token.operatorMinus) {\n      final expression = parsePrimary();\n      if (expression is NumLiteral) {\n        return NumLiteral(-expression.value);\n      } else if (expression.isNumeric) {\n        return Negate(expression as NumExpression);\n      } else {\n        typeError('unary minus can only be applied to numbers', position - 1);\n      }\n    } else if (token.isNumber) {\n      return NumLiteral(num.parse(token.content));\n    } else if (token.isString) {\n      return StringLiteral(token.content);\n    } else if (token == Token.constTrue || token == Token.constFalse) {\n      return BoolLiteral(token == Token.constTrue);\n    } else if (token.isVariable) {\n      final name = token.content;\n      if (localVariables?.hasVariable(name) ?? false) {\n        return localVariables!.getVariableAsExpression(name);\n      } else if (project.variables.hasVariable(name)) {\n        return project.variables.getVariableAsExpression(name);\n      } else {\n        nameError('variable $name is not defined', position - 1);\n      }\n    } else if (token.isId) {\n      final name = token.content;\n      final builder =\n          builtinFunctions[name] ?? project.functions.builderForFunction(name);\n      if (builder == null) {\n        nameError('unknown function name $name', position - 1);\n      }\n      take(Token.startParenthesis, 'an opening parenthesis \"(\" is expected');\n      final arguments = parseFunctionArguments();\n      final functionExpr = builder(arguments, project, typeError);\n      take(Token.endParenthesis, 'missing closing \")\"');\n      return functionExpr;\n    } else if (token == Token.operatorNot) {\n      final position0 = position;\n      final lhs = parsePrimary();\n      final arg = _parseExpressionImpl(lhs, precedences[Token.operatorNot]!);\n      if (!arg.isBoolean) {\n        typeError('operator `not` can only be applied to booleans', position0);\n      }\n      return Not(arg as BoolExpression);\n    }\n    position -= 1;\n    return constVoid;\n  }\n\n  List<FunctionArgument> parseFunctionArguments() {\n    final out = <FunctionArgument>[];\n    while (true) {\n      final position0 = position;\n      final expression = parseExpression();\n      if (expression == constVoid) {\n        break;\n      }\n      out.add(FunctionArgument(expression, position0));\n      final nextToken = peekToken();\n      if (nextToken == Token.comma) {\n        position += 1;\n      } else if (nextToken == Token.endParenthesis) {\n        break;\n      } else {\n        syntaxError('unexpected token');\n      }\n    }\n    return out;\n  }\n\n  static final Map<Token, Expression> typesToDefaultValues = {\n    Token.typeBool: constFalse,\n    Token.typeNumber: constZero,\n    Token.typeString: constEmptyString,\n  };\n\n  late Map<Token, Token> assignmentTokensToOperators = {\n    Token.operatorDivideAssign: Token.operatorDivide,\n    Token.operatorMinusAssign: Token.operatorMinus,\n    Token.operatorModuloAssign: Token.operatorModulo,\n    Token.operatorMultiplyAssign: Token.operatorMultiply,\n    Token.operatorPlusAssign: Token.operatorPlus,\n  };\n\n  static final Map<Token, int> precedences = {\n    Token.operatorMultiply: 6,\n    Token.operatorDivide: 6,\n    Token.operatorModulo: 6,\n    //\n    Token.operatorMinus: 5,\n    Token.operatorPlus: 5,\n    //\n    Token.operatorEqual: 4,\n    Token.operatorNotEqual: 4,\n    Token.operatorGreaterOrEqual: 4,\n    Token.operatorGreaterThan: 4,\n    Token.operatorLessOrEqual: 4,\n    Token.operatorLessThan: 4,\n    //\n    Token.operatorNot: 3,\n    Token.operatorAnd: 2,\n    Token.operatorXor: 2,\n    Token.operatorOr: 1,\n  };\n\n  //#endregion\n\n  //----------------------------------------------------------------------------\n  // All `take*` methods will consume a single token of the specified kind,\n  // advance the parsing [position], and return `true` (for chaining purposes).\n  // If, on the other hand, the specified token cannot be found, an exception\n  // 'unexpected token' will be thrown.\n  //----------------------------------------------------------------------------\n\n  Token peekToken([int delta = 0]) {\n    return position + delta < tokens.length\n        ? tokens[position + delta]\n        : Token.eof;\n  }\n\n  bool takeId() => takeTokenType(TokenType.id);\n\n  bool takeText() => takeTokenType(TokenType.text);\n\n  bool takePerson() => takeTokenType(TokenType.person);\n\n  bool takeNewline() {\n    if (position >= tokens.length) {\n      return true;\n    } else if (tokens[position] == Token.newline) {\n      position += 1;\n      return true;\n    }\n    syntaxError('expected end of line');\n  }\n\n  bool take(Token token, [String? message]) {\n    if (position >= tokens.length) {\n      syntaxError('unexpected end of file'); // coverage:ignore-line\n    }\n    if (tokens[position] == token) {\n      position += 1;\n      return true;\n    }\n    return syntaxError(message ?? 'unexpected token');\n  }\n\n  bool takeTokenType(TokenType type) {\n    if (tokens[position].type == type) {\n      position += 1;\n      return true;\n    }\n    return syntaxError('unexpected token');\n  }\n\n  Never nameError(String message, [int? position]) =>\n      _error(message, position, NameError.new);\n\n  Never syntaxError(String message, [int? position]) =>\n      _error(message, position, SyntaxError.new);\n\n  Never typeError(String message, [int? position]) =>\n      _error(message, position, TypeError.new);\n\n  Never _error(\n    String message,\n    int? position,\n    Exception Function(String) errorConstructor,\n  ) {\n    final errorPosition = position ?? this.position;\n    final newTokens = tokenize(text, addErrorTokenAtIndex: errorPosition);\n    final errorToken = newTokens[errorPosition];\n    final location = errorToken.content;\n    throw errorConstructor('$message\\n$location\\n');\n  }\n}\n\nclass _NodeHeader {\n  _NodeHeader(this.title, this.tags);\n\n  String? title;\n  Map<String, String>? tags;\n}\n\nclass _Markup {\n  bool closing = false;\n  bool selfClosing = false;\n  String? name;\n  int? startTextPosition;\n  int? endTextPosition;\n  int? startSubIndex;\n  int? endSubIndex;\n  Map<String, Expression> parameters = {};\n\n  MarkupAttribute build() {\n    assert(!closing);\n    return MarkupAttribute(\n      name!,\n      startTextPosition!,\n      endTextPosition!,\n      startSubIndex!,\n      endSubIndex!,\n      parameters.isEmpty ? null : parameters,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/parse/token.dart",
    "content": "import 'package:meta/meta.dart';\n\n/// [Token] is a unit of output during the lexing stage.\n@internal\n@immutable\nclass Token {\n  const Token._(this.type, [this._content]);\n\n  const Token.command(String text) : this._(TokenType.command, text);\n  const Token.hashtag(String text) : this._(TokenType.hashtag, text);\n  const Token.id(String text) : this._(TokenType.id, text);\n  const Token.number(String text) : this._(TokenType.number, text);\n  const Token.person(String text) : this._(TokenType.person, text);\n  const Token.string(String text) : this._(TokenType.string, text);\n  const Token.text(String text) : this._(TokenType.text, text);\n  const Token.variable(String text) : this._(TokenType.variable, text);\n  const Token.error(String text) : this._(TokenType.error, text);\n\n  static const arrow = Token._(TokenType.arrow);\n  static const asType = Token._(TokenType.asType);\n  static const closeMarkupTag = Token._(TokenType.closeMarkupTag);\n  static const colon = Token._(TokenType.colon);\n  static const comma = Token._(TokenType.comma);\n  static const commandCharacter = Token._(TokenType.commandCharacter);\n  static const commandDeclare = Token._(TokenType.commandDeclare);\n  static const commandElse = Token._(TokenType.commandElse);\n  static const commandElseif = Token._(TokenType.commandElseif);\n  static const commandEndif = Token._(TokenType.commandEndif);\n  static const commandIf = Token._(TokenType.commandIf);\n  static const commandJump = Token._(TokenType.commandJump);\n  static const commandLocal = Token._(TokenType.commandLocal);\n  static const commandSet = Token._(TokenType.commandSet);\n  static const commandStop = Token._(TokenType.commandStop);\n  static const commandVisit = Token._(TokenType.commandVisit);\n  static const commandWait = Token._(TokenType.commandWait);\n  static const constFalse = Token._(TokenType.constFalse);\n  static const constTrue = Token._(TokenType.constTrue);\n  static const endBody = Token._(TokenType.endBody);\n  static const endCommand = Token._(TokenType.endCommand);\n  static const endExpression = Token._(TokenType.endExpression);\n  static const endHeader = Token._(TokenType.endHeader);\n  static const endIndent = Token._(TokenType.endIndent);\n  static const endMarkupTag = Token._(TokenType.endMarkupTag);\n  static const endParenthesis = Token._(TokenType.endParenthesis);\n  static const eof = Token._(TokenType.eof);\n  static const newline = Token._(TokenType.newline);\n  static const operatorAnd = Token._(TokenType.operatorAnd);\n  static const operatorAssign = Token._(TokenType.operatorAssign);\n  static const operatorDivide = Token._(TokenType.operatorDivide);\n  static const operatorDivideAssign = Token._(TokenType.operatorDivideAssign);\n  static const operatorEqual = Token._(TokenType.operatorEqual);\n  static const operatorGreaterOrEqual = Token._(\n    TokenType.operatorGreaterOrEqual,\n  );\n  static const operatorGreaterThan = Token._(TokenType.operatorGreaterThan);\n  static const operatorLessOrEqual = Token._(TokenType.operatorLessOrEqual);\n  static const operatorLessThan = Token._(TokenType.operatorLessThan);\n  static const operatorMinus = Token._(TokenType.operatorMinus);\n  static const operatorMinusAssign = Token._(TokenType.operatorMinusAssign);\n  static const operatorModulo = Token._(TokenType.operatorModulo);\n  static const operatorModuloAssign = Token._(TokenType.operatorModuloAssign);\n  static const operatorMultiply = Token._(TokenType.operatorMultiply);\n  static const operatorMultiplyAssign = Token._(\n    TokenType.operatorMultiplyAssign,\n  );\n  static const operatorNotEqual = Token._(TokenType.operatorNotEqual);\n  static const operatorNot = Token._(TokenType.operatorNot);\n  static const operatorOr = Token._(TokenType.operatorOr);\n  static const operatorPlus = Token._(TokenType.operatorPlus);\n  static const operatorPlusAssign = Token._(TokenType.operatorPlusAssign);\n  static const operatorXor = Token._(TokenType.operatorXor);\n  static const startBody = Token._(TokenType.startBody);\n  static const startCommand = Token._(TokenType.startCommand);\n  static const startExpression = Token._(TokenType.startExpression);\n  static const startHeader = Token._(TokenType.startHeader);\n  static const startIndent = Token._(TokenType.startIndent);\n  static const startMarkupTag = Token._(TokenType.startMarkupTag);\n  static const startParenthesis = Token._(TokenType.startParenthesis);\n  static const typeBool = Token._(TokenType.typeBool);\n  static const typeNumber = Token._(TokenType.typeNumber);\n  static const typeString = Token._(TokenType.typeString);\n\n  final TokenType type;\n  final String? _content;\n\n  bool get isCommand => type == TokenType.command;\n  bool get isHashtag => type == TokenType.hashtag;\n  bool get isId => type == TokenType.id;\n  bool get isNumber => type == TokenType.number;\n  bool get isPerson => type == TokenType.person;\n  bool get isString => type == TokenType.string;\n  bool get isText => type == TokenType.text;\n  bool get isVariable => type == TokenType.variable;\n\n  /// The content can only be accessed for tokens of type \"text\", \"number\",\n  /// \"string\", \"command\", \"variable\", \"person\", and \"id\".\n  String get content => _content!;\n\n  @override\n  String toString() =>\n      'Token.${type.name}${_content == null ? '' : \"('$_content')\"}';\n\n  @override\n  int get hashCode => Object.hash(type, _content);\n\n  @override\n  bool operator ==(Object other) =>\n      other is Token && other.type == type && other._content == _content;\n}\n\n@internal\nenum TokenType {\n  command,\n  hashtag,\n  id,\n  number,\n  person,\n  string,\n  text,\n  variable,\n\n  arrow, //                  '->'\n  asType, //                 'as'\n  closeMarkupTag, //         '/'  (e.g. in \"[br/]\")\n  colon, //                  ':'\n  comma, //                  ','\n  commandCharacter, //       'character'\n  commandDeclare, //         'declare'\n  commandElse, //            'else'\n  commandElseif, //          'elseif'\n  commandEndif, //           'endif'\n  commandIf, //              'if'\n  commandJump, //            'jump'\n  commandLocal, //           'local'\n  commandSet, //             'set'\n  commandStop, //            'stop'\n  commandVisit, //           'visit'\n  commandWait, //            'wait'\n  constFalse, //             'false'\n  constTrue, //              'true'\n  endBody, //                '==='\n  endCommand, //             '>>'\n  endExpression, //          '}'\n  endHeader, //              '---' '-'*\n  endIndent, //              RegExp(r'^\\s*')\n  endMarkupTag, //           ']'\n  endParenthesis, //         ')'\n  newline, //                '\\r' | '\\n' | '\\r\\n'\n  operatorAnd, //            'and' | '&&'\n  operatorAssign, //         'to' | '='\n  operatorDivide, //         '/'\n  operatorDivideAssign, //   '/='\n  operatorEqual, //          'is' | 'eq' | '=='\n  operatorGreaterOrEqual, // 'ge' | 'gte' | '>='\n  operatorGreaterThan, //    'gt' | '>'\n  operatorLessOrEqual, //    'le' | 'lte | '<='\n  operatorLessThan, //       'lt' | '<'\n  operatorMinus, //          '-'\n  operatorMinusAssign, //    '-='\n  operatorModulo, //         '%'\n  operatorModuloAssign, //   '%='\n  operatorMultiply, //       '*'\n  operatorMultiplyAssign, // '*='\n  operatorNot, //            'not' | '!'\n  operatorNotEqual, //       'ne' | 'neq' | '!='\n  operatorOr, //             'or' | '||'\n  operatorPlus, //           '+'\n  operatorPlusAssign, //     '+='\n  operatorXor, //            'xor' | '^'\n  startBody, //              '---' '-'*\n  startCommand, //           '<<'\n  startExpression, //        '{'\n  startHeader, //            ('---' '-'*)?\n  startIndent, //            RegExp(r'^\\s*')\n  startMarkupTag, //         '['\n  startParenthesis, //       '('\n  typeBool, //               'Bool'\n  typeNumber, //             'Number'\n  typeString, //             'String'\n\n  error,\n  eof,\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/parse/tokenize.dart",
    "content": "import 'package:jenny/src/errors.dart';\nimport 'package:jenny/src/parse/ascii.dart';\nimport 'package:jenny/src/parse/token.dart';\nimport 'package:meta/meta.dart';\n\n/// Parses the [input] into a stream of [Token]s, according to the Yarn syntax.\n///\n/// If [addErrorTokenAtIndex] argument is given, then it would cause the lexer\n/// to insert a [Token.error] at the specified index in the stream. This token\n/// will be formatted to contain the line and column number at that parsing\n/// position, as well as the line sample. This functionality can be used when\n/// we need to throw an error from the parsing stage.\n@internal\nList<Token> tokenize(String input, {int addErrorTokenAtIndex = -2}) {\n  return _Lexer(input, addErrorTokenAtIndex).parse();\n}\n\n/// Main working class for the [tokenize] function -- produces a stream of\n/// [Token]s via its [parse] method.\n///\n/// The tokens emitted by the class follow these general considerations:\n///   - Every `start*` token should have a corresponding `end*` token, properly\n///     nested;\n///   - Tokens `startBody`/`endBody` denote a Node body -- any content outside\n///     is a Node's header;\n///   - Whitespace and comments are discarded and do not produce tokens;\n///   - Individual input lines within Node's header or body are separated with\n///     newline tokens;\n///   - The lexer is deterministic: given the same input, it should always\n///     produce the same output.\nclass _Lexer {\n  _Lexer(this.text, this.addErrorTokenAtIndex)\n    : position = 0,\n      lineNumber = 1,\n      lineStart = 0,\n      tokens = [],\n      modeStack = [],\n      indentStack = [],\n      assert(\n        commandTokens.length ==\n            simpleCommands.length +\n                bareExpressionCommands.length +\n                nodeTargetingCommands.length,\n      );\n\n  final String text;\n  final List<Token> tokens;\n  final List<_ModeFn> modeStack;\n  final List<int> indentStack;\n  final int addErrorTokenAtIndex;\n\n  /// Current parsing position, an offset within the [text].\n  int position;\n\n  /// Current line number, for error reporting.\n  int lineNumber;\n\n  /// Offset of the start of the current line, used for error reporting.\n  int lineStart;\n\n  /// Main parsing function which handles transition between modes. This method\n  /// will parse the [text] and return a list of [tokens]. This function can\n  /// only be called once for the given [_Lexer] object.\n  List<Token> parse() {\n    assert(position == 0 && lineNumber == 1 && lineStart == 0);\n    indentStack.add(0);\n    pushMode(modeMain);\n    while (!eof) {\n      final ok = modeStack.last();\n      if (!ok) {\n        error('invalid token');\n      }\n    }\n    if (modeStack.length > 1) {\n      error('incomplete node body');\n    }\n    popMode(modeMain);\n    if (tokens.length == addErrorTokenAtIndex) {\n      tokens.add(Token.error(_errorMessageAtPosition(position)));\n    }\n    return tokens;\n  }\n\n  /// Has current parse position reached the end of file?\n  bool get eof => position == text.length;\n\n  /// Returns the integer code unit at the current parse position, or -1 if we\n  /// reached the end of input.\n  int get currentCodeUnit =>\n      position < text.length ? text.codeUnitAt(position) : -1;\n\n  int get nextCodeUnit =>\n      position < text.length - 1 ? text.codeUnitAt(position + 1) : -1;\n\n  /// Pushes a new mode into the mode stack and returns `true`.\n  bool pushMode(_ModeFn mode) {\n    modeStack.add(mode);\n    return true;\n  }\n\n  /// Pops the last mode from the stack, checks that it was [mode], and returns\n  /// `true`.\n  bool popMode(_ModeFn mode) {\n    final removed = modeStack.removeLast();\n    assert(removed == mode, 'Expected $mode but found $removed');\n    return true;\n  }\n\n  /// Pushes a new [token] into the output and returns `true`.\n  ///\n  /// The [tokenStartPosition] indicates the position in the stream where the\n  /// current token starts.\n  bool pushToken(Token token, int tokenStartPosition) {\n    if (tokens.length == addErrorTokenAtIndex) {\n      tokens.add(Token.error(_errorMessageAtPosition(tokenStartPosition)));\n    }\n    tokens.add(token);\n    return true;\n  }\n\n  /// Pops the last token from the output stack and checks that it is equal to\n  /// [token].\n  Token popToken([Token? token]) {\n    final removed = tokens.removeLast();\n    assert(token == null || removed == token);\n    if (tokens.length == addErrorTokenAtIndex + 1) {\n      final removed = tokens.removeLast();\n      assert(removed.type == TokenType.error);\n    }\n    return removed;\n  }\n\n  //----------------------------------------------------------------------------\n  // All `mode*()` methods have the [_ModeFn] signature, and are designed to be\n  // put onto the mode stack. The collection of all modes and transitions\n  // between them represent the lexer's Finite State Machine.\n  //\n  // Each `mode*()` method returns true if it was able to proceed in parsing,\n  // or false if it cannot recognize the syntax. In the latter case a syntax\n  // error will be thrown.\n  //----------------------------------------------------------------------------\n\n  /// Initial mode, outside of the nodes. The following syntaxes are allowed\n  /// here: file-level hashtags, and commands. The mode ends when it encounters\n  /// a start-of-header sequence '---', or if there is any content other than\n  /// hashtags and commands.\n  ///\n  /// Note that this mode is never popped off the mode stack.\n  bool modeMain() {\n    return eatEmptyLine() ||\n        eatComment() ||\n        eatHashtagLine() ||\n        (eatCommandStart() && pushMode(modeCommand)) ||\n        (eatHeaderStart() && pushMode(modeNodeHeader)) ||\n        (pushToken(Token.startHeader, position) && pushMode(modeNodeHeader));\n  }\n\n  /// Parsing node header, this mode is only active at a start of a line, and\n  /// after parsing an initial token, it pushes [modeNodeHeaderLine] which\n  /// remains active until the end of the line.\n  ///\n  /// The mode switches to [modeNodeBody] upon encountering the '---' sequence.\n  bool modeNodeHeader() {\n    return eatEmptyLine() ||\n        eatComment() ||\n        (eatId() && pushMode(modeNodeHeaderLine)) ||\n        (eatWhitespace() && error('unexpected indentation')) ||\n        (eatHeaderEnd() &&\n            popMode(modeNodeHeader) &&\n            pushToken(Token.startBody, position) &&\n            pushMode(modeNodeBody)) ||\n        error('expected end-of-header marker \"---\"');\n  }\n\n  /// Mode which activates at each line of [modeNodeHeader] after an ID at the\n  /// start of the line is consumed, and remains active until the end of the\n  /// line.\n  bool modeNodeHeaderLine() {\n    return eatWhitespace() ||\n        (eat($colon) && pushToken(Token.colon, position - 1)) ||\n        (eatHeaderRestOfLine() && popMode(modeNodeHeaderLine));\n  }\n\n  /// The top-level mode for parsing the body of a Node. This mode is active at\n  /// the start of each line only, and will turn into [modeNodeBodyLine] after\n  /// taking care of whitespace.\n  bool modeNodeBody() {\n    return eatEmptyLine() ||\n        eatComment() ||\n        (eatBodyEnd() && popMode(modeNodeBody)) ||\n        (eatIndent() && pushMode(modeNodeBodyLine));\n  }\n\n  /// The mode for parsing regular lines of a node body. This mode is active at\n  /// the beginning of the line only (after the indent), where it attempts to\n  /// disambiguate between what kind of line it is and then switches to either\n  /// [modeCommand] or [modeText].\n  bool modeNodeBodyLine() {\n    return eatWhitespace() ||\n        eatArrow() ||\n        (eatCommandStart() && pushMode(modeCommand)) ||\n        (eatCharacterName() && pushMode(modeText)) ||\n        (eatNewline() && popMode(modeNodeBodyLine)) ||\n        pushMode(modeText);\n  }\n\n  /// The mode of a regular text line within the node body. This mode will\n  /// consume the input until the end of the line, and switch to [modeTextEnd].\n  bool modeText() {\n    return eatTextEscapeSequence() ||\n        eatPlainText() ||\n        eatCommandEndAsText() ||\n        (eatExpressionStart() && pushMode(modeTextExpression)) ||\n        (eatMarkupStart() && pushMode(modeMarkup)) ||\n        (popMode(modeText) && pushMode(modeTextEnd));\n  }\n\n  /// Mode at the end of a line, allows hashtags and comments.\n  bool modeTextEnd() {\n    return eatWhitespace() ||\n        eatComment() ||\n        eatHashtag() ||\n        (eatCommandStart() && pushMode(modeCommand)) ||\n        (eatNewline() && popMode(modeTextEnd) && popMode(modeNodeBodyLine));\n  }\n\n  /// The command mode starts with a '<<', then consumes the command name, and\n  /// after that either switches to a different mode depending on which command\n  /// was encountered.\n  bool modeCommand() {\n    return eatWhitespace() ||\n        (eatCommandName() &&\n            (false || // subsequent mode will depend on the command type\n                (simpleCommands.contains(tokens.last)) ||\n                (bareExpressionCommands.contains(tokens.last) &&\n                    pushToken(Token.startExpression, position) &&\n                    pushMode(modeCommandExpression)) ||\n                (nodeTargetingCommands.contains(tokens.last) &&\n                    (eatId() ||\n                        (eatExpressionStart() &&\n                            pushMode(modeTextExpression)) ||\n                        error(\n                          'an ID or an expression in curly braces '\n                          'expected',\n                        ))) ||\n                (tokens.last.isCommand && // user-defined commands\n                    pushMode(modeCommandText)))) ||\n        (eatCommandEnd() && popMode(modeCommand)) ||\n        checkNewlineInCommand();\n  }\n\n  /// Mode for the content of a user-defined command. It is almost the same as\n  /// [modeText], except that it ends at '>>'.\n  bool modeCommandText() {\n    return (eatExpressionStart() && pushMode(modeTextExpression)) ||\n        (eatCommandEnd() && popMode(modeCommandText) && popMode(modeCommand)) ||\n        eatTextEscapeSequence() ||\n        eatPlainText();\n  }\n\n  /// An expression within a [modeText] or [modeCommandText]. The expression\n  /// is surrounded with curly braces `{ }`.\n  bool modeTextExpression() {\n    return eatWhitespace() ||\n        eatExpressionId() ||\n        eatExpressionVariable() ||\n        eatNumber() ||\n        eatString() ||\n        eatOperator() ||\n        (eatExpressionEnd() && popMode(modeTextExpression));\n  }\n\n  bool modeMarkupExpression() {\n    return eatWhitespace() ||\n        eatExpressionId() ||\n        eatExpressionVariable() ||\n        eatNumber() ||\n        eatString() ||\n        eatOperator() ||\n        popMode(modeMarkupExpression);\n  }\n\n  /// An expression within a [modeCommand]. Such expression starts immediately\n  /// after the command name and ends at `>>`.\n  bool modeCommandExpression() {\n    return eatWhitespace() ||\n        (eatExpressionCommandEnd() &&\n            popMode(modeCommandExpression) &&\n            popMode(modeCommand)) ||\n        eatExpressionId() ||\n        eatExpressionVariable() ||\n        eatNumber() ||\n        eatString() ||\n        eatOperator();\n  }\n\n  bool modeMarkup() {\n    return checkNewlineInMarkup() ||\n        eatMarkupCloseTag() ||\n        (eatMarkupEnd() && popMode(modeMarkup)) ||\n        pushMode(modeMarkupExpression);\n  }\n\n  //----------------------------------------------------------------------------\n  // All `eat*()` methods will attempt to consume a specific type of syntax at\n  // the current parsing location. If successful, the functions will:\n  //   - advance the parsing position [position];\n  //   - emit 0 or more tokens into the [tokens] stream;\n  //   - return `true`.\n  // Otherwise, the function will:\n  //   - leave [position] unmodified;\n  //   - return `false`.\n  //----------------------------------------------------------------------------\n\n  /// Consumes a single character with code unit [codeUnit].\n  bool eat(int codeUnit) {\n    if (currentCodeUnit == codeUnit) {\n      position += 1;\n      return true;\n    }\n    return false;\n  }\n\n  /// Consumes an empty line, i.e. a line consisting of whitespace only. Emits\n  /// a newline token.\n  bool eatEmptyLine() {\n    final position0 = position;\n    eatWhitespace();\n    if (eof) {\n      return true;\n    }\n    if (eatNewline()) {\n      return true;\n    } else {\n      position = position0;\n      return false;\n    }\n  }\n\n  /// Consumes a comment line: `\\s*//.*` up to but not including the newline,\n  /// emitting no tokens.\n  bool eatComment() {\n    final position0 = position;\n    eatWhitespace();\n    if (eat($slash) && eat($slash)) {\n      while (!eof) {\n        final cu = currentCodeUnit;\n        if (cu == $carriageReturn || cu == $lineFeed) {\n          return true;\n        }\n        position += 1;\n      }\n      return true;\n    }\n    position = position0;\n    return false;\n  }\n\n  /// Consumes all available whitespace at the current parsing position, without\n  /// emitting any tokens.\n  bool eatWhitespace() {\n    final position0 = position;\n    while (true) {\n      final cu = currentCodeUnit;\n      if (!(cu == $space || cu == $tab)) {\n        break;\n      }\n      position += 1;\n    }\n    return position > position0;\n  }\n\n  /// Consumes a newline character, which can also be a Windows newline (\\r\\n),\n  /// and emits a newline token.\n  bool eatNewline() {\n    final cu = currentCodeUnit;\n    if (cu == $carriageReturn || cu == $lineFeed) {\n      final position0 = position;\n      position += 1;\n      if (cu == $carriageReturn && currentCodeUnit == $lineFeed) {\n        position += 1;\n      }\n      lineNumber += 1;\n      lineStart = position;\n      pushToken(Token.newline, position0);\n      return true;\n    }\n    return false;\n  }\n\n  /// Consumes a hashtag '#' followed by all characters until the end of the\n  /// line. This is used for file-level tags. This rule is only used in\n  /// [modeMain].\n  bool eatHashtagLine() {\n    final position0 = position;\n    if (eat($hash)) {\n      eatWhitespace();\n      while (!eof) {\n        final cu = currentCodeUnit;\n        if (cu == $carriageReturn || cu == $lineFeed) {\n          break;\n        }\n        position += 1;\n      }\n      pushToken(\n        Token.hashtag(text.substring(position0, position)),\n        position0,\n      );\n      if (!eatNewline()) {\n        pushToken(Token.newline, position);\n      }\n      return true;\n    }\n    return false;\n  }\n\n  /// Consumes a start-of-header token (3 or more '-') followed by a newline.\n  /// Note that the same character sequence encountered within the header body\n  /// would mean the end of header section.\n  bool eatHeaderStart() {\n    final position0 = position;\n    var numMinuses = 0;\n    while (eat($minus)) {\n      numMinuses++;\n    }\n    if (numMinuses >= 3 && eatNewline()) {\n      popToken(Token.newline);\n      pushToken(Token.startHeader, position0);\n      return true;\n    }\n    position = position0;\n    return false;\n  }\n\n  /// Consumes an end-of-header token '---' followed by a newline, and emits a\n  /// corresponding token.\n  bool eatHeaderEnd() {\n    final position0 = position;\n    var numMinuses = 0;\n    while (eat($minus)) {\n      numMinuses++;\n    }\n    if (numMinuses >= 3 && eatNewline()) {\n      popToken(Token.newline);\n      pushToken(Token.endHeader, position0);\n      return true;\n    }\n    position = position0;\n    return false;\n  }\n\n  /// Consumes+emits an end-of-body token '===' followed by a newline.\n  bool eatBodyEnd() {\n    final position0 = position;\n    if (eat($equals) && eat($equals) && eat($equals)) {\n      position -= 3;\n      eatIndent(); // ensures that dedent tokens are properly inserted\n      position += 3;\n      if (eatNewline()) {\n        popToken(Token.newline);\n      } else if (!eof) {\n        position = position0;\n        return false;\n      }\n      pushToken(Token.endBody, position0);\n      return true;\n    }\n    position = position0;\n    return false;\n  }\n\n  /// Consumes a word that looks like an identifier, and emits an .id token.\n  bool eatId() {\n    final position0 = position;\n    var cu = currentCodeUnit;\n    if (isAsciiIdentifierStart(cu)) {\n      position += 1;\n      while (true) {\n        cu = currentCodeUnit;\n        if (isAsciiIdentifierChar(cu)) {\n          position += 1;\n        } else {\n          break;\n        }\n      }\n      pushToken(Token.id(text.substring(position0, position)), position0);\n      return true;\n    }\n    return false;\n  }\n\n  /// Consumes a plain text until the end of the line and emits it as a plain\n  /// text token, followed by a newline token. Always returns `true`.\n  bool eatHeaderRestOfLine() {\n    final position0 = position;\n    for (; !eof; position++) {\n      final cu = currentCodeUnit;\n      if (cu == $carriageReturn ||\n          cu == $lineFeed ||\n          (cu == $slash && nextCodeUnit == $slash)) {\n        break;\n      }\n    }\n    pushToken(Token.text(text.substring(position0, position)), position0);\n    eatComment();\n    eatNewline();\n    return true;\n  }\n\n  /// Consumes whitespace at the start of the line (if any) and emits indent/\n  /// dedent tokens according to the indent stack. Always returns true (even if\n  /// a line had 0 spaces).\n  bool eatIndent() {\n    final position0 = position;\n    var lineIndent = 0;\n    while (true) {\n      final cu = currentCodeUnit;\n      if (cu == $space) {\n        lineIndent += 1;\n      } else if (cu == $tab) {\n        lineIndent += 4;\n      } else {\n        break;\n      }\n      position += 1;\n    }\n    if (lineIndent > indentStack.last) {\n      indentStack.add(lineIndent);\n      pushToken(Token.startIndent, position0);\n    }\n    while (lineIndent < indentStack.last) {\n      indentStack.removeLast();\n      pushToken(Token.endIndent, position0);\n    }\n    if (lineIndent > indentStack.last) {\n      error('inconsistent indentation');\n    }\n    return true;\n  }\n\n  /// Consumes an arrow token '->' and emits it.\n  bool eatArrow() {\n    final position0 = position;\n    if (eat($minus) && eat($greaterThan)) {\n      pushToken(Token.arrow, position0);\n      return true;\n    }\n    position = position0;\n    return false;\n  }\n\n  /// Consumes+emits a command-start token '<<'.\n  bool eatCommandStart() {\n    final position0 = position;\n    if (eat($lessThan) && eat($lessThan)) {\n      pushToken(Token.startCommand, position0);\n      return true;\n    }\n    position = position0;\n    return false;\n  }\n\n  /// Consumes+emits the end-of-command token '>>', and pops the [modeCommand].\n  bool eatCommandEnd() {\n    final position0 = position;\n    if (eat($greaterThan) && eat($greaterThan)) {\n      pushToken(Token.endCommand, position0);\n      return true;\n    }\n    position = position0;\n    return false;\n  }\n\n  /// Consumes a Unicode ID at the start of the line, followed by a ':', then\n  /// emits a [Token.person] and a [Token.colon], and also switches into the\n  /// [modeText].\n  ///\n  /// Note: we have to detect both the character name and the subsequent\n  /// ':' at the same time, because without the colon a simple word at a start\n  /// of the line must be considered the plain text.\n  bool eatCharacterName() {\n    final position0 = position;\n    final it = RuneIterator.at(text, position);\n    if (it.moveNext() && isUnicodeIdentifierStart(it.current)) {\n      while (it.moveNext() && isUnicodeIdentifierChar(it.current)) {}\n      position = it.rawIndex;\n      final position1 = position;\n      eatWhitespace();\n      if (eat($colon)) {\n        eatWhitespace();\n        final name = Token.person(text.substring(position0, position1));\n        pushToken(name, position0);\n        pushToken(Token.colon, position1);\n        return true;\n      }\n    }\n    position = position0;\n    return false;\n  }\n\n  /// Consumes an escape syntax while in the [modeText]. An escape syntax\n  /// consists of a backslash followed by the character being escaped. An error\n  /// will be raised if the escaped character is invalid (e.g. '\\1'). Emits a\n  /// text token with the unescaped character.\n  bool eatTextEscapeSequence() {\n    if (eat($backslash)) {\n      if (eatNewline()) {\n        popToken(Token.newline);\n        eatWhitespace();\n      } else {\n        final cu = currentCodeUnit;\n        if (cu == $backslash ||\n            cu == $colon ||\n            cu == $slash ||\n            cu == $hash ||\n            cu == $minus ||\n            cu == $lessThan ||\n            cu == $greaterThan ||\n            cu == $leftBrace ||\n            cu == $rightBrace ||\n            cu == $leftBracket ||\n            cu == $rightBracket) {\n          pushToken(Token.text(String.fromCharCode(cu)), position);\n          position += 1;\n        } else if (cu == $lowercaseN) {\n          pushToken(const Token.text('\\n'), position);\n          position += 1;\n        } else {\n          error('invalid escape sequence');\n        }\n      }\n      return true;\n    }\n    return false;\n  }\n\n  /// Consumes '[' character.\n  bool eatMarkupStart() {\n    return eat($leftBracket) && pushToken(Token.startMarkupTag, position - 1);\n  }\n\n  /// Consumes '/' character inside a markup tag.\n  bool eatMarkupCloseTag() {\n    return eat($slash) && pushToken(Token.closeMarkupTag, position - 1);\n  }\n\n  /// Consumes ']' character. If previous character was '/' and it was parsed\n  /// as an `operatorDivide` token, then replace that token with\n  /// [Token.closeMarkupTag].\n  bool eatMarkupEnd() {\n    if (eat($rightBracket)) {\n      final previousCodeUnit = text.codeUnitAt(position - 2);\n      if (previousCodeUnit == $slash && tokens.last == Token.operatorDivide) {\n        popToken(Token.operatorDivide);\n        pushToken(Token.closeMarkupTag, position - 2);\n      }\n      if (tokens.last == Token.closeMarkupTag) {\n        final iterable = tokens.reversed\n            .skipWhile((token) => token != Token.startMarkupTag)\n            .skip(1);\n        if (iterable.isNotEmpty) {\n          final tokenBeforeMarkupStart = iterable.first;\n          if (tokenBeforeMarkupStart.isText &&\n              tokenBeforeMarkupStart.content.endsWith(' ')) {\n            // Self-closing markup tag such as `[img/]`: consume a single\n            // whitespace character after such tag (if present).\n            eat($space);\n          }\n        }\n      }\n      pushToken(Token.endMarkupTag, position - 1);\n      return true;\n    }\n    return false;\n  }\n\n  /// Consumes '{' token.\n  bool eatExpressionStart() {\n    return eat($leftBrace) && pushToken(Token.startExpression, position - 1);\n  }\n\n  /// Consumes '}' token.\n  bool eatExpressionEnd() {\n    return eat($rightBrace) && pushToken(Token.endExpression, position - 1);\n  }\n\n  /// Consumes '>>' while in the expression mode, and leaves both the expression\n  /// mode and the [modeCommand].\n  /// This rule is only allowed within an expression within a command mode.\n  bool eatExpressionCommandEnd() {\n    final position0 = position;\n    if (eat($greaterThan) && eat($greaterThan)) {\n      pushToken(Token.endExpression, position0);\n      pushToken(Token.endCommand, position0);\n      return true;\n    }\n    position = position0;\n    return false;\n  }\n\n  /// Consumes regular text within the [modeText]. Stops upon seeing one of the\n  /// special characters such as '\\n', '\\r', '#', '\\\\', '{', '<<', or '//'.\n  /// Emits the text token corresponding to the text processed.\n  bool eatPlainText() {\n    final position0 = position;\n    var positionBeforeWhitespace = position;\n    while (!eof) {\n      final cu = currentCodeUnit;\n      // Stop when seeing (\\n|\\r|#|//|<<) and discard any preceding whitespace\n      if ((cu == $slash && nextCodeUnit == $slash) ||\n          (cu == $lessThan && nextCodeUnit == $lessThan) ||\n          cu == $hash ||\n          cu == $carriageReturn ||\n          cu == $lineFeed) {\n        position = positionBeforeWhitespace;\n        break;\n      }\n      // Stop when seeing (>>|\\\\|{|[) and keep the whitespace\n      else if ((cu == $greaterThan && nextCodeUnit == $greaterThan) ||\n          cu == $backslash ||\n          cu == $leftBrace ||\n          cu == $leftBracket) {\n        break;\n      }\n      // Error when seeing unescaped (]|})\n      else if (cu == $rightBrace || cu == $rightBracket) {\n        error('special character needs to be escaped');\n      }\n      position += 1;\n      if (!(cu == $space || cu == $tab)) {\n        positionBeforeWhitespace = position;\n      }\n    }\n    if (position > position0) {\n      pushToken(Token.text(text.substring(position0, position)), position0);\n      return true;\n    }\n    return false;\n  }\n\n  bool eatCommandEndAsText() {\n    if (currentCodeUnit == $greaterThan && nextCodeUnit == $greaterThan) {\n      pushToken(const Token.text('>>'), position);\n      position += 2;\n      return true;\n    }\n    return false;\n  }\n\n  /// Consumes a plain id within an expression, which is then emitted as either\n  /// one of the [keywords] tokens, or as plain [Token.id].\n  bool eatExpressionId() {\n    final position0 = position;\n    if (eatId()) {\n      final name = tokens.last.content;\n      final keywordToken = keywords[name];\n      if (keywordToken != null) {\n        popToken();\n        pushToken(keywordToken, position0);\n      }\n      return true;\n    }\n    return false;\n  }\n\n  /// Consumes a variable within an expression. A variable is just a '$' sign\n  /// followed by an id. Emits a [Token.variable].\n  bool eatExpressionVariable() {\n    final position0 = position;\n    if (eat($dollar)) {\n      if (eatId()) {\n        final token = popToken();\n        pushToken(Token.variable(r'$' + token.content), position0);\n        return true;\n      }\n      position--;\n      error('invalid variable name');\n    }\n    return false;\n  }\n\n  /// Consumes a number in the form of `DIGITS (. DIGITS)?`, and emits a\n  /// corresponding token.\n  bool eatNumber() {\n    final position0 = position;\n    if (eatDigits()) {\n      eat($dot) && eatDigits();\n      pushToken(Token.number(text.substring(position0, position)), position0);\n      return true;\n    }\n    return false;\n  }\n\n  /// Helper for [eatNumber]: consumes a simple run of digits.\n  bool eatDigits() {\n    var found = false;\n    while (!eof) {\n      final cu = currentCodeUnit;\n      if (cu >= $digit0 && cu <= $digit9) {\n        found = true;\n        position++;\n      } else {\n        break;\n      }\n    }\n    return found;\n  }\n\n  /// Consumes one of the operators defined in the [keywords] map, and emits\n  /// a corresponding token.\n  bool eatOperator() {\n    if (position + 1 < text.length) {\n      final op2 = text.substring(position, position + 2);\n      final keyword = keywords[op2];\n      if (keyword != null) {\n        pushToken(keyword, position);\n        position += 2;\n        return true;\n      }\n    }\n    if (position < text.length) {\n      final op1 = text.substring(position, position + 1);\n      final keyword = keywords[op1];\n      if (keyword != null) {\n        pushToken(keyword, position);\n        position += 1;\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /// Consumes a string literal in single or double quotes and emits a .string\n  /// token where all escape characters has been unescaped. Returns false if the\n  /// string is invalid (for example ends without a closing quote, or contains\n  /// an unknown escape sequence).\n  bool eatString() {\n    final position0 = position;\n    final quote = currentCodeUnit;\n    if (quote == $doubleQuote || quote == $singleQuote) {\n      final buffer = StringBuffer();\n      position += 1;\n      while (!eof) {\n        final cu = currentCodeUnit;\n        if (cu == quote) {\n          position += 1;\n          pushToken(Token.string(buffer.toString()), position0);\n          return true;\n        } else if (cu == $carriageReturn || cu == $lineFeed) {\n          error('unexpected end of line while parsing a string');\n          break;\n        } else if (cu == $backslash) {\n          position += 1;\n          final cu2 = currentCodeUnit;\n          position += 1;\n          if (cu2 == $singleQuote || cu2 == $doubleQuote || cu2 == $backslash) {\n            buffer.writeCharCode(cu2);\n          } else if (cu2 == $lowercaseN) {\n            buffer.writeCharCode($lineFeed);\n          } else {\n            break;\n          }\n        } else {\n          buffer.writeCharCode(cu);\n          position += 1;\n        }\n      }\n    }\n    position = position0;\n    return false;\n  }\n\n  /// Consumes a name of the command (ID) and emits it as a token.\n  bool eatCommandName() {\n    final position0 = position;\n    if (eatId()) {\n      eatWhitespace();\n      final token = popToken();\n      final commandToken = commandTokens[token.content];\n      if (commandToken != null) {\n        pushToken(commandToken, position0);\n      } else {\n        pushToken(Token.command(token.content), position0);\n      }\n      return true;\n    }\n    return false;\n  }\n\n  /// Check whether a command terminated prematurely.\n  bool checkNewlineInCommand() {\n    final cu = currentCodeUnit;\n    if (cu == $carriageReturn || cu == $lineFeed) {\n      error('missing command close token \">>\"');\n    }\n    return false;\n  }\n\n  /// Check whether a command terminated prematurely.\n  bool checkNewlineInMarkup() {\n    final cu = currentCodeUnit;\n    if (cu == $carriageReturn || cu == $lineFeed) {\n      error('missing markup tag close token \"]\"');\n    }\n    return false;\n  }\n\n  /// Consumes a simple hash-tag sequence, consisting of '#' followed by any\n  /// number of non-control characters.\n  bool eatHashtag() {\n    final position0 = position;\n    if (eat($hash)) {\n      while (!eof) {\n        final cu = currentCodeUnit;\n        if ((cu == $slash && nextCodeUnit == $slash) ||\n            cu == $lineFeed ||\n            cu == $carriageReturn ||\n            cu == $space ||\n            cu == $tab ||\n            cu == $hash ||\n            cu == $dollar ||\n            cu == $lessThan) {\n          break;\n        }\n        position += 1;\n      }\n      if (position > position0 + 1) {\n        final tag = text.substring(position0, position);\n        pushToken(Token.hashtag(tag), position0);\n        return true;\n      }\n    }\n    position = position0;\n    return false;\n  }\n\n  static const Map<String, Token> keywords = {\n    'true': Token.constTrue,\n    'false': Token.constFalse,\n    'String': Token.typeString,\n    'Number': Token.typeNumber,\n    'Bool': Token.typeBool,\n    'as': Token.asType,\n    'to': Token.operatorAssign,\n    '=': Token.operatorAssign,\n    'is': Token.operatorEqual,\n    'eq': Token.operatorEqual,\n    '==': Token.operatorEqual,\n    'neq': Token.operatorNotEqual,\n    'ne': Token.operatorNotEqual,\n    '!=': Token.operatorNotEqual,\n    'le': Token.operatorLessOrEqual,\n    'lte': Token.operatorLessOrEqual,\n    '<=': Token.operatorLessOrEqual,\n    'ge': Token.operatorGreaterOrEqual,\n    'gte': Token.operatorGreaterOrEqual,\n    '>=': Token.operatorGreaterOrEqual,\n    'lt': Token.operatorLessThan,\n    '<': Token.operatorLessThan,\n    'gt': Token.operatorGreaterThan,\n    '>': Token.operatorGreaterThan,\n    'and': Token.operatorAnd,\n    '&&': Token.operatorAnd,\n    'or': Token.operatorOr,\n    '||': Token.operatorOr,\n    'xor': Token.operatorXor,\n    '^': Token.operatorXor,\n    'not': Token.operatorNot,\n    '!': Token.operatorNot,\n    '+': Token.operatorPlus,\n    '-': Token.operatorMinus,\n    '*': Token.operatorMultiply,\n    '/': Token.operatorDivide,\n    '%': Token.operatorModulo,\n    '+=': Token.operatorPlusAssign,\n    '-=': Token.operatorMinusAssign,\n    '*=': Token.operatorMultiplyAssign,\n    '/=': Token.operatorDivideAssign,\n    '%=': Token.operatorModuloAssign,\n    ',': Token.comma,\n    '(': Token.startParenthesis,\n    ')': Token.endParenthesis,\n  };\n  static const Map<String, Token> commandTokens = {\n    'character': Token.commandCharacter,\n    'declare': Token.commandDeclare,\n    'else': Token.commandElse,\n    'elseif': Token.commandElseif,\n    'endif': Token.commandEndif,\n    'if': Token.commandIf,\n    'jump': Token.commandJump,\n    'local': Token.commandLocal,\n    'set': Token.commandSet,\n    'stop': Token.commandStop,\n    'visit': Token.commandVisit,\n    'wait': Token.commandWait,\n  };\n\n  /// Built-in commands that have no arguments.\n  static final Set<Token> simpleCommands = {\n    Token.commandElse,\n    Token.commandEndif,\n    Token.commandStop,\n  };\n\n  /// Built-in commands that are followed by an expression (without `{}`).\n  static final Set<Token> bareExpressionCommands = {\n    Token.commandCharacter,\n    Token.commandDeclare,\n    Token.commandElseif,\n    Token.commandIf,\n    Token.commandLocal,\n    Token.commandSet,\n    Token.commandWait,\n  };\n\n  static final Set<Token> nodeTargetingCommands = {\n    Token.commandJump,\n    Token.commandVisit,\n  };\n\n  /// Throws a [SyntaxError] with the given [message], augmenting it with the\n  /// information about the current parsing location.\n  ///\n  /// The return type is bool, although the method never actually returns\n  /// anything -- this is so that the method can be used in a parsing chain:\n  /// ```dart\n  /// eatThis() || eatThat() || error('oops, did not expect that');\n  /// ```\n  bool error(String message) {\n    final locationDescription = _errorMessageAtPosition(position);\n    throw SyntaxError('$message\\n$locationDescription\\n');\n  }\n\n  String _errorMessageAtPosition(int position) {\n    final lineEnd = _findLineEnd(position);\n    final lineStart = _findLineStart(position);\n    String lineFragment;\n    String markerIndent;\n    if (lineEnd - lineStart <= 74) {\n      lineFragment = text.substring(lineStart, lineEnd);\n      markerIndent = ' ' * (position - lineStart);\n    } else if (position - lineStart <= 50) {\n      lineFragment = '${text.substring(lineStart, lineStart + 74)}...';\n      markerIndent = ' ' * (position - lineStart);\n    } else if (lineEnd - position <= 40) {\n      lineFragment = '...${text.substring(lineEnd - 77, lineEnd)}';\n      markerIndent = ' ' * (position - lineEnd + 80);\n    } else {\n      lineFragment = '...${text.substring(position - 36, position + 35)}...';\n      markerIndent = ' ' * 39;\n    }\n    final line = this.position <= lineEnd ? lineNumber : lineNumber - 1;\n    return '>  at line $line column ${position - lineStart + 1}:\\n'\n        '>  $lineFragment\\n'\n        '>  $markerIndent^';\n  }\n\n  int _findLineStart(int position) {\n    var i = position - 1;\n    while (i >= 0) {\n      final cu = text.codeUnitAt(i);\n      if (cu == $lineFeed || cu == $carriageReturn) {\n        break;\n      }\n      i -= 1;\n    }\n    return i + 1;\n  }\n\n  /// Returns the position where the current (starting at [position]) line ends,\n  /// without altering the parsing location.\n  int _findLineEnd(int position) {\n    var i = position;\n    while (i < text.length) {\n      final cu = text.codeUnitAt(i);\n      if (cu == $lineFeed || cu == $carriageReturn) {\n        break;\n      }\n      i += 1;\n    }\n    return i;\n  }\n\n  /// Is [cp] a valid code-point to start an ASCII identifier?\n  static bool isAsciiIdentifierStart(int cp) {\n    return (cp >= $lowercaseA && cp <= $lowercaseZ) ||\n        (cp >= $uppercaseA && cp <= $uppercaseZ) ||\n        cp == $underscore;\n  }\n\n  /// Is [cp] a valid code-point to continue an ASCII identifier?\n  static bool isAsciiIdentifierChar(int cp) {\n    return isAsciiIdentifierStart(cp) || (cp >= $digit0 && cp <= $digit9);\n  }\n\n  /// Is [cp] a valid code-point to start a Unicode identifier?\n  static bool isUnicodeIdentifierStart(int cp) {\n    if (cp < 0x80) {\n      return isAsciiIdentifierStart(cp);\n    } else if (cp <= 0x1FFF) {\n      return (cp == 0xA8 || cp == 0xAA || cp == 0xAD || cp == 0xAF) ||\n          (cp >= 0xB2 && cp <= 0xBE && cp != 0xB6 && cp != 0xBB) ||\n          (cp >= 0xC0 && cp <= 0x2FF && cp != 0xD7 && cp != 0xF7) ||\n          (cp >= 0x370 && cp <= 0x1DBF && cp != 0x1680 && cp != 0x180E) ||\n          (cp >= 0x1E00 && cp <= 0x1FFF);\n    } else {\n      return (cp >= 0x200B && cp <= 0x200D) ||\n          (cp >= 0x202A && cp <= 0x202E) ||\n          (cp >= 0x203F && cp <= 0x2040) ||\n          (cp == 0x2054) ||\n          (cp >= 0x2060 && cp <= 0x20CF) ||\n          (cp >= 0x2100 && cp <= 0x218F) ||\n          (cp >= 0x2460 && cp <= 0x24FF) ||\n          (cp >= 0x2776 && cp <= 0x2793) ||\n          (cp >= 0x2C00 && cp <= 0x2DFF) ||\n          (cp >= 0x2E80 && cp <= 0x2FFF) ||\n          (cp >= 0x3004 && cp <= 0x3007) ||\n          (cp >= 0x3021 && cp <= 0xD7FF && cp != 0x3030) ||\n          (cp >= 0xF900 && cp <= 0xFD3D) ||\n          (cp >= 0xFD40 && cp <= 0xFDCF) ||\n          (cp >= 0xFDF0 && cp <= 0xFE1F) ||\n          (cp >= 0xFE30 && cp <= 0xFE44) ||\n          (cp >= 0xFE47 && cp <= 0xFFFD) ||\n          (cp >= 0x10000 && cp <= 0xEFFFF && ((cp + 2) & 0xFFFF) >= 2);\n    }\n  }\n\n  /// Is [cp] a valid code-point to continue a Unicode identifier?\n  static bool isUnicodeIdentifierChar(int cp) {\n    if (cp < 0x80) {\n      return isAsciiIdentifierChar(cp);\n    } else {\n      return isUnicodeIdentifierStart(cp) ||\n          (cp >= 0x300 && cp <= 0x36F) ||\n          (cp >= 0x1DC0 && cp <= 0x1DFF) ||\n          (cp >= 0x20D0 && cp <= 0x20FF) ||\n          (cp >= 0xFE20 && cp <= 0xFE2F);\n    }\n  }\n}\n\ntypedef _ModeFn = bool Function();\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/block.dart",
    "content": "import 'package:jenny/src/structure/dialogue_entry.dart';\n\nclass Block {\n  const Block(this.lines);\n  const Block.empty() : lines = const [];\n\n  final List<DialogueEntry> lines;\n\n  int get length => lines.length;\n  bool get isEmpty => lines.isEmpty;\n  bool get isNotEmpty => lines.isNotEmpty;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/commands/character_command.dart",
    "content": "import 'package:jenny/src/dialogue_runner.dart';\nimport 'package:jenny/src/structure/commands/command.dart';\n\nclass CharacterCommand extends Command {\n  const CharacterCommand();\n\n  @override\n  String get name => 'character';\n\n  @override\n  void execute(DialogueRunner dialogue) => throw AssertionError();\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/commands/command.dart",
    "content": "import 'dart:async';\n\nimport 'package:jenny/src/dialogue_runner.dart';\nimport 'package:jenny/src/structure/dialogue_entry.dart';\n\nabstract class Command extends DialogueEntry {\n  const Command();\n\n  FutureOr<void> execute(DialogueRunner dialogue);\n\n  String get name;\n\n  @override\n  Future<void> processInDialogueRunner(DialogueRunner dialogueRunner) {\n    return dialogueRunner.deliverCommand(this);\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/commands/declare_command.dart",
    "content": "import 'package:jenny/src/dialogue_runner.dart';\nimport 'package:jenny/src/structure/commands/command.dart';\n\n/// The `<<declare>>` command in yarn script.\n///\n/// The purpose of this command is to introduce a new variable, giving it a\n/// specific type and initial value. All variables must be declared via this\n/// command before they can be used.\n///\n/// The `<<declare>>` command can only appear outside of nodes (at the \"root\"\n/// level of the file), and it is executed at compile-time. For this reason the\n/// [DeclareCommand] class has no content: it is only used internally during\n/// parsing, and will never be seen by a dialogue runner.\n///\n/// The command itself can take one of the several forms:\n/// ```\n/// <<declare $variable = 7>>\n/// <<declare $variable as Number>>  // initial value will be 0\n/// <<declare $variable = 7 as Number>>\n/// ```\nclass DeclareCommand extends Command {\n  const DeclareCommand();\n\n  @override\n  String get name => 'declare';\n\n  @override\n  void execute(DialogueRunner dialogue) => throw AssertionError();\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/commands/if_command.dart",
    "content": "import 'package:jenny/src/dialogue_runner.dart';\nimport 'package:jenny/src/structure/block.dart';\nimport 'package:jenny/src/structure/commands/command.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\n\nclass IfCommand extends Command {\n  const IfCommand(this.ifs);\n\n  /// First entry here is the `<<if>>` command, subsequent entries are the\n  /// `<<elseif>>` commands, and the last entry is the `<<else>>` block (if\n  /// present), which is represented as an [IfBlock] with `condition =\n  /// constTrue`.\n  final List<IfBlock> ifs;\n\n  @override\n  String get name => 'if';\n\n  @override\n  void execute(DialogueRunner dialogue) {\n    for (final ifBlock in ifs) {\n      if (ifBlock.condition.value) {\n        dialogue.enterBlock(ifBlock.block);\n        break;\n      }\n    }\n  }\n}\n\nclass IfBlock {\n  const IfBlock(this.condition, this.block);\n\n  final BoolExpression condition;\n  final Block block;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/commands/jump_command.dart",
    "content": "import 'package:jenny/src/dialogue_runner.dart';\nimport 'package:jenny/src/structure/commands/command.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\n\nclass JumpCommand extends Command {\n  const JumpCommand(this.target);\n\n  final StringExpression target;\n\n  @override\n  String get name => 'jump';\n\n  @override\n  void execute(DialogueRunner dialogue) {\n    dialogue.jumpToNode(target.value);\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/commands/local_command.dart",
    "content": "import 'package:jenny/src/dialogue_runner.dart';\nimport 'package:jenny/src/structure/commands/command.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/variable_storage.dart';\n\nclass LocalCommand extends Command {\n  const LocalCommand(this.variable, this.expression, this.storage);\n\n  final String variable;\n  final Expression expression;\n  final VariableStorage storage;\n\n  @override\n  String get name => 'local';\n\n  @override\n  void execute(DialogueRunner dialogue) {\n    storage.setVariable(variable, expression.value);\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/commands/set_command.dart",
    "content": "import 'package:jenny/src/dialogue_runner.dart';\nimport 'package:jenny/src/structure/commands/command.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/variable_storage.dart';\n\nclass SetCommand extends Command {\n  const SetCommand(this.variable, this.expression, this.storage);\n\n  final String variable;\n  final Expression expression;\n  final VariableStorage storage;\n\n  @override\n  String get name => 'set';\n\n  @override\n  void execute(DialogueRunner dialogue) {\n    storage.setVariable(variable, expression.value);\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/commands/stop_command.dart",
    "content": "import 'package:jenny/src/dialogue_runner.dart';\nimport 'package:jenny/src/structure/commands/command.dart';\n\nclass StopCommand extends Command {\n  const StopCommand();\n\n  @override\n  String get name => 'stop';\n\n  @override\n  void execute(DialogueRunner dialogue) {\n    dialogue.jumpToNode(null);\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/commands/user_defined_command.dart",
    "content": "import 'dart:async';\n\nimport 'package:jenny/src/dialogue_runner.dart';\nimport 'package:jenny/src/structure/commands/command.dart';\nimport 'package:jenny/src/structure/line_content.dart';\nimport 'package:meta/meta.dart';\n\nclass UserDefinedCommand extends Command {\n  UserDefinedCommand(this.name, this._content);\n\n  @override\n  final String name;\n  final LineContent _content;\n  late String _argumentString;\n  late List<dynamic>? _arguments;\n\n  /// The arguments to this command, as a single non-parsed string.\n  String get argumentString => _argumentString;\n\n  /// Parsed arguments, as a list. This may be `null` if a command was declared\n  /// without a signature (i.e. as a \"dialogue command\").\n  List<dynamic>? get arguments => _arguments;\n\n  @override\n  FutureOr<void> execute(DialogueRunner dialogue) {\n    return dialogue.project.commands.runCommand(this);\n  }\n\n  @override\n  String toString() => 'Command($name)';\n\n  @internal\n  LineContent get content => _content;\n\n  @internal\n  set argumentString(String value) => _argumentString = value;\n\n  @internal\n  set arguments(List<dynamic>? value) => _arguments = value;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/commands/visit_command.dart",
    "content": "import 'package:jenny/src/dialogue_runner.dart';\nimport 'package:jenny/src/structure/commands/command.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\n\nclass VisitCommand extends Command {\n  VisitCommand(this.target);\n\n  final StringExpression target;\n\n  @override\n  Future<void> execute(DialogueRunner dialogue) {\n    return dialogue.visitNode(target.value);\n  }\n\n  @override\n  String get name => 'visit';\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/commands/wait_command.dart",
    "content": "import 'package:jenny/src/dialogue_runner.dart';\nimport 'package:jenny/src/errors.dart';\nimport 'package:jenny/src/structure/commands/command.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\n\nclass WaitCommand extends Command {\n  const WaitCommand(this.arg);\n\n  final NumExpression arg;\n\n  @override\n  String get name => 'wait';\n\n  @override\n  Future<void> execute(DialogueRunner dialogue) {\n    final seconds = arg.value.toDouble();\n    if (seconds < 0) {\n      throw DialogueError('<<wait>> command with negative duration: $seconds');\n    }\n    return Future.delayed(\n      Duration(microseconds: (seconds * 1000000).toInt()),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/dialogue_choice.dart",
    "content": "import 'package:jenny/src/dialogue_runner.dart';\nimport 'package:jenny/src/structure/dialogue_entry.dart';\nimport 'package:jenny/src/structure/dialogue_option.dart';\n\nclass DialogueChoice extends DialogueEntry {\n  const DialogueChoice(this.options);\n\n  final List<DialogueOption> options;\n\n  @override\n  Future<void> processInDialogueRunner(DialogueRunner dialogueRunner) {\n    for (final option in options) {\n      option.evaluate();\n    }\n    return dialogueRunner.deliverChoices(this);\n  }\n\n  @override\n  String toString() => 'DialogueChoice($options)';\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/dialogue_entry.dart",
    "content": "import 'package:jenny/src/dialogue_runner.dart';\nimport 'package:jenny/src/structure/commands/command.dart';\nimport 'package:jenny/src/structure/dialogue_choice.dart';\nimport 'package:jenny/src/structure/dialogue_line.dart';\n\n/// Base class for all entries in a dialogue.\n///\n/// Each entry is processed independently and sequentially by the dialogue\n/// runner, as if they were statements in a program.\n///\n/// There are 3 kinds of dialogue entries:\n/// - [Command]\n/// - [DialogueChoice]\n/// - [DialogueLine]\nabstract class DialogueEntry {\n  const DialogueEntry();\n\n  /// Runs the entry within the context of a dialogue runner.\n  ///\n  /// This method is invoked by the [dialogueRunner] itself, at the right time.\n  Future<void> processInDialogueRunner(DialogueRunner dialogueRunner);\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/dialogue_line.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:jenny/src/structure/dialogue_entry.dart';\nimport 'package:jenny/src/structure/line_content.dart';\n\n/// The **DialogueLine** class represents a single line of text within the\n/// dialogue [[Line]].\n///\n/// The `DialogueLine` objects will be delivered to your [DialogueView] with\n/// methods `onLineStart()`, `onLineSignal()`, `onLineStop()`, and\n/// `onLineFinish()`.\n///\n/// A dialogue line may contain a [character] (the name of the entity who is\n/// speaking), and [tags] -- a list of hashtag tokens that specify some meta\n/// information about the line.\n///\n/// Example of a dialogue line in yarn script:\n/// ```yarn\n/// Hermione: Holy cricket! You're Harry Potter.  #surprised\n/// ```\n/// Here the [character] is \"Hermione\", the [text] of the line is \"Holy\n/// cricket! You're Harry Potter.\", and the [tags] list will contain a single\n/// entry \"#surprised\".\n///\n/// A dialogue line may also contain inline expressions, which will be\n/// re-evaluated every time the line is executed by the dialogue runner:\n/// ```yarn\n/// Jenny: My favorite color is {$favoriteColor}, what about you?\n/// ```\n/// After evaluation, the resulting string may be \"My favorite color is\n/// vantablack, what about you?\".\n///\n/// Lastly, a dialogue line may have markup attributes. These are similar to\n/// HTML tags, only they use square brackets:\n/// ```yarn\n/// Jenny: My [i]favorite[/i] color is [bb color=$color]{$color}[/bb].\n/// ```\n/// These markup attributes will not be visible in the output (i.e. the\n/// resulting text is still \"My favorite color is vantablack\"), but they can be\n/// queried in the [attributes] list, which will specify that there is an\n/// attribute `[i]` around the word \"favorite\", and another attribute `[bb]`\n/// with parameter `color` around the word \"vantablack\".\n///\n/// Inline expressions cannot contain markup attributes.\nclass DialogueLine extends DialogueEntry {\n  DialogueLine({\n    required LineContent content,\n    Character? character,\n    List<String>? tags,\n  }) : _content = content,\n       _character = character,\n       _tags = tags,\n       _value = content.isConst ? content.text : null;\n\n  final Character? _character;\n  final List<String>? _tags;\n  final LineContent _content;\n  String? _value;\n\n  /// The content of this Line.\n  LineContent? get content => _content;\n\n  /// The character who is speaking the line. This can be null if the line does\n  /// not contain a speaker.\n  Character? get character => _character;\n\n  /// The computed text of the line, after substituting all inline expressions,\n  /// stripping the markup, and processing the escape sequences.\n  ///\n  /// This value can only be accessed after the line was [evaluate]d. It may\n  /// change upon subsequent re-evaluations of the line (which occur each time\n  /// the line goes through a [DialogueRunner]).\n  String get text {\n    assert(_value != null, 'Line was not evaluated');\n    return _value!;\n  }\n\n  /// The list of hashtags associated with the line. If there are no hashtags,\n  /// the list will be empty.\n  ///\n  /// Each value in the list will start with the `#` symbol.\n  List<String> get tags => _tags ?? const [];\n\n  /// The list of markup spans associated with the line.\n  List<MarkupAttribute> get attributes => _content.attributes ?? const [];\n\n  /// True if the line will never change upon subsequent reruns. That is, when\n  /// the line does not depend on any dynamic expressions.\n  bool get isConst => _content.isConst;\n\n  @override\n  Future<void> processInDialogueRunner(DialogueRunner dialogueRunner) {\n    evaluate();\n    return dialogueRunner.deliverLine(this);\n  }\n\n  /// Computes the [text] of the line, substituting the current values of all\n  /// inline expressions.\n  ///\n  /// Normally, you wouldn't need to call this method manually -- the\n  /// [DialogueRunner] will take care to do that for you. However, it may be\n  /// necessary to call this if you need to access `DialogueLine`s outside of\n  /// a dialogue runner.\n  void evaluate() {\n    _value = _content.evaluate();\n  }\n\n  @override\n  String toString() {\n    final prefix = character == null ? '' : '${character!.name}: ';\n    final text = _value ?? '<unevaluated>';\n    return 'DialogueLine($prefix$text)';\n  }\n\n  @override\n  bool operator ==(Object other) =>\n      other is DialogueLine &&\n      text == other.text &&\n      character == other.character;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/dialogue_option.dart",
    "content": "import 'package:jenny/src/structure/block.dart';\nimport 'package:jenny/src/structure/dialogue_line.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\n\nclass DialogueOption extends DialogueLine {\n  DialogueOption({\n    required super.content,\n    super.character,\n    super.tags,\n    BoolExpression? condition,\n    this.block = const Block([]),\n  }) : _condition = condition;\n\n  final BoolExpression? _condition;\n  final Block block;\n  bool _available = true;\n\n  bool get isAvailable => _available;\n  bool get isDisabled => !_available;\n\n  @override\n  void evaluate() {\n    super.evaluate();\n    _available = _condition?.value ?? true;\n  }\n\n  @override\n  String toString() {\n    final prefix = character == null ? '' : '${character!.name}: ';\n    final suffix = _available ? '' : ' #disabled';\n    return 'Option($prefix$text$suffix)';\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/expression.dart",
    "content": "abstract class Expression {\n  const Expression();\n\n  dynamic get value;\n\n  bool get isNumeric => type == ExpressionType.numeric;\n  bool get isBoolean => type == ExpressionType.boolean;\n  bool get isString => type == ExpressionType.string;\n\n  ExpressionType get type {\n    return switch (this) {\n      NumExpression() => ExpressionType.numeric,\n      BoolExpression() => ExpressionType.boolean,\n      StringExpression() => ExpressionType.string,\n      _ => ExpressionType.unknown,\n    };\n  }\n}\n\nenum ExpressionType {\n  unknown,\n  boolean,\n  numeric,\n  string,\n}\n\nabstract class NumExpression extends Expression {\n  const NumExpression();\n\n  @override\n  num get value;\n}\n\nabstract class StringExpression extends Expression {\n  const StringExpression();\n\n  @override\n  String get value;\n}\n\nabstract class BoolExpression extends Expression {\n  const BoolExpression();\n\n  @override\n  bool get value;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/_common.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/bool.dart';\nimport 'package:jenny/src/structure/expressions/functions/ceil.dart';\nimport 'package:jenny/src/structure/expressions/functions/dec.dart';\nimport 'package:jenny/src/structure/expressions/functions/decimal.dart';\nimport 'package:jenny/src/structure/expressions/functions/dice.dart';\nimport 'package:jenny/src/structure/expressions/functions/floor.dart';\nimport 'package:jenny/src/structure/expressions/functions/if.dart';\nimport 'package:jenny/src/structure/expressions/functions/inc.dart';\nimport 'package:jenny/src/structure/expressions/functions/int.dart';\nimport 'package:jenny/src/structure/expressions/functions/number.dart';\nimport 'package:jenny/src/structure/expressions/functions/plural.dart';\nimport 'package:jenny/src/structure/expressions/functions/random.dart';\nimport 'package:jenny/src/structure/expressions/functions/random_range.dart';\nimport 'package:jenny/src/structure/expressions/functions/round.dart';\nimport 'package:jenny/src/structure/expressions/functions/round_places.dart';\nimport 'package:jenny/src/structure/expressions/functions/string.dart';\nimport 'package:jenny/src/structure/expressions/functions/visit_count.dart';\nimport 'package:jenny/src/structure/expressions/functions/visited.dart';\nimport 'package:jenny/src/yarn_project.dart';\nimport 'package:meta/meta.dart';\n\ntypedef ErrorFn = Never Function(String message, [int? position]);\n\nclass FunctionArgument {\n  FunctionArgument(this.expression, this.position);\n  final Expression expression;\n  final int position;\n}\n\ntypedef FunctionBuilder =\n    Expression Function(\n      List<FunctionArgument>,\n      YarnProject,\n      ErrorFn,\n    );\n\n/// This is a complete list of all builtin functions in Jenny.\n///\n/// When adding a new function, make sure to also update the list in\n/// /doc/_sphinx/extensions/yarn_lexer.py.\nconst Map<String, FunctionBuilder> builtinFunctions = {\n  'bool': BoolFn.make,\n  'ceil': CeilFn.make,\n  'dec': DecFn.make,\n  'decimal': DecimalFn.make,\n  'dice': DiceFn.make,\n  'floor': FloorFn.make,\n  'if': makeIfFn,\n  'inc': IncFn.make,\n  'int': IntFn.make,\n  'number': NumberFn.make,\n  'plural': PluralFn.make,\n  'random': RandomFn.make,\n  'random_range': RandomRangeFn.make,\n  'round': RoundFn.make,\n  'round_places': RoundPlacesFn.make,\n  'string': StringFn.make,\n  'visit_count': VisitCountFn.make,\n  'visited_count': VisitCountFn.make,\n  'visited': VisitedFn.make,\n};\n\n@internal\nExpression num1Builder(\n  String name,\n  Expression Function(NumExpression) constructor,\n  List<FunctionArgument> args,\n  ErrorFn errorFn,\n) {\n  if (args.length != 1) {\n    errorFn(\n      'function $name() requires a single argument',\n      args.isEmpty ? null : args[1].position,\n    );\n  }\n  if (!args[0].expression.isNumeric) {\n    errorFn('the argument in $name() should be numeric', args[0].position);\n  }\n  return constructor(args[0].expression as NumExpression);\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/bool.dart",
    "content": "import 'package:jenny/src/errors.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\n/// Function `bool(x)` converts its argument into a boolean.\n///\n/// If `x` is numeric, then the value 0 becomes `false` and all other values\n/// become `true`.\nclass BoolFn extends BoolExpression {\n  const BoolFn(this._arg);\n\n  final Expression _arg;\n\n  /// Static constructor to be used in parse.dart.\n  static Expression make(\n    List<FunctionArgument> args,\n    YarnProject yarnProject,\n    ErrorFn errorFn,\n  ) {\n    if (args.length != 1) {\n      errorFn(\n        'function bool() requires a single argument',\n        args.isEmpty ? null : args[1].position,\n      );\n    }\n    return BoolFn(args[0].expression);\n  }\n\n  @override\n  bool get value {\n    final dynamic x = _arg.value;\n    if (x is num) {\n      return x != 0;\n    }\n    if (x is String) {\n      final value = x.trim();\n      if (YarnProject.trueValues.contains(value)) {\n        return true;\n      }\n      if (YarnProject.falseValues.contains(value)) {\n        return false;\n      }\n      throw DialogueError(\n        'String value \"$x\" cannot be interpreted as a boolean',\n      );\n    }\n    return x as bool;\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/ceil.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\n/// Function `ceil(x)` will round `x` up towards positive infinity.\nclass CeilFn extends NumExpression {\n  const CeilFn(this.arg);\n\n  final NumExpression arg;\n\n  static Expression make(\n    List<FunctionArgument> args,\n    YarnProject yarnProject,\n    ErrorFn errorFn,\n  ) => num1Builder('ceil', CeilFn.new, args, errorFn);\n\n  @override\n  num get value => arg.value.ceil();\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/dec.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\n/// Function `dec(x)` decreases `x` towards previous integer. It is equal to\n/// `x - 1` if `x` is already integer, or `floor(x)` if `x` is not integer.\nclass DecFn extends NumExpression {\n  const DecFn(this.arg);\n\n  final NumExpression arg;\n\n  static Expression make(\n    List<FunctionArgument> args,\n    YarnProject yarnProject,\n    ErrorFn errorFn,\n  ) => num1Builder('dec', DecFn.new, args, errorFn);\n\n  @override\n  num get value {\n    final x = arg.value;\n    final y = x.floor();\n    return x == y ? y - 1 : y;\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/decimal.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\n/// Function `decimal(x)` returns a fractional point of `x`.\n///\n/// For positive `x` the value is from 0 to 1, for negative `x` the value is\n/// between 0 and -1. For any `x` it should be true that\n/// `x == int(x) + decimal(x)`.\nclass DecimalFn extends NumExpression {\n  const DecimalFn(this.arg);\n\n  final NumExpression arg;\n\n  static Expression make(\n    List<FunctionArgument> args,\n    YarnProject yarnProject,\n    ErrorFn errorFn,\n  ) => num1Builder('decimal', DecimalFn.new, args, errorFn);\n\n  @override\n  num get value {\n    final x = arg.value;\n    return x - x.toInt();\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/dice.dart",
    "content": "import 'package:jenny/src/errors.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\n/// Function `dice(n)` returns a random integer between 1 and `n`, inclusive.\nclass DiceFn extends NumExpression {\n  const DiceFn(this._n, this._yarn);\n\n  final NumExpression _n;\n  final YarnProject _yarn;\n\n  static Expression make(\n    List<FunctionArgument> args,\n    YarnProject yarnProject,\n    ErrorFn errorFn,\n  ) {\n    if (args.length != 1) {\n      errorFn(\n        'function dice() requires a single argument',\n        args.isEmpty ? null : args[1].position,\n      );\n    }\n    if (!args[0].expression.isNumeric) {\n      errorFn('the argument should be numeric', args[0].position);\n    }\n    return DiceFn(args[0].expression as NumExpression, yarnProject);\n  }\n\n  @override\n  num get value {\n    final n = _n.value.toInt();\n    if (n <= 0) {\n      throw DialogueError('Argument to dice() must be positive: $n');\n    }\n    return _yarn.random.nextInt(n) + 1;\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/floor.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\n/// Function `floor(x)` will round `x` down towards negative infinity.\nclass FloorFn extends NumExpression {\n  const FloorFn(this.arg);\n\n  final NumExpression arg;\n\n  static Expression make(\n    List<FunctionArgument> args,\n    YarnProject yarnProject,\n    ErrorFn errorFn,\n  ) => num1Builder('floor', FloorFn.new, args, errorFn);\n\n  @override\n  num get value => arg.value.floor();\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/if.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\nExpression makeIfFn(\n  List<FunctionArgument> args,\n  YarnProject yarnProject,\n  ErrorFn errorFn,\n) {\n  if (args.length != 3) {\n    errorFn(\n      'function if() requires three arguments',\n      args.length < 3 ? null : args[3].position,\n    );\n  }\n  if (!args[0].expression.isBoolean) {\n    errorFn(\n      'first argument in if() should be a boolean condition',\n      args[0].position,\n    );\n  }\n  final type1 = args[1].expression.type;\n  final type2 = args[2].expression.type;\n  if (type1 != type2) {\n    errorFn(\n      'the types of the second and the third arguments in if() must be the '\n      'same, instead they were ${type1.name} and ${type2.name}',\n      args[2].position,\n    );\n  }\n  if (args[1].expression.isBoolean) {\n    return _IfFnBoolean(\n      args[0].expression as BoolExpression,\n      args[1].expression as BoolExpression,\n      args[2].expression as BoolExpression,\n    );\n  } else if (args[1].expression.isNumeric) {\n    return _IfFnNumeric(\n      args[0].expression as BoolExpression,\n      args[1].expression as NumExpression,\n      args[2].expression as NumExpression,\n    );\n  } else {\n    assert(args[1].expression.isString);\n    return _IfFnString(\n      args[0].expression as BoolExpression,\n      args[1].expression as StringExpression,\n      args[2].expression as StringExpression,\n    );\n  }\n}\n\nclass _IfFnBoolean extends BoolExpression {\n  _IfFnBoolean(this._condition, this._then, this._else);\n\n  final BoolExpression _condition;\n  final BoolExpression _then;\n  final BoolExpression _else;\n\n  @override\n  bool get value => _condition.value ? _then.value : _else.value;\n}\n\nclass _IfFnNumeric extends NumExpression {\n  _IfFnNumeric(this._condition, this._then, this._else);\n\n  final BoolExpression _condition;\n  final NumExpression _then;\n  final NumExpression _else;\n\n  @override\n  num get value => _condition.value ? _then.value : _else.value;\n}\n\nclass _IfFnString extends StringExpression {\n  _IfFnString(this._condition, this._then, this._else);\n\n  final BoolExpression _condition;\n  final StringExpression _then;\n  final StringExpression _else;\n\n  @override\n  String get value => _condition.value ? _then.value : _else.value;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/inc.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\n/// Function `inc(x)` increases `x` towards next integer. It is equal to `x + 1`\n/// if `x` is already integer, or `ceil(x)` if `x` is not integer.\nclass IncFn extends NumExpression {\n  const IncFn(this.arg);\n\n  final NumExpression arg;\n\n  static Expression make(\n    List<FunctionArgument> args,\n    YarnProject yarnProject,\n    ErrorFn errorFn,\n  ) => num1Builder('inc', IncFn.new, args, errorFn);\n\n  @override\n  num get value {\n    final x = arg.value;\n    final y = x.ceil();\n    return x == y ? y + 1 : y;\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/int.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\n/// Function `int(x)` truncates fractional part of `x`, rounding it towards 0.\nclass IntFn extends NumExpression {\n  const IntFn(this.arg);\n\n  final NumExpression arg;\n\n  static Expression make(\n    List<FunctionArgument> args,\n    YarnProject yarnProject,\n    ErrorFn errorFn,\n  ) => num1Builder('int', IntFn.new, args, errorFn);\n\n  @override\n  num get value => arg.value.truncate();\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/number.dart",
    "content": "import 'package:jenny/src/errors.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\n/// Function `number(x)` converts any `x` into a number, if possible.\n///\n/// If `x` is boolean, then it will be converted into numbers 0 or 1. If `x` is\n/// a string, then\nclass NumberFn extends NumExpression {\n  const NumberFn(this._arg);\n\n  final Expression _arg;\n\n  /// Static constructor to be used in parse.dart.\n  static Expression make(\n    List<FunctionArgument> args,\n    YarnProject yarnProject,\n    ErrorFn errorFn,\n  ) {\n    if (args.length != 1) {\n      errorFn(\n        'function number() requires a single argument',\n        args.isEmpty ? null : args[1].position,\n      );\n    }\n    return NumberFn(args[0].expression);\n  }\n\n  @override\n  num get value {\n    final dynamic value = _arg.value;\n    if (value is bool) {\n      return value ? 1 : 0;\n    }\n    if (value is String) {\n      final result = num.tryParse(value);\n      if (result == null) {\n        throw DialogueError('Unable to convert string \"$value\" into a number');\n      }\n      return result;\n    }\n    return value as num;\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/plural.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\nclass PluralFn extends StringExpression {\n  PluralFn(this._num, this._words, this._yarn);\n\n  final NumExpression _num;\n  final List<StringExpression> _words;\n  final YarnProject _yarn;\n\n  /// Static constructor, used by parse.dart.\n  static Expression make(\n    List<FunctionArgument> arguments,\n    YarnProject yarnProject,\n    ErrorFn errorFn,\n  ) {\n    final min = 1 + yarnProject.localization.pluralMinWordCount;\n    final max = 1 + yarnProject.localization.pluralMaxWordCount;\n    if (arguments.length < min) {\n      errorFn('function plural() requires at least $min arguments');\n    }\n    if (arguments.length > max) {\n      errorFn(\n        'function plural() requires at most $max arguments',\n        arguments[max].position,\n      );\n    }\n    if (!arguments[0].expression.isNumeric) {\n      errorFn(\n        'the first argument in plural() should be numeric',\n        arguments[0].position,\n      );\n    }\n    final convertedArgs = <StringExpression>[];\n    for (final argument in arguments.skip(1)) {\n      if (!argument.expression.isString) {\n        errorFn('a string argument is expected', argument.position);\n      }\n      convertedArgs.add(argument.expression as StringExpression);\n    }\n    return PluralFn(\n      arguments[0].expression as NumExpression,\n      convertedArgs,\n      yarnProject,\n    );\n  }\n\n  @override\n  String get value {\n    final value = _num.value;\n    final words = _words.map((w) => w.value).toList(growable: false);\n    final result = _yarn.localization.pluralFunction(value, words);\n    if (result.contains('%')) {\n      return result.replaceAll('%', value.toString());\n    } else {\n      return result;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/random.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\n/// Function `random()` returns a random double between 0 and 1.\nclass RandomFn extends NumExpression {\n  RandomFn(this._yarn);\n\n  final YarnProject _yarn;\n\n  static Expression make(\n    List<FunctionArgument> arguments,\n    YarnProject yarnProject,\n    ErrorFn errorFn,\n  ) {\n    if (arguments.isNotEmpty) {\n      errorFn('function random() requires no arguments', arguments[0].position);\n    }\n    return RandomFn(yarnProject);\n  }\n\n  @override\n  num get value => _yarn.random.nextDouble();\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/random_range.dart",
    "content": "import 'package:jenny/src/errors.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\n/// Function `random_range(a, b)` returns a random integer between `a` and `b`,\n/// inclusive.\nclass RandomRangeFn extends NumExpression {\n  const RandomRangeFn(this._a, this._b, this._yarn);\n\n  final NumExpression _a;\n  final NumExpression _b;\n  final YarnProject _yarn;\n\n  /// Static constructor, used by parse.dart.\n  static Expression make(\n    List<FunctionArgument> args,\n    YarnProject yarnProject,\n    ErrorFn errorFn,\n  ) {\n    if (args.length != 2) {\n      errorFn('function random_range() requires two arguments');\n    }\n    if (!args[0].expression.isNumeric) {\n      errorFn('the first argument should be numeric', args[0].position);\n    }\n    if (!args[1].expression.isNumeric) {\n      errorFn('the second argument should be numeric', args[1].position);\n    }\n    return RandomRangeFn(\n      args[0].expression as NumExpression,\n      args[1].expression as NumExpression,\n      yarnProject,\n    );\n  }\n\n  @override\n  num get value {\n    final lower = _a.value.toInt();\n    final upper = _b.value.toInt();\n    if (upper < lower) {\n      throw DialogueError(\n        'In random_range(a=$lower, b=$upper) the upper bound cannot be less '\n        'than the lower bound',\n      );\n    }\n    return _yarn.random.nextInt(upper - lower + 1) + lower;\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/round.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\n/// Function `round(x)` will round `x` to the nearest integer.\nclass RoundFn extends NumExpression {\n  const RoundFn(this.arg);\n\n  final NumExpression arg;\n\n  static Expression make(\n    List<FunctionArgument> args,\n    YarnProject yarnProject,\n    ErrorFn errorFn,\n  ) => num1Builder('round', RoundFn.new, args, errorFn);\n\n  @override\n  num get value => arg.value.round();\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/round_places.dart",
    "content": "import 'dart:math';\n\nimport 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\n/// Function `round_places(x, n)` will round `x` to `n` decimal places.\nclass RoundPlacesFn extends NumExpression {\n  const RoundPlacesFn(this.arg, this.places);\n\n  final NumExpression arg;\n  final NumExpression places;\n\n  static Expression make(\n    List<FunctionArgument> args,\n    YarnProject yarnProject,\n    ErrorFn errorFn,\n  ) {\n    if (args.length != 2) {\n      errorFn(\n        'function round_places() requires two arguments',\n        args.length < 2 ? null : args[2].position,\n      );\n    }\n    if (!args[0].expression.isNumeric) {\n      errorFn(\n        'first argument in round_places() should be numeric',\n        args[0].position,\n      );\n    }\n    if (!args[1].expression.isNumeric) {\n      errorFn(\n        'second argument in round_places() should be numeric',\n        args[1].position,\n      );\n    }\n    return RoundPlacesFn(\n      args[0].expression as NumExpression,\n      args[1].expression as NumExpression,\n    );\n  }\n\n  @override\n  num get value {\n    final precision = places.value.toInt();\n    final factor = pow(10, precision);\n    return (arg.value * factor).roundToDouble() / factor;\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/string.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\n/// Function `string(x)` converts a numeric or boolean `x` into a string.\nclass StringFn extends StringExpression {\n  StringFn(this._arg);\n\n  final Expression _arg;\n\n  /// Static constructor to be used in parse.dart.\n  static Expression make(\n    List<FunctionArgument> args,\n    YarnProject yarnProject,\n    ErrorFn errorFn,\n  ) {\n    if (args.length != 1) {\n      errorFn(\n        'function string() requires a single argument',\n        args.isEmpty ? null : args[1].position,\n      );\n    }\n    return StringFn(args[0].expression);\n  }\n\n  @override\n  String get value {\n    final dynamic v = _arg.value;\n    if (v is double) {\n      final i = v.toInt();\n      if (v == i) {\n        // Make sure that double such as 3.0 stringifies as if it was\n        // an integer: \"3\"\n        return i.toString();\n      }\n    }\n    return v.toString();\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/user_defined_function.dart",
    "content": "import 'package:jenny/src/function_storage.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\n\n/// Expression for a user-defined function that returns a numeric result.\nclass NumericUserDefinedFn extends NumExpression {\n  NumericUserDefinedFn(this._udf, this._arguments);\n\n  final Udf _udf;\n  final List<Expression> _arguments;\n\n  @override\n  num get value => _udf.run(_arguments) as num;\n}\n\n/// Expression for a user-defined function that returns a boolean result.\nclass BooleanUserDefinedFn extends BoolExpression {\n  BooleanUserDefinedFn(this._udf, this._arguments);\n\n  final Udf _udf;\n  final List<Expression> _arguments;\n\n  @override\n  bool get value => _udf.run(_arguments) as bool;\n}\n\n/// Expression for a user-defined function that returns a string result.\nclass StringUserDefinedFn extends StringExpression {\n  StringUserDefinedFn(this._udf, this._arguments);\n\n  final Udf _udf;\n  final List<Expression> _arguments;\n\n  @override\n  String get value => _udf.run(_arguments) as String;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/visit_count.dart",
    "content": "import 'package:jenny/src/errors.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\nclass VisitCountFn extends NumExpression {\n  VisitCountFn(this._node, this._yarn);\n\n  final StringExpression _node;\n  final YarnProject _yarn;\n\n  static Expression make(\n    List<FunctionArgument> args,\n    YarnProject yarnProject,\n    ErrorFn errorFn,\n  ) {\n    if (args.length != 1) {\n      errorFn(\n        'function visit_count() requires a single argument',\n        args.isEmpty ? null : args[1].position,\n      );\n    }\n    if (!args[0].expression.isString) {\n      errorFn('the argument should be a string', args[0].position);\n    }\n    return VisitCountFn(args[0].expression as StringExpression, yarnProject);\n  }\n\n  @override\n  num get value {\n    final node = _node.value;\n    final variableName = '@$node';\n    if (_yarn.variables.hasVariable(variableName)) {\n      return _yarn.variables.getNumericValue(variableName);\n    } else {\n      throw DialogueError('Unknown node name \"$node\"');\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/functions/visited.dart",
    "content": "import 'package:jenny/src/errors.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/functions/_common.dart';\nimport 'package:jenny/src/yarn_project.dart';\n\nclass VisitedFn extends BoolExpression {\n  VisitedFn(this._node, this._yarn);\n\n  final StringExpression _node;\n  final YarnProject _yarn;\n\n  static Expression make(\n    List<FunctionArgument> args,\n    YarnProject yarnProject,\n    ErrorFn errorFn,\n  ) {\n    if (args.length != 1) {\n      errorFn(\n        'function visited() requires a single argument',\n        args.isEmpty ? null : args[1].position,\n      );\n    }\n    if (!args[0].expression.isString) {\n      errorFn('the argument should be a string', args[0].position);\n    }\n    return VisitedFn(args[0].expression as StringExpression, yarnProject);\n  }\n\n  @override\n  bool get value {\n    final node = _node.value;\n    final variableName = '@$node';\n    if (_yarn.variables.hasVariable(variableName)) {\n      final visitCount = _yarn.variables.getNumericValue(variableName);\n      return visitCount > 0;\n    } else {\n      throw DialogueError('Unknown node name \"$node\"');\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/literal.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\n\nclass NumLiteral extends NumExpression {\n  const NumLiteral(this.value);\n\n  @override\n  final num value;\n}\n\nclass StringLiteral extends StringExpression {\n  const StringLiteral(this.value);\n\n  @override\n  final String value;\n}\n\nclass BoolLiteral extends BoolExpression {\n  // ignore: avoid_positional_boolean_parameters\n  const BoolLiteral(this.value);\n\n  @override\n  final bool value;\n}\n\nclass VoidLiteral extends Expression {\n  const VoidLiteral();\n\n  @override\n  dynamic get value => null;\n}\n\nconst constEmptyString = StringLiteral('');\nconst constTrue = BoolLiteral(true);\nconst constFalse = BoolLiteral(false);\nconst constVoid = VoidLiteral();\nconst constZero = NumLiteral(0);\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/operators/_common.dart",
    "content": "import 'package:jenny/src/parse/token.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/operators/add.dart';\nimport 'package:jenny/src/structure/expressions/operators/and.dart';\nimport 'package:jenny/src/structure/expressions/operators/divide.dart';\nimport 'package:jenny/src/structure/expressions/operators/equal.dart';\nimport 'package:jenny/src/structure/expressions/operators/greater_or_equal.dart';\nimport 'package:jenny/src/structure/expressions/operators/greater_than.dart';\nimport 'package:jenny/src/structure/expressions/operators/less_or_equal.dart';\nimport 'package:jenny/src/structure/expressions/operators/less_than.dart';\nimport 'package:jenny/src/structure/expressions/operators/modulo.dart';\nimport 'package:jenny/src/structure/expressions/operators/multiply.dart';\nimport 'package:jenny/src/structure/expressions/operators/not_equal.dart';\nimport 'package:jenny/src/structure/expressions/operators/or.dart';\nimport 'package:jenny/src/structure/expressions/operators/subtract.dart';\nimport 'package:jenny/src/structure/expressions/operators/xor.dart';\n\ntypedef ErrorFn = Never Function(String message, [int? position]);\ntypedef BinaryOperatorBuilder =\n    Expression Function(\n      Expression lhs,\n      Expression rhs,\n      int operatorPosition,\n      ErrorFn errorFn,\n    );\n\n/// Static constructor of expressions involving binary operators. Used by\n/// parse.dart.\nExpression makeBinaryOpExpression(\n  Token operator,\n  Expression lhs,\n  Expression rhs,\n  int operatorPosition,\n  ErrorFn errorFn,\n) {\n  final builder = _builders[operator];\n  return builder!(lhs, rhs, operatorPosition, errorFn);\n}\n\nfinal Map<Token, BinaryOperatorBuilder> _builders = {\n  Token.operatorAnd: And.make,\n  Token.operatorDivide: Divide.make,\n  Token.operatorEqual: Equal.make,\n  Token.operatorGreaterOrEqual: GreaterOrEqual.make,\n  Token.operatorGreaterThan: GreaterThan.make,\n  Token.operatorLessOrEqual: LessOrEqual.make,\n  Token.operatorLessThan: LessThan.make,\n  Token.operatorMinus: Subtract.make,\n  Token.operatorModulo: Modulo.make,\n  Token.operatorMultiply: Multiply.make,\n  Token.operatorNotEqual: NotEqual.make,\n  Token.operatorOr: Or.make,\n  Token.operatorPlus: Add.make,\n  Token.operatorXor: Xor.make,\n};\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/operators/add.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/operators/_common.dart';\n\n/// Common class for the PLUS (+) operator, with separate implementations for\n/// the numeric and string arguments.\nabstract class Add extends Expression {\n  factory Add.make(\n    Expression lhs,\n    Expression rhs,\n    int operatorPosition,\n    ErrorFn errorFn,\n  ) {\n    if (lhs.isNumeric && rhs.isNumeric) {\n      return _NumAdd(lhs as NumExpression, rhs as NumExpression);\n    }\n    if (lhs.isString && rhs.isString) {\n      return _StringAdd(lhs as StringExpression, rhs as StringExpression);\n    }\n    errorFn(\n      'both left and right sides of `+` must be numeric or strings, instead '\n      'the types are (${lhs.type.name}, ${rhs.type.name})',\n      operatorPosition,\n    );\n  }\n}\n\n/// Operator PLUS (+) for numeric arguments.\nclass _NumAdd extends NumExpression implements Add {\n  _NumAdd(this._lhs, this._rhs);\n\n  final NumExpression _lhs;\n  final NumExpression _rhs;\n\n  @override\n  num get value => _lhs.value + _rhs.value;\n}\n\n/// Operator PLUS (+) for string arguments. This operator simply concatenates\n/// strings.\nclass _StringAdd extends StringExpression implements Add {\n  _StringAdd(this._lhs, this._rhs);\n\n  final StringExpression _lhs;\n  final StringExpression _rhs;\n\n  @override\n  String get value => _lhs.value + _rhs.value;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/operators/and.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/operators/_common.dart';\n\n/// Logical AND operator, applies to binary operands only.\n///\n/// The AND operator returns `true` if both of its operands are `true`, and\n/// `false` otherwise.\nclass And extends BoolExpression {\n  And(this._lhs, this._rhs);\n\n  final BoolExpression _lhs;\n  final BoolExpression _rhs;\n\n  /// Static constructor, used by parse.dart.\n  factory And.make(\n    Expression lhs,\n    Expression rhs,\n    int operatorPosition,\n    ErrorFn errorFn,\n  ) {\n    if (lhs.isBoolean && rhs.isBoolean) {\n      return And(lhs as BoolExpression, rhs as BoolExpression);\n    }\n    errorFn(\n      'both left and right sides of `&&` must be boolean, instead the types '\n      'are (${lhs.type.name}, ${rhs.type.name})',\n      operatorPosition,\n    );\n  }\n\n  @override\n  bool get value => _lhs.value && _rhs.value;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/operators/divide.dart",
    "content": "import 'package:jenny/src/errors.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/operators/_common.dart';\n\n/// Operator DIVIDE (/), applies to numeric arguments only.\nclass Divide extends NumExpression {\n  const Divide(this._lhs, this._rhs);\n\n  final NumExpression _lhs;\n  final NumExpression _rhs;\n\n  /// Static constructor, used by parse.dart\n  factory Divide.make(\n    Expression lhs,\n    Expression rhs,\n    int operatorPosition,\n    ErrorFn errorFn,\n  ) {\n    if (lhs.isNumeric && rhs.isNumeric) {\n      return Divide(lhs as NumExpression, rhs as NumExpression);\n    }\n    errorFn(\n      'both left and right sides of `/` must be numeric, instead the types are '\n      '(${lhs.type.name}, ${rhs.type.name})',\n      operatorPosition,\n    );\n  }\n\n  @override\n  num get value {\n    final x = _lhs.value.toDouble();\n    final y = _rhs.value.toDouble();\n    if (y == 0) {\n      throw DialogueError('Division by zero');\n    }\n    return x / y;\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/operators/equal.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/operators/_common.dart';\n\n/// Common class for the EQUAL (==) operator, with separate implementations for\n/// each argument type.\nabstract class Equal extends Expression {\n  factory Equal.make(\n    Expression lhs,\n    Expression rhs,\n    int operatorPosition,\n    ErrorFn errorFn,\n  ) {\n    if (lhs.isNumeric && rhs.isNumeric) {\n      return _NumericEqual(lhs as NumExpression, rhs as NumExpression);\n    }\n    if (lhs.isString && rhs.isString) {\n      return _StringEqual(lhs as StringExpression, rhs as StringExpression);\n    }\n    if (lhs.isBoolean && rhs.isBoolean) {\n      return _BooleanEqual(lhs as BoolExpression, rhs as BoolExpression);\n    }\n    errorFn(\n      'equality operator between operands of unrelated types ${lhs.type.name} '\n      'and ${rhs.type.name}',\n      operatorPosition,\n    );\n  }\n}\n\n/// Operator EQUAL (==) for numeric arguments.\nclass _NumericEqual extends BoolExpression implements Equal {\n  const _NumericEqual(this._lhs, this._rhs);\n\n  final NumExpression _lhs;\n  final NumExpression _rhs;\n\n  @override\n  bool get value => _lhs.value == _rhs.value;\n}\n\n/// Operator EQUAL (==) for string arguments.\nclass _StringEqual extends BoolExpression implements Equal {\n  const _StringEqual(this._lhs, this._rhs);\n\n  final StringExpression _lhs;\n  final StringExpression _rhs;\n\n  @override\n  bool get value => _lhs.value == _rhs.value;\n}\n\n/// Operator EQUAL (==) for boolean arguments.\nclass _BooleanEqual extends BoolExpression implements Equal {\n  const _BooleanEqual(this._lhs, this._rhs);\n\n  final BoolExpression _lhs;\n  final BoolExpression _rhs;\n\n  @override\n  bool get value => _lhs.value == _rhs.value;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/operators/greater_or_equal.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/operators/_common.dart';\n\n/// Operator GREATER_OR_EQUAL (>=), applies to numeric operands only.\nclass GreaterOrEqual extends BoolExpression {\n  const GreaterOrEqual(this._lhs, this._rhs);\n\n  final NumExpression _lhs;\n  final NumExpression _rhs;\n\n  /// Static constructor, used by parse.dart\n  factory GreaterOrEqual.make(\n    Expression lhs,\n    Expression rhs,\n    int operatorPosition,\n    ErrorFn errorFn,\n  ) {\n    if (lhs.isNumeric && rhs.isNumeric) {\n      return GreaterOrEqual(lhs as NumExpression, rhs as NumExpression);\n    }\n    errorFn(\n      'both left and right sides of `>=` must be numeric, instead the types '\n      'are (${lhs.type.name}, ${rhs.type.name})',\n      operatorPosition,\n    );\n  }\n\n  @override\n  bool get value => _lhs.value >= _rhs.value;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/operators/greater_than.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/operators/_common.dart';\n\n/// Operator GREATER_THAN(>), applies to numeric operands only.\nclass GreaterThan extends BoolExpression {\n  const GreaterThan(this._lhs, this._rhs);\n\n  final NumExpression _lhs;\n  final NumExpression _rhs;\n\n  /// Static constructor, used by parse.dart\n  factory GreaterThan.make(\n    Expression lhs,\n    Expression rhs,\n    int operatorPosition,\n    ErrorFn errorFn,\n  ) {\n    if (lhs.isNumeric && rhs.isNumeric) {\n      return GreaterThan(lhs as NumExpression, rhs as NumExpression);\n    }\n    errorFn(\n      'both left and right sides of `>` must be numeric, instead the types '\n      'are (${lhs.type.name}, ${rhs.type.name})',\n      operatorPosition,\n    );\n  }\n\n  @override\n  bool get value => _lhs.value > _rhs.value;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/operators/less_or_equal.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/operators/_common.dart';\n\n/// Operator LESS_OR_EQUAL (<=), applies to numeric operands only.\nclass LessOrEqual extends BoolExpression {\n  const LessOrEqual(this._lhs, this._rhs);\n\n  final NumExpression _lhs;\n  final NumExpression _rhs;\n\n  /// Static constructor, used by parse.dart\n  factory LessOrEqual.make(\n    Expression lhs,\n    Expression rhs,\n    int operatorPosition,\n    ErrorFn errorFn,\n  ) {\n    if (lhs.isNumeric && rhs.isNumeric) {\n      return LessOrEqual(lhs as NumExpression, rhs as NumExpression);\n    }\n    errorFn(\n      'both left and right sides of `<=` must be numeric, instead the types '\n      'are (${lhs.type.name}, ${rhs.type.name})',\n      operatorPosition,\n    );\n  }\n\n  @override\n  bool get value => _lhs.value <= _rhs.value;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/operators/less_than.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/operators/_common.dart';\n\n/// Operator LESS_THAN (<), applies to numeric operands only.\nclass LessThan extends BoolExpression {\n  const LessThan(this._lhs, this._rhs);\n\n  final NumExpression _lhs;\n  final NumExpression _rhs;\n\n  /// Static constructor, used by parse.dart\n  factory LessThan.make(\n    Expression lhs,\n    Expression rhs,\n    int operatorPosition,\n    ErrorFn errorFn,\n  ) {\n    if (lhs.isNumeric && rhs.isNumeric) {\n      return LessThan(lhs as NumExpression, rhs as NumExpression);\n    }\n    errorFn(\n      'both left and right sides of `<` must be numeric, instead the types are '\n      '(${lhs.type.name}, ${rhs.type.name})',\n      operatorPosition,\n    );\n  }\n\n  @override\n  bool get value => _lhs.value < _rhs.value;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/operators/modulo.dart",
    "content": "import 'package:jenny/src/errors.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/operators/_common.dart';\n\n/// Operator MODULO (%), applies to numeric arguments only.\n///\n/// The divisor of a modulo must be a positive number. The result of `x % y` is\n/// always a number between 0 and `y`, regardless of the sign of `x`.\nclass Modulo extends NumExpression {\n  const Modulo(this._lhs, this._rhs);\n\n  final NumExpression _lhs;\n  final NumExpression _rhs;\n\n  /// Static constructor, used by parse.dart\n  factory Modulo.make(\n    Expression lhs,\n    Expression rhs,\n    int operatorPosition,\n    ErrorFn errorFn,\n  ) {\n    if (lhs.isNumeric && rhs.isNumeric) {\n      return Modulo(lhs as NumExpression, rhs as NumExpression);\n    }\n    errorFn(\n      'both left and right sides of `%` must be numeric, instead the types are '\n      '(${lhs.type.name}, ${rhs.type.name})',\n      operatorPosition,\n    );\n  }\n\n  @override\n  num get value {\n    final x = _lhs.value;\n    final y = _rhs.value;\n    if (y <= 0) {\n      throw DialogueError(\n        'The divisor of a modulo is not a positive number: $y',\n      );\n    }\n    return x % y;\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/operators/multiply.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/operators/_common.dart';\n\n/// Operator MULTIPLY (*), applies to numeric arguments only.\nclass Multiply extends NumExpression {\n  const Multiply(this._lhs, this._rhs);\n\n  final NumExpression _lhs;\n  final NumExpression _rhs;\n\n  /// Static constructor, used by parse.dart\n  factory Multiply.make(\n    Expression lhs,\n    Expression rhs,\n    int operatorPosition,\n    ErrorFn errorFn,\n  ) {\n    if (lhs.isNumeric && rhs.isNumeric) {\n      return Multiply(lhs as NumExpression, rhs as NumExpression);\n    }\n    errorFn(\n      'both left and right sides of `*` must be numeric, instead the types are '\n      '(${lhs.type.name}, ${rhs.type.name})',\n      operatorPosition,\n    );\n  }\n\n  @override\n  num get value => _lhs.value * _rhs.value;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/operators/negate.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\n\n/// Operator UNARY MINUS (-).\nclass Negate extends NumExpression {\n  const Negate(this._arg);\n\n  final NumExpression _arg;\n\n  @override\n  num get value => -_arg.value;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/operators/not.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\n\n/// Logical NOT applied to a boolean expression.\n///\n/// In a Yarn script this can be written as `!x` or `not x`.\nclass Not extends BoolExpression {\n  Not(this._arg);\n\n  final BoolExpression _arg;\n\n  @override\n  bool get value => !_arg.value;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/operators/not_equal.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/operators/_common.dart';\n\n/// Common class for the NOT_EQUAL (!=) operator, with separate implementations\n/// for each argument type.\nabstract class NotEqual extends Expression {\n  factory NotEqual.make(\n    Expression lhs,\n    Expression rhs,\n    int operatorPosition,\n    ErrorFn errorFn,\n  ) {\n    if (lhs.isNumeric && rhs.isNumeric) {\n      return _NumericNotEqual(lhs as NumExpression, rhs as NumExpression);\n    }\n    if (lhs.isString && rhs.isString) {\n      return _StringNotEqual(lhs as StringExpression, rhs as StringExpression);\n    }\n    if (lhs.isBoolean && rhs.isBoolean) {\n      return _BooleanNotEqual(lhs as BoolExpression, rhs as BoolExpression);\n    }\n    errorFn(\n      'inequality operator between operands of unrelated types '\n      '${lhs.type.name} and ${rhs.type.name}',\n      operatorPosition,\n    );\n  }\n}\n\n/// Operator NOT_EQUAL (!=) for numeric arguments.\nclass _NumericNotEqual extends BoolExpression implements NotEqual {\n  const _NumericNotEqual(this._lhs, this._rhs);\n\n  final NumExpression _lhs;\n  final NumExpression _rhs;\n\n  @override\n  bool get value => _lhs.value != _rhs.value;\n}\n\n/// Operator NOT_EQUAL (!=) for string arguments.\nclass _StringNotEqual extends BoolExpression implements NotEqual {\n  const _StringNotEqual(this._lhs, this._rhs);\n\n  final StringExpression _lhs;\n  final StringExpression _rhs;\n\n  @override\n  bool get value => _lhs.value != _rhs.value;\n}\n\n/// Operator NOT_EQUAL (!=) for boolean arguments.\nclass _BooleanNotEqual extends BoolExpression implements NotEqual {\n  const _BooleanNotEqual(this._lhs, this._rhs);\n\n  final BoolExpression _lhs;\n  final BoolExpression _rhs;\n\n  @override\n  bool get value => _lhs.value != _rhs.value;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/operators/or.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/operators/_common.dart';\n\n/// Logical OR operator, applies to binary operands only.\n///\n/// The OR operator returns `false` if both of its operands are `false`, and\n/// `true` otherwise.\nclass Or extends BoolExpression {\n  const Or(this._lhs, this._rhs);\n\n  final BoolExpression _lhs;\n  final BoolExpression _rhs;\n\n  factory Or.make(\n    Expression lhs,\n    Expression rhs,\n    int operatorPosition,\n    ErrorFn errorFn,\n  ) {\n    if (lhs.isBoolean && rhs.isBoolean) {\n      return Or(lhs as BoolExpression, rhs as BoolExpression);\n    }\n    errorFn(\n      'both left and right sides of `||` must be boolean, instead the types '\n      'are (${lhs.type.name}, ${rhs.type.name})',\n      operatorPosition,\n    );\n  }\n\n  @override\n  bool get value => _lhs.value || _rhs.value;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/operators/subtract.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/operators/_common.dart';\n\n/// Common class for the MINUS (-) operator, with separate implementations for\n/// the numeric and string arguments.\nabstract class Subtract extends Expression {\n  factory Subtract.make(\n    Expression lhs,\n    Expression rhs,\n    int operatorPosition,\n    ErrorFn errorFn,\n  ) {\n    if (lhs.isNumeric && rhs.isNumeric) {\n      return _NumSubtract(lhs as NumExpression, rhs as NumExpression);\n    }\n    if (lhs.isString && rhs.isString) {\n      return _StringSubtract(lhs as StringExpression, rhs as StringExpression);\n    }\n    errorFn(\n      'both left and right sides of `-` must be numeric or strings, instead '\n      'the types are (${lhs.type.name}, ${rhs.type.name})',\n      operatorPosition,\n    );\n  }\n}\n\n/// Operator MINUS (-) for numeric arguments.\nclass _NumSubtract extends NumExpression implements Subtract {\n  _NumSubtract(this._lhs, this._rhs);\n\n  final NumExpression _lhs;\n  final NumExpression _rhs;\n\n  @override\n  num get value => _lhs.value - _rhs.value;\n}\n\n/// Operator MINUS (-) for string arguments.\n///\n/// In the expression `x - y`, the first occurrence of string `y` is removed\n/// from `x`. For example, `\"YarnSpinner\" - \"n\" == \"YarSpinner\"`. If there is\n/// no string `y` in `x`, then `x` is returned unmodified.\nclass _StringSubtract extends StringExpression implements Subtract {\n  _StringSubtract(this._lhs, this._rhs);\n\n  final StringExpression _lhs;\n  final StringExpression _rhs;\n\n  @override\n  String get value {\n    final lhsValue = _lhs.value;\n    final rhsValue = _rhs.value;\n    final i = lhsValue.indexOf(rhsValue);\n    if (i == -1) {\n      return lhsValue;\n    } else {\n      return lhsValue.substring(0, i) + lhsValue.substring(i + rhsValue.length);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/operators/xor.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/operators/_common.dart';\n\n/// Logical XOR, applies to boolean arguments only.\n///\n/// The XOR operator returns `false` when both of its operands are the same,\n/// and `true` when they are different.\nclass Xor extends BoolExpression {\n  Xor(this._lhs, this._rhs);\n\n  final BoolExpression _lhs;\n  final BoolExpression _rhs;\n\n  /// Static constructor, used by parse.dart.\n  factory Xor.make(\n    Expression lhs,\n    Expression rhs,\n    int operatorPosition,\n    ErrorFn errorFn,\n  ) {\n    if (lhs.isBoolean && rhs.isBoolean) {\n      return Xor(lhs as BoolExpression, rhs as BoolExpression);\n    }\n    errorFn(\n      'both left and right sides of `^` must be boolean, instead the types '\n      'are (${lhs.type.name}, ${rhs.type.name})',\n      operatorPosition,\n    );\n  }\n\n  @override\n  bool get value => _lhs.value ^ _rhs.value;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/expressions/variables.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/variable_storage.dart';\n\nclass NumericVariable extends NumExpression {\n  const NumericVariable(this.name, this.storage);\n\n  final String name;\n  final VariableStorage storage;\n\n  @override\n  num get value => storage.getNumericValue(name);\n}\n\nclass StringVariable extends StringExpression {\n  const StringVariable(this.name, this.storage);\n\n  final String name;\n  final VariableStorage storage;\n\n  @override\n  String get value => storage.getStringValue(name);\n}\n\nclass BooleanVariable extends BoolExpression {\n  const BooleanVariable(this.name, this.storage);\n\n  final String name;\n  final VariableStorage storage;\n\n  @override\n  bool get value => storage.getBooleanValue(name);\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/line_content.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/markup_attribute.dart';\nimport 'package:meta/meta.dart';\n\n/// The content of a single line in a dialogue. This contains both regular text,\n/// inline expressions, and markup attributes.\n@internal\nclass LineContent {\n  LineContent(this.text, [this.expressions, this.attributes]);\n\n  final String text;\n  final List<InlineExpression>? expressions;\n  final List<MarkupAttribute>? attributes;\n\n  bool get isConst => expressions == null;\n\n  /// Evaluates the line, substituting all inline expressions, and updating the\n  /// markup attributes accordingly.\n  String evaluate() {\n    attributes?.forEach((a) => a.reset());\n    if (expressions == null) {\n      return text;\n    }\n    final out = StringBuffer();\n    var previousPosition = 0;\n    var subIndex = 0;\n    for (final e in expressions!) {\n      if (previousPosition < e.position) {\n        out.write(text.substring(previousPosition, e.position));\n        subIndex = 0;\n      }\n      final insert = e.expression.value;\n      attributes?.forEach((a) {\n        a.maybeShift(e.position, insert.length, subIndex);\n      });\n      out.write(insert);\n      subIndex += 1;\n      previousPosition = e.position;\n    }\n    out.write(text.substring(previousPosition));\n    return out.toString();\n  }\n}\n\n@internal\nclass InlineExpression {\n  InlineExpression(this.position, this.expression);\n  final int position;\n  final StringExpression expression;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/markup_attribute.dart",
    "content": "import 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:meta/meta.dart';\n\nclass MarkupAttribute {\n  MarkupAttribute(\n    this._name,\n    int startTextPosition,\n    int endTextPosition, [\n    int startSubIndex = 0,\n    int endSubIndex = 1,\n    Map<String, Expression>? parameters,\n  ]) : _startTextPosition = startTextPosition + startSubIndex * _subFactor,\n       _endTextPosition = endTextPosition + endSubIndex * _subFactor,\n       _start = 0,\n       _end = 0,\n       _parameterExpressions = parameters,\n       _parameterValues = parameters == null ? null : <String, dynamic>{} {\n    reset();\n  }\n\n  final String _name;\n  final double _startTextPosition;\n  final double _endTextPosition;\n  final Map<String, Expression>? _parameterExpressions;\n  final Map<String, dynamic>? _parameterValues;\n  int _start;\n  int _end;\n\n  /// The name of the attribute, for example the name of markup attribute\n  /// `[flame]` is \"flame\". This name is always a valid ID.\n  String get name => _name;\n\n  /// The range [start]..[end] represents the range of text spanned by this\n  /// markup attribute.\n  ///\n  /// The first index is inclusive, and the second is exclusive. Thus, the\n  /// marked text can be obtained as `text.substring(start, end)`. Properties\n  /// [start] and [end] may change when the underlying text of changes.\n  int get start => _start;\n  int get end => _end;\n\n  /// The length of the span of text covered by this markup attribute.\n  int get length => end - start;\n\n  /// The map of all parameter values of this markup attribute.\n  ///\n  /// These will be recalculated upon each [reset], which occurs every time the\n  /// parent DialogueLine is served in the dialogue runner.\n  Map<String, dynamic> get parameters =>\n      _parameterValues ?? const <String, dynamic>{};\n\n  @internal\n  void reset() {\n    _start = _startTextPosition.floor();\n    _end = _endTextPosition.floor();\n    _parameterExpressions?.forEach((key, expression) {\n      _parameterValues![key] = expression.value;\n    });\n  }\n\n  @internal\n  void maybeShift(int position, int length, int subIndex) {\n    final fractionalPosition = position + subIndex * _subFactor;\n    if (_startTextPosition > fractionalPosition) {\n      _start += length;\n    }\n    if (_endTextPosition > fractionalPosition) {\n      _end += length;\n    }\n  }\n\n  /// Used for calculating fractional text positions.\n  static const double _subFactor = 1.0 / 1024;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/structure/node.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:jenny/src/structure/block.dart';\nimport 'package:jenny/src/structure/dialogue_entry.dart';\nimport 'package:meta/meta.dart';\n\nclass Node extends Iterable<DialogueEntry> {\n  const Node({\n    required this.title,\n    required Block content,\n    Map<String, String>? tags,\n    VariableStorage? variables,\n  }) : _content = content,\n       _tags = tags,\n       _variables = variables;\n\n  final String title;\n  final Map<String, String>? _tags;\n  final Block _content;\n  final VariableStorage? _variables;\n\n  /// The list of extra tags specified in the node header.\n  Map<String, String> get tags => _tags ?? const <String, String>{};\n\n  /// Local variable storage for this node. This can be `null` if the node does\n  /// not define any local variables.\n  VariableStorage? get variables => _variables;\n\n  /// The iterator over the content of the node.\n  @override\n  NodeIterator get iterator => NodeIterator(this);\n\n  @visibleForTesting\n  List<DialogueEntry> get lines => _content.lines;\n\n  @override\n  String toString() => 'Node($title)';\n}\n\nclass NodeIterator implements Iterator<DialogueEntry> {\n  NodeIterator(this.node) {\n    diveInto(node._content);\n  }\n\n  final Node node;\n  final List<Block> blocks = [];\n  final List<int> multiIndex = [];\n\n  @override\n  DialogueEntry get current => blocks.last.lines[multiIndex.last];\n\n  @override\n  bool moveNext() {\n    while (multiIndex.isNotEmpty) {\n      final lastIndex = multiIndex.last;\n      final lastBlock = blocks.last;\n      if (lastIndex + 1 < lastBlock.length) {\n        multiIndex.last += 1;\n        return true;\n      } else {\n        multiIndex.removeLast();\n        blocks.removeLast();\n      }\n    }\n    return false;\n  }\n\n  void diveInto(Block block) {\n    blocks.add(block);\n    multiIndex.add(-1);\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/variable_storage.dart",
    "content": "import 'package:jenny/src/errors.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:jenny/src/structure/expressions/variables.dart';\n\nclass VariableStorage {\n  final Map<String, dynamic> variables = <String, dynamic>{};\n\n  int get length => variables.length;\n  bool get isEmpty => variables.isEmpty;\n  bool get isNotEmpty => variables.isNotEmpty;\n\n  bool getBooleanValue(String name) => variables[name]! as bool;\n  num getNumericValue(String name) => variables[name]! as num;\n  String getStringValue(String name) => variables[name]! as String;\n\n  bool hasVariable(String name) => variables.containsKey(name);\n  dynamic getVariable(String name) => variables[name]!;\n\n  Expression getVariableAsExpression(String name) {\n    final dynamic value = variables[name];\n    return switch (value) {\n      String() => StringVariable(name, this),\n      num() => NumericVariable(name, this),\n      bool() => BooleanVariable(name, this),\n      _ => throw DialogueError(\n        'Cannot convert variable $name with type ${value.runtimeType} to '\n        'an expression',\n      ),\n    };\n  }\n\n  ExpressionType getVariableType(String name) {\n    final dynamic value = variables[name];\n    return switch (value) {\n      String() => ExpressionType.string,\n      num() => ExpressionType.numeric,\n      bool() => ExpressionType.boolean,\n      _ => ExpressionType.unknown,\n    };\n  }\n\n  void setVariable(String name, dynamic value) {\n    final dynamic oldValue = variables[name];\n    if (!(value is String || value is num || value is bool)) {\n      throw DialogueError(\n        'Cannot set variable $name to a value with type ${value.runtimeType}',\n      );\n    }\n    if (oldValue != null &&\n        !(value is String && oldValue is String) &&\n        !(value is num && oldValue is num) &&\n        !(value is bool && oldValue is bool)) {\n      throw DialogueError(\n        'Redefinition of variable $name from type ${oldValue.runtimeType} to '\n        '${value.runtimeType} is not allowed',\n      );\n    }\n    variables[name] = value;\n  }\n\n  /// Clear all variables. By default node visit counts will not be cleared.\n  /// To remove node visit counts as well, set [clearNodeVisits] to `true`.\n  ///\n  /// Note that node visit variable names are prefixed with an @ symbol.\n  /// If you have custom variables that start with an @ symbol these will\n  /// also be retained if [clearNodeVisits] is `false`. These will need to be\n  /// removed individually using [remove].\n  void clear({bool clearNodeVisits = false}) {\n    if (!clearNodeVisits) {\n      variables.removeWhere((key, _) => !key.startsWith('@'));\n    } else {\n      variables.clear();\n    }\n  }\n\n  /// Remove a variable by [name].\n  void remove(String name) {\n    variables.remove(name);\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/lib/src/yarn_project.dart",
    "content": "import 'dart:math';\n\nimport 'package:jenny/src/character_storage.dart';\nimport 'package:jenny/src/command_storage.dart';\nimport 'package:jenny/src/errors.dart';\nimport 'package:jenny/src/function_storage.dart';\nimport 'package:jenny/src/localization.dart';\nimport 'package:jenny/src/parse/parse.dart' as impl;\nimport 'package:jenny/src/structure/node.dart';\nimport 'package:jenny/src/variable_storage.dart';\nimport 'package:meta/meta.dart';\n\n/// [YarnProject] is a central place where all dialogue-related information\n/// is held:\n/// - [nodes]: the map of nodes parsed from yarn files;\n/// - [variables]: the repository of all global variables accessible to yarn\n///                scripts;\n/// - [functions]: user-defined functions;\n/// - [commands]: user-defined commands;\n///\nclass YarnProject {\n  YarnProject()\n    : nodes = <String, Node>{},\n      variables = VariableStorage(),\n      functions = FunctionStorage(),\n      commands = CommandStorage(),\n      characters = CharacterStorage(),\n      random = Random() {\n    locale = 'en';\n  }\n\n  late String _locale;\n\n  /// All parsed [Node]s, keyed by their titles.\n  final Map<String, Node> nodes;\n\n  /// All global variables accessible within the yarn scripts are stored here.\n  /// In addition, this also keeps information about node visit counts.\n  final VariableStorage variables;\n\n  /// User-defined functions are stored here.\n  final FunctionStorage functions;\n\n  /// Repository for user-defined commands.\n  final CommandStorage commands;\n\n  final CharacterStorage characters;\n\n  bool strictCharacterNames = true;\n\n  /// Tokens that represent valid true/false values when converting an argument\n  /// into a boolean. These sets can be modified by the user.\n  static Set<String> trueValues = {'true', 'yes', 'on', '+', 'T', '1'};\n  static Set<String> falseValues = {'false', 'no', 'off', '-', 'F', '0'};\n\n  /// Random number generator used by the dialogue whenever randomization is\n  /// needed.\n  Random random;\n\n  /// Locale (language) used in this YarnProject.\n  ///\n  /// The default locale is 'en' (English), but you can select a different\n  /// locale after a YarnProject is created.\n  ///\n  /// If you need a locale that is not currently supported by Jenny, you can add\n  /// the corresponding entry into the `localizationInfo` map.\n  String get locale => _locale;\n  set locale(String value) {\n    final entry = localizationInfo[value];\n    if (entry == null) {\n      throw DialogueError('Unknown locale \"$value\"');\n    }\n    if (nodes.isNotEmpty) {\n      throw DialogueError(\n        'The locale cannot be changed after nodes have been added',\n      );\n    }\n    localization = entry;\n    _locale = value;\n  }\n\n  /// Settings related to localization.\n  @internal\n  late Localization localization;\n\n  /// Parses a single yarn file, given as a [text] string.\n  ///\n  /// This method may be called multiple times, in order to load as many yarn\n  /// scripts as necessary.\n  void parse(String text) {\n    impl.parse(text, this);\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/pubspec.yaml",
    "content": "name: jenny\nresolution: workspace\ndescription: YarnSpinner narrative game tools equivalent for Dart.\nversion: 1.5.1\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_jenny/jenny\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - yarnspinner\n  - text\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  meta: ^1.12.0\n\ndev_dependencies:\n  dartdoc: ^9.0.0\n  flame_lint: ^1.4.3\n  test: any\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/character_storage_test.dart",
    "content": "import 'package:jenny/src/character.dart';\nimport 'package:jenny/src/character_storage.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('CharacterStorage', () {\n    test('Should store character', () {\n      final storage = CharacterStorage();\n\n      expect(storage.isEmpty, true);\n\n      final character = Character('Jenny');\n      storage.add(character);\n\n      expect(storage.isEmpty, false);\n      expect(storage.isNotEmpty, true);\n\n      expect(storage.contains(character.name), true);\n    });\n\n    test('Should store character aliases', () {\n      final storage = CharacterStorage();\n      final character = Character('Jenny', aliases: ['Jen', 'Jennifer']);\n      storage.add(character);\n\n      expect(storage.contains(character.name), true);\n      expect(storage.contains(character.aliases[0]), true);\n      expect(storage.contains(character.aliases[1]), true);\n    });\n\n    test('Should remove character and aliases by name', () {\n      final storage = CharacterStorage();\n      final character = Character('Jenny', aliases: ['Jen', 'Jennifer']);\n      storage.add(character);\n\n      expect(storage.contains(character.name), true);\n      expect(storage.contains(character.aliases[0]), true);\n      expect(storage.contains(character.aliases[1]), true);\n\n      storage.remove(character.name);\n\n      expect(storage.contains(character.name), false);\n      expect(storage.contains(character.aliases[0]), false);\n      expect(storage.contains(character.aliases[1]), false);\n    });\n\n    test('Should remove character and aliases by alias', () {\n      final storage = CharacterStorage();\n      final character = Character('Jenny', aliases: ['Jen', 'Jennifer']);\n      storage.add(character);\n\n      expect(storage.contains(character.name), true);\n      expect(storage.contains(character.aliases[0]), true);\n      expect(storage.contains(character.aliases[1]), true);\n\n      storage.remove(character.aliases[1]);\n\n      expect(storage.contains(character.name), false);\n      expect(storage.contains(character.aliases[0]), false);\n      expect(storage.contains(character.aliases[1]), false);\n    });\n\n    test('Should clear all characters', () {\n      final storage = CharacterStorage();\n      storage.add(Character('Jenny', aliases: ['Jen', 'Jennifer']));\n      storage.add(Character('Thomas'));\n      storage.add(Character('Leon'));\n\n      storage.clear();\n\n      expect(storage.isEmpty, true);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/command_storage_test.dart",
    "content": "import 'package:jenny/src/command_storage.dart';\nimport 'package:jenny/src/structure/commands/user_defined_command.dart';\nimport 'package:jenny/src/structure/line_content.dart';\nimport 'package:test/test.dart';\n\nimport 'utils.dart';\n\nvoid main() {\n  group('CommandStorage', () {\n    test('A dialogue command', () {\n      final storage = CommandStorage();\n      storage.addOrphanedCommand('simple');\n      expect(storage.hasCommand('simple'), true);\n\n      storage.runCommand(\n        UserDefinedCommand('simple', LineContent('')),\n      );\n      storage.runCommand(\n        UserDefinedCommand('simple', LineContent('1 2 3')),\n      );\n      expect(\n        () => storage.addCommand0('simple', () => null),\n        throwsAssertionError('Command <<simple>> has already been defined'),\n      );\n    });\n\n    test('A no-argument command', () {\n      final storage = CommandStorage();\n      storage.addCommand0('foo', () => null);\n\n      expect(storage.hasCommand('foo'), true);\n      expect(storage.hasCommand('bar'), false);\n    });\n\n    test('An integer-argument command', () {\n      var value = -1;\n      final storage = CommandStorage();\n      storage.addCommand1('one', (int x) => value = x);\n      expect(storage.hasCommand('one'), true);\n\n      void check(String arg, int expectedValue) {\n        storage.runCommand(UserDefinedCommand('one', LineContent(arg)));\n        expect(value, expectedValue);\n      }\n\n      check('23', 23);\n      check('  117 ', 117);\n      check('-3', -3);\n      check('+666', 666);\n      check(' \"42\" ', 42);\n      expect(\n        () => check('2.0', 2),\n        hasTypeError(\n          'TypeError: Argument 1 for command <<one>> has value \"2.0\", which '\n          'is not integer',\n        ),\n      );\n    });\n\n    test('A boolean-argument command', () {\n      var value = false;\n      final storage = CommandStorage();\n      storage.addCommand1('check', (bool x) => value = x);\n      expect(storage.hasCommand('check'), true);\n\n      void check(String arg, {required bool expectedValue}) {\n        storage.runCommand(UserDefinedCommand('check', LineContent(arg)));\n        expect(value, expectedValue);\n      }\n\n      check('true', expectedValue: true);\n      check('  false ', expectedValue: false);\n      check('+', expectedValue: true);\n      check('-', expectedValue: false);\n      check(' \"on\"', expectedValue: true);\n      check('off ', expectedValue: false);\n      check('yes', expectedValue: true);\n      check('no', expectedValue: false);\n      check('T', expectedValue: true);\n      check('F', expectedValue: false);\n      check('1', expectedValue: true);\n      check('0', expectedValue: false);\n      expect(\n        () => check('12', expectedValue: true),\n        hasTypeError(\n          'TypeError: Argument 1 for command <<check>> has value \"12\", which '\n          'is not a boolean',\n        ),\n      );\n    });\n\n    test('A two-argument command', () {\n      var value1 = double.nan;\n      var value2 = '';\n      final storage = CommandStorage();\n      storage.addCommand2('two', (double x, String y) {\n        value1 = x;\n        value2 = y;\n      });\n      expect(storage.hasCommand('two'), true);\n\n      void check(String args, double expectedValue1, String expectedValue2) {\n        storage.runCommand(UserDefinedCommand('two', LineContent(args)));\n        expect(value1, expectedValue1);\n        expect(value2, expectedValue2);\n      }\n\n      check('1 2', 1.0, '2');\n      check('2.12 Jenny', 2.12, 'Jenny');\n      check('-0.001 \"Yarn Spinner\"', -0.001, 'Yarn Spinner');\n      check(r'+3e+100 \"\\\"\"', 3e100, '\"');\n      expect(\n        () => check('2.0.3 error', 2.03, 'error'),\n        hasTypeError(\n          'TypeError: Argument 1 for command <<two>> has value \"2.0.3\", which '\n          'is not a floating-point value',\n        ),\n      );\n    });\n\n    test('A three-argument command', () {\n      num value1 = 0;\n      num value2 = 0;\n      num value3 = 0;\n      final storage = CommandStorage();\n      storage.addCommand3('three', (num x, num y, num z) {\n        value1 = x;\n        value2 = y;\n        value3 = z;\n      });\n      expect(storage.hasCommand('three'), true);\n\n      void check(String args, num v1, num v2, num v3) {\n        storage.runCommand(UserDefinedCommand('three', LineContent(args)));\n        expect(value1, v1);\n        expect(value2, v2);\n        expect(value3, v3);\n      }\n\n      check('1 2 3', 1, 2, 3);\n      check('1.1 2.2 3.3', 1.1, 2.2, 3.3);\n      check('Infinity -0.0 333', double.infinity, 0, 333);\n      expect(\n        () => check('0 0 error', 0, 0, 0),\n        hasTypeError(\n          'TypeError: Argument 3 for command <<three>> has value \"error\", '\n          'which is not numeric',\n        ),\n      );\n    });\n\n    test('A four-argument command', () {\n      num value1 = 0;\n      num value2 = 0;\n      num value3 = 0;\n      num value4 = 0;\n      final storage = CommandStorage();\n      storage.addCommand4('four', (num w, num x, num y, num z) {\n        value1 = w;\n        value2 = x;\n        value3 = y;\n        value4 = z;\n      });\n      expect(storage.hasCommand('four'), true);\n\n      void check(String args, num v1, num v2, num v3, num v4) {\n        storage.runCommand(UserDefinedCommand('four', LineContent(args)));\n        expect(value1, v1);\n        expect(value2, v2);\n        expect(value3, v3);\n        expect(value4, v4);\n      }\n\n      check('1 2 3 4', 1, 2, 3, 4);\n      check('1.1 2.2 3.3 4.4', 1.1, 2.2, 3.3, 4.4);\n      check('Infinity -0.0 333 1.2', double.infinity, 0, 333, 1.2);\n      expect(\n        () => check('0 0 0 error', 0, 0, 0, 0),\n        hasTypeError(\n          'TypeError: Argument 4 for command <<four>> has value \"error\", '\n          'which is not numeric',\n        ),\n      );\n    });\n\n    test('A five-argument command', () {\n      num value1 = 0;\n      num value2 = 0;\n      num value3 = 0;\n      num value4 = 0;\n      num value5 = 0;\n      final storage = CommandStorage();\n      storage.addCommand5('five', (num v, num w, num x, num y, num z) {\n        value1 = v;\n        value2 = w;\n        value3 = x;\n        value4 = y;\n        value5 = z;\n      });\n      expect(storage.hasCommand('five'), true);\n\n      void check(String args, num v1, num v2, num v3, num v4, num v5) {\n        storage.runCommand(UserDefinedCommand('five', LineContent(args)));\n        expect(value1, v1);\n        expect(value2, v2);\n        expect(value3, v3);\n        expect(value4, v4);\n        expect(value5, v5);\n      }\n\n      check('1 2 3 4 5', 1, 2, 3, 4, 5);\n      check('1.1 2.2 3.3 4.4 5.5', 1.1, 2.2, 3.3, 4.4, 5.5);\n      check('Infinity -0.0 333 1.2 -2', double.infinity, 0, 333, 1.2, -2);\n      expect(\n        () => check('0 0 0 0 error', 0, 0, 0, 0, 0),\n        hasTypeError(\n          'TypeError: Argument 5 for command <<five>> has value \"error\", '\n          'which is not numeric',\n        ),\n      );\n    });\n\n    test('Command with trailing booleans', () {\n      var value1 = false;\n      var value2 = false;\n      var value3 = false;\n      final storage = CommandStorage();\n      storage.addCommand3('three', (bool x, bool y, bool z) {\n        value1 = x;\n        value2 = y;\n        value3 = z;\n      });\n      expect(storage.hasCommand('three'), true);\n\n      // ignore: avoid_positional_boolean_parameters\n      void check(String args, bool v1, bool v2, bool v3) {\n        storage.runCommand(UserDefinedCommand('three', LineContent(args)));\n        expect(value1, v1);\n        expect(value2, v2);\n        expect(value3, v3);\n      }\n\n      check('true true true', true, true, true);\n      check('true true', true, true, false);\n      check('true', true, false, false);\n      check('', false, false, false);\n    });\n\n    group('errors', () {\n      test('Add a duplicate command', () {\n        final storage = CommandStorage();\n        storage.addCommand0('foo', () => null);\n        expect(\n          () => storage.addCommand0('foo', () => null),\n          throwsAssertionError('Command <<foo>> has already been defined'),\n        );\n      });\n\n      test('Add a built-in command', () {\n        for (final cmd in ['if', 'set', 'for', 'while', 'local']) {\n          expect(\n            () => CommandStorage().addCommand0(cmd, () => null),\n            throwsAssertionError('Command <<$cmd>> is built-in'),\n          );\n        }\n      });\n\n      test('Bad command name', () {\n        for (final cmd in ['', '---', 'hello world', r'$fun']) {\n          expect(\n            () => CommandStorage().addCommand0(cmd, () => null),\n            throwsAssertionError('Command name \"$cmd\" is not an identifier'),\n          );\n        }\n      });\n\n      test('Command with unsupported type', () {\n        expect(\n          () => CommandStorage().addCommand1('abc', (List x) => null),\n          throwsAssertionError('Unsupported type List<dynamic> of argument 1'),\n        );\n      });\n\n      test('Wrong number of arguments', () {\n        final storage = CommandStorage()\n          ..addCommand2('xyz', (int z, bool f) => null);\n        expect(\n          () => storage.runCommand(\n            UserDefinedCommand('xyz', LineContent('1 true 3')),\n          ),\n          hasTypeError(\n            'TypeError: Command <<xyz>> expects 2 arguments but received 3 '\n            'arguments',\n          ),\n        );\n        expect(\n          () => storage.runCommand(UserDefinedCommand('xyz', LineContent(''))),\n          hasTypeError(\n            'TypeError: Command <<xyz>> expects 2 arguments but received 0 '\n            'arguments',\n          ),\n        );\n      });\n\n      test('Clear all commands', () {\n        final storage = CommandStorage();\n        storage.addCommand0('foo', () => null);\n        storage.addCommand1('bar', (int n) => n);\n\n        expect(storage.isEmpty, false);\n\n        storage.clear();\n\n        expect(storage.isEmpty, true);\n      });\n\n      test('Remove a command', () {\n        final storage = CommandStorage();\n        storage.addCommand0('foo', () => null);\n        storage.addCommand1('bar', (int n) => n);\n\n        expect(storage.hasCommand('foo'), true);\n        expect(storage.hasCommand('bar'), true);\n\n        storage.remove('foo');\n\n        expect(storage.hasCommand('foo'), false);\n        expect(storage.hasCommand('bar'), true);\n      });\n    });\n  });\n\n  group('ArgumentsLexer', () {\n    List<String> tokenize(String text) => ArgumentsLexer(text).tokenize();\n\n    test('empty string', () {\n      expect(tokenize(''), <String>[]);\n      expect(tokenize(' '), <String>[]);\n      expect(tokenize(' \\t  '), <String>[]);\n    });\n\n    test('single argument', () {\n      expect(tokenize('bob'), ['bob']);\n      expect(tokenize('   mary '), ['mary']);\n      expect(tokenize(' 1234.5'), ['1234.5']);\n      expect(tokenize('[\"Flame\"]'), ['[\"Flame\"]']);\n    });\n\n    test('quoted argument', () {\n      expect(tokenize('\"Hello\"'), ['Hello']);\n      expect(tokenize('\"Hello World\"'), ['Hello World']);\n      expect(tokenize(r' \"Hel\\\"lo\\\" World\\\\\"'), [r'Hel\"lo\" World\\']);\n      expect(tokenize(r'\"\\n\"'), ['\\n']);\n    });\n\n    test('multiple arguments', () {\n      expect(tokenize('1 2 3 4 53'), ['1', '2', '3', '4', '53']);\n      expect(tokenize('Hello World'), ['Hello', 'World']);\n      expect(tokenize('flame   \\t awesome'), ['flame', 'awesome']);\n      expect(tokenize(' \"one\"  \"two\" \"three\"'), ['one', 'two', 'three']);\n    });\n\n    group('errors', () {\n      test('unterminated quoted string', () {\n        expect(\n          () => tokenize('\"hello'),\n          hasDialogueError('Unterminated quoted string'),\n        );\n      });\n\n      test('unrecognized escape sequence', () {\n        expect(\n          () => tokenize(r'\"hello \\u1234 world\"'),\n          hasDialogueError(r'Unrecognized escape sequence \\u'),\n        );\n      });\n\n      test('no space after a quoted string', () {\n        expect(\n          () => tokenize('\"hello\"next'),\n          hasDialogueError('Whitespace is required after a quoted argument'),\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/dialogue_runner_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport 'test_scenario.dart';\nimport 'utils.dart';\n\nvoid main() {\n  group('DialogueRunner', () {\n    test('plain dialogue', () async {\n      final yarn = YarnProject()\n        ..strictCharacterNames = false\n        ..parse(\n          '-------------\\n'\n          'title: Hamlet\\n'\n          '-------------\\n'\n          \"Bernardo:  Who's there?\\n\"\n          'Francisco: Nay, answer me. Stand and unfold yourself.\\n'\n          'Bernardo:  Long live the King!\\n'\n          'Francisco: Bernardo?\\n'\n          'Bernardo:  He\\n'\n          'Francisco: You come most carefully upon your hour.\\n'\n          \"Bernardo:  'Tis now struck twelve. Get thee to bed, Francisco.\\n\"\n          \"Francisco: For this relief much thanks. 'Tis bitter cold, \"\n          'And I am sick at heart.\\n'\n          '===\\n',\n        );\n      final view = _RecordingDialogueView();\n      final dialogue = DialogueRunner(yarnProject: yarn, dialogueViews: [view]);\n      await dialogue.startDialogue('Hamlet');\n      expect(\n        view.events,\n        [\n          // ignore_for_file: prefer_adjacent_string_concatenation\n          '[*] onDialogueStart()',\n          '[*] onNodeStart(Node(Hamlet))',\n          \"[*] onLineStart(DialogueLine(Bernardo: Who's there?))\",\n          \"[*] onLineFinish(DialogueLine(Bernardo: Who's there?))\",\n          '[*] onLineStart(DialogueLine(Francisco: Nay, answer me. ' +\n              'Stand and unfold yourself.))',\n          '[*] onLineFinish(DialogueLine(Francisco: Nay, answer me. Stand ' +\n              'and unfold yourself.))',\n          '[*] onLineStart(DialogueLine(Bernardo: Long live the King!))',\n          '[*] onLineFinish(DialogueLine(Bernardo: Long live the King!))',\n          '[*] onLineStart(DialogueLine(Francisco: Bernardo?))',\n          '[*] onLineFinish(DialogueLine(Francisco: Bernardo?))',\n          '[*] onLineStart(DialogueLine(Bernardo: He))',\n          '[*] onLineFinish(DialogueLine(Bernardo: He))',\n          '[*] onLineStart(DialogueLine(Francisco: You come most carefully ' +\n              'upon your hour.))',\n          '[*] onLineFinish(DialogueLine(Francisco: You come most carefully ' +\n              'upon your hour.))',\n          \"[*] onLineStart(DialogueLine(Bernardo: 'Tis now struck twelve. \" +\n              'Get thee to bed, Francisco.))',\n          \"[*] onLineFinish(DialogueLine(Bernardo: 'Tis now struck twelve. \" +\n              'Get thee to bed, Francisco.))',\n          '[*] onLineStart(DialogueLine(Francisco: For this relief much ' +\n              \"thanks. 'Tis bitter cold, And I am sick at heart.))\",\n          '[*] onLineFinish(DialogueLine(Francisco: For this relief much ' +\n              \"thanks. 'Tis bitter cold, And I am sick at heart.))\",\n          '[*] onDialogueFinish()',\n        ],\n      );\n    });\n\n    test('DialogueViews with delays', () async {\n      final yarn = YarnProject()\n        ..strictCharacterNames = false\n        ..parse(\n          'title: The Robot and the Mattress\\n'\n          '---\\n'\n          'Zem: Hello, robot\\n'\n          'Marvin: Blah\\n'\n          '===\\n',\n        );\n      final events = <String>[];\n      final view1 = _DelayedDialogueView(\n        target: events,\n        name: 'A',\n        dialogueStartDelay: 0.1,\n        lineStartDelay: 0.1,\n        lineFinishDelay: 0.02,\n      );\n      final view2 = _DelayedDialogueView(\n        target: events,\n        name: 'B',\n        lineFinishDelay: 0.05,\n      );\n      final view3 = _DelayedDialogueView(\n        target: events,\n        name: 'C',\n        dialogueStartDelay: 0.25,\n        lineStartDelay: 0.15,\n      );\n      final dialogue = DialogueRunner(\n        yarnProject: yarn,\n        dialogueViews: [view1, view2, view3],\n      );\n      await dialogue.startDialogue('The Robot and the Mattress');\n      expect(events, [\n        '[A] onDialogueStart()',\n        '[B] onDialogueStart()',\n        '[C] onDialogueStart()',\n        '[A] onNodeStart(Node(The Robot and the Mattress))',\n        '[B] onNodeStart(Node(The Robot and the Mattress))',\n        '[C] onNodeStart(Node(The Robot and the Mattress))',\n        '[A] onLineStart(DialogueLine(Zem: Hello, robot))',\n        '[B] onLineStart(DialogueLine(Zem: Hello, robot))',\n        '[C] onLineStart(DialogueLine(Zem: Hello, robot))',\n        '[A] onLineFinish(DialogueLine(Zem: Hello, robot))',\n        '[B] onLineFinish(DialogueLine(Zem: Hello, robot))',\n        '[C] onLineFinish(DialogueLine(Zem: Hello, robot))',\n        '[A] onLineStart(DialogueLine(Marvin: Blah))',\n        '[B] onLineStart(DialogueLine(Marvin: Blah))',\n        '[C] onLineStart(DialogueLine(Marvin: Blah))',\n        '[A] onLineFinish(DialogueLine(Marvin: Blah))',\n        '[B] onLineFinish(DialogueLine(Marvin: Blah))',\n        '[C] onLineFinish(DialogueLine(Marvin: Blah))',\n        '[A] onDialogueFinish()',\n        '[B] onDialogueFinish()',\n        '[C] onDialogueFinish()',\n      ]);\n    });\n\n    test('dialogue with choices', () async {\n      final yarn = YarnProject()\n        ..parse(\n          'title: X\\n---\\n'\n          'Question?\\n'\n          '-> Hi there\\n'\n          '-> Howdy\\n'\n          '   Greetings to you too\\n'\n          '-> Yo! <<if false>>\\n'\n          'Kk-thx-bye\\n'\n          '===\\n',\n        );\n      final view = _RecordingDialogueView(choices: [1]);\n      final dialogue = DialogueRunner(yarnProject: yarn, dialogueViews: [view]);\n      await dialogue.startDialogue('X');\n      expect(\n        view.events,\n        [\n          '[*] onDialogueStart()',\n          '[*] onNodeStart(Node(X))',\n          '[*] onLineStart(DialogueLine(Question?)[#lastline])',\n          '[*] onLineFinish(DialogueLine(Question?))',\n          '[*] onChoiceStart(DialogueChoice([Option(Hi there), ' +\n              'Option(Howdy), Option(Yo! #disabled)])) -> 1',\n          '[*] onChoiceFinish(Option(Howdy))',\n          '[*] onLineStart(DialogueLine(Greetings to you too))',\n          '[*] onLineFinish(DialogueLine(Greetings to you too))',\n          '[*] onLineStart(DialogueLine(Kk-thx-bye))',\n          '[*] onLineFinish(DialogueLine(Kk-thx-bye))',\n          '[*] onDialogueFinish()',\n        ],\n      );\n\n      view.events.clear();\n      view.choices.add(0);\n      await dialogue.startDialogue('X');\n      expect(\n        view.events,\n        [\n          '[*] onDialogueStart()',\n          '[*] onNodeStart(Node(X))',\n          '[*] onLineStart(DialogueLine(Question?)[#lastline])',\n          '[*] onLineFinish(DialogueLine(Question?))',\n          '[*] onChoiceStart(DialogueChoice([Option(Hi there), ' +\n              'Option(Howdy), Option(Yo! #disabled)])) -> 0',\n          '[*] onChoiceFinish(Option(Hi there))',\n          '[*] onLineStart(DialogueLine(Kk-thx-bye))',\n          '[*] onLineFinish(DialogueLine(Kk-thx-bye))',\n          '[*] onDialogueFinish()',\n        ],\n      );\n    });\n\n    test('invalid dialogue choices', () async {\n      final yarn = YarnProject()\n        ..parse(\n          'title:A\\n---\\n'\n          '-> Only one\\n'\n          '-> Only two <<if false>>\\n'\n          '===\\n',\n        );\n      final view = _RecordingDialogueView(choices: [2, 1]);\n      final dialogue = DialogueRunner(yarnProject: yarn, dialogueViews: [view]);\n      await expectLater(\n        () => dialogue.startDialogue('A'),\n        hasDialogueError('Invalid option index chosen in a dialogue: 2'),\n      );\n      await expectLater(\n        () => dialogue.startDialogue('A'),\n        hasDialogueError(\n          'A dialogue view selected a disabled option: '\n          'Option(Only two #disabled)',\n        ),\n      );\n    });\n\n    test('no decision-making views', () {\n      final yarn = YarnProject()..parse('title:A\\n---\\n-> One\\n===\\n');\n      final dialogue = DialogueRunner(\n        yarnProject: yarn,\n        dialogueViews: [_SimpleDialogueView(), _SimpleDialogueView()],\n      );\n      expect(\n        () => dialogue.startDialogue('A'),\n        hasDialogueError('No option selected in a DialogueChoice'),\n      );\n    });\n\n    test('dialogue view that makes immediate choice selection', () async {\n      final yarn = YarnProject()..parse('title:A\\n---\\n-> One\\n===\\n');\n      final view = _ImmediateChoiceDialogueView();\n      final dialogue = DialogueRunner(yarnProject: yarn, dialogueViews: [view]);\n      await dialogue.startDialogue('A');\n      expect(view.finished, true);\n    });\n\n    testScenario(\n      testName: 'Example.plan',\n      input: '''\n        title: Start\n        tags: \n        colorID: 0\n        position: 592,181\n        ---\n        A: Hey, I'm a character in a script!\n        B: And I am too! You are talking to me!\n        -> What's going on\n            A: Why this is a demo of the script system!\n            B: And you're in it!\n        -> Um ok\n        A: How delightful!\n        B: What would you prefer to do next?\n        -> Leave\n            <<jump Leave>>\n        -> Learn more\n            <<jump LearnMore>>\n        ===\n        title: Leave\n        tags: \n        colorID: 0\n        position: 387,487\n        ---\n        A: Oh, goodbye!\n        B: You'll be back soon!\n        ===\n        title: LearnMore\n        tags: rawText\n        colorID: 0\n        position: 763,472\n        ---\n        A: HA HA HA\n        ===''',\n      testPlan: '''\n        line: A: Hey, I'm a character in a script!\n        line: B: And I am too! You are talking to me!\n        option: What's going on    \n        option: Um ok\n        select: 1\n        line: A: Why this is a demo of the script system!\n        line: B: And you're in it!\n        line: A: How delightful!\n        line: B: What would you prefer to do next?\n        option: Leave\n        option: Learn more\n        select: 1\n        line: A: Oh, goodbye!\n        line: B: You'll be back soon!\n      ''',\n    );\n\n    testScenario(\n      testName: 'Compiler.plan',\n      input: r'''\n        <<declare $foo as Number>>\n        \n        title: Start\n        ---\n        // Compiler tests\n        This is a line!\n\n        <<if false>>\n          What what this is also a line!\n        <<endif>>\n\n        <<this is a custom command>>\n\n        <<set $foo to 1+2>>\n\n        <<if $foo is 3>>\n          Foo is 3!\n        <<elseif $foo is 4>>\n          Foo is 4!\n        <<else>>\n          Foo is something TOTALLY DIFFERENT.\n        <<endif>>\n\n        -> This is a shortcut option that you'll never see <<if false>>\n            Nice.\n        -> This is a different shortcut option\n            Sweet, but what about this?\n            -> It's ok\n                Cool.\n            -> Huh?\n        -> This is a shortcut option with no consequential text.\n\n        All done with the shortcut options!\n        ===\n      ''',\n      testPlan: '''\n        line: This is a line!\n        command: this is a custom command\n        line: Foo is 3!\n        option: This is a shortcut option that you'll never see [disabled]\n        option: This is a different shortcut option\n        option: This is a shortcut option with no consequential text.\n        select: 2\n        line: Sweet, but what about this?\n        option: It's ok\n        option: Huh?\n        select: 1\n        line: Cool.\n        line: All done with the shortcut options!\n      ''',\n      commands: ['this'],\n    );\n\n    test('Dialogue runs node before finishing the previous one', () {\n      final yarn = YarnProject()\n        ..parse(\n          dedent('''\n            title: Start\n            ---\n            First line\n            Second line\n            ===\n            title: Other\n            ---\n            Third line\n            ===\n          '''),\n        );\n      final view = _RecordingDialogueView();\n      final dialogue = DialogueRunner(yarnProject: yarn, dialogueViews: [view]);\n      unawaited(dialogue.startDialogue('Start'));\n      expect(\n        () => dialogue.startDialogue('Other'),\n        hasDialogueError(\n          'Cannot run node \"Other\" because another node is currently running: '\n          '\"Start\"',\n        ),\n      );\n    });\n  });\n}\n\nclass _SimpleDialogueView extends DialogueView {}\n\nclass _RecordingDialogueView extends DialogueView {\n  _RecordingDialogueView({\n    List<String>? target,\n    String? name,\n    List<int>? choices,\n  }) : events = target ?? [],\n       name = name ?? '*',\n       choices = choices ?? [];\n\n  final List<String> events;\n  final String name;\n  final List<int> choices;\n\n  @override\n  void onDialogueStart() => _record('onDialogueStart()');\n\n  @override\n  void onDialogueFinish() => _record('onDialogueFinish()');\n\n  @override\n  void onNodeStart(Node node) => _record('onNodeStart($node)');\n\n  @override\n  FutureOr<bool> onLineStart(DialogueLine line) =>\n      _record('onLineStart($line${line.tags.isNotEmpty ? line.tags : \"\"})');\n\n  @override\n  void onLineFinish(DialogueLine line) => _record('onLineFinish($line)');\n\n  @override\n  Future<int> onChoiceStart(DialogueChoice choice) {\n    final selection = choices.isEmpty ? 0 : choices.removeAt(0);\n    _record('onChoiceStart($choice) -> $selection');\n    return Future<int>.value(selection);\n  }\n\n  @override\n  void onChoiceFinish(DialogueOption option) =>\n      _record('onChoiceFinish($option)');\n\n  bool _record(String event) {\n    events.add('[$name] $event');\n    return true;\n  }\n}\n\nclass _DelayedDialogueView extends _RecordingDialogueView {\n  _DelayedDialogueView({\n    super.target,\n    super.name,\n    this.dialogueStartDelay = 0,\n    this.lineStartDelay = 0,\n    this.lineFinishDelay = 0,\n  });\n\n  final double dialogueStartDelay;\n  final double lineStartDelay;\n  final double lineFinishDelay;\n\n  @override\n  FutureOr<void> onDialogueStart() {\n    super.onDialogueStart();\n    if (dialogueStartDelay > 0) {\n      final delay = Duration(milliseconds: (dialogueStartDelay * 1000).toInt());\n      return Future.delayed(delay, () => null);\n    }\n  }\n\n  @override\n  Future<bool> onLineStart(DialogueLine line) {\n    super.onLineStart(line);\n    final delay = Duration(milliseconds: (lineStartDelay * 1000).toInt());\n    return Future<bool>.delayed(delay, () => true);\n  }\n\n  @override\n  Future<void> onLineFinish(DialogueLine line) {\n    super.onLineFinish(line);\n    final delay = Duration(milliseconds: (lineFinishDelay * 1000).toInt());\n    return Future.delayed(delay);\n  }\n}\n\nclass _ImmediateChoiceDialogueView extends DialogueView {\n  bool finished = false;\n\n  @override\n  int onChoiceStart(DialogueChoice choice) => 0;\n\n  @override\n  void onDialogueFinish() => finished = true;\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/dialogue_view_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport 'test_scenario.dart';\nimport 'utils.dart';\n\nvoid main() {\n  group('DialogueView', () {\n    test('run a sample dialogue', () async {\n      final yarn = YarnProject()\n        ..commands.addOrphanedCommand('myCommand')\n        ..parse(\n          dedent('''\n            title: Start\n            ---\n            First line\n            -> Option 1\n                Continuation line 1\n            -> Option 2\n                Continuation line 2\n            Last line\n            <<myCommand 123 boo>>\n            ===\n          '''),\n        );\n      final view1 = _DefaultDialogueView();\n      final view2 = _RecordingDialogueView();\n      final dialogueRunner = DialogueRunner(\n        yarnProject: yarn,\n        dialogueViews: [view1, view2],\n      );\n      await dialogueRunner.startDialogue('Start');\n      expect(\n        view2.events,\n        const [\n          'onDialogueStart',\n          'onNodeStart(Start)',\n          'onLineStart(First line)',\n          'onLineFinish(First line)',\n          'onChoiceStart([-> Option 1][-> Option 2])',\n          'onChoiceFinish(-> Option 2)',\n          'onLineStart(Continuation line 2)',\n          'onLineFinish(Continuation line 2)',\n          'onLineStart(Last line)',\n          'onLineFinish(Last line)',\n          'onCommand(<<Command(myCommand)>>)',\n          'onCommandFinish(<<Command(myCommand)>>)',\n          'onNodeFinish(Start)',\n          'onDialogueFinish()',\n        ],\n      );\n    });\n\n    test('run a sample dialogue using mixin', () async {\n      final yarn = YarnProject()\n        ..commands.addOrphanedCommand('myCommand')\n        ..parse(\n          dedent('''\n            title: Start\n            ---\n            First line\n            -> Option 1\n                Continuation line 1\n            -> Option 2\n                Continuation line 2\n            Last line\n            <<myCommand 123 boo>>\n            ===\n          '''),\n        );\n      final view1 = _DefaultDialogueView();\n      final view2 = _RecordingDialogueViewAsMixin();\n      final dialogueRunner = DialogueRunner(\n        yarnProject: yarn,\n        dialogueViews: [view1, view2],\n      );\n      await dialogueRunner.startDialogue('Start');\n      expect(\n        view2.events,\n        const [\n          'onDialogueStart',\n          'onNodeStart(Start)',\n          'onLineStart(First line)',\n          'onLineFinish(First line)',\n          'onChoiceStart([-> Option 1][-> Option 2])',\n          'onChoiceFinish(-> Option 2)',\n          'onLineStart(Continuation line 2)',\n          'onLineFinish(Continuation line 2)',\n          'onLineStart(Last line)',\n          'onLineFinish(Last line)',\n          'onCommand(<<Command(myCommand)>>)',\n          'onCommandFinish(<<Command(myCommand)>>)',\n          'onNodeFinish(Start)',\n          'onDialogueFinish()',\n        ],\n      );\n    });\n\n    test('jumps and visits', () async {\n      final yarn = YarnProject()\n        ..parse(\n          dedent('''\n            title: Start\n            ---\n            First line\n            <<visit AnotherNode>>\n            Second line\n            <<jump SomewhereElse>>\n            ===\n            title: AnotherNode\n            ---\n            Inside another node\n            <<jump SomewhereElse>>\n            ===\n            title: SomewhereElse\n            ---\n            This is nowhere...\n            ===\n          '''),\n        );\n      final view1 = _DefaultDialogueView();\n      final view2 = _RecordingDialogueView();\n      final dialogueRunner = DialogueRunner(\n        yarnProject: yarn,\n        dialogueViews: [view1, view2],\n      );\n      await dialogueRunner.startDialogue('Start');\n      expect(\n        view2.events,\n        const [\n          'onDialogueStart',\n          'onNodeStart(Start)',\n          'onLineStart(First line)',\n          'onLineFinish(First line)',\n          'onNodeStart(AnotherNode)',\n          'onLineStart(Inside another node)',\n          'onLineFinish(Inside another node)',\n          'onNodeFinish(AnotherNode)',\n          'onNodeStart(SomewhereElse)',\n          'onLineStart(This is nowhere...)',\n          'onLineFinish(This is nowhere...)',\n          'onNodeFinish(SomewhereElse)',\n          'onLineStart(Second line)',\n          'onLineFinish(Second line)',\n          'onNodeFinish(Start)',\n          'onNodeStart(SomewhereElse)',\n          'onLineStart(This is nowhere...)',\n          'onLineFinish(This is nowhere...)',\n          'onNodeFinish(SomewhereElse)',\n          'onDialogueFinish()',\n        ],\n      );\n    });\n\n    test('stop line', () async {\n      final yarn = YarnProject()\n        ..parse(\n          dedent('''\n            title: Start\n            ---\n            First line\n            ===\n          '''),\n        );\n      final view1 = _DefaultDialogueView();\n      final view2 = _RecordingDialogueView(const Duration(milliseconds: 500));\n      final view3 = _InterruptingCow();\n      final dialogueRunner = DialogueRunner(\n        yarnProject: yarn,\n        dialogueViews: [view1, view2, view3],\n      );\n      await dialogueRunner.startDialogue('Start');\n      expect(\n        view2.events,\n        const [\n          'onDialogueStart',\n          'onNodeStart(Start)',\n          'onLineStart(First line)',\n          'onLineSignal(line=\"First line\", signal=<I\\'m a banana!>)',\n          'onLineStop(First line)',\n          'onLineFinish(First line)',\n          'onNodeFinish(Start)',\n          'onDialogueFinish()',\n        ],\n      );\n    });\n\n    test('dialogue view cannot be attached to two dialogue runners', () {\n      final yarn = YarnProject()..parse('title:A\\n---\\nOne\\n===\\n');\n      final view = _DefaultDialogueView();\n      final d1 = DialogueRunner(yarnProject: yarn, dialogueViews: [view]);\n      final d2 = DialogueRunner(yarnProject: yarn, dialogueViews: [view]);\n      expect(\n        () async {\n          await Future.wait([\n            d1.startDialogue('A'),\n            d2.startDialogue('A'),\n          ]);\n        },\n        hasDialogueError(\n          'DialogueView is currently attached to another DialogueRunner',\n        ),\n      );\n    });\n  });\n}\n\nclass _DefaultDialogueView extends DialogueView {}\n\nclass _RecordingDialogueView extends DialogueView {\n  _RecordingDialogueView([this.waitDuration = Duration.zero]);\n  final List<String> events = [];\n  final Duration waitDuration;\n\n  @override\n  FutureOr<void> onDialogueStart() {\n    events.add('onDialogueStart');\n  }\n\n  @override\n  FutureOr<void> onNodeStart(Node node) {\n    events.add('onNodeStart(${node.title})');\n  }\n\n  @override\n  FutureOr<void> onNodeFinish(Node node) {\n    events.add('onNodeFinish(${node.title})');\n  }\n\n  @override\n  FutureOr<bool> onLineStart(DialogueLine line) async {\n    events.add('onLineStart(${line.text})');\n    if (waitDuration != Duration.zero) {\n      await Future.delayed(waitDuration, () {});\n    }\n    return true;\n  }\n\n  @override\n  void onLineSignal(DialogueLine line, dynamic signal) {\n    super.onLineSignal(line, signal);\n    events.add('onLineSignal(line=\"${line.text}\", signal=<$signal>)');\n  }\n\n  @override\n  FutureOr<void> onLineStop(DialogueLine line) {\n    super.onLineStop(line);\n    events.add('onLineStop(${line.text})');\n  }\n\n  @override\n  FutureOr<void> onLineFinish(DialogueLine line) {\n    events.add('onLineFinish(${line.text})');\n  }\n\n  @override\n  Future<int> onChoiceStart(DialogueChoice choice) async {\n    final options = [\n      for (final option in choice.options) '[-> ${option.text}]',\n    ].join();\n    events.add('onChoiceStart($options)');\n    return 1;\n  }\n\n  @override\n  FutureOr<void> onChoiceFinish(DialogueOption option) {\n    events.add('onChoiceFinish(-> ${option.text})');\n  }\n\n  @override\n  FutureOr<void> onCommand(UserDefinedCommand command) {\n    events.add('onCommand(<<$command>>)');\n  }\n\n  @override\n  FutureOr<void> onCommandFinish(UserDefinedCommand command) {\n    events.add('onCommandFinish(<<$command>>)');\n  }\n\n  @override\n  FutureOr<void> onDialogueFinish() {\n    events.add('onDialogueFinish()');\n  }\n}\n\nclass _SomeOtherBaseClass {}\n\nclass _RecordingDialogueViewAsMixin extends _SomeOtherBaseClass\n    with DialogueView {\n  _RecordingDialogueViewAsMixin();\n  final List<String> events = [];\n\n  @override\n  FutureOr<void> onDialogueStart() {\n    events.add('onDialogueStart');\n  }\n\n  @override\n  FutureOr<void> onNodeStart(Node node) {\n    events.add('onNodeStart(${node.title})');\n  }\n\n  @override\n  FutureOr<void> onNodeFinish(Node node) {\n    events.add('onNodeFinish(${node.title})');\n  }\n\n  @override\n  FutureOr<bool> onLineStart(DialogueLine line) async {\n    events.add('onLineStart(${line.text})');\n    return true;\n  }\n\n  @override\n  void onLineSignal(DialogueLine line, dynamic signal) {\n    super.onLineSignal(line, signal);\n    events.add('onLineSignal(line=\"${line.text}\", signal=<$signal>)');\n  }\n\n  @override\n  FutureOr<void> onLineStop(DialogueLine line) {\n    super.onLineStop(line);\n    events.add('onLineStop(${line.text})');\n  }\n\n  @override\n  FutureOr<void> onLineFinish(DialogueLine line) {\n    events.add('onLineFinish(${line.text})');\n  }\n\n  @override\n  Future<int> onChoiceStart(DialogueChoice choice) async {\n    final options = [\n      for (final option in choice.options) '[-> ${option.text}]',\n    ].join();\n    events.add('onChoiceStart($options)');\n    return 1;\n  }\n\n  @override\n  FutureOr<void> onChoiceFinish(DialogueOption option) {\n    events.add('onChoiceFinish(-> ${option.text})');\n  }\n\n  @override\n  FutureOr<void> onCommand(UserDefinedCommand command) {\n    events.add('onCommand(<<$command>>)');\n  }\n\n  @override\n  FutureOr<void> onCommandFinish(UserDefinedCommand command) {\n    events.add('onCommandFinish(<<$command>>)');\n  }\n\n  @override\n  FutureOr<void> onDialogueFinish() {\n    events.add('onDialogueFinish()');\n  }\n}\n\nclass _InterruptingCow extends DialogueView {\n  @override\n  FutureOr<bool> onLineStart(DialogueLine line) async {\n    dialogueRunner!.sendSignal(\"I'm a banana!\");\n    dialogueRunner!.stopLine();\n    return false;\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/function_storage_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport 'test_scenario.dart';\nimport 'utils.dart';\n\nvoid main() {\n  group('FunctionStorage', () {\n    test('basic properties', () {\n      final functions = FunctionStorage();\n      expect(functions.length, 0);\n      expect(functions.isEmpty, true);\n      expect(functions.isNotEmpty, false);\n\n      functions.addFunction0('t', () => 0);\n      expect(functions.length, 1);\n      expect(functions.isEmpty, false);\n      expect(functions.isNotEmpty, true);\n    });\n\n    test('valid return types', () {\n      final yarn = YarnProject();\n      yarn.functions\n        ..addFunction0('fn_bool', () => false)\n        ..addFunction0('fn_int', () => 1)\n        ..addFunction0('fn_double', () => 1.5)\n        ..addFunction0('fn_num', () => num.parse('3'))\n        ..addFunction0('fn_string', () => 'Jenny');\n      testScenario(\n        yarn: yarn,\n        input: '''\n          title: Start\n          ---\n          {fn_bool()}\n          {fn_int()}\n          {fn_double()}\n          {fn_num()}\n          {fn_string()}\n          ===\n        ''',\n        testPlan: '''\n          line: false\n          line: 1\n          line: 1.5\n          line: 3\n          line: Jenny\n        ''',\n      );\n    });\n\n    test('valid argument types', () {\n      final yarn = YarnProject();\n      yarn.functions\n        ..addFunction2('fn_bool', (bool x, bool? y) => y ?? x)\n        ..addFunction2('fn_int', (int x, int? y) => y ?? x)\n        ..addFunction2('fn_double', (double x, double? y) => y ?? x)\n        ..addFunction2('fn_num', (num x, num? y) => y ?? x)\n        ..addFunction2('fn_string', (String x, String? y) => y ?? x)\n        ..addFunction1('fn_yarn', (YarnProject yarn) => yarn.functions.length);\n\n      testScenario(\n        yarn: yarn,\n        input: '''\n          title: Start\n          ---\n          {fn_bool(true, false)}\n          {fn_int(5, 9)}\n          {fn_double(2.2, 1.5)}\n          {fn_num(3, 7.5)}\n          {fn_string('Jenny', 'Flame')}\n          number of user-defined functions = {fn_yarn()}\n          ===\n        ''',\n        testPlan: '''\n          line: false\n          line: 9\n          line: 1.5\n          line: 7.5\n          line: Flame\n          line: number of user-defined functions = 6\n        ''',\n      );\n    });\n\n    test('functions with different arities', () {\n      final yarn = YarnProject();\n      yarn.functions\n        ..addFunction0('sum0', () => 0)\n        ..addFunction1('sum1', (num x) => x)\n        ..addFunction2('sum2', (num x, num y) => x + y)\n        ..addFunction3('sum3', (num x, num y, num z) => x + y + z)\n        ..addFunction4('sum4', (num w, num x, num y, num z) => w + x + y + z);\n\n      testScenario(\n        yarn: yarn,\n        input: '''\n          title: Start\n          ---\n          {sum0()}\n          {sum1(1)}\n          {sum2(1, 2)}\n          {sum3(1, 2, 3)}\n          {sum4(1, 2, 3, 4)}\n          ===\n        ''',\n        testPlan: '''\n          line: 0\n          line: 1\n          line: 3\n          line: 6\n          line: 10\n        ''',\n      );\n    });\n\n    test('functions with optional arguments', () {\n      final yarn = YarnProject();\n      yarn.functions.addFunction4(\n        'sum',\n        (num? w, num? x, num? y, num? z) =>\n            (w ?? 0) + (x ?? 0) + (y ?? 0) + (z ?? 0),\n      );\n      testScenario(\n        yarn: yarn,\n        input: '''\n          title: Start\n          ---\n          {sum()}\n          {sum(1)}\n          {sum(1, 2)}\n          {sum(1, 2, 3)}\n          {sum(1, 2, 3, 4)}\n          {sum(100)}\n          ===\n        ''',\n        testPlan: '''\n          line: 0\n          line: 1\n          line: 3\n          line: 6\n          line: 10\n          line: 100\n        ''',\n      );\n    });\n\n    test('Functions.yarn', () async {\n      final yarn = YarnProject();\n      yarn.functions.addFunction3(\n        'add_three_operands',\n        (num x, num y, num z) => x + y + z,\n      );\n      await testScenario(\n        yarn: yarn,\n        input: '''\n          title: Start\n          ---\n          // Function tests\n          // \"add_three_operands\" is a function that sums three operands\n          assert -> { add_three_operands(1, 2, 4*1) == 7 }\n          \n          // function calls as parameters\n          assert -> {add_three_operands(1, 2, add_three_operands(1,2,3)) == 9}\n          ===\n        ''',\n        testPlan: '''\n          line: assert -> true\n          line: assert -> true\n        ''',\n      );\n    });\n\n    group('errors', () {\n      test('invalid function name', () {\n        for (final name in ['', '++', 'very nice', '1object']) {\n          expect(\n            () => FunctionStorage().addFunction0(name, () => 0),\n            throwsAssertionError('Function name \"$name\" is not an identifier'),\n          );\n        }\n      });\n\n      test('overriding builtin functions', () {\n        for (final name in ['bool', 'random', 'dice', 'string']) {\n          expect(\n            () => FunctionStorage().addFunction0(name, () => 0),\n            throwsAssertionError('Function $name() is built-in'),\n          );\n        }\n      });\n\n      test('invalid return types', () {\n        expect(\n          () => YarnProject().functions.addFunction0('test', () => null),\n          hasTypeError(\n            'TypeError: Unsupported return type <Null>, expected one of: bool, '\n            'int, double, num, or String',\n          ),\n        );\n        expect(\n          () => YarnProject().functions.addFunction0('test', () => [3]),\n          hasTypeError(\n            'TypeError: Unsupported return type <List<int>>, expected one of: '\n            'bool, int, double, num, or String',\n          ),\n        );\n        expect(\n          () => YarnProject().functions.addFunction1(\n            'test',\n            (int x) => x > 0 ? x : null,\n          ),\n          hasTypeError(\n            'TypeError: Unsupported return type <int?>, expected one of: '\n            'bool, int, double, num, or String',\n          ),\n        );\n      });\n\n      test('invalid argument types', () {\n        final functions = FunctionStorage();\n        expect(\n          () => functions.addFunction1('t', (Type t) => 1),\n          hasTypeError(\n            'TypeError: Unsupported type <Type> for argument at index 0',\n          ),\n        );\n        expect(\n          () => functions.addFunction2('tt', (int x, List<int> z) => 1),\n          hasTypeError(\n            'TypeError: Unsupported type <List<int>> for argument at index 1',\n          ),\n        );\n      });\n\n      test('misplaced YarnProject argument', () {\n        expect(\n          () => FunctionStorage().addFunction2(\n            'test',\n            (int x, YarnProject yarn) => 0,\n          ),\n          hasTypeError(\n            'TypeError: Argument of type YarnProject must be the first in a '\n            'function',\n          ),\n        );\n      });\n\n      test('misplaced optional arguments', () {\n        expect(\n          () => FunctionStorage()..addFunction2('t', (int? x, int y) => y),\n          hasTypeError(\n            'TypeError: Required arguments must come before the optional',\n          ),\n        );\n      });\n\n      test('too few arguments in a function call', () {\n        final yarn = YarnProject();\n        yarn.functions.addFunction3('sum', (num x, num y, num? z) => x + y);\n        expect(\n          () => yarn.parse(\n            'title:A\\n---\\n'\n            '{sum(5)}\\n'\n            '===\\n',\n          ),\n          hasTypeError(\n            'TypeError: Function sum() expects at least 2 arguments\\n'\n            '>  at line 3 column 7:\\n'\n            '>  {sum(5)}\\n'\n            '>        ^\\n',\n          ),\n        );\n      });\n\n      test('too many arguments in a function call', () {\n        final yarn = YarnProject();\n        yarn.functions.addFunction1('add1', (num x) => x + 1);\n        expect(\n          () => yarn.parse(\n            'title:A\\n---\\n'\n            '{add1(5, 7, 11)}\\n'\n            '===\\n',\n          ),\n          hasTypeError(\n            'TypeError: Function add1() expects 1 argument\\n'\n            '>  at line 3 column 10:\\n'\n            '>  {add1(5, 7, 11)}\\n'\n            '>           ^\\n',\n          ),\n        );\n      });\n\n      test('invalid argument types', () {\n        final yarn = YarnProject();\n        yarn.functions.addFunction3(\n          'sum',\n          (num x, num y, num z) => x + y + z,\n        );\n        expect(\n          () => yarn.parse(\n            'title:A\\n---\\n'\n            '{sum(5, false, 11)}\\n'\n            '===\\n',\n          ),\n          hasTypeError(\n            'TypeError: Invalid type for argument 1: expected numeric but '\n            'received boolean\\n'\n            '>  at line 3 column 9:\\n'\n            '>  {sum(5, false, 11)}\\n'\n            '>          ^\\n',\n          ),\n        );\n      });\n\n      test('clear all functions', () {\n        final functions = FunctionStorage();\n        functions.addFunction0('t0', () => 0);\n        functions.addFunction1('add2', (int n) => n + 2);\n        expect(functions.isEmpty, false);\n\n        functions.clear();\n\n        expect(functions.isEmpty, true);\n      });\n\n      test('remove a function', () {\n        final functions = FunctionStorage();\n        functions.addFunction0('t0', () => 0);\n        functions.addFunction1('add2', (int n) => n + 2);\n        expect(functions.hasFunction('t0'), true);\n        expect(functions.hasFunction('add2'), true);\n\n        functions.remove('t0');\n\n        expect(functions.hasFunction('t0'), false);\n        expect(functions.hasFunction('add2'), true);\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/localization_test.dart",
    "content": "import 'package:jenny/src/localization.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('Localization', () {\n    test('en (English)', () {\n      final plural = localizationInfo['en']!.pluralFunction;\n      expect(plural(0, ['boy']), 'boys');\n      expect(plural(1, ['toy']), 'toy');\n      expect(plural(2, ['baby']), 'babies');\n      expect(plural(3, ['man', 'men']), 'men');\n    });\n\n    test('ja (Japanese)', () {\n      final plural = localizationInfo['ja']!.pluralFunction;\n      expect(plural(1, ['人']), '人');\n      expect(plural(2, ['人']), '人');\n      expect(plural(10, ['人']), '人');\n    });\n\n    test('fr (French)', () {\n      final plural = localizationInfo['fr']!.pluralFunction;\n      expect(plural(0, ['ail', 'ails']), 'ail');\n      expect(plural(1, ['ail', 'ails']), 'ail');\n      expect(plural(2, ['ail', 'ails']), 'ails');\n    });\n\n    test('de (German)', () {\n      final plural = localizationInfo['de']!.pluralFunction;\n      expect(plural(0, ['Tee', 'Tees']), 'Tees');\n      expect(plural(1, ['Tee', 'Tees']), 'Tee');\n      expect(plural(2, ['Tee', 'Tees']), 'Tees');\n    });\n\n    test('es (Spanish)', () {\n      final plural = localizationInfo['es']!.pluralFunction;\n      expect(plural(-1, ['té', 'tés']), 'té');\n      expect(plural(0, ['té', 'tés']), 'tés');\n      expect(plural(1, ['té', 'tés']), 'té');\n      expect(plural(2, ['té', 'tés']), 'tés');\n    });\n\n    test('pl (Polish)', () {\n      final plural = localizationInfo['pl']!.pluralFunction;\n      // cSpell:ignore głowa, głowy, głow\n      expect(plural(0, ['głowa', 'głowy', 'głow']), 'głow');\n      expect(plural(1, ['głowa', 'głowy', 'głow']), 'głowa');\n      expect(plural(2, ['głowa', 'głowy', 'głow']), 'głowy');\n      expect(plural(3, ['głowa', 'głowy', 'głow']), 'głowy');\n      expect(plural(4, ['głowa', 'głowy', 'głow']), 'głowy');\n      expect(plural(5, ['głowa', 'głowy', 'głow']), 'głow');\n      expect(plural(11, ['głowa', 'głowy', 'głow']), 'głow');\n      expect(plural(21, ['głowa', 'głowy', 'głow']), 'głow');\n    });\n\n    test('uk (Ukrainian)', () {\n      final plural = localizationInfo['uk']!.pluralFunction;\n      // cSpell:ignore хвиля, хвилі, хвиль\n      expect(plural(0, ['хвиля', 'хвилі', 'хвиль']), 'хвиль');\n      expect(plural(1, ['хвиля', 'хвилі', 'хвиль']), 'хвиля');\n      expect(plural(2, ['хвиля', 'хвилі', 'хвиль']), 'хвилі');\n      expect(plural(3, ['хвиля', 'хвилі', 'хвиль']), 'хвилі');\n      expect(plural(4, ['хвиля', 'хвилі', 'хвиль']), 'хвилі');\n      expect(plural(5, ['хвиля', 'хвилі', 'хвиль']), 'хвиль');\n      expect(plural(10, ['хвиля', 'хвилі', 'хвиль']), 'хвиль');\n      expect(plural(11, ['хвиля', 'хвилі', 'хвиль']), 'хвиль');\n      expect(plural(101, ['хвиля', 'хвилі', 'хвиль']), 'хвиля');\n      expect(plural(111, ['хвиля', 'хвилі', 'хвиль']), 'хвиль');\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/parse/ascii_test.dart",
    "content": "import 'package:jenny/src/parse/ascii.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  test('ASCII', () {\n    _check($tab, '\\t');\n    _check($lineFeed, '\\n');\n    _check($carriageReturn, '\\r');\n    _check($space, ' ');\n    _check($exclamation, '!');\n    _check($doubleQuote, '\"');\n    _check($hash, '#');\n    _check($dollar, r'$');\n    _check($percent, '%');\n    _check($ampersand, '&');\n    _check($singleQuote, \"'\");\n    _check($leftParenthesis, '(');\n    _check($rightParenthesis, ')');\n    _check($asterisk, '*');\n    _check($plus, '+');\n    _check($comma, ',');\n    _check($minus, '-');\n    _check($dot, '.');\n    _check($slash, '/');\n    _check($digit0, '0');\n    _check($digit9, '9');\n    _check($colon, ':');\n    _check($lessThan, '<');\n    _check($equals, '=');\n    _check($greaterThan, '>');\n    _check($uppercaseA, 'A');\n    _check($uppercaseZ, 'Z');\n    _check($leftBracket, '[');\n    _check($backslash, r'\\');\n    _check($rightBracket, ']');\n    _check($caret, '^');\n    _check($underscore, '_');\n    _check($lowercaseA, 'a');\n    _check($lowercaseN, 'n');\n    _check($lowercaseZ, 'z');\n    _check($leftBrace, '{');\n    _check($pipe, '|');\n    _check($rightBrace, '}');\n  });\n}\n\nvoid _check(int codeUnit, String text) {\n  expect(String.fromCharCode(codeUnit), text);\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/parse/parse_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:jenny/src/parse/parse.dart';\nimport 'package:jenny/src/structure/commands/if_command.dart';\nimport 'package:jenny/src/structure/dialogue_entry.dart';\nimport 'package:test/test.dart';\n\nimport '../test_scenario.dart';\nimport '../utils.dart';\n\nvoid main() {\n  // The tests here are further organized into subgroups according to which\n  // part of the parser they engage.\n  group('parse', () {\n    group('parseMain', () {\n      test('empty input', () {\n        final yarn = YarnProject();\n        parse('', yarn);\n        expect(yarn.nodes.isEmpty, true);\n        expect(yarn.variables.isEmpty, true);\n      });\n\n      test('single node', () {\n        final yarn = YarnProject();\n        parse(\n          'title: Alpha\\n'\n          '---\\n'\n          '===\\n',\n          yarn,\n        );\n        expect(yarn.nodes.length, 1);\n        expect(yarn.nodes.containsKey('Alpha'), true);\n      });\n\n      test('multiple nodes', () {\n        final yarn = YarnProject();\n        parse(\n          'title: Alpha\\n---\\n===\\n'\n          ' // another node\\n'\n          'title: Beta\\n---\\n===\\n'\n          'title: Gamma\\n---\\n===\\n',\n          yarn,\n        );\n        expect(yarn.nodes.length, 3);\n        expect(yarn.nodes.containsKey('Alpha'), true);\n        expect(yarn.nodes.containsKey('Beta'), true);\n        expect(yarn.nodes.containsKey('Gamma'), true);\n      });\n    });\n\n    group('parseNodeHeader', () {\n      test('node with tags', () {\n        // cSpell:ignore Montagues, Capulets\n        final yarn = YarnProject();\n        yarn.parse(\n          'title: Romeo_v_Juliette\\n'\n          'requires: Montagues and Capulets\\n'\n          '\\n'\n          '// comment\\n'\n          'location: fair Verona\\n'\n          '---\\n===\\n',\n        );\n        final node = yarn.nodes['Romeo_v_Juliette'];\n        expect(node, isNotNull);\n        expect(node!.title, 'Romeo_v_Juliette');\n        expect(node.tags, isNotNull);\n        expect(node.tags['requires'], 'Montagues and Capulets');\n        expect(node.tags['location'], 'fair Verona');\n      });\n\n      test('multiple colons', () {\n        expect(\n          () => YarnProject().parse('title:: Hamlet\\n---\\n===\\n'),\n          hasSyntaxError(\n            'SyntaxError: unexpected token\\n'\n            '>  at line 1 column 7:\\n'\n            '>  title:: Hamlet\\n'\n            '>        ^\\n',\n          ),\n        );\n      });\n\n      test('node without a title', () {\n        expect(\n          () => YarnProject().parse('Title: Despicable Me!\\n---\\n===\\n'),\n          hasSyntaxError(\n            'SyntaxError: node does not have a title\\n'\n            '>  at line 2 column 1:\\n'\n            '>  ---\\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n\n      test('node with multiple titles', () {\n        expect(\n          () => YarnProject().parse(\n            '\\n'\n            'title: one\\n'\n            'keyword: value\\n'\n            'title: two\\n'\n            '---\\n===\\n',\n          ),\n          hasSyntaxError(\n            'SyntaxError: a node can only have one title\\n'\n            '>  at line 4 column 1:\\n'\n            '>  title: two\\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n\n      test('multiple nodes with same titles', () {\n        expect(\n          () => YarnProject().parse(\n            '\\n'\n            'title: xyz\\n'\n            '---\\n===\\n'\n            'title: xyz\\n'\n            '---\\n===\\n',\n          ),\n          hasNameError(\n            'NameError: node with title \"xyz\" has already been defined\\n'\n            '>  at line 5 column 1:\\n'\n            '>  title: xyz\\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n\n      test('Headers.yarn', () {\n        final yarn = YarnProject();\n        yarn.parse(\n          dedent('''\n            title: EmptyTags\n            tags:\n            ---\n            In this test, the 'tags' header is provided, but has no value.\n            ===\n            title: Tags\n            tags: one two three\n            ---\n            In this test, the 'tags' header is provided, and has three values.\n            ===\n            title: ArbitraryHeaderWithValue\n            // test\n            arbitraryHeader: some-arbitrary-text\n            ---\n            In this test, an arbitrary header is defined with some text.\n            ===\n            title: Comments\n            tags: one two three\n            metadata:\n            ---\n            This node demonstrates the use of comments in node headers.\n            ===\n          '''),\n        );\n        final node1 = yarn.nodes['EmptyTags']!;\n        expect(node1.tags, isNotEmpty);\n        expect(node1.tags['tags'], '');\n\n        final node2 = yarn.nodes['Tags']!;\n        expect(node2.tags['tags'], 'one two three');\n\n        final node3 = yarn.nodes['ArbitraryHeaderWithValue']!;\n        expect(node3.tags['arbitraryHeader'], 'some-arbitrary-text');\n\n        final node4 = yarn.nodes['Comments']!;\n        expect(node4.tags['tags'], 'one two three');\n        expect(node4.tags['metadata'], '');\n      });\n\n      test(\n        'InvalidNodeTitle.yarn',\n        () {\n          expect(\n            () => YarnProject().parse('title: \\$InvalidTitle\\n---\\n===\\n'),\n            hasNameError(r'NameError: invalid title name \"$InvalidTitle\"'),\n          );\n        },\n        skip: true,\n      );\n    });\n\n    group('parseNodeBody', () {\n      test('indent in a body', () {\n        expect(\n          () => YarnProject().parse('title:a\\n---\\n    hi\\n===\\n'),\n          hasSyntaxError(\n            'SyntaxError: unexpected indent\\n'\n            '>  at line 3 column 1:\\n'\n            '>      hi\\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n    });\n\n    group('parseStatementList', () {\n      test('multiple lines', () {\n        final yarn = YarnProject()\n          ..parse(\n            'title: test\\n'\n            '---\\n'\n            'Jupyter\\n'\n            'Saturn\\n\\n'\n            'Uranus  // LOL\\n'\n            '===\\n',\n          );\n        final lines = yarn.nodes['test']!.lines;\n        expect(lines.length, 3);\n        for (var i = 0; i < 3; i++) {\n          expect(lines[i], isA<DialogueLine>());\n          final line = lines[i] as DialogueLine;\n          expect(line.character, isNull);\n          expect(line.tags, isEmpty);\n          expect(line.text, ['Jupyter', 'Saturn', 'Uranus'][i]);\n        }\n      });\n    });\n\n    group('parseDialogueLine', () {\n      test('line with a speaker', () {\n        final yarn = YarnProject()\n          ..strictCharacterNames = false\n          ..parse('title:A\\n---\\nMrGoo: whatever\\n===\\n');\n        expect(yarn.nodes['A']!.lines.first, isA<DialogueLine>());\n        final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n        expect(line.character, isA<Character>());\n        expect(line.character!.name, 'MrGoo');\n        expect(line.text, 'whatever');\n      });\n\n      test('line with multiple expressions', () {\n        final yarn = YarnProject()\n          ..parse('title:A\\n---\\n{1} {false} {\"fake news\"}\\n===\\n');\n        expect(yarn.nodes['A']!.lines.first, isA<DialogueLine>());\n        final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n        line.evaluate();\n        expect(line.character, isNull);\n        expect(line.text, '1 false fake news');\n      });\n\n      test('line with hashtags', () {\n        final yarn = YarnProject()\n          ..parse('title:A\\n---\\n.hello #here #zzz\\n===\\n');\n        final node = yarn.nodes['A']!;\n        expect(node.lines.length, 1);\n        final line = node.lines[0] as DialogueLine;\n        expect(line.tags.length, 2);\n        expect(line.tags, contains('#here'));\n        expect(line.tags, contains('#zzz'));\n      });\n\n      test('line starting with an escaped character', () {\n        final yarn = YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '\\\\{ curly text \\\\}\\n'\n            '===\\n',\n          );\n        expect(\n          (yarn.nodes['A']!.lines[0] as DialogueLine).text,\n          '{ curly text }',\n        );\n      });\n\n      test('line with a command', () {\n        expect(\n          () => YarnProject().parse('title:A\\n---\\nz <<if true>>\\n===\\n'),\n          hasSyntaxError(\n            'SyntaxError: commands are not allowed on a dialogue line\\n'\n            '>  at line 3 column 3:\\n'\n            '>  z <<if true>>\\n'\n            '>    ^\\n',\n          ),\n        );\n      });\n\n      test('line with no content but a hashtag', () {\n        expect(\n          () => YarnProject().parse('title:A\\n---\\n#tag\\n===\\n'),\n          hasSyntaxError(\n            'SyntaxError: unexpected token\\n'\n            '>  at line 3 column 1:\\n'\n            '>  #tag\\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n    });\n\n    group('parseOption', () {\n      test('simple options', () {\n        final yarn = YarnProject()\n          ..parse(\n            'title: test\\n---\\n'\n            '-> Alpha\\n'\n            '-> Beta\\n'\n            '->    Gamma\\n'\n            '===\\n',\n          );\n        final node = yarn.nodes['test']!;\n        expect(node.lines.length, 1);\n        final choiceSet = node.lines.first as DialogueChoice;\n        expect(choiceSet.options.length, 3);\n        for (var i = 0; i < 3; i++) {\n          final line = choiceSet.options[i];\n          expect(line.character, isNull);\n          expect(line.tags, isEmpty);\n          expect(line.isAvailable, true);\n          expect(line.block, isEmpty);\n          expect(line.text, ['Alpha', 'Beta', 'Gamma'][i]);\n        }\n      });\n\n      test('speakers in options', () {\n        final yarn = YarnProject()\n          ..strictCharacterNames = false\n          ..parse(\n            'title:A\\n---\\n'\n            '-> Alice: Hello!\\n'\n            '-> Bob: Hi: there!\\n'\n            '===\\n',\n          );\n        final node = yarn.nodes['A']!;\n        final choice = node.lines[0] as DialogueChoice;\n        final option0 = choice.options[0];\n        final option1 = choice.options[1];\n        expect(option0.character, isA<Character>());\n        expect(option0.character!.name, 'Alice');\n        expect(option1.character!.name, 'Bob');\n        expect(option0.text, 'Hello!');\n        expect(option1.text, 'Hi: there!');\n      });\n\n      test('option with a followup dialogue', () {\n        final yarn = YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '-> choice one\\n'\n            '    Nice one, James!\\n'\n            '    Back to ya!\\n'\n            '-> choice two\\n'\n            '    My condolences...\\n'\n            '===\\n',\n          );\n        final node = yarn.nodes['A']!;\n        final choiceSet = node.lines[0] as DialogueChoice;\n        expect(choiceSet.options.length, 2);\n        final choice1 = choiceSet.options[0];\n        final choice2 = choiceSet.options[1];\n        expect(choice1.text, 'choice one');\n        expect(choice1.block, isNotNull);\n        expect(choice1.block.length, 2);\n        expect(\n          (choice1.block.lines[0] as DialogueLine).text,\n          'Nice one, James!',\n        );\n        expect(\n          (choice1.block.lines[1] as DialogueLine).text,\n          'Back to ya!',\n        );\n        expect(choice2.text, 'choice two');\n        expect(choice2.block, isNotNull);\n        expect(choice2.block.lines.length, 1);\n        expect(\n          (choice2.block.lines[0] as DialogueLine).text,\n          'My condolences...',\n        );\n      });\n\n      test('option with a non-if command', () {\n        expect(\n          () => YarnProject().parse(\n            'title:A\\n---\\n-> ok! <<stop>>\\n===\\n',\n          ),\n          hasSyntaxError(\n            'SyntaxError: only \"if\" command is allowed for an option\\n'\n            '>  at line 3 column 10:\\n'\n            '>  -> ok! <<stop>>\\n'\n            '>           ^\\n',\n          ),\n        );\n      });\n\n      test('option with a non-boolean condition', () {\n        expect(\n          () => YarnProject().parse(\n            'title:A\\n---\\n'\n            '-> ok! <<if 42 % 2>>\\n'\n            '===\\n',\n          ),\n          hasTypeError(\n            'TypeError: the condition in \"if\" should be boolean\\n'\n            '>  at line 3 column 13:\\n'\n            '>  -> ok! <<if 42 % 2>>\\n'\n            '>              ^\\n',\n          ),\n        );\n      });\n\n      test('option with multiple conditions', () {\n        expect(\n          () => YarnProject().parse(\n            'title:A\\n---\\n'\n            '-> ok! <<if true>> <<if false>>\\n'\n            '===\\n',\n          ),\n          hasSyntaxError(\n            'SyntaxError: multiple commands are not allowed on an option '\n            'line\\n'\n            '>  at line 3 column 20:\\n'\n            '>  -> ok! <<if true>> <<if false>>\\n'\n            '>                     ^\\n',\n          ),\n        );\n      });\n    });\n\n    group('parseExpression', () {\n      List<String> linesToText(Iterable<DialogueEntry> lines) {\n        return lines\n            .whereType<DialogueLine>()\n            .map((line) => (line..evaluate()).text)\n            .toList();\n      }\n\n      test('complicated expressions', () {\n        final yarn = YarnProject()\n          ..parse(\n            'title: test\\n---\\n'\n            '{(4 - 5)}\\n'\n            '{ 2 + 7 * 3 - 1 }\\n'\n            '{ 44 / (3 - 1) % 15 }\\n'\n            '===\\n',\n          );\n        final node = yarn.nodes['test']!;\n        expect(node.lines.length, 3);\n        expect(\n          linesToText(yarn.nodes['test']!.lines),\n          ['-1', '22', '7'],\n        );\n      });\n\n      test('unknown variable', () {\n        expect(\n          () => YarnProject().parse('title:A\\n---\\n{ \\$x + 1 }\\n===\\n'),\n          hasNameError(\n            'NameError: variable \\$x is not defined\\n'\n            '>  at line 3 column 3:\\n'\n            '>  { \\$x + 1 }\\n'\n            '>    ^\\n',\n          ),\n        );\n      });\n\n      test('unknown function', () {\n        expect(\n          () => YarnProject().parse('title:A\\n---\\n{ foo() }\\n===\\n'),\n          hasNameError(\n            'NameError: unknown function name foo\\n'\n            '>  at line 3 column 3:\\n'\n            '>  { foo() }\\n'\n            '>    ^\\n',\n          ),\n        );\n      });\n\n      test('invalid expression', () {\n        expect(\n          () => YarnProject().parse(\n            'title:A\\n---\\n'\n            '{ 1 + * 5 }\\n'\n            '===\\n',\n          ),\n          hasSyntaxError(\n            'SyntaxError: unexpected expression\\n'\n            '>  at line 3 column 7:\\n'\n            '>  { 1 + * 5 }\\n'\n            '>        ^\\n',\n          ),\n        );\n      });\n\n      test('mismatched parentheses', () {\n        expect(\n          () => YarnProject().parse(\n            'title:A\\n---\\n'\n            '{ (12 }\\n'\n            '===\\n',\n          ),\n          hasSyntaxError(\n            'SyntaxError: missing closing \")\"\\n'\n            '>  at line 3 column 7:\\n'\n            '>  { (12 }\\n'\n            '>        ^\\n',\n          ),\n        );\n      });\n\n      test('invalid function expression', () {\n        expect(\n          () => YarnProject().parse(\n            'title:A\\n---\\n'\n            '{ random_range(1, 3 ()) }\\n'\n            '===\\n',\n          ),\n          hasSyntaxError(\n            'SyntaxError: unexpected token\\n'\n            '>  at line 3 column 21:\\n'\n            '>  { random_range(1, 3 ()) }\\n'\n            '>                      ^\\n',\n          ),\n        );\n      });\n    });\n\n    group('parseCommand', () {\n      test('<<if>>', () {\n        final yarn = YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '<<if 2 > 0>>\\n'\n            '  First!\\n'\n            '<<else>>\\n'\n            '  Second\\n'\n            '<<endif>>\\n'\n            '===\\n',\n          );\n        final node = yarn.nodes['A']!;\n        expect(node.lines.length, 1);\n        expect(node.lines[0], isA<IfCommand>());\n        final command = node.lines[0] as IfCommand;\n        expect(command.ifs.length, 2);\n        expect(command.ifs[0].condition.value, true);\n        expect(command.ifs[0].block.lines[0], isA<DialogueLine>());\n        expect(\n          (command.ifs[0].block.lines[0] as DialogueLine).text,\n          'First!',\n        );\n        expect(command.ifs[1].condition.value, true);\n        expect(\n          (command.ifs[1].block.lines[0] as DialogueLine).text,\n          'Second',\n        );\n      });\n\n      test('<<elseif>>s', () {\n        final yarn = YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '<<if true>>\\n'\n            '  First!\\n'\n            '<<elseif false>>\\n'\n            '  Second\\n'\n            '<<elseif true>>\\n'\n            '  Third\\n'\n            '<<endif>>\\n'\n            '===\\n',\n          );\n        final node = yarn.nodes['A']!;\n        final command = node.lines[0] as IfCommand;\n        expect(command.ifs.length, 3);\n        expect(command.ifs[0].condition.value, true);\n        expect(command.ifs[1].condition.value, false);\n        expect(command.ifs[2].condition.value, true);\n      });\n\n      test('no <<endif>> 1', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              'title:A\\n---\\n'\n              '<<if true>>\\n'\n              '  ha\\n'\n              '===\\n',\n            ),\n          hasSyntaxError(\n            'SyntaxError: <<endif>> expected\\n'\n            '>  at line 5 column 1:\\n'\n            '>  ===\\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n\n      test('no <<endif>> 2', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              'title:A\\n---\\n'\n              '<<if true>>\\n'\n              '<<stop>>\\n'\n              '===\\n',\n            ),\n          hasSyntaxError(\n            'SyntaxError: <<endif>> expected\\n'\n            '>  at line 4 column 1:\\n'\n            '>  <<stop>>\\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n\n      test('double <<else>>', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              'title:A\\n---\\n'\n              '<<if true>>\\n'\n              '<<else>>\\n'\n              '<<else>>\\n'\n              '<<endif>>\\n'\n              '===\\n',\n            ),\n          hasSyntaxError(\n            'SyntaxError: only one <<else>> is allowed\\n'\n            '>  at line 5 column 1:\\n'\n            '>  <<else>>\\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n\n      test('no indentation in <<if>>', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              'title:A\\n---\\n'\n              '<<if true>>\\n'\n              'text\\n'\n              '<<endif>>\\n'\n              '===\\n',\n            ),\n          hasSyntaxError(\n            'SyntaxError: the body of the <<if>> command must be indented\\n'\n            '>  at line 4 column 1:\\n'\n            '>  text\\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n\n      test('no indentation in <<else>>', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              'title:A\\n---\\n'\n              '<<if true>>\\n'\n              '<<else>>\\n'\n              'text\\n'\n              '<<endif>>\\n'\n              '===\\n',\n            ),\n          hasSyntaxError(\n            'SyntaxError: the body of the <<else>> command must be indented\\n'\n            '>  at line 5 column 1:\\n'\n            '>  text\\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n\n      test('double command on a line', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              'title:A\\n---\\n'\n              '<<wait 1>> <<wait 2>>\\n'\n              '===\\n',\n            ),\n          hasSyntaxError(\n            'SyntaxError: expected end of line\\n'\n            '>  at line 3 column 12:\\n'\n            '>  <<wait 1>> <<wait 2>>\\n'\n            '>             ^\\n',\n          ),\n        );\n      });\n    });\n\n    group('parseMarkupTag', () {\n      test('parse simple markup tag', () {\n        final yarn = YarnProject()\n          ..parse(\n            'title: A\\n---\\n'\n            'Hello, [big]world[/big]!\\n'\n            '===\\n',\n          );\n        expect(yarn.nodes['A']!.lines.length, 1);\n        final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n        expect(line.text, 'Hello, world!');\n        expect(line.tags, isEmpty);\n        expect(line.attributes.length, 1);\n        final attribute = line.attributes[0];\n        expect(attribute.name, 'big');\n        expect(attribute.start, 7);\n        expect(attribute.end, 12);\n        expect(attribute.parameters, isEmpty);\n        expect(line.text.substring(attribute.start, attribute.end), 'world');\n      });\n\n      test('parse self-closing tag', () {\n        final yarn = YarnProject()\n          ..parse(\n            'title: A\\n---\\n'\n            'Hello, [wave /] world!\\n'\n            '===\\n',\n          );\n        final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n        final attribute = line.attributes[0];\n        // Note that the space after the tag was removed\n        expect(line.text, 'Hello, world!');\n        expect(attribute.name, 'wave');\n        expect(attribute.start, 7);\n        expect(attribute.end, 7);\n        expect(attribute.parameters, isEmpty);\n      });\n\n      test('parse nested tags', () {\n        final yarn = YarnProject()\n          ..strictCharacterNames = false\n          ..parse(\n            'title: A\\n---\\n'\n            'Warning: [a]Spinning [b][c]Je[/c]nny[/b][/a]\\n'\n            '===\\n',\n          );\n        final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n        expect(line.character, isA<Character>());\n        expect(line.character!.name, 'Warning');\n        expect(line.text, 'Spinning Jenny');\n\n        expect(line.attributes.length, 3);\n        final attrA = line.attributes[2];\n        final attrB = line.attributes[1];\n        final attrC = line.attributes[0];\n        expect(attrA.name, 'a');\n        expect(attrB.name, 'b');\n        expect(attrC.name, 'c');\n        expect(line.text.substring(attrA.start, attrA.end), 'Spinning Jenny');\n        expect(line.text.substring(attrB.start, attrB.end), 'Jenny');\n        expect(line.text.substring(attrC.start, attrC.end), 'Je');\n      });\n\n      test('markup tags at start of line', () {\n        final yarn = YarnProject()\n          ..parse(\n            'title: A\\n---\\n'\n            '[blue]wave[/blue]\\n'\n            '===\\n',\n          );\n        final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n        final attribute = line.attributes[0];\n        expect(line.text, 'wave');\n        expect(attribute.name, 'blue');\n        expect(attribute.start, 0);\n        expect(attribute.end, 4);\n      });\n\n      test('close-all tag [/]', () {\n        final yarn = YarnProject()\n          ..parse(\n            'title: A\\n---\\n'\n            '[a][b][c] hello [/]\\n'\n            '===\\n',\n          );\n        final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n        expect(line.text, ' hello ');\n        for (final attribute in line.attributes) {\n          expect(attribute.name, isIn(['a', 'b', 'c']));\n          expect(attribute.start, 0);\n          expect(attribute.end, 7);\n        }\n      });\n\n      test('markup tag with parameters', () {\n        final yarn = YarnProject()\n          ..parse(\n            'title: A\\n---\\n'\n            '[color r=0 g=false b=100 name=\"BLUE\"]wave[/color]\\n'\n            '===\\n',\n          );\n        final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n        final attr = line.attributes[0];\n        expect(line.text, 'wave');\n        expect(attr.name, 'color');\n        expect(attr.start, 0);\n        expect(attr.end, 4);\n        expect(attr.parameters, isNotNull);\n        final parameters = attr.parameters;\n        expect(parameters.length, 4);\n        expect(parameters.keys.toSet(), {'r', 'g', 'b', 'name'});\n        expect(parameters['r'], 0);\n        expect(parameters['g'], false);\n        expect(parameters['b'], 100);\n        expect(parameters['name'], 'BLUE');\n      });\n\n      test('markup tag with bare parameters', () {\n        final yarn = YarnProject()\n          ..parse(\n            'title: A\\n---\\n'\n            '[color blue]wave[/color]\\n'\n            '===\\n',\n          );\n        final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n        final attr = line.attributes[0];\n        expect(attr.parameters, isNotNull);\n        final parameters = attr.parameters;\n        expect(parameters.length, 1);\n        expect(parameters['blue'], true);\n      });\n\n      test('markup tags with inline expressions', () {\n        final yarn = YarnProject()\n          ..variables.setVariable(r'$x', 'world')\n          ..parse(\n            'title: A\\n---\\n'\n            'Hello [color]{\\$x}[/color]\\n'\n            '===\\n',\n          );\n        final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n        line.evaluate();\n        expect(line.text, 'Hello world');\n        final attr = line.attributes[0];\n        expect(attr.name, 'color');\n        expect(attr.start, 6);\n        expect(attr.end, 11);\n        expect(line.text.substring(attr.start, attr.end), 'world');\n\n        for (final x in ['YarnSpinner', 'Blue Fire', 'me']) {\n          yarn.variables.setVariable(r'$x', x);\n          line.evaluate();\n          expect(line.text, 'Hello $x');\n          expect(line.text.substring(attr.start, attr.end), x);\n        }\n      });\n\n      test('more inline expressions', () {\n        final yarn = YarnProject()\n          ..variables.setVariable(r'$w', 'welcome')\n          ..variables.setVariable(r'$x', 'citizen')\n          ..variables.setVariable(r'$y', 'Paradise City')\n          ..variables.setVariable(r'$z', '...')\n          ..parse(\n            'title: A\\n---\\n'\n            'Hello {\\$x}, and [color]{\\$w} to {\\$y}[/color] {\\$z}\\n'\n            '===\\n',\n          );\n        final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n        line.evaluate();\n        expect(line.text, 'Hello citizen, and welcome to Paradise City ...');\n        final attr = line.attributes[0];\n        expect(attr.name, 'color');\n        expect(\n          line.text.substring(attr.start, attr.end),\n          'welcome to Paradise City',\n        );\n\n        yarn.variables\n          ..setVariable(r'$x', 'unidentified')\n          ..setVariable(r'$y', '[INVALID]')\n          ..setVariable(r'$z', '😠');\n        line.evaluate();\n        expect(line.text, 'Hello unidentified, and welcome to [INVALID] 😠');\n        expect(\n          line.text.substring(attr.start, attr.end),\n          'welcome to [INVALID]',\n        );\n      });\n\n      test('adjacent inline expressions 1', () {\n        final yarn = YarnProject()\n          ..variables.setVariable(r'$w', 'some ')\n          ..variables.setVariable(r'$x', 'arbitrary ')\n          ..variables.setVariable(r'$y', 'text')\n          ..variables.setVariable(r'$z', '?')\n          ..parse(\n            'title: A\\n---\\n'\n            r'{$w}[wavy]{$x}{$y}[/]{$z}'\n            '\\n===\\n',\n          );\n        final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n        line.evaluate();\n        expect(line.text, 'some arbitrary text?');\n        expect(line.attributes.length, 1);\n\n        final attr = line.attributes[0];\n        expect(line.text.substring(attr.start, attr.end), 'arbitrary text');\n      });\n\n      test('adjacent inline expressions 2', () {\n        final yarn = YarnProject()\n          ..variables.setVariable(r'$x1', 'One')\n          ..variables.setVariable(r'$x2', 'Double')\n          ..variables.setVariable(r'$x3', 'Three')\n          ..parse(\n            'title: A\\n---\\n'\n            r'{$x1}[a/]{$x2}[b/]{$x3}'\n            '\\n===\\n',\n          );\n        final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n        line.evaluate();\n        expect(line.text, 'OneDoubleThree');\n        expect(line.attributes.length, 2);\n\n        final attrA = line.attributes[0];\n        expect(attrA.name, 'a');\n        expect(attrA.length, 0);\n        expect(attrA.start, 'One'.length);\n\n        final attrB = line.attributes[1];\n        expect(attrB.name, 'b');\n        expect(attrB.length, 0);\n        expect(attrB.start, 'OneDouble'.length);\n      });\n\n      group('errors', () {\n        test('invalid opening markup tag', () {\n          expect(\n            () => YarnProject()\n              ..parse(\n                'title:X\\n---\\n'\n                '[+1]\\n'\n                '===\\n',\n              ),\n            hasSyntaxError(\n              'SyntaxError: a markup tag name is expected\\n'\n              '>  at line 3 column 2:\\n'\n              '>  [+1]\\n'\n              '>   ^\\n',\n            ),\n          );\n        });\n\n        test('invalid closing markup tag', () {\n          expect(\n            () => YarnProject()\n              ..parse(\n                'title:X\\n---\\n'\n                '[/1]\\n'\n                '===\\n',\n              ),\n            hasSyntaxError(\n              'SyntaxError: a markup tag name is expected\\n'\n              '>  at line 3 column 3:\\n'\n              '>  [/1]\\n'\n              '>    ^\\n',\n            ),\n          );\n        });\n\n        test('closing tag without opening', () {\n          expect(\n            () => YarnProject()\n              ..parse(\n                'title:X\\n---\\n'\n                'text [/rgb]\\n'\n                '===\\n',\n              ),\n            hasSyntaxError(\n              'SyntaxError: unexpected closing markup tag\\n'\n              '>  at line 3 column 7:\\n'\n              '>  text [/rgb]\\n'\n              '>        ^\\n',\n            ),\n          );\n        });\n\n        test('close-all tag without opening', () {\n          expect(\n            () => YarnProject()\n              ..parse(\n                'title:X\\n---\\n'\n                'text [/]\\n'\n                '===\\n',\n              ),\n            hasSyntaxError(\n              'SyntaxError: unexpected closing markup tag\\n'\n              '>  at line 3 column 7:\\n'\n              '>  text [/]\\n'\n              '>        ^\\n',\n            ),\n          );\n        });\n\n        test('incorrectly nested markup tags', () {\n          expect(\n            () => YarnProject()\n              ..parse(\n                'title:X\\n---\\n'\n                '[r][g][b] text [/g][/r][/b]\\n'\n                '===\\n',\n              ),\n            hasSyntaxError(\n              'SyntaxError: Expected closing tag for [b]\\n'\n              '>  at line 3 column 18:\\n'\n              '>  [r][g][b] text [/g][/r][/b]\\n'\n              '>                   ^\\n',\n            ),\n          );\n        });\n\n        test('unclosed markup tag', () {\n          expect(\n            () => YarnProject()\n              ..parse(\n                'title:X\\n---\\n'\n                '[hi] text\\n'\n                '===\\n',\n              ),\n            hasSyntaxError(\n              'SyntaxError: markup tag [hi] was not closed\\n'\n              '>  at line 3 column 10:\\n'\n              '>  [hi] text\\n'\n              '>           ^\\n',\n            ),\n          );\n        });\n\n        test('unfinished markup tag', () {\n          expect(\n            () => YarnProject()\n              ..parse(\n                'title: X\\n---\\n'\n                '[hi text\\n'\n                '===\\n',\n              ),\n            hasSyntaxError(\n              'SyntaxError: missing markup tag close token \"]\"\\n'\n              '>  at line 3 column 9:\\n'\n              '>  [hi text\\n'\n              '>          ^\\n',\n            ),\n          );\n        });\n\n        test('repeated attribute name', () {\n          expect(\n            () => YarnProject()\n              ..parse(\n                'title: X\\n---\\n'\n                '[color r=1 r=2]text[/color]\\n'\n                '===\\n',\n              ),\n            hasSyntaxError(\n              'SyntaxError: duplicate parameter r in a markup attribute\\n'\n              '>  at line 3 column 12:\\n'\n              '>  [color r=1 r=2]text[/color]\\n'\n              '>             ^\\n',\n            ),\n          );\n        });\n\n        test('invalid attribute content', () {\n          expect(\n            () => YarnProject()\n              ..parse(\n                'title: X\\n---\\n'\n                '[color r += 1]text[/color]\\n'\n                '===\\n',\n              ),\n            hasSyntaxError(\n              'SyntaxError: unexpected token\\n'\n              '>  at line 3 column 10:\\n'\n              '>  [color r += 1]text[/color]\\n'\n              '>           ^\\n',\n            ),\n          );\n        });\n      });\n    });\n\n    testScenario(\n      testName: 'Escaping.yarn',\n      input: r'''\n        title: Start\n        ---\n        Here's a line with a hashtag #hashtag\n        Here's a line with an escaped hashtag \\#hashtag\n        Here's a line with an expression {0}\n        Here's a line with an escaped expression \\{0\\}\n\n        // Commented out because this isn't actually allowed, but we're just\n        // maintaining the pattern here:\n        // Here's a line with a command <<foo>\n        Here's a line with an escaped command \\<\\<foo\\>\\>\n        Here's a line with a comment // wow\n        Here's a line with an escaped comment \\/\\/ wow\n        Here's a line with an escaped backslash \\\\\n        Here's some styling with a color code: <color=\\#fff>wow</color>\n        Here's a url: http:\\/\\/github.com\\/YarnSpinnerTool\n\n        // Escaped markup is handled by the LineParser class, not the main\n        // grammar itself\n        Here's some markup: [a]hello[/a]\n        Here's some escaped markup: \\[a\\]hello\\[/a\\]\n\n        -> Here's an option with a hashtag #hashtag\n        -> Here's an option with an escaped hashtag \\#hashtag\n\n        -> Here's an option with an expression {0}\n        -> Here's an option with an escaped expression \\{0\\}\n\n        // Commented out because this isn't actually allowed, but we're just\n        // maintaining the pattern here:\n        -> Here's an option with a condition <<if true>>\n        -> Here's an option with an escaped condition \\<\\<if true\\>\\>\n        -> Here's an option with a comment // wow\n        -> Here's an option with an escaped comment \\/\\/ wow\n        -> Here's an option with an escaped backslash \\\\\n        -> Here's some styling with a color code: <color=\\#fff>wow</color>\n        -> Here's a url: http:\\/\\/github.com\\/YarnSpinnerTool\n\n        // Escaped markup is handled by the LineParser class, not the main\n        // grammar itself\n        -> Here's some markup: [a]hello[/a]\n        -> Here's some escaped markup: \\[a\\]hello\\[/a\\]\n        ===\n      ''',\n      testPlan: r'''\n        line: Here's a line with a hashtag\n        line: Here's a line with an escaped hashtag #hashtag\n        line: Here's a line with an expression 0\n        line: Here's a line with an escaped expression {0}\n        line: Here's a line with an escaped command <<foo>>\n        line: Here's a line with a comment\n        line: Here's a line with an escaped comment // wow\n        line: Here's a line with an escaped backslash \\\n        line: Here's some styling with a color code: <color=#fff>wow</color>\n        line: Here's a url: http://github.com/YarnSpinnerTool\n        line: Here's some markup: hello\n        line: Here's some escaped markup: [a]hello[/a]\n\n        option: Here's an option with a hashtag\n        option: Here's an option with an escaped hashtag #hashtag\n        option: Here's an option with an expression 0\n        option: Here's an option with an escaped expression {0}\n        option: Here's an option with a condition\n        option: Here's an option with an escaped condition <<if true>>\n        option: Here's an option with a comment\n        option: Here's an option with an escaped comment // wow\n        option: Here's an option with an escaped backslash \\\n        option: Here's some styling with a color code: <color=#fff>wow</color>\n        option: Here's a url: http://github.com/YarnSpinnerTool\n        option: Here's some markup: hello\n        option: Here's some escaped markup: [a]hello[/a]\n        select: 1\n      ''',\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/parse/token_test.dart",
    "content": "import 'package:jenny/src/parse/token.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('Token', () {\n    test('tokens without args', () {\n      expect('${Token.asType}', 'Token.asType');\n      expect('${Token.closeMarkupTag}', 'Token.closeMarkupTag');\n      expect('${Token.colon}', 'Token.colon');\n      expect('${Token.commandCharacter}', 'Token.commandCharacter');\n      expect('${Token.commandEndif}', 'Token.commandEndif');\n      expect('${Token.commandLocal}', 'Token.commandLocal');\n      expect('${Token.commandVisit}', 'Token.commandVisit');\n      expect('${Token.constTrue}', 'Token.constTrue');\n      expect('${Token.endIndent}', 'Token.endIndent');\n      expect('${Token.endMarkupTag}', 'Token.endMarkupTag');\n      expect('${Token.operatorMinusAssign}', 'Token.operatorMinusAssign');\n      expect('${Token.operatorXor}', 'Token.operatorXor');\n      expect('${Token.startExpression}', 'Token.startExpression');\n      expect('${Token.startMarkupTag}', 'Token.startMarkupTag');\n      expect('${Token.typeString}', 'Token.typeString');\n\n      expect(Token.comma.isText, false);\n      expect(Token.comma.isId, false);\n      expect(Token.comma.isVariable, false);\n      expect(Token.comma.isNumber, false);\n      expect(Token.comma.isPerson, false);\n      expect(Token.comma.isCommand, false);\n      expect(Token.comma.isString, false);\n      expect(Token.comma.isHashtag, false);\n    });\n\n    test('Token.text', () {\n      const token = Token.text('some text');\n      expect('$token', \"Token.text('some text')\");\n      expect(token.isText, true);\n      expect(token.type, TokenType.text);\n      expect(token.content, 'some text');\n    });\n\n    test('Token.number', () {\n      const token = Token.number('3.14159');\n      expect('$token', \"Token.number('3.14159')\");\n      expect(token.isNumber, true);\n      expect(token.type, TokenType.number);\n      expect(token.content, '3.14159');\n    });\n\n    test('Token.id', () {\n      const token = Token.id('xyz127');\n      expect('$token', \"Token.id('xyz127')\");\n      expect(token.isId, true);\n      expect(token.type, TokenType.id);\n      expect(token.content, 'xyz127');\n    });\n\n    test('Token.variable', () {\n      const token = Token.variable('flame');\n      expect('$token', \"Token.variable('flame')\");\n      expect(token.isVariable, true);\n      expect(token.type, TokenType.variable);\n      expect(token.content, 'flame');\n    });\n\n    test('Token.person', () {\n      const token = Token.person('Mr_Obama');\n      expect('$token', \"Token.person('Mr_Obama')\");\n      expect(token.isPerson, true);\n      expect(token.type, TokenType.person);\n      expect(token.content, 'Mr_Obama');\n    });\n\n    test('Token.command', () {\n      const token = Token.command('go');\n      expect('$token', \"Token.command('go')\");\n      expect(token.isCommand, true);\n      expect(token.type, TokenType.command);\n      expect(token.content, 'go');\n    });\n\n    test('equality', () {\n      expect(Token.startParenthesis == Token.startParenthesis, true);\n      expect(Token.startParenthesis == Token.endParenthesis, false);\n      expect(const Token.text('foo') == const Token.text('foo'), true);\n      expect(const Token.text('foo') == const Token.string('foo'), false);\n      expect(const Token.text('foo') == const Token.text('bar'), false);\n\n      expect(Token.arrow.hashCode == Token.comma.hashCode, false);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/parse/tokenize_test.dart",
    "content": "import 'package:jenny/src/parse/token.dart';\nimport 'package:jenny/src/parse/tokenize.dart';\nimport 'package:test/test.dart';\n\nimport '../utils.dart';\n\nvoid main() {\n  // Tests are organized according to which lexing Mode they are checking\n  group('tokenize', () {\n    group('modeMain', () {\n      test('empty input', () {\n        expect(tokenize(''), <Token>[]);\n        expect(tokenize(' '), <Token>[]);\n        expect(tokenize(' \\t  \\n'), [Token.newline]);\n        expect(tokenize('\\r\\n\\n   '), [Token.newline, Token.newline]);\n      });\n\n      test('comments only', () {\n        expect(tokenize('// hello'), const <Token>[]);\n        expect(tokenize('// world\\n\\n'), [Token.newline, Token.newline]);\n        expect(\n          tokenize(\n            '\\n'\n            '//--------------------\\n'\n            '// BOILER PLATE       \\n'\n            '//--------------------\\n'\n            '\\n',\n          ),\n          const [\n            Token.newline,\n            Token.newline,\n            Token.newline,\n            Token.newline,\n            Token.newline,\n          ],\n        );\n      });\n\n      test('only header separator', () {\n        expect(\n          () => tokenize('---\\n'),\n          hasSyntaxError(\n            'SyntaxError: incomplete node body\\n'\n            '>  at line 2 column 1:\\n'\n            '>  \\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n\n      test('main-mode tags', () {\n        expect(\n          tokenize(\n            '# version: 2.3\\n'\n            '#ok',\n          ),\n          const [\n            Token.hashtag('# version: 2.3'),\n            Token.newline,\n            Token.hashtag('#ok'),\n            Token.newline,\n          ],\n        );\n      });\n\n      test('main-mode commands', () {\n        expect(\n          tokenize('<<stop>>\\n'),\n          [\n            Token.startCommand,\n            Token.commandStop,\n            Token.endCommand,\n            Token.newline,\n          ],\n        );\n      });\n    });\n\n    group('modeNodeHeader', () {\n      test('no header lines', () {\n        expect(\n          tokenize('---\\n---\\n===\\n'),\n          [Token.startHeader, Token.endHeader, Token.startBody, Token.endBody],\n        );\n      });\n\n      test('simple header', () {\n        expect(\n          tokenize(\n            '\\n'\n            'title: node: 1\\n'\n            '\\n'\n            '---\\n===\\n',\n          ),\n          const [\n            Token.newline,\n            Token.startHeader,\n            Token.id('title'),\n            Token.colon,\n            Token.text('node: 1'),\n            Token.newline,\n            Token.newline,\n            Token.endHeader,\n            Token.startBody,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('multi-line header', () {\n        expect(\n          tokenize(\n            '\\n'\n            'title: Some Long title\\n'\n            'title : Another title\\n'\n            'some_other_keyword:1\\n'\n            '---\\n===\\n',\n          ),\n          const [\n            Token.newline,\n            Token.startHeader,\n            Token.id('title'),\n            Token.colon,\n            Token.text('Some Long title'),\n            Token.newline,\n            Token.id('title'),\n            Token.colon,\n            Token.text('Another title'),\n            Token.newline,\n            Token.id('some_other_keyword'),\n            Token.colon,\n            Token.text('1'),\n            Token.newline,\n            Token.endHeader,\n            Token.startBody,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('empty continuation of header line', () {\n        expect(\n          tokenize('foo:\\n---\\n===\\n'),\n          const [\n            Token.startHeader,\n            Token.id('foo'),\n            Token.colon,\n            Token.text(''),\n            Token.newline,\n            Token.endHeader,\n            Token.startBody,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('multiple colons', () {\n        expect(tokenize('foo::bar\\n---\\n===\\n'), const [\n          Token.startHeader,\n          Token.id('foo'),\n          Token.colon,\n          Token.colon,\n          Token.text('bar'),\n          Token.newline,\n          Token.endHeader,\n          Token.startBody,\n          Token.endBody,\n        ]);\n      });\n\n      test('header lines with comments', () {\n        expect(\n          tokenize(\n            'one: // comment\\n'\n            '\\n'\n            'two: some data //comment 2\\n'\n            '---\\n===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.id('one'),\n            Token.colon,\n            Token.text(''),\n            Token.newline,\n            Token.newline,\n            Token.id('two'),\n            Token.colon,\n            Token.text('some data '),\n            Token.newline,\n            Token.endHeader,\n            Token.startBody,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('extra whitespace', () {\n        expect(\n          () => tokenize('  title: this\\n---\\n===\\n'),\n          hasSyntaxError(\n            'SyntaxError: unexpected indentation\\n'\n            '>  at line 1 column 3:\\n'\n            '>    title: this\\n'\n            '>    ^\\n',\n          ),\n        );\n      });\n\n      test('without id', () {\n        expect(\n          () => tokenize(':\\n---\\n===\\n'),\n          hasSyntaxError(\n            'SyntaxError: expected end-of-header marker \"---\"\\n'\n            '>  at line 1 column 1:\\n'\n            '>  :\\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n\n      test('short separator', () {\n        expect(\n          () => tokenize('--\\n===\\n'),\n          hasSyntaxError(\n            'SyntaxError: expected end-of-header marker \"---\"\\n'\n            '>  at line 1 column 1:\\n'\n            '>  --\\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n\n      test('overlong separator', () {\n        expect(\n          tokenize('----\\n----\\n===\\n'),\n          [Token.startHeader, Token.endHeader, Token.startBody, Token.endBody],\n        );\n        expect(\n          tokenize('------------------------------------------\\n---\\n===\\n'),\n          [Token.startHeader, Token.endHeader, Token.startBody, Token.endBody],\n        );\n      });\n\n      test('explicit start&end of header', () {\n        expect(\n          tokenize(\n            '-----------------------------------------------\\n'\n            'title: The_Best_Node\\n'\n            '-----------------------------------------------\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.id('title'),\n            Token.colon,\n            Token.text('The_Best_Node'),\n            Token.newline,\n            Token.endHeader,\n            Token.startBody,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('header with wrong content', () {\n        expect(\n          () => tokenize(\n            '---\\n'\n            '# abc\\n'\n            '---\\n'\n            '===\\n',\n          ),\n          hasSyntaxError(\n            'SyntaxError: expected end-of-header marker \"---\"\\n'\n            '>  at line 2 column 1:\\n'\n            '>  # abc\\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n    });\n\n    group('modeNodeBody', () {\n      test('without final newline', () {\n        expect(\n          tokenize('---\\n---\\n==='),\n          [Token.startHeader, Token.endHeader, Token.startBody, Token.endBody],\n        );\n      });\n\n      test('whitespace in body', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            '\\n'\n            '   \\t  \\r\\n'\n            ' // also could be some comments here\\n'\n            '===',\n          ),\n          [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.newline,\n            Token.newline,\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('indentation', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            '  alpha\\n'\n            '   beta\\n'\n            '\\t     gamma\\n'\n            '  delta\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.startIndent,\n            Token.text('alpha'),\n            Token.newline,\n            Token.startIndent,\n            Token.text('beta'),\n            Token.newline,\n            Token.startIndent,\n            Token.text('gamma'),\n            Token.newline,\n            Token.endIndent,\n            Token.endIndent,\n            Token.text('delta'),\n            Token.newline,\n            Token.endIndent,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('invalid indentation', () {\n        expect(\n          () => tokenize(\n            '---\\n---\\n'\n            ' alpha\\n'\n            '     beta\\n'\n            '  gamma\\n'\n            '===\\n',\n          ),\n          hasSyntaxError(\n            'SyntaxError: inconsistent indentation\\n'\n            '>  at line 5 column 3:\\n'\n            '>    gamma\\n'\n            '>    ^\\n',\n          ),\n        );\n      });\n\n      test('de-indents at end of body', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            'one\\n'\n            '  two\\n'\n            '    three\\n'\n            '===',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.text('one'),\n            Token.newline,\n            Token.startIndent,\n            Token.text('two'),\n            Token.newline,\n            Token.startIndent,\n            Token.text('three'),\n            Token.newline,\n            Token.endIndent,\n            Token.endIndent,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('invalid body end', () {\n        expect(\n          () => tokenize('---\\n---\\n===='),\n          hasSyntaxError(\n            'SyntaxError: incomplete node body\\n'\n            '>  at line 3 column 5:\\n'\n            '>  ====\\n'\n            '>      ^\\n',\n          ),\n        );\n      });\n    });\n\n    group('modeNodeBodyLine', () {\n      test('option lines', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            '->something\\n'\n            '  -> other\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.arrow,\n            Token.text('something'),\n            Token.newline,\n            Token.startIndent,\n            Token.arrow,\n            Token.text('other'),\n            Token.newline,\n            Token.endIndent,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('commands', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            '<< >>\\n'\n            '<< stop >>\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.startCommand,\n            Token.endCommand,\n            Token.newline,\n            Token.startCommand,\n            Token.commandStop,\n            Token.endCommand,\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('line speakers', () {\n        // cSpell:ignore Пан_Голова, Ḟḷḁṃḙ\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            'Marge: Hello!\\n'\n            'Mr Smith: You too\\n'\n            'Пан_Голова :...\\n'\n            'Ḟḷḁṃḙ: // nothing\\n'\n            '𐀆𒐰ï︮𒐜   :::\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.person('Marge'),\n            Token.colon,\n            Token.text('Hello!'),\n            Token.newline,\n            Token.text('Mr Smith: You too'),\n            Token.newline,\n            Token.person('Пан_Голова'),\n            Token.colon,\n            Token.text('...'),\n            Token.newline,\n            Token.person('Ḟḷḁṃḙ'),\n            Token.colon,\n            Token.newline,\n            Token.person('𐀆𒐰ï︮𒐜'),\n            Token.colon,\n            Token.text('::'),\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('repeated arrow', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            '-> -> -> \\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.arrow,\n            Token.arrow,\n            Token.arrow,\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('repeated character name', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            'Pig: Horse: Moo!\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.person('Pig'),\n            Token.colon,\n            Token.text('Horse: Moo!'),\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('line with hash tags', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            'Some text #with-tag\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.text('Some text'),\n            Token.hashtag('#with-tag'),\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n    });\n\n    group('modeText', () {\n      test('text with comment', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            'some text // here be dragons\\n'\n            'other text   \\t\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.text('some text'),\n            Token.newline,\n            Token.text('other text'),\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('escape sequences', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            r'\\<\\{ inside \\}\\>'\n            '\\n'\n            'very long \\\\\\n'\n            '  text\\n'\n            'line with a newline:\\\\n ok\\n'\n            '\\\\-> text with arrow\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.text('<'),\n            Token.text('{'),\n            Token.text(' inside '),\n            Token.text('}'),\n            Token.text('>'),\n            Token.newline,\n            Token.text('very long '),\n            Token.text('text'),\n            Token.newline,\n            Token.text('line with a newline:'),\n            Token.text('\\n'),\n            Token.text(' ok'),\n            Token.newline,\n            Token.text('-'),\n            Token.text('> text with arrow'),\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('escaped colon', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            'One\\\\: two\\n'\n            'One two three\\\\:\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.text('One'),\n            Token.text(':'),\n            Token.text(' two'),\n            Token.newline,\n            Token.text('One two three'),\n            Token.text(':'),\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('\">>\" sequence', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            '>> hello\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.text('>>'),\n            Token.text(' hello'),\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('invalid escape sequence', () {\n        expect(\n          () => tokenize(\n            '---\\n---\\n'\n            'some text \\\\a\\n'\n            '===\\n',\n          ),\n          hasSyntaxError(\n            'SyntaxError: invalid escape sequence\\n'\n            '>  at line 3 column 12:\\n'\n            '>  some text \\\\a\\n'\n            '>             ^\\n',\n          ),\n        );\n      });\n\n      test('missing escape sequence', () {\n        for (final ch in [']', '}']) {\n          expect(\n            () => tokenize('---\\n---\\ntext $ch\\n'),\n            hasSyntaxError(\n              'SyntaxError: special character needs to be escaped\\n'\n              '>  at line 3 column 6:\\n'\n              '>  text $ch\\n'\n              '>       ^\\n',\n            ),\n          );\n        }\n      });\n\n      test('expressions', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            '{ } // noop\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.startExpression,\n            Token.endExpression,\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n    });\n\n    group('modeExpression', () {\n      test('expression with assorted tokens', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            '{ \\$x += 33 - 7/random() }\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.startExpression,\n            Token.variable(r'$x'),\n            Token.operatorPlusAssign,\n            Token.number('33'),\n            Token.operatorMinus,\n            Token.number('7'),\n            Token.operatorDivide,\n            Token.id('random'),\n            Token.startParenthesis,\n            Token.endParenthesis,\n            Token.endExpression,\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('expression with keywords', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            '{ true * false as String }\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.startExpression,\n            Token.constTrue,\n            Token.operatorMultiply,\n            Token.constFalse,\n            Token.asType,\n            Token.typeString,\n            Token.endExpression,\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('expression with strings', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            '{ \\$x = \"hello\" + \", world\" }\\n'\n            '{ \"one\\' two\", \\'\"\\' }\\n'\n            '{ \"last \\\\\\' \\\\\" \\\\\\\\ one\\\\n\" }\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.startExpression,\n            Token.variable(r'$x'),\n            Token.operatorAssign,\n            Token.string('hello'),\n            Token.operatorPlus,\n            Token.string(', world'),\n            Token.endExpression,\n            Token.newline,\n            Token.startExpression,\n            Token.string(\"one' two\"),\n            Token.comma,\n            Token.string('\"'),\n            Token.endExpression,\n            Token.newline,\n            Token.startExpression,\n            Token.string('last \\' \" \\\\ one\\n'),\n            Token.endExpression,\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('expression with numbers', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            '{ 0 -1  239444  0.5  17.1  2.  3.1415926535 111}\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.startExpression,\n            Token.number('0'),\n            Token.operatorMinus,\n            Token.number('1'),\n            Token.number('239444'),\n            Token.number('0.5'),\n            Token.number('17.1'),\n            Token.number('2.'),\n            Token.number('3.1415926535'),\n            Token.number('111'),\n            Token.endExpression,\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('close command within a plain text expression', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            '{ a >> b }\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.startExpression,\n            Token.id('a'),\n            Token.operatorGreaterThan,\n            Token.operatorGreaterThan,\n            Token.id('b'),\n            Token.endExpression,\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('invalid variable name', () {\n        expect(\n          () => tokenize(\n            '---\\n---\\n'\n            '{ \\$a = \\$7b }\\n'\n            '===\\n',\n          ),\n          hasSyntaxError(\n            'SyntaxError: invalid variable name\\n'\n            '>  at line 3 column 8:\\n'\n            '>  { \\$a = \\$7b }\\n'\n            '>         ^\\n',\n          ),\n        );\n      });\n\n      test('unicode variable names', () {\n        // cSpell:ignore эксперимент\n        expect(\n          () => tokenize(\n            '---\\n---\\n'\n            '{ \\$эксперимент }\\n'\n            '===\\n',\n          ),\n          hasSyntaxError(\n            'SyntaxError: invalid variable name\\n'\n            '>  at line 3 column 3:\\n'\n            '>  { \\$эксперимент }\\n'\n            '>    ^\\n',\n          ),\n        );\n      });\n\n      test('invalid string', () {\n        expect(\n          () => tokenize(\n            '---\\n---\\n'\n            '{ \"starting... }\\n'\n            '===\\n',\n          ),\n          hasSyntaxError(\n            'SyntaxError: unexpected end of line while parsing a string\\n'\n            '>  at line 3 column 17:\\n'\n            '>  { \"starting... }\\n'\n            '>                  ^\\n',\n          ),\n        );\n      });\n    });\n\n    group('modeCommand', () {\n      test('normal commands', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            '<< stop >>\\n'\n            '<< fullStop >>\\n'\n            '<< jump places >>\\n'\n            '<<wait 2>>\\n'\n            '<< set \\$n = 2 >>  // simple\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.startCommand,\n            Token.commandStop,\n            Token.endCommand,\n            Token.newline,\n            Token.startCommand,\n            Token.command('fullStop'),\n            Token.endCommand,\n            Token.newline,\n            Token.startCommand,\n            Token.commandJump,\n            Token.id('places'),\n            Token.endCommand,\n            Token.newline,\n            Token.startCommand,\n            Token.commandWait,\n            Token.startExpression,\n            Token.number('2'),\n            Token.endExpression,\n            Token.endCommand,\n            Token.newline,\n            Token.startCommand,\n            Token.commandSet,\n            Token.startExpression,\n            Token.variable(r'$n'),\n            Token.operatorAssign,\n            Token.number('2'),\n            Token.endExpression,\n            Token.endCommand,\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('if-else', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            '<<if \\$gold_amount < 10>>\\n'\n            \"    Baker: Well, you can't afford one!\\n\"\n            '<<endif>>\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.startCommand,\n            Token.commandIf,\n            Token.startExpression,\n            Token.variable(r'$gold_amount'),\n            Token.operatorLessThan,\n            Token.number('10'),\n            Token.endExpression,\n            Token.endCommand,\n            Token.newline,\n            Token.startIndent,\n            Token.person('Baker'),\n            Token.colon,\n            Token.text(\"Well, you can't afford one!\"),\n            Token.newline,\n            Token.endIndent,\n            Token.startCommand,\n            Token.commandEndif,\n            Token.endCommand,\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('user-defined commands', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            '<<hello one two three>>\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.startCommand,\n            Token.command('hello'),\n            Token.text('one two three'),\n            Token.endCommand,\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('closing brace', () {\n        expect(\n          () => tokenize(\n            '---\\n---\\n'\n            '<< hello } >>\\n'\n            '===\\n',\n          ),\n          hasSyntaxError(\n            'SyntaxError: special character needs to be escaped\\n'\n            '>  at line 3 column 10:\\n'\n            '>  << hello } >>\\n'\n            '>           ^\\n',\n          ),\n        );\n      });\n\n      test('incomplete command', () {\n        expect(\n          () => tokenize(\n            '---\\n---\\n'\n            '<< stop\\n'\n            '===\\n',\n          ),\n          hasSyntaxError(\n            'SyntaxError: missing command close token \">>\"\\n'\n            '>  at line 3 column 8:\\n'\n            '>  << stop\\n'\n            '>         ^\\n',\n          ),\n        );\n      });\n    });\n\n    group('modeMarkup', () {\n      test('tokenize simple markup tag', () {\n        expect(\n          tokenize('---\\n---\\n[wave]\\n===\\n'),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.startMarkupTag,\n            Token.id('wave'),\n            Token.endMarkupTag,\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('tokenize closing markup tag', () {\n        expect(\n          tokenize('---\\n---\\n[/wave]\\n===\\n'),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.startMarkupTag,\n            Token.closeMarkupTag,\n            Token.id('wave'),\n            Token.endMarkupTag,\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('tokenize self-closing markup tag', () {\n        expect(\n          tokenize('---\\n---\\n[wave/]\\n===\\n'),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.startMarkupTag,\n            Token.id('wave'),\n            Token.closeMarkupTag,\n            Token.endMarkupTag,\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('tokenize complex markup tag', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            'One [red shade=12 hex=\"#ff0000\"]color[/red]\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.text('One '),\n            Token.startMarkupTag,\n            Token.id('red'),\n            Token.id('shade'),\n            Token.operatorAssign,\n            Token.number('12'),\n            Token.id('hex'),\n            Token.operatorAssign,\n            Token.string('#ff0000'),\n            Token.endMarkupTag,\n            Token.text('color'),\n            Token.startMarkupTag,\n            Token.closeMarkupTag,\n            Token.id('red'),\n            Token.endMarkupTag,\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('whitespace after self-closing command', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            'Hello [yes/] world!\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.text('Hello '),\n            Token.startMarkupTag,\n            Token.id('yes'),\n            Token.closeMarkupTag,\n            Token.endMarkupTag,\n            Token.text('world!'),\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('unclosed markup tag', () {\n        expect(\n          () => tokenize('---\\n---\\n[blue\\n===\\n'),\n          hasSyntaxError(\n            'SyntaxError: missing markup tag close token \"]\"\\n'\n            '>  at line 3 column 6:\\n'\n            '>  [blue\\n'\n            '>       ^\\n',\n          ),\n        );\n      });\n    });\n\n    group('modeTextEnd', () {\n      test('hashtags in lines', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            'line1 #tag #some:other@tag! // whatever\\n'\n            'line2 { 33 } #here-be-dragons//2\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.text('line1'),\n            Token.hashtag('#tag'),\n            Token.hashtag('#some:other@tag!'),\n            Token.newline,\n            Token.text('line2 '),\n            Token.startExpression,\n            Token.number('33'),\n            Token.endExpression,\n            Token.hashtag('#here-be-dragons'),\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('comments in lines', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            'line1 // whatever\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.text('line1'),\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('commands in lines', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            '-> Sure I am! The boss knows me! <<if \\$reputation > 10>>\\n'\n            '-> Please?\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.arrow,\n            Token.text('Sure I am! The boss knows me!'),\n            Token.startCommand,\n            Token.commandIf,\n            Token.startExpression,\n            Token.variable(r'$reputation'),\n            Token.operatorGreaterThan,\n            Token.number('10'),\n            Token.endExpression,\n            Token.endCommand,\n            Token.newline,\n            Token.arrow,\n            Token.text('Please?'),\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('multiple commands and hashtags', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            '#one <<two>> <<stop>> #four\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.hashtag('#one'),\n            Token.startCommand,\n            Token.command('two'),\n            Token.endCommand,\n            Token.startCommand,\n            Token.commandStop,\n            Token.endCommand,\n            Token.hashtag('#four'),\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n\n      test('text with escaped content', () {\n        expect(\n          tokenize(\n            '---\\n---\\n'\n            'One \\\\{ two\\n'\n            '===\\n',\n          ),\n          const [\n            Token.startHeader,\n            Token.endHeader,\n            Token.startBody,\n            Token.text('One '),\n            Token.text('{'),\n            Token.text(' two'),\n            Token.newline,\n            Token.endBody,\n          ],\n        );\n      });\n    });\n\n    // This group is for testing the error mechanism itself, not any particular\n    // error conditions.\n    group('errors', () {\n      test('long line, error near the start', () {\n        expect(\n          () => tokenize(\n            '---\\n---\\n'\n            '<<set alpha beta gamma delta epsilon ~ zeta eta theta iota '\n            'kappa lambda mu nu xi omicron pi rho sigma tau >>\\n'\n            '===\\n',\n          ),\n          hasSyntaxError(\n            'SyntaxError: invalid token\\n'\n            '>  at line 3 column 38:\\n'\n            '>  <<set alpha beta gamma delta epsilon ~ zeta eta theta iota '\n            'kappa lambda mu...\\n'\n            '>                                       ^\\n',\n          ),\n        );\n      });\n\n      test('long line, error near the end', () {\n        expect(\n          () => tokenize(\n            '---\\n---\\n'\n            '<<set alpha beta gamma delta epsilon zeta eta theta iota kappa '\n            'lambda mu nu xi omicron pi rho @ sigma tau upsilon phi chi>>\\n'\n            '===\\n',\n          ),\n          hasSyntaxError(\n            'SyntaxError: invalid token\\n'\n            '>  at line 3 column 95:\\n'\n            '>  ...theta iota kappa lambda mu nu xi omicron pi rho @ sigma '\n            'tau upsilon phi chi>>\\n'\n            '>                                                     ^\\n',\n          ),\n        );\n      });\n\n      test('long line, error in the middle', () {\n        expect(\n          () => tokenize(\n            '---\\n---\\n'\n            '<<set alpha beta gamma delta epsilon zeta eta theta iota kappa '\n            'lambda ` mu nu xi omicron pi rho sigma tau upsilon phi chi psi '\n            'omega>>\\n'\n            '===\\n',\n          ),\n          hasSyntaxError(\n            'SyntaxError: invalid token\\n'\n            '>  at line 3 column 71:\\n'\n            '>  ...on zeta eta theta iota kappa lambda ` mu nu xi omicron '\n            'pi rho sigma tau...\\n'\n            '>                                         ^\\n',\n          ),\n        );\n      });\n\n      test('error at end of line', () {\n        const text = '---\\n---\\nSome text\\n===\\n';\n        final tokens0 = tokenize(text);\n        expect(tokens0[4], Token.newline);\n\n        final tokens1 = tokenize(text, addErrorTokenAtIndex: 4);\n        final errorToken = tokens1.removeAt(4);\n        expect(tokens1, tokens0);\n        expect(errorToken.type, TokenType.error);\n        expect(\n          errorToken.content,\n          '>  at line 3 column 10:\\n'\n          '>  Some text\\n'\n          '>           ^',\n        );\n      });\n\n      test('error at the end of text', () {\n        const text = '---\\n---\\nSome text\\n===\\n';\n        final tokens0 = tokenize(text);\n        expect(tokens0.length, 6);\n\n        final tokens1 = tokenize(text, addErrorTokenAtIndex: 6);\n        final errorToken = tokens1.removeAt(6);\n        expect(tokens1, tokens0);\n        expect(errorToken.type, TokenType.error);\n        expect(\n          errorToken.content,\n          '>  at line 5 column 1:\\n'\n          '>  \\n'\n          '>  ^',\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/block_test.dart",
    "content": "import 'package:jenny/src/structure/block.dart';\nimport 'package:jenny/src/structure/dialogue_line.dart';\nimport 'package:jenny/src/structure/line_content.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('Block', () {\n    test('empty block', () {\n      const block = Block.empty();\n      expect(block.length, 0);\n      expect(block.isEmpty, true);\n      expect(block.isNotEmpty, false);\n    });\n\n    test('non-empty block', () {\n      final line0 = DialogueLine(content: LineContent(''));\n      final line1 = DialogueLine(content: LineContent('one'));\n      final line2 = DialogueLine(content: LineContent('two'));\n      final block = Block([line0, line1, line2]);\n      expect(block.length, 3);\n      expect(block.isEmpty, false);\n      expect(block.isNotEmpty, true);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/commands/character_command_test.dart",
    "content": "import 'package:jenny/src/parse/token.dart';\nimport 'package:jenny/src/parse/tokenize.dart';\nimport 'package:jenny/src/yarn_project.dart';\nimport 'package:test/test.dart';\n\nimport '../../test_scenario.dart';\nimport '../../utils.dart';\n\nvoid main() {\n  group('CharacterCommand', () {\n    test('tokenize <<character>> command', () {\n      expect(\n        tokenize('<<character \"Agent Smith\" Smith AgSmith>>'),\n        const [\n          Token.startCommand,\n          Token.commandCharacter,\n          Token.startExpression,\n          Token.string('Agent Smith'),\n          Token.id('Smith'),\n          Token.id('AgSmith'),\n          Token.endExpression,\n          Token.endCommand,\n        ],\n      );\n    });\n\n    test('<<character>> command declares new characters', () {\n      final yarn = YarnProject();\n      yarn.parse(\n        '<<character \"Harry Potter\" Harry HP>>\\n'\n        '<<character \"Hermione Granger\" Hermione HG>>\\n'\n        '<<character \"Ron Weasley\" Ron RW>>\\n'\n        '<<character Peeves>>\\n',\n      );\n      expect(yarn.characters.isEmpty, false);\n      expect(yarn.characters.isNotEmpty, true);\n\n      expect(yarn.characters.contains('HP'), true);\n      expect(yarn.characters.contains('HG'), true);\n      expect(yarn.characters.contains('RW'), true);\n      expect(yarn.characters.contains('HW'), false);\n      expect(yarn.characters.contains('Hermione'), true);\n      expect(yarn.characters.contains('Peeves'), true);\n      expect(yarn.characters.contains('Harry'), true);\n      expect(yarn.characters.contains('Ron'), true);\n      expect(yarn.characters.contains('harry'), false);\n\n      final harry = yarn.characters['Harry']!;\n      expect(harry.name, 'Harry Potter');\n      expect(harry.aliases, ['Harry', 'HP']);\n      expect(harry.data, isEmpty);\n      harry.data['age'] = 11;\n      harry.data['affiliation'] = 'Dumbledore';\n      expect(harry.data['age'], 11);\n      expect(harry.data['affiliation'], 'Dumbledore');\n\n      final peeves = yarn.characters['Peeves']!;\n      expect(peeves.name, 'Peeves');\n      expect(peeves.aliases, isEmpty);\n\n      expect(yarn.characters['Hermione'], isNotNull);\n      expect(yarn.characters['Voldemort'], isNull);\n    });\n\n    test('script with strict characters', () async {\n      await testScenario(\n        input: '''\n          <<character Alice>>\n          <<character Cat>>\n          ---\n          title: Start\n          ---\n          Alice: Cheshire Puss, would you tell me which way to go from here?\n          Cat:   That depends a great deal on where you want to get to\n          Alice: I don't much care where --\n          Cat:   Then it doesn't matter which way you go\n          Alice: so long as I get [i]somewhere[/i]\n          Cat:   Oh, you're sure to do that, if you only walk long enough\n          ===\n        ''',\n        testPlan: '''\n          line: Alice: Cheshire Puss, would you tell me which way to go from here?\n          line: Cat: That depends a great deal on where you want to get to\n          line: Alice: I don't much care where --\n          line: Cat: Then it doesn't matter which way you go\n          line: Alice: so long as I get somewhere\n          line: Cat: Oh, you're sure to do that, if you only walk long enough\n        ''',\n      );\n    });\n\n    test('script without character declared', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title: Start\\n'\n            '---\\n'\n            'Alice: Do cats eat bats?\\n'\n            '===\\n',\n          ),\n        hasNameError(\n          'NameError: unknown character \"Alice\"\\n'\n          '>  at line 3 column 1:\\n'\n          '>  Alice: Do cats eat bats?\\n'\n          '>  ^\\n',\n        ),\n      );\n    });\n\n    group('errors', () {\n      test('invalid syntax', () {\n        expect(\n          () => YarnProject()..parse(r'<<character \"Me\" = $foo>>'),\n          hasSyntaxError(\n            'SyntaxError: unexpected token\\n'\n            '>  at line 1 column 18:\\n'\n            '>  <<character \"Me\" = \\$foo>>\\n'\n            '>                   ^\\n',\n          ),\n        );\n      });\n\n      test('no character name or ids', () {\n        expect(\n          () => YarnProject()..parse('<<character>>'),\n          hasSyntaxError(\n            'SyntaxError: at least one character id is required\\n'\n            '>  at line 1 column 12:\\n'\n            '>  <<character>>\\n'\n            '>             ^\\n',\n          ),\n        );\n      });\n\n      test('only character name', () {\n        expect(\n          () => YarnProject()..parse('<<character \"Bozo\">>'),\n          hasSyntaxError(\n            'SyntaxError: at least one character id is required\\n'\n            '>  at line 1 column 19:\\n'\n            '>  <<character \"Bozo\">>\\n'\n            '>                    ^\\n',\n          ),\n        );\n      });\n\n      test('duplicate character id', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              '<<character \"Fancy Bob\" FancyBob bob>>\\n'\n              '<<character \"Lame Bob\" LameBob bob>>\\n',\n            ),\n          hasNameError(\n            'NameError: character \"bob\" was already defined: Character(Fancy '\n            'Bob)\\n'\n            '>  at line 2 column 32:\\n'\n            '>  <<character \"Lame Bob\" LameBob bob>>\\n'\n            '>                                 ^\\n',\n          ),\n        );\n      });\n\n      test('character command inside a node', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              'title: X\\n'\n              '---\\n'\n              '<<character Charlie>>\\n'\n              '===\\n',\n            ),\n          hasSyntaxError(\n            'SyntaxError: <<character>> command cannot be used inside a node\\n'\n            '>  at line 3 column 1:\\n'\n            '>  <<character Charlie>>\\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/commands/declare_command_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:jenny/src/parse/token.dart';\nimport 'package:jenny/src/parse/tokenize.dart';\nimport 'package:jenny/src/structure/commands/declare_command.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:test/test.dart';\n\nimport '../../utils.dart';\n\nvoid main() {\n  group('DeclareCommand', () {\n    test('tokenize declare', () {\n      expect(\n        tokenize(r'<<declare $x = 3>>'),\n        const [\n          Token.startCommand,\n          Token.commandDeclare,\n          Token.startExpression,\n          Token.variable(r'$x'),\n          Token.operatorAssign,\n          Token.number('3'),\n          Token.endExpression,\n          Token.endCommand,\n        ],\n      );\n    });\n\n    test('tokenize declare with as', () {\n      expect(\n        tokenize(r'<<declare $x = true as Bool>>'),\n        const [\n          Token.startCommand,\n          Token.commandDeclare,\n          Token.startExpression,\n          Token.variable(r'$x'),\n          Token.operatorAssign,\n          Token.constTrue,\n          Token.asType,\n          Token.typeBool,\n          Token.endExpression,\n          Token.endCommand,\n        ],\n      );\n    });\n\n    test('declare command', () {\n      const command = DeclareCommand();\n      expect(command.name, 'declare');\n      expect(\n        () => command.execute(\n          DialogueRunner(yarnProject: YarnProject(), dialogueViews: []),\n        ),\n        throwsA(isA<AssertionError>()),\n      );\n    });\n\n    test('declare with value', () {\n      final yarn = YarnProject()..parse(r'<<declare $x = 3>>');\n      expect(yarn.variables.hasVariable(r'$x'), true);\n      expect(yarn.variables.getVariableType(r'$x'), ExpressionType.numeric);\n      expect(yarn.variables.getNumericValue(r'$x'), 3);\n    });\n\n    test('declare with type', () {\n      final yarn = YarnProject()..parse(r'<<declare $y as String>>');\n      expect(yarn.variables.hasVariable(r'$y'), true);\n      expect(yarn.variables.getVariableType(r'$y'), ExpressionType.string);\n      expect(yarn.variables.getStringValue(r'$y'), '');\n    });\n\n    test('declare with value and type', () {\n      final yarn = YarnProject()..parse(r'<<declare $y = \"Flame\" as String>>');\n      expect(yarn.variables.hasVariable(r'$y'), true);\n      expect(yarn.variables.getVariableType(r'$y'), ExpressionType.string);\n      expect(yarn.variables.getStringValue(r'$y'), 'Flame');\n    });\n\n    test('declare multiple variables', () {\n      final yarn = YarnProject()\n        ..parse(\n          '<<declare \\$u as Bool>>\\n'\n          '<<declare \\$v as Number>>\\n'\n          '<<declare \\$w as String>>\\n'\n          '<<declare \\$x = true>>\\n'\n          '<<declare \\$y = 123>>\\n'\n          '<<declare \\$z = \"zzz\">>\\n',\n        );\n      expect(yarn.variables.hasVariable(r'$u'), true);\n      expect(yarn.variables.hasVariable(r'$v'), true);\n      expect(yarn.variables.hasVariable(r'$w'), true);\n      expect(yarn.variables.hasVariable(r'$x'), true);\n      expect(yarn.variables.hasVariable(r'$y'), true);\n      expect(yarn.variables.hasVariable(r'$z'), true);\n      expect(yarn.variables.getVariableType(r'$u'), ExpressionType.boolean);\n      expect(yarn.variables.getVariableType(r'$v'), ExpressionType.numeric);\n      expect(yarn.variables.getVariableType(r'$w'), ExpressionType.string);\n      expect(yarn.variables.getVariableType(r'$x'), ExpressionType.boolean);\n      expect(yarn.variables.getVariableType(r'$y'), ExpressionType.numeric);\n      expect(yarn.variables.getVariableType(r'$z'), ExpressionType.string);\n      expect(yarn.variables.getBooleanValue(r'$u'), false);\n      expect(yarn.variables.getNumericValue(r'$v'), 0);\n      expect(yarn.variables.getStringValue(r'$w'), '');\n      expect(yarn.variables.getBooleanValue(r'$x'), true);\n      expect(yarn.variables.getNumericValue(r'$y'), 123);\n      expect(yarn.variables.getStringValue(r'$z'), 'zzz');\n    });\n\n    test('declare with a comment', () {\n      final yarn = YarnProject()\n        ..parse('<<declare \\$x as String>>  // oh rly?\\n');\n      expect(yarn.variables.hasVariable(r'$x'), true);\n      expect(yarn.variables.getVariableType(r'$x'), ExpressionType.string);\n    });\n\n    group('errors', () {\n      test('missing variable name', () {\n        expect(\n          () => YarnProject()..parse('<<declare foo>>'),\n          hasSyntaxError(\n            'SyntaxError: variable name expected\\n'\n            '>  at line 1 column 11:\\n'\n            '>  <<declare foo>>\\n'\n            '>            ^\\n',\n          ),\n        );\n      });\n\n      test('no assignment', () {\n        expect(\n          () => YarnProject()..parse(r'<<declare $error>>'),\n          hasSyntaxError(\n            'SyntaxError: expected `= value` or `as Type`\\n'\n            '>  at line 1 column 17:\\n'\n            '>  <<declare \\$error>>\\n'\n            '>                  ^\\n',\n          ),\n        );\n      });\n\n      test('variable redeclaration', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              '<<declare \\$error = 0>>\\n'\n              '<<declare \\$error = 1>>\\n',\n            ),\n          hasNameError(\n            'NameError: variable \\$error has already been declared\\n'\n            '>  at line 2 column 11:\\n'\n            '>  <<declare \\$error = 1>>\\n'\n            '>            ^\\n',\n          ),\n        );\n      });\n\n      test('no type keyword after as 1', () {\n        expect(\n          () => YarnProject()..parse('<<declare \\$error as 1>>\\n'),\n          hasSyntaxError(\n            'SyntaxError: a type is expected\\n'\n            '>  at line 1 column 21:\\n'\n            '>  <<declare \\$error as 1>>\\n'\n            '>                      ^\\n',\n          ),\n        );\n      });\n\n      test('no type keyword after as 2', () {\n        expect(\n          () => YarnProject()..parse('<<declare \\$error = 0 as 1>>\\n'),\n          hasSyntaxError(\n            'SyntaxError: a type is expected\\n'\n            '>  at line 1 column 25:\\n'\n            '>  <<declare \\$error = 0 as 1>>\\n'\n            '>                          ^\\n',\n          ),\n        );\n      });\n\n      test('incompatible type declaration', () {\n        expect(\n          () => YarnProject()..parse('<<declare \\$error = 0 as Bool>>\\n'),\n          hasTypeError(\n            'TypeError: the expression evaluates to numeric type\\n'\n            '>  at line 1 column 25:\\n'\n            '>  <<declare \\$error = 0 as Bool>>\\n'\n            '>                          ^\\n',\n          ),\n        );\n      });\n\n      test('declare inside a node', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              'title: error\\n---\\n'\n              '<<declare \\$x = 1>>\\n'\n              '===\\n',\n            ),\n          hasSyntaxError(\n            'SyntaxError: <<declare>> command cannot be used inside a node\\n'\n            '>  at line 3 column 1:\\n'\n            '>  <<declare \\$x = 1>>\\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/commands/if_command_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:jenny/src/structure/block.dart';\nimport 'package:jenny/src/structure/commands/if_command.dart';\nimport 'package:jenny/src/structure/expressions/literal.dart';\nimport 'package:test/test.dart';\n\nimport '../../test_scenario.dart';\nimport '../../utils.dart';\n\nvoid main() {\n  group('IfCommand', () {\n    test('command name', () {\n      const command = IfCommand([IfBlock(constFalse, Block([]))]);\n      expect(command.name, 'if');\n    });\n\n    testScenario(\n      testName: 'IfStatements.yarn',\n      input: '''\n        title: Start\n        ---\n        <<if true>>\n          Player: Hey, Sally. #line:794945\n          Sally: Oh! Hi. #line:2dc39b\n          Sally: You snuck up on me. #line:34de2f\n          Sally: Don't do that. #line:dcc2bc\n        <<else>>\n          Player: Hey. #line:a8e70c\n          Sally: Hi. #line:305cde\n        <<endif>>\n        ===\n      ''',\n      testPlan: '''\n        line: Player: Hey, Sally.\n        line: Sally: Oh! Hi.\n        line: Sally: You snuck up on me.\n        line: Sally: Don't do that.\n      ''',\n    );\n\n    test('non-boolean condition in <<if>>', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '<<if \"true\">>\\n'\n            '    text\\n'\n            '<<endif>>\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: expression in an <<if>> command must be boolean\\n'\n          '>  at line 3 column 6:\\n'\n          '>  <<if \"true\">>\\n'\n          '>       ^\\n',\n        ),\n      );\n    });\n\n    test('<<else>> without an <<if>>', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '<<else>>\\n'\n            '<<endif>>\\n'\n            '===\\n',\n          ),\n        hasSyntaxError(\n          'SyntaxError: this command is only allowed after an <<if>>\\n'\n          '>  at line 3 column 3:\\n'\n          '>  <<else>>\\n'\n          '>    ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/commands/jump_command_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../test_scenario.dart';\nimport '../../utils.dart';\n\nvoid main() {\n  group('JumpCommand', () {\n    test('<<jump>> command', () {\n      final yarn = YarnProject()\n        ..variables.setVariable(r'$target', 'DOWN')\n        ..parse(\n          'title:A\\n---\\n'\n          '<<jump UP>>\\n'\n          '<<jump {\\$target}>>\\n'\n          '===\\n',\n        );\n      final node = yarn.nodes['A']!;\n      expect(node.lines.length, 2);\n      expect(node.lines[0], isA<JumpCommand>());\n      expect(node.lines[1], isA<JumpCommand>());\n      expect((node.lines[0] as JumpCommand).target.value, 'UP');\n      expect((node.lines[1] as JumpCommand).target.value, 'DOWN');\n    });\n\n    test('Jumps.yarn', () async {\n      await testScenario(\n        input: r'''\n          title: Start\n          ---\n          Start\n          // Jump to a node name\n          <<jump NodeNameDestination>>\n          Error! This line should not be seen.\n          ===\n          title: NodeNameDestination\n          ---\n          NodeNameDestination\n          // Jump to a node based on a constant expression\n          <<jump {\"NodeNameConstant\" + \"Expression\"}>>\n          ===\n          title: NodeNameConstantExpression\n          ---\n          NodeNameExpression\n          // Jump to a node based on a non-constant expression\n          <<local $myNodeName = \"NodeNameVariableExpression\">>\n          <<jump {$myNodeName}>>\n          ===\n          title: NodeNameVariableExpression\n          ---\n          NodeNameVariableExpression\n          ===\n        ''',\n        testPlan: '''\n          line: Start\n          line: NodeNameDestination\n          line: NodeNameExpression\n          line: NodeNameVariableExpression\n        ''',\n      );\n    });\n\n    test('<<jump>> at root level', () {\n      expect(\n        () => YarnProject()..parse('<<jump Start>>\\n'),\n        hasTypeError(\n          'TypeError: command <<jump>> is only allowed inside nodes\\n'\n          '>  at line 1 column 1:\\n'\n          '>  <<jump Start>>\\n'\n          '>  ^\\n',\n        ),\n      );\n    });\n\n    test('<<jump>> invalid syntax', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '<<jump 1>>\\n'\n            '===\\n',\n          ),\n        hasSyntaxError(\n          'SyntaxError: an ID or an expression in curly braces expected\\n'\n          '>  at line 3 column 8:\\n'\n          '>  <<jump 1>>\\n'\n          '>         ^\\n',\n        ),\n      );\n    });\n\n    test('<<jump>> with non-string-valued argument', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '<<jump {1.5}>>\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: target of <<jump>> must be a string expression\\n'\n          '>  at line 3 column 9:\\n'\n          '>  <<jump {1.5}>>\\n'\n          '>          ^\\n',\n        ),\n      );\n    });\n\n    test('<<jump>> with unknown destination', () {\n      final yarn = YarnProject()\n        ..parse(\n          'title:A\\n'\n          '---\\n'\n          '<<jump Up>>\\n'\n          '===\\n',\n        );\n      expect(\n        () => DialogueRunner(\n          yarnProject: yarn,\n          dialogueViews: [],\n        ).startDialogue('A'),\n        hasNameError('NameError: Node \"Up\" could not be found'),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/commands/local_command_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:jenny/src/parse/token.dart';\nimport 'package:jenny/src/parse/tokenize.dart';\nimport 'package:jenny/src/structure/commands/local_command.dart';\nimport 'package:test/test.dart';\n\nimport '../../test_scenario.dart';\nimport '../../utils.dart';\n\nvoid main() {\n  group('CommandLocal', () {\n    test('tokenize', () {\n      expect(\n        tokenize(\n          '---\\n---\\n'\n          '<<local \\$x = 3>>\\n'\n          '===\\n',\n        ),\n        const [\n          Token.startHeader,\n          Token.endHeader,\n          Token.startBody,\n          Token.startCommand,\n          Token.commandLocal,\n          Token.startExpression,\n          Token.variable(r'$x'),\n          Token.operatorAssign,\n          Token.number('3'),\n          Token.endExpression,\n          Token.endCommand,\n          Token.newline,\n          Token.endBody,\n        ],\n      );\n    });\n\n    test('parse', () {\n      final yarn = YarnProject()\n        ..parse(\n          'title: Start\\n'\n          '---\\n'\n          '<<local \\$x = 3>>\\n'\n          '===\\n',\n        );\n      final node = yarn.nodes['Start']!;\n      expect(node.lines.length, 1);\n      expect(node.lines[0], isA<LocalCommand>());\n      expect(node.variables, isNotNull);\n\n      final command = node.lines[0] as LocalCommand;\n      expect(command.name, 'local');\n      expect(command.variable, r'$x');\n      expect(command.storage == yarn.variables, false);\n      expect(command.storage == node.variables, true);\n\n      expect(yarn.variables.hasVariable(r'$x'), false);\n      expect(node.variables!.hasVariable(r'$x'), true);\n    });\n\n    testScenario(\n      testName: 'simple local command',\n      input: r'''\n        title: Start\n        ---\n        <<local $i = 3>>\n        Value of $i = {$i}\n        <<set $i += 10>>\n        Value of $i = {$i}\n        ===\n      ''',\n      testPlan: r'''\n        line: Value of $i = 3\n        line: Value of $i = 13\n      ''',\n    );\n\n    testScenario(\n      testName: 'local command executed multiple times',\n      input: r'''\n        <<declare $counter = 0>>\n        ---\n        title: Start\n        ---\n        <<local $i = 5 - $counter>>\n        Countdown ...{$i}\n        \n        <<set $counter += 1>>\n        <<if $counter < 5>>\n          <<jump Start>>\n        <<else>>\n          GO!\n        <<endif>>\n        ===\n      ''',\n      testPlan: '''\n        line: Countdown ...5\n        line: Countdown ...4\n        line: Countdown ...3\n        line: Countdown ...2\n        line: Countdown ...1\n        line: GO!\n      ''',\n    );\n\n    testScenario(\n      testName: 'local command visibility',\n      input: r'''\n        title: Start\n        ---\n        First line\n        <<if false>>\n          <<local $x = 137>>\n        <<else>>\n          Value of x is {$x}\n          <<set $x += 5>>\n        <<endif>>\n        Now x = {$x}\n        ===\n      ''',\n      testPlan: '''\n        line: First line\n        line: Value of x is 0\n        line: Now x = 5\n      ''',\n    );\n\n    group('errors', () {\n      test('declare local variable twice', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              'title:A\\n---\\n'\n              '<<local \\$x = 1>>\\n'\n              '<<local \\$x = 2>>\\n'\n              '===\\n',\n            ),\n          hasNameError(\n            'NameError: redeclaration of local variable \\$x\\n'\n            '>  at line 4 column 9:\\n'\n            '>  <<local \\$x = 2>>\\n'\n            '>          ^\\n',\n          ),\n        );\n      });\n\n      test('global and local with the same name', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              '<<declare \\$x as Number>>\\n'\n              'title:A\\n---\\n'\n              '<<local \\$x = 1>>\\n'\n              '===\\n',\n            ),\n          hasNameError(\n            r'NameError: variable $x shadows a global variable with the same '\n            'name\\n'\n            '>  at line 4 column 9:\\n'\n            '>  <<local \\$x = 1>>\\n'\n            '>          ^\\n',\n          ),\n        );\n      });\n\n      test('... as Type', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              'title:A\\n---\\n'\n              '<<local \\$x as String>>\\n'\n              '===\\n',\n            ),\n          hasSyntaxError(\n            'SyntaxError: assignment operator is expected\\n'\n            '>  at line 3 column 12:\\n'\n            '>  <<local \\$x as String>>\\n'\n            '>             ^\\n',\n          ),\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/commands/set_command_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:jenny/src/parse/token.dart';\nimport 'package:jenny/src/parse/tokenize.dart';\nimport 'package:test/test.dart';\n\nimport '../../test_scenario.dart';\nimport '../../utils.dart';\n\nvoid main() {\n  group('SetCommand', () {\n    test('tokenize <<set>>', () {\n      expect(\n        tokenize(r'<<set $x = 3>>'),\n        const [\n          Token.startCommand,\n          Token.commandSet,\n          Token.startExpression,\n          Token.variable(r'$x'),\n          Token.operatorAssign,\n          Token.number('3'),\n          Token.endExpression,\n          Token.endCommand,\n        ],\n      );\n    });\n\n    test('AnalysisTest.plan', () async {\n      await testScenario(\n        input: r'''\n          <<declare $foo = 0>> // used\n          <<declare $bar = 0>> // written to but never read\n          ---\n          title: Start\n          ---\n          <<set $foo to 1>>\n          <<set $bar to $foo>>\n          {$foo} {$bar}\n          ===\n        ''',\n        testPlan: 'line: 1 1',\n      );\n    });\n\n    test('Basic.plan', () async {\n      // cSpell:ignore noooo haha aargh\n      await testScenario(\n        input: r'''\n          <<declare $foo as Number>>\n          ---\n          title: Start\n          ---\n          whoa what here's some text\n          <<set $foo to (1+3*3/9)-1>>\n\n          <<if $foo is 1>> // testing a comment\n              this should appear :)\n              <<if 1 is 1>>\n                  NESTED IF BLOCK WHA-A-AT\n                  <<set $foo += 47 + 6>>\n              <<endif>>\n          <<else>>\n              oh noooo it didn't work :(\n          <<endif>>\n\n          <<if $foo is 54>>\n              haha nice now 'set' works even when deeply nested\n          <<else>>\n              aargh >:(\n          <<endif>>\n          ===\n        ''',\n        testPlan: '''\n          line: whoa what here's some text\n          line: this should appear :)\n          line: NESTED IF BLOCK WHA-A-AT\n          line: haha nice now 'set' works even when deeply nested\n        ''',\n      );\n    });\n\n    test('modifying assignments', () async {\n      await testScenario(\n        input: r'''\n          <<declare $x = 0>>\n          <<declare $s = 'Hello'>>\n          ---\n          title: Start\n          ---\n          <<set $x += 12>>\n          $x = {$x}\n          <<set $x *= 4>>\n          $x = {$x}\n          <<set $x /= 6>>\n          $x = {$x}\n          <<set $x %= 5>>\n          $x = {$x}\n          <<set $x -= 2>>\n          $x = {$x}\n          <<set $s += \" world\">>\n          $s = '{$s}'\n          ===\n        ''',\n        testPlan: r'''\n          line: $x = 12\n          line: $x = 48\n          line: $x = 8\n          line: $x = 3\n          line: $x = 1\n          line: $s = 'Hello world'\n        ''',\n      );\n    });\n\n    group('errors', () {\n      test('no variable', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              'title: W\\n'\n              '---\\n'\n              '<<set>>\\n'\n              '===\\n',\n            ),\n          hasSyntaxError(\n            'SyntaxError: variable expected\\n'\n            '>  at line 3 column 6:\\n'\n            '>  <<set>>\\n'\n            '>       ^\\n',\n          ),\n        );\n      });\n\n      test('undeclared variable', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              'title:E\\n'\n              '---\\n'\n              '<<set \\$foo = 123>>\\n'\n              '===\\n',\n            ),\n          hasNameError(\n            'NameError: variable \\$foo has not been declared\\n'\n            '>  at line 3 column 7:\\n'\n            '>  <<set \\$foo = 123>>\\n'\n            '>        ^\\n',\n          ),\n        );\n      });\n\n      test('no assignment', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              '<<declare \\$x as Number>>\\n'\n              'title: X\\n'\n              '---\\n'\n              '<<set \\$x as String>>\\n'\n              '===\\n',\n            ),\n          hasSyntaxError(\n            'SyntaxError: an assignment operator is expected\\n'\n            '>  at line 4 column 10:\\n'\n            '>  <<set \\$x as String>>\\n'\n            '>           ^\\n',\n          ),\n        );\n      });\n\n      test('wrong type in assignment', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              '<<declare \\$x as String>>\\n'\n              'title: A\\n'\n              '---\\n'\n              '<<set \\$x = 12>>\\n'\n              '===\\n',\n            ),\n          hasTypeError(\n            r'TypeError: variable $x of type string cannot be assigned a '\n            'value of type numeric\\n'\n            '>  at line 4 column 12:\\n'\n            '>  <<set \\$x = 12>>\\n'\n            '>             ^\\n',\n          ),\n        );\n      });\n\n      test('<<set>> at root level', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              '<<declare \\$x as Number>>\\n'\n              '<<set \\$x = 1>>\\n',\n            ),\n          hasTypeError(\n            'TypeError: command <<set>> is only allowed inside nodes\\n'\n            '>  at line 2 column 1:\\n'\n            '>  <<set \\$x = 1>>\\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/commands/stop_command_test.dart",
    "content": "import 'package:jenny/src/parse/token.dart';\nimport 'package:jenny/src/parse/tokenize.dart';\nimport 'package:jenny/src/structure/commands/stop_command.dart';\nimport 'package:jenny/src/yarn_project.dart';\nimport 'package:test/test.dart';\n\nimport '../../test_scenario.dart';\nimport '../../utils.dart';\n\nvoid main() {\n  group('StopCommand', () {\n    test('tokenize <<stop>>', () {\n      expect(\n        tokenize('<<stop>>'),\n        const [\n          Token.startCommand,\n          Token.commandStop,\n          Token.endCommand,\n        ],\n      );\n    });\n\n    test('command name', () {\n      const command = StopCommand();\n      expect(command.name, 'stop');\n    });\n\n    test('normal command <<stop>>', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          before stop\n          <<stop>>\n          after stop\n          ===\n        ''',\n        testPlan: '''\n          line: before stop\n        ''',\n      );\n    });\n\n    test('invalid syntax for <<stop>>', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '<<stop 5>>\\n'\n            '===\\n',\n          ),\n        hasSyntaxError(\n          'SyntaxError: invalid token\\n'\n          '>  at line 3 column 8:\\n'\n          '>  <<stop 5>>\\n'\n          '>         ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/commands/user_defined_command_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:jenny/src/parse/token.dart';\nimport 'package:jenny/src/parse/tokenize.dart';\nimport 'package:test/test.dart';\n\nimport '../../test_scenario.dart';\nimport '../../utils.dart';\n\nvoid main() {\n  group('UserDefinedCommand', () {\n    test('tokenization', () {\n      expect(\n        tokenize(\n          '---\\n---\\n'\n          '<<hello world {\\$exclamation}>>\\n'\n          '===',\n        ),\n        const [\n          Token.startHeader,\n          Token.endHeader,\n          Token.startBody,\n          Token.startCommand,\n          Token.command('hello'),\n          Token.text('world '),\n          Token.startExpression,\n          Token.variable(r'$exclamation'),\n          Token.endExpression,\n          Token.endCommand,\n          Token.newline,\n          Token.endBody,\n        ],\n      );\n    });\n\n    test('parse simple dialogue command', () {\n      final project = YarnProject()\n        ..commands.addOrphanedCommand('hello')\n        ..parse(\n          'title: start\\n---\\n'\n          '<<hello world {\"A\" + \"B\"}>>\\n'\n          '===',\n        );\n      expect(project.nodes['start']!.lines.length, 1);\n      expect(project.nodes['start']!.lines[0], isA<UserDefinedCommand>());\n      final cmd = project.nodes['start']!.lines[0] as UserDefinedCommand;\n      expect(cmd.name, 'hello');\n      project.commands.runCommand(cmd);\n      expect(cmd.argumentString, 'world AB');\n    });\n\n    test('execute a live command', () {\n      var x = 0;\n      var y = '';\n      final project = YarnProject()\n        ..commands.addCommand2('hello', (int a, String b) {\n          x = a;\n          y = b;\n        })\n        ..parse(\n          'title: start\\n---\\n'\n          '<<hello 3 world>>\\n'\n          '===',\n        );\n      final runner = DialogueRunner(yarnProject: project, dialogueViews: []);\n      expect(project.nodes['start']!.lines.length, 1);\n      expect(project.nodes['start']!.lines[0], isA<UserDefinedCommand>());\n      final cmd = project.nodes['start']!.lines[0] as UserDefinedCommand;\n      expect(cmd.name, 'hello');\n      cmd.execute(runner);\n      expect(x, 3);\n      expect(y, 'world');\n    });\n\n    test('undeclared user-defined command', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n<<jenny>>\\n===\\n'),\n        hasNameError(\n          'NameError: Unknown user-defined command <<jenny>>\\n'\n          '>  at line 3 column 3:\\n'\n          '>  <<jenny>>\\n'\n          '>    ^\\n',\n        ),\n      );\n    });\n\n    test('markup within user-defined command', () {\n      expect(\n        () => YarnProject()\n          ..commands.addOrphanedCommand('hello')\n          ..parse(\n            'title:A\\n---\\n'\n            '<<hello Big [bad/] Wolf>>\\n'\n            '===\\n',\n          ),\n        hasSyntaxError(\n          'SyntaxError: invalid token\\n'\n          '>  at line 3 column 13:\\n'\n          '>  <<hello Big [bad/] Wolf>>\\n'\n          '>              ^\\n',\n        ),\n      );\n    });\n\n    test('evaluate a command multiple times', () async {\n      var counter = 0;\n      await testScenario(\n        yarn: YarnProject()..functions.addFunction0('next', () => counter += 1),\n        input: r'''\n          <<declare $i = 0>> \n          title: Start\n          ---\n          <<print {$i} {next()}>>\n          <<set $i += 1>>\n          <<if $i < 5>>\n            <<jump Start>>\n          <<endif>>\n          ===\n        ''',\n        testPlan: '''\n          command: print 0 1\n          command: print 1 2\n          command: print 2 3\n          command: print 3 4\n          command: print 4 5\n        ''',\n        commands: ['print'],\n      );\n    });\n\n    test('access command arguments', () async {\n      var fnCounter = 0;\n      final yarn = YarnProject()\n        ..commands.addCommand3('xyz', (String p0, String p1, String p2) => null)\n        ..functions.addFunction0('fn', () => fnCounter += 1)\n        ..parse(\n          dedent('''\n            title: Start\n            ---\n            <<xyz a b {fn()}>>\n            ===\n            '''),\n        );\n      final view1 = _CommandDialogueView();\n      final view2 = _CommandDialogueView();\n      final dialogueRunner = DialogueRunner(\n        yarnProject: yarn,\n        dialogueViews: [view1, view2],\n      );\n\n      await dialogueRunner.startDialogue('Start');\n      expect(fnCounter, 1);\n      expect(view1.numCalled, 1);\n      expect(view1.argumentString, 'a b 1');\n      expect(view1.arguments, ['a', 'b', '1']);\n      expect(view2.numCalled, 1);\n      expect(view2.argumentString, 'a b 1');\n      expect(view2.arguments, ['a', 'b', '1']);\n\n      await dialogueRunner.startDialogue('Start');\n      expect(fnCounter, 2);\n      expect(view2.numCalled, 2);\n      expect(view2.argumentString, 'a b 2');\n      expect(view2.arguments, ['a', 'b', '2']);\n    });\n\n    testScenario(\n      testName: 'Commands.yarn',\n      input: '''\n        title: Start\n        ---\n        // Testing commands\n        <<flip Harley3 +1>>\n\n        // Commands that begin with keywords\n        <<toggle>>\n        <<settings>>\n        <<iffy>>\n        <<nullify>>\n        <<orion>>\n        <<andromeda>>\n        <<note>>\n        <<isActive>>\n\n        // Commands with a single character\n        <<p>>\n\n        // Commands with colons\n        <<hide Collision:GermOnPorch>>\n        ===\n      ''',\n      testPlan: '''\n        command: flip Harley3 +1\n        command: toggle\n        command: settings\n        command: iffy\n        command: nullify\n        command: orion\n        command: andromeda\n        command: note\n        command: isActive\n        command: p\n        command: hide Collision:GermOnPorch\n      ''',\n      commands: [\n        'flip',\n        'toggle',\n        'settings',\n        'iffy',\n        'nullify',\n        'orion',\n        'andromeda',\n        'note',\n        'isActive',\n        'p',\n        'hide',\n      ],\n    );\n  });\n}\n\nclass _CommandDialogueView extends DialogueView {\n  int numCalled = 0;\n  String argumentString = '';\n  List<dynamic> arguments = <int>[];\n\n  @override\n  void onCommand(UserDefinedCommand command) {\n    numCalled += 1;\n    arguments = command.arguments!;\n    argumentString = command.argumentString;\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/commands/visit_command_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:jenny/src/parse/token.dart';\nimport 'package:jenny/src/parse/tokenize.dart';\nimport 'package:test/test.dart';\n\nimport '../../test_scenario.dart';\nimport '../../utils.dart';\n\nvoid main() {\n  group('VisitCommand', () {\n    test('tokenize bare-word node target', () {\n      expect(\n        tokenize('<<visit WhiteHouse>>'),\n        const [\n          Token.startCommand,\n          Token.commandVisit,\n          Token.id('WhiteHouse'),\n          Token.endCommand,\n        ],\n      );\n    });\n\n    test('tokenize expression node target', () {\n      expect(\n        tokenize(r'<<visit {$destination}>>'),\n        const [\n          Token.startCommand,\n          Token.commandVisit,\n          Token.startExpression,\n          Token.variable(r'$destination'),\n          Token.endExpression,\n          Token.endCommand,\n        ],\n      );\n    });\n\n    test('<<visit>> command parsing', () {\n      final yarn = YarnProject()\n        ..variables.setVariable(r'$target', 'Y')\n        ..parse(\n          'title:A\\n---\\n'\n          '<<visit X>>\\n'\n          '<<visit {\\$target}>>\\n'\n          '===\\n',\n        );\n      final node = yarn.nodes['A']!;\n      expect(node.lines.length, 2);\n      expect(node.lines[0], isA<VisitCommand>());\n      expect(node.lines[1], isA<VisitCommand>());\n      expect((node.lines[0] as VisitCommand).target.value, 'X');\n      expect((node.lines[1] as VisitCommand).target.value, 'Y');\n    });\n\n    test('visiting another node', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          First line\n          <<visit Second>>\n          Second line\n          ===\n\n          title: Second\n          ---\n          Just visiting!\n          ===\n        ''',\n        testPlan: '''\n          line: First line\n          line: Just visiting!\n          line: Second line\n        ''',\n      );\n    });\n\n    test('nested visits', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          Alpha\n          <<visit V1>>\n          Beta\n          <<visit V2>>\n          Gamma\n          ===\n\n          title: V1\n          ---\n          Delta\n          <<visit V2>>\n          Eta\n          ===\n\n          title: V2\n          ---\n          <<visit V3>>\n          Omega\n          ===\n\n          title: V3\n          ---\n          Rho\n          ===\n        ''',\n        testPlan: '''\n          line: Alpha\n          line: Delta\n          line: Rho\n          line: Omega\n          line: Eta\n          line: Beta\n          line: Rho\n          line: Omega\n          line: Gamma\n        ''',\n      );\n    });\n\n    test('visit node from expression', () async {\n      await testScenario(\n        input: r'''\n          title: Start\n          ---\n          <<local $index = 2>>\n          Line before\n          <<visit {\"Destination\" + \"_\" + string($index)}>>\n          Line after\n          ===\n          ---\n          title: Destination_2\n          ---\n          Here\n          ===\n        ''',\n        testPlan: '''\n          line: Line before\n          line: Here\n          line: Line after\n        ''',\n      );\n    });\n\n    test('visit node with jumps', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          First line of <Start> node\n          <<visit Another>>\n          And we're back in the <Start> node\n          ===\n          --------------\n          title: Another\n          --------------\n          Inside <Another> node\n          <<jump Somewhere>>\n          This line shouldn't be seen...\n          ===\n          ----------------\n          title: Somewhere\n          ----------------\n          Reached <Somewhere> node\n          <<stop>>\n          ERROR!\n          ===\n        ''',\n        testPlan: '''\n          line: First line of <Start> node\n          line: Inside <Another> node\n          line: Reached <Somewhere> node\n          line: And we're back in the <Start> node\n        ''',\n      );\n    });\n\n    group('errors', () {\n      test('<<visit>> at root level', () {\n        expect(\n          () => YarnProject()..parse('<<visit Start>>\\n'),\n          hasTypeError(\n            'TypeError: command <<visit>> is only allowed inside nodes\\n'\n            '>  at line 1 column 1:\\n'\n            '>  <<visit Start>>\\n'\n            '>  ^\\n',\n          ),\n        );\n      });\n\n      test('<<visit>> invalid syntax', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              'title:A\\n---\\n'\n              '<<visit \"Target_\\$index\">>\\n'\n              '===\\n',\n            ),\n          hasSyntaxError(\n            'SyntaxError: an ID or an expression in curly braces expected\\n'\n            '>  at line 3 column 9:\\n'\n            '>  <<visit \"Target_\\$index\">>\\n'\n            '>          ^\\n',\n          ),\n        );\n      });\n\n      test('<<visit>> with unknown destination', () {\n        final yarn = YarnProject()\n          ..parse(\n            'title: A\\n'\n            '---\\n'\n            '<<visit Somewhere>>\\n'\n            '===\\n',\n          );\n        expect(\n          () => DialogueRunner(\n            yarnProject: yarn,\n            dialogueViews: [],\n          ).startDialogue('A'),\n          hasNameError('NameError: Node \"Somewhere\" could not be found'),\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/commands/wait_command_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:jenny/src/parse/token.dart';\nimport 'package:jenny/src/parse/tokenize.dart';\nimport 'package:jenny/src/structure/commands/wait_command.dart';\nimport 'package:jenny/src/structure/expressions/literal.dart';\nimport 'package:test/test.dart';\n\nimport '../../test_scenario.dart';\nimport '../../utils.dart';\n\nvoid main() {\n  group('WaitCommand', () {\n    test('tokenize <<wait>>', () {\n      expect(\n        tokenize('<<wait 3>>'),\n        const [\n          Token.startCommand,\n          Token.commandWait,\n          Token.startExpression,\n          Token.number('3'),\n          Token.endExpression,\n          Token.endCommand,\n        ],\n      );\n    });\n\n    test('normal command <<wait>>', () async {\n      int getTime() => DateTime.now().millisecondsSinceEpoch;\n      late int t0;\n      late int t1;\n      await testScenario(\n        yarn: YarnProject()\n          ..commands.addCommand0('startTimer', () => t0 = getTime())\n          ..commands.addCommand0('finishTimer', () => t1 = getTime()),\n        input: r'''\n          title: Start\n          ---\n          <<local $duration = 1.0>>\n          before wait\n          <<startTimer>>\n          <<wait $duration>>\n          <<finishTimer>>\n          after wait\n          ===\n        ''',\n        testPlan: '''\n          line: before wait\n          command: startTimer\n          command: finishTimer\n          line: after wait\n        ''',\n      );\n      final elapsedTimeMs = t1 - t0;\n      expect(elapsedTimeMs, greaterThanOrEqualTo(1000));\n      // Locally, the measured timer is around 1015ms. However, when running in\n      // the server test pipeline, the timer is significantly higher for some\n      // reason, around 1400ms.\n      // expect(elapsedTimeMs, lessThan(1100));\n    });\n\n    test('wrong argument type', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '<<wait \"two seconds\">>\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: <<wait>> command expects a numeric argument\\n'\n          '>  at line 3 column 8:\\n'\n          '>  <<wait \"two seconds\">>\\n'\n          '>         ^\\n',\n        ),\n      );\n    });\n\n    test('wrong command syntax', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '<<wait 2 seconds>>\\n'\n            '===\\n',\n          ),\n        hasSyntaxError(\n          'SyntaxError: unexpected token\\n'\n          '>  at line 3 column 10:\\n'\n          '>  <<wait 2 seconds>>\\n'\n          '>           ^\\n',\n        ),\n      );\n    });\n\n    test('negative duration', () {\n      const command = WaitCommand(NumLiteral(-1.0));\n      expect(command.name, 'wait');\n      expect(\n        () => command.execute(\n          DialogueRunner(yarnProject: YarnProject(), dialogueViews: []),\n        ),\n        hasDialogueError('<<wait>> command with negative duration: -1.0'),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/dialogue_choice_test.dart",
    "content": "import 'package:jenny/src/structure/dialogue_choice.dart';\nimport 'package:test/test.dart';\n\nimport '../test_scenario.dart';\n\nvoid main() {\n  group('DialogueChoice', () {\n    test('.kind', () {\n      const choiceSet = DialogueChoice([]);\n      expect(choiceSet.options.isEmpty, true);\n    });\n\n    test('Options.yarn', () {\n      testScenario(\n        input: '''\n          title: A\n          ---\n          -> Go to B\n              <<jump B>>\n          -> Go to C\n              <<jump C>>\n          ===\n          title: B\n          ---\n          Node B\n          ===\n          title: C\n          ---\n          Node C\n          ===\n        ''',\n        testPlan: '''\n          run: A\n          option: Go to B\n          option: Go to C\n          select: 2\n          line: Node C\n        ''',\n      );\n    });\n\n    test('SkippedOptions.yarn', () {\n      testScenario(\n        input: '''\n          title: Start\n          ---\n          // These options exist in the node, but are never actually added to \n          // the set of options at runtime.\n          <<if false>>\n              -> Shh...\n              -> Ugh fine sure\n          <<endif>>\n          ===\n        ''',\n        testPlan: '',\n      );\n    });\n\n    test('options with dynamic text', () {\n      testScenario(\n        input: r'''\n          <<declare $money = 100>>\n          <<declare $player = \"Steve\">>\n          ------------\n          title: Start\n          ------------\n          -> Hi, My name is [bold]{$player}[/bold]\n          -> I can give you only {$money / 2} coins -- that's all I have\n          \\-> Not an option!\n          ===\n        ''',\n        testPlan: '''\n          option: Hi, My name is Steve\n          option: I can give you only 50 coins -- that's all I have\n          select: 1\n          line: -> Not an option!\n        ''',\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/dialogue_line_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:jenny/src/structure/line_content.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('DialogueLine', () {\n    test('empty line', () {\n      final line = DialogueLine(content: LineContent(''));\n      expect(line.character, isNull);\n      expect(line.tags, isEmpty);\n      expect(line.attributes, isEmpty);\n      expect(line.text, '');\n      expect(line.isConst, true);\n      expect('$line', 'DialogueLine()');\n    });\n\n    test('line with meta information', () {\n      final line = DialogueLine(\n        character: Character('Bob'),\n        content: LineContent('Hello!'),\n        tags: ['#red', '#fast'],\n      );\n      expect(line.text, 'Hello!');\n      expect(line.character!.name, 'Bob');\n      expect(line.tags, ['#red', '#fast']);\n      expect('$line', 'DialogueLine(Bob: Hello!)');\n    });\n\n    test('line with markup', () {\n      final line = DialogueLine(\n        content: LineContent(\n          'once upon a time',\n          null,\n          [MarkupAttribute('i', 0, 4), MarkupAttribute('b', 12, 16)],\n        ),\n      );\n      expect(line.text, 'once upon a time');\n      expect(line.isConst, true);\n      expect(line.tags, isEmpty);\n      expect(line.attributes[0].name, 'i');\n      expect(line.attributes[1].name, 'b');\n    });\n\n    test('line with inline expressions', () {\n      final yarn = YarnProject()\n        ..variables.setVariable(r'$value', 'ok')\n        ..parse(\n          'title: A\\n'\n          '---\\n'\n          'Test line: {\\$value}\\n'\n          '===\\n',\n        );\n      final line = yarn.nodes['A']!.lines.first as DialogueLine;\n      expect('$line', 'DialogueLine(<unevaluated>)');\n      line.evaluate();\n      expect('$line', 'DialogueLine(Test line: ok)');\n    });\n\n    test('dynamic markup attributes', () {\n      final yarn = YarnProject()\n        ..variables.setVariable(r'$index', 1)\n        ..parse(\n          'title: A\\n---\\n'\n          'X [box index=\\$index /] Y Z\\n'\n          '===\\n',\n        );\n      final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n\n      line.evaluate();\n      expect(line.text, 'X Y Z');\n      expect(line.attributes[0].name, 'box');\n      expect(line.attributes[0].parameters['index'], 1);\n\n      yarn.variables.setVariable(r'$index', 42);\n      line.evaluate();\n      expect(line.text, 'X Y Z');\n      expect(line.attributes[0].name, 'box');\n      expect(line.attributes[0].parameters['index'], 42);\n    });\n\n    test('space after markup attribute', () {\n      final yarn = YarnProject()\n        ..parse(\n          'title: A\\n---\\n'\n          'The price is 243[gp/] per item\\n'\n          '[test attr=0/] ?\\n'\n          '===\\n',\n        );\n      final line1 = yarn.nodes['A']!.lines[0] as DialogueLine;\n      line1.evaluate();\n      expect(line1.text, 'The price is 243 per item');\n      expect(line1.attributes[0].name, 'gp');\n      expect(line1.attributes[0].length, 0);\n      expect(line1.attributes[0].start, 'The price is 243'.length);\n\n      final line2 = yarn.nodes['A']!.lines[1] as DialogueLine;\n      line2.evaluate();\n      expect(line2.text, ' ?');\n      expect(line2.attributes[0].name, 'test');\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/dialogue_option_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:jenny/src/structure/expressions/literal.dart';\nimport 'package:jenny/src/structure/line_content.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('DialogueOption', () {\n    test('empty option', () {\n      final option = DialogueOption(content: LineContent(''));\n      option.evaluate();\n      expect(option.character, isNull);\n      expect(option.tags, isEmpty);\n      expect(option.attributes, isEmpty);\n      expect(option.text, '');\n      expect(option.isConst, true);\n      expect(option.isAvailable, true);\n      expect(option.isDisabled, false);\n      expect('$option', 'Option()');\n    });\n\n    test('simple option', () {\n      final option = DialogueOption(\n        content: LineContent('me'),\n        character: Character('Rook'),\n        condition: constFalse,\n      );\n      option.evaluate();\n      expect(option.character!.name, 'Rook');\n      expect(option.tags, isEmpty);\n      expect(option.attributes, isEmpty);\n      expect(option.text, 'me');\n      expect(option.isAvailable, false);\n      expect(option.isDisabled, true);\n      expect('$option', 'Option(Rook: me #disabled)');\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/arithmetic_test.dart",
    "content": "import 'package:test/test.dart';\n\nimport '../../test_scenario.dart';\n\nvoid main() {\n  group('arithmetic operations', () {\n    testScenario(\n      testName: 'Expressions.yarn',\n      input: r'''\n        <<declare $int = 0>>\n        <<declare $bool = true>>\n        <<declare $math = 0>>\n        ---\n        title: Start\n        ---\n        // Expression testing\n        <<set $int to 1>>\n        1. {$int == 1}\n        2. {$int != 2}\n\n        // Test unary operators\n        3. {!$bool == false}\n        4. {-$int == -1}\n        5. {-$int == 0 - 1}\n\n        // Test more complex expressions\n        <<set $math = 5 * 2 - 2 * -1 >>\n        6. {$math is 12}\n\n        // Test % operator\n        <<set $math = 12 >>\n        <<set $math = $math % 5 >>\n        7. {$math is 2}\n\n        // Test floating point math\n        8. {1 / 2 == 0.5}\n        9. {0.1 + 0.1 == 0.2}\n        ===\n      ''',\n      testPlan: '''\n        line: 1. true\n        line: 2. true\n        line: 3. true\n        line: 4. true\n        line: 5. true\n        line: 6. true\n        line: 7. true\n        line: 8. true\n        line: 9. true\n      ''',\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/bool_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('BoolFn', () {\n    test('bool() normal case', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          {bool(0)} {bool(1)} {bool(0.005)}\n          {bool(false)} {bool(true)}\n          {bool(\"true\")} {bool(\"yes\")} {bool(\"  true  \")}\n          {bool(\"false\")} {bool(\"off\")} {bool(\"no\")} {bool(\"F\")}\n          ===\n        ''',\n        testPlan: '''\n          line: false true true\n          line: false true\n          line: true true true\n          line: false false false false\n        ''',\n      );\n    });\n\n    test('bool() with bad argument', () {\n      void expectFails(String arg) {\n        final yarn = YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{bool(\"$arg\")}\\n'\n            '===\\n',\n          );\n        final line = yarn.nodes['A']!.lines.first as DialogueLine;\n        expect(\n          line.evaluate,\n          hasDialogueError(\n            'String value \"$arg\" cannot be interpreted as a boolean',\n          ),\n        );\n      }\n\n      expectFails('');\n      expectFails('t');\n      expectFails('t r u e');\n      expectFails('tru');\n      expectFails('true 1');\n      expectFails('1.0');\n    });\n\n    test('too few arguments', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{bool()}\\n===\\n'),\n        hasTypeError(\n          'TypeError: function bool() requires a single argument\\n'\n          '>  at line 3 column 7:\\n'\n          '>  {bool()}\\n'\n          '>        ^\\n',\n        ),\n      );\n    });\n\n    test('too many arguments', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{bool(3, 6)}\\n===\\n'),\n        hasTypeError(\n          'TypeError: function bool() requires a single argument\\n'\n          '>  at line 3 column 10:\\n'\n          '>  {bool(3, 6)}\\n'\n          '>           ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/ceil_test.dart",
    "content": "import 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\n\nvoid main() {\n  group('FloorFn', () {\n    test('floor() normal case', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          2     -> {ceil(2)}\n          2.3   -> {ceil(2.3)}\n          2.5   -> {ceil(2.5)}\n          2.99  -> {ceil(2.99)}\n          -0.3  -> {ceil(-0.3)}\n          -0.95 -> {ceil(-0.95)}\n          ===\n        ''',\n        testPlan: '''\n          line: 2     -> 2\n          line: 2.3   -> 3\n          line: 2.5   -> 3\n          line: 2.99  -> 3\n          line: -0.3  -> 0\n          line: -0.95 -> 0\n        ''',\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/dec_test.dart",
    "content": "import 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\n\nvoid main() {\n  group('DecFn', () {\n    test('dec() normal case', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          2     -> {dec(2)}\n          2.3   -> {dec(2.3)}\n          2.5   -> {dec(2.5)}\n          2.99  -> {dec(2.99)}\n          -0.3  -> {dec(-0.3)}\n          -0.95 -> {dec(-0.95)}\n          -1.05 -> {dec(-1.05)}\n          ===\n        ''',\n        testPlan: '''\n          line: 2     -> 1\n          line: 2.3   -> 2\n          line: 2.5   -> 2\n          line: 2.99  -> 2\n          line: -0.3  -> -1\n          line: -0.95 -> -1\n          line: -1.05 -> -2\n        ''',\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/decimal_test.dart",
    "content": "import 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\n\nvoid main() {\n  group('DecimalFn', () {\n    test('decimal() normal case', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          2     -> {decimal(2)}\n          2.3   -> {decimal(2.3)}\n          2.5   -> {decimal(2.5)}\n          2.99  -> {decimal(2.99)}\n          -0.3  -> {decimal(-0.3)}\n          -0.95 -> {decimal(-0.95)}\n          -1.05 -> {decimal(-1.05)}\n          ===\n        ''',\n        testPlan: '''\n          line: 2     -> 0\n          line: 2.3   -> 0.2999999999999998\n          line: 2.5   -> 0.5\n          line: 2.99  -> 0.9900000000000002\n          line: -0.3  -> -0.3\n          line: -0.95 -> -0.95\n          line: -1.05 -> -0.050000000000000044\n        ''',\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/dice_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:jenny/jenny.dart';\nimport 'package:jenny/src/structure/expressions/functions/dice.dart';\nimport 'package:jenny/src/structure/expressions/literal.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('DiceFn', () {\n    test('simple invocation', () async {\n      await testScenario(\n        yarn: YarnProject()..random = Random(420),\n        input: '''\n          title: Start\n          ---\n          Roll 1: {dice(6)}\n          Roll 2: {dice(6)} {dice(6)}\n          Roll 3: {dice(6)} {dice(6)} {dice(6)}\n          Roll 4: {dice(6)} {dice(6)} {dice(6)} {dice(6)}\n          ===\n        ''',\n        testPlan: '''\n          line: Roll 1: 3\n          line: Roll 2: 6 3\n          line: Roll 3: 2 1 2\n          line: Roll 4: 1 6 4 3\n        ''',\n      );\n    });\n\n    test('dice() with fractional value', () {\n      final diceFn = DiceFn(\n        const NumLiteral(3.14),\n        YarnProject()..random = Random(12345),\n      );\n      expect(diceFn.value, 1);\n      expect(diceFn.value, 1);\n      expect(diceFn.value, 2);\n      expect(diceFn.value, 3);\n    });\n\n    test('dice(0)', () {\n      final diceFn = DiceFn(const NumLiteral(0), YarnProject());\n      expect(\n        () => diceFn.value,\n        hasDialogueError('Argument to dice() must be positive: 0'),\n      );\n    });\n\n    test('too few arguments', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{dice()}\\n===\\n'),\n        hasTypeError(\n          'TypeError: function dice() requires a single argument\\n'\n          '>  at line 3 column 7:\\n'\n          '>  {dice()}\\n'\n          '>        ^\\n',\n        ),\n      );\n    });\n\n    test('too many arguments', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{dice(3, 6)}\\n===\\n'),\n        hasTypeError(\n          'TypeError: function dice() requires a single argument\\n'\n          '>  at line 3 column 10:\\n'\n          '>  {dice(3, 6)}\\n'\n          '>           ^\\n',\n        ),\n      );\n    });\n\n    test('invalid argument type', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{dice(\"three\")}\\n===\\n'),\n        hasTypeError(\n          'TypeError: the argument should be numeric\\n'\n          '>  at line 3 column 7:\\n'\n          '>  {dice(\"three\")}\\n'\n          '>        ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/floor_test.dart",
    "content": "import 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\n\nvoid main() {\n  group('FloorFn', () {\n    test('floor() normal case', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          2     -> {floor(2)}\n          2.3   -> {floor(2.3)}\n          2.5   -> {floor(2.5)}\n          2.99  -> {floor(2.99)}\n          -0.3  -> {floor(-0.3)}\n          -0.95 -> {floor(-0.95)}\n          ===\n        ''',\n        testPlan: '''\n          line: 2     -> 2\n          line: 2.3   -> 2\n          line: 2.5   -> 2\n          line: 2.99  -> 2\n          line: -0.3  -> -1\n          line: -0.95 -> -1\n        ''',\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/if_test.dart",
    "content": "import 'package:jenny/src/yarn_project.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('if()', () {\n    test('if() normal case', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          { if(true, 17, -1) }\n          { if(false, 17, -1) }\n          { if(true, false, true) }\n          { if(true, \"orange\", \"magenta\") }\n          { if(false, \"orange\", \"magenta\") }\n          ===\n        ''',\n        testPlan: '''\n          line: 17\n          line: -1\n          line: false\n          line: orange\n          line: magenta\n        ''',\n      );\n    });\n\n    test('then/else evaluate only if necessary', () async {\n      var invocationCount = 0;\n      num t() => invocationCount++;\n\n      await testScenario(\n        yarn: YarnProject()..functions.addFunction0('t', t),\n        input: '''\n          title: Start\n          ---\n          { if(true, t() + 1, t() + 5) }\n          { if(false, t() + 1, t() + 5) }\n          ===\n        ''',\n        testPlan: '''\n          line: 1\n          line: 6\n        ''',\n      );\n      expect(invocationCount, 2);\n    });\n\n    group('errors', () {\n      test('too few arguments', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              'title:A\\n---\\n{if(true, 1)}\\n===\\n',\n            ),\n          hasTypeError(\n            'TypeError: function if() requires three arguments\\n'\n            '>  at line 3 column 12:\\n'\n            '>  {if(true, 1)}\\n'\n            '>             ^\\n',\n          ),\n        );\n      });\n\n      test('too many arguments', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              'title:A\\n---\\n{if(true, 1, 3, 6)}\\n===\\n',\n            ),\n          hasTypeError(\n            'TypeError: function if() requires three arguments\\n'\n            '>  at line 3 column 17:\\n'\n            '>  {if(true, 1, 3, 6)}\\n'\n            '>                  ^\\n',\n          ),\n        );\n      });\n\n      test('first argument is not boolean', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              'title:A\\n---\\n{if(1, 3, 6)}\\n===\\n',\n            ),\n          hasTypeError(\n            'TypeError: first argument in if() should be a boolean condition\\n'\n            '>  at line 3 column 5:\\n'\n            '>  {if(1, 3, 6)}\\n'\n            '>      ^\\n',\n          ),\n        );\n      });\n\n      test('incompatible argument types', () {\n        expect(\n          () => YarnProject()\n            ..parse(\n              'title:A\\n---\\n{if(true, 3, \"no\")}\\n===\\n',\n            ),\n          hasTypeError(\n            'TypeError: the types of the second and the third arguments in '\n            'if() must be the same, instead they were numeric and string\\n'\n            '>  at line 3 column 14:\\n'\n            '>  {if(true, 3, \"no\")}\\n'\n            '>               ^\\n',\n          ),\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/inc_test.dart",
    "content": "import 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\n\nvoid main() {\n  group('IncFn', () {\n    test('inc() normal case', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          2     -> {inc(2)}\n          2.3   -> {inc(2.3)}\n          2.5   -> {inc(2.5)}\n          2.99  -> {inc(2.99)}\n          -0.3  -> {inc(-0.3)}\n          -0.95 -> {inc(-0.95)}\n          -1.05 -> {inc(-1.05)}\n          ===\n        ''',\n        testPlan: '''\n          line: 2     -> 3\n          line: 2.3   -> 3\n          line: 2.5   -> 3\n          line: 2.99  -> 3\n          line: -0.3  -> 0\n          line: -0.95 -> 0\n          line: -1.05 -> -1\n        ''',\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/int_test.dart",
    "content": "import 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\n\nvoid main() {\n  group('IntFn', () {\n    test('int() normal case', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          2     -> {int(2)}\n          2.3   -> {int(2.3)}\n          2.5   -> {int(2.5)}\n          2.99  -> {int(2.99)}\n          -0.3  -> {int(-0.3)}\n          -0.95 -> {int(-0.95)}\n          -1.05 -> {int(-1.05)}\n          ===\n        ''',\n        testPlan: '''\n          line: 2     -> 2\n          line: 2.3   -> 2\n          line: 2.5   -> 2\n          line: 2.99  -> 2\n          line: -0.3  -> 0\n          line: -0.95 -> 0\n          line: -1.05 -> -1\n        ''',\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/number_test.dart",
    "content": "import 'package:jenny/src/structure/dialogue_line.dart';\nimport 'package:jenny/src/yarn_project.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('NumberFn', () {\n    test('number() normal case', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          {number(1)} {number(2.5)}\n          {1 + number(2 + 2)}\n          {number(true)}\n          {number(false)}\n          {number(\"1\")}\n          {number(\"123\") - 1}\n          {number(\"2e2\")}\n          {number(\"3.11e-05\")}\n          {number(\"   2e-1  \")}\n          {number(\"0x100\")}\n          {number(\"-72.001\")}\n          {number(\".5\")} {number(\"5.\")}\n          ===\n        ''',\n        testPlan: '''\n          line: 1 2.5\n          line: 5\n          line: 1\n          line: 0\n          line: 1\n          line: 122\n          line: 200\n          line: 0.0000311\n          line: 0.2\n          line: 256\n          line: -72.001\n          line: 0.5 5\n        ''',\n      );\n    });\n\n    test('number() with bad arguments', () {\n      void expectFails(String arg) {\n        final yarn = YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{number(\"$arg\")}\\n'\n            '===\\n',\n          );\n        final line = yarn.nodes['A']!.lines.first as DialogueLine;\n        expect(\n          line.evaluate,\n          hasDialogueError(\n            'Unable to convert string \"$arg\" into a number',\n          ),\n        );\n      }\n\n      expectFails('');\n      expectFails('one');\n      expectFails('0x100G');\n      expectFails('1 + 2');\n      expectFails('1.2.3');\n      expectFails('--8');\n      expectFails('2,3');\n    });\n\n    test('too few arguments', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{number()}\\n===\\n'),\n        hasTypeError(\n          'TypeError: function number() requires a single argument\\n'\n          '>  at line 3 column 9:\\n'\n          '>  {number()}\\n'\n          '>          ^\\n',\n        ),\n      );\n    });\n\n    test('too many arguments', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{number(3, 6)}\\n===\\n'),\n        hasTypeError(\n          'TypeError: function number() requires a single argument\\n'\n          '>  at line 3 column 12:\\n'\n          '>  {number(3, 6)}\\n'\n          '>             ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/plural_test.dart",
    "content": "import 'package:jenny/src/structure/expressions/functions/plural.dart';\nimport 'package:jenny/src/structure/expressions/literal.dart';\nimport 'package:jenny/src/yarn_project.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('PluralFn', () {\n    test('apples-apples-apples', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          0 = {plural(0, \"apple\")} \n          1 = {plural(1, \"apple\")} \n          2 = {plural(2, \"apple\")} \n          11 = {plural(11, \"apple\")} \n          20 = {plural(20, \"apple\")} \n          21 = {plural(21, \"apple\")} \n          101 = {plural(101, \"apple\")} \n          111 = {plural(111, \"apple\")} \n          1111 = {plural(1111, \"apple\")}\n          \n          {plural(1, \"% table\")} - {plural(5, \"% table\")}\n          {plural(1, \"% foot\", \"% feet\")} - {plural(20, \"% foot\", \"% feet\")}\n          ===\n        ''',\n        testPlan: '''\n          line: 0 = apples\n          line: 1 = apple\n          line: 2 = apples\n          line: 11 = apples\n          line: 20 = apples\n          line: 21 = apples\n          line: 101 = apples\n          line: 111 = apples\n          line: 1111 = apples\n          line: 1 table - 5 tables\n          line: 1 foot - 20 feet\n        ''',\n      );\n    });\n\n    test('auto plurals', () {\n      String plural(int n, String word) {\n        final f = PluralFn(NumLiteral(n), [StringLiteral(word)], YarnProject());\n        return '$n ${f.value}';\n      }\n\n      expect(plural(1, 'spoon'), '1 spoon');\n      expect(plural(2, 'spoon'), '2 spoons');\n      expect(plural(21, 'fox'), '21 foxes');\n      expect(plural(-5, 'dollar'), '-5 dollars');\n      expect(plural(-1, 'dollar'), '-1 dollars');\n      expect(plural(17, 'bench'), '17 benches');\n      expect(plural(2, 'stash'), '2 stashes');\n      expect(plural(200, 'ass'), '200 asses');\n      expect(plural(100500, 'zombie'), '100500 zombies');\n      expect(plural(3, 'ally'), '3 allies');\n    });\n\n    test('too few arguments', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n{plural(1)}\\n===\\n',\n          ),\n        hasTypeError(\n          'TypeError: function plural() requires at least 2 arguments\\n'\n          '>  at line 3 column 10:\\n'\n          '>  {plural(1)}\\n'\n          '>           ^\\n',\n        ),\n      );\n    });\n\n    test('too many arguments', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n{plural(1, 2, 3, 4)}\\n===\\n',\n          ),\n        hasTypeError(\n          'TypeError: function plural() requires at most 3 arguments\\n'\n          '>  at line 3 column 18:\\n'\n          '>  {plural(1, 2, 3, 4)}\\n'\n          '>                   ^\\n',\n        ),\n      );\n    });\n\n    test('invalid argument 1 type', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n{plural(\"\", \"apple\")}\\n===\\n',\n          ),\n        hasTypeError(\n          'TypeError: the first argument in plural() should be numeric\\n'\n          '>  at line 3 column 9:\\n'\n          '>  {plural(\"\", \"apple\")}\\n'\n          '>          ^\\n',\n        ),\n      );\n    });\n\n    test('invalid argument 2 type', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n{plural(7, 3, \"apple\")}\\n===\\n',\n          ),\n        hasTypeError(\n          'TypeError: a string argument is expected\\n'\n          '>  at line 3 column 12:\\n'\n          '>  {plural(7, 3, \"apple\")}\\n'\n          '>             ^\\n',\n        ),\n      );\n    });\n\n    test('invalid argument 3 type', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n{plural(7, \"apple\", false)}\\n===\\n',\n          ),\n        hasTypeError(\n          'TypeError: a string argument is expected\\n'\n          '>  at line 3 column 21:\\n'\n          '>  {plural(7, \"apple\", false)}\\n'\n          '>                      ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/random_range_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:jenny/jenny.dart';\nimport 'package:jenny/src/structure/expressions/functions/random_range.dart';\nimport 'package:jenny/src/structure/expressions/literal.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('RandomRangeFn', () {\n    test('normal invocation', () async {\n      await testScenario(\n        yarn: YarnProject()..random = Random(7),\n        input: '''\n          title: Start\n          ---\n          1: {random_range(3, 6)}\n          2: {random_range(-12, 12)}\n          3: {random_range(0, 100)}\n          4: {random_range(50, 70)}\n          ===\n        ''',\n        testPlan: '''\n          line: 1: 3\n          line: 2: 0\n          line: 3: 57\n          line: 4: 59\n        ''',\n      );\n    });\n\n    test('random_range() stress test', () {\n      final fn = RandomRangeFn(\n        const NumLiteral(-20),\n        const NumLiteral(20),\n        YarnProject()..random = Random(12345),\n      );\n      num min = 1000;\n      num max = -1000;\n      for (var i = 0; i < 100; i++) {\n        final value = fn.value;\n        if (value < min) {\n          min = value;\n        }\n        if (value > max) {\n          max = value;\n        }\n      }\n      expect(min, -20);\n      expect(max, 20);\n    });\n\n    test('random range with min == max', () {\n      final fn = RandomRangeFn(\n        const NumLiteral(0),\n        const NumLiteral(0),\n        YarnProject(),\n      );\n      expect(fn.value, 0);\n    });\n\n    test('random range with min > max', () {\n      final fn = RandomRangeFn(\n        const NumLiteral(10),\n        const NumLiteral(0),\n        YarnProject(),\n      );\n      expect(\n        () => fn.value,\n        hasDialogueError(\n          'In random_range(a=10, b=0) the upper bound cannot be less than the '\n          'lower bound',\n        ),\n      );\n    });\n\n    test('too few arguments', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n{random_range(1)}\\n===\\n',\n          ),\n        hasTypeError(\n          'TypeError: function random_range() requires two arguments\\n'\n          '>  at line 3 column 16:\\n'\n          '>  {random_range(1)}\\n'\n          '>                 ^\\n',\n        ),\n      );\n    });\n\n    test('invalid argument 1 type', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n{random_range(true, 12)}\\n===\\n',\n          ),\n        hasTypeError(\n          'TypeError: the first argument should be numeric\\n'\n          '>  at line 3 column 15:\\n'\n          '>  {random_range(true, 12)}\\n'\n          '>                ^\\n',\n        ),\n      );\n    });\n\n    test('invalid argument 2 type', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n{random_range(3, \"seventeen\")}\\n===\\n',\n          ),\n        hasTypeError(\n          'TypeError: the second argument should be numeric\\n'\n          '>  at line 3 column 18:\\n'\n          '>  {random_range(3, \"seventeen\")}\\n'\n          '>                   ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/random_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('RandomFn', () {\n    test('normal case', () async {\n      await testScenario(\n        yarn: YarnProject()..random = Random(42),\n        input: '''\n          title: Start\n          ---\n          Roll 1: {random()}\n          Roll 2: {random()}\n          Roll 3: {random()}\n          ===\n        ''',\n        testPlan: '''\n          line: Roll 1: 0.15092545597797424\n          line: Roll 2: 0.6041479725361217\n          line: Roll 3: 0.6616810157870647\n        ''',\n      );\n    });\n\n    test('too many arguments', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{random(3, 0)}\\n===\\n'),\n        hasTypeError(\n          'TypeError: function random() requires no arguments\\n'\n          '>  at line 3 column 9:\\n'\n          '>  {random(3, 0)}\\n'\n          '>          ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/round_places_test.dart",
    "content": "import 'package:jenny/src/yarn_project.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('RoundPlacesFn', () {\n    test('round_places() normal case', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          2 -> {round_places(2, 0)}, {round_places(2, 1)}, {round_places(2, 2)}\n          2.3 -> {round_places(2.3, 0)}, {round_places(2.3, 1)}\n          7.001 -> {round_places(7.001, 0)}, {round_places(7.001, 2)}\n          1/7 -> {round_places(1/7, 3)}, {round_places(1/7, 5)}\n          -1/7 -> {round_places(-1/7, 3)}, {round_places(-1/7, 5)}\n          274.5 -> {round_places(274.5, -1)}, {round_places(274.5, -2)}\n          ===\n        ''',\n        testPlan: '''\n          line: 2 -> 2, 2, 2\n          line: 2.3 -> 2, 2.3\n          line: 7.001 -> 7, 7\n          line: 1/7 -> 0.143, 0.14286\n          line: -1/7 -> -0.143, -0.14286\n          line: 274.5 -> 270, 300\n        ''',\n      );\n    });\n\n    test('too few arguments', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{round_places(1)}\\n===\\n'),\n        hasTypeError(\n          'TypeError: function round_places() requires two arguments\\n'\n          '>  at line 3 column 16:\\n'\n          '>  {round_places(1)}\\n'\n          '>                 ^\\n',\n        ),\n      );\n    });\n\n    test('too many arguments', () {\n      expect(\n        () =>\n            YarnProject()\n              ..parse('title:A\\n---\\n{round_places(3, 6, 1, 7)}\\n===\\n'),\n        hasTypeError(\n          'TypeError: function round_places() requires two arguments\\n'\n          '>  at line 3 column 21:\\n'\n          '>  {round_places(3, 6, 1, 7)}\\n'\n          '>                      ^\\n',\n        ),\n      );\n    });\n\n    test('first argument not numeric', () {\n      expect(\n        () =>\n            YarnProject()\n              ..parse('title:A\\n---\\n{round_places(\"one\", 1)}\\n===\\n'),\n        hasTypeError(\n          'TypeError: first argument in round_places() should be numeric\\n'\n          '>  at line 3 column 15:\\n'\n          '>  {round_places(\"one\", 1)}\\n'\n          '>                ^\\n',\n        ),\n      );\n    });\n\n    test('second argument not numeric', () {\n      expect(\n        () =>\n            YarnProject()\n              ..parse('title:A\\n---\\n{round_places(3, \"one\")}\\n===\\n'),\n        hasTypeError(\n          'TypeError: second argument in round_places() should be numeric\\n'\n          '>  at line 3 column 18:\\n'\n          '>  {round_places(3, \"one\")}\\n'\n          '>                   ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/round_test.dart",
    "content": "import 'package:jenny/src/yarn_project.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('RoundFn', () {\n    test('round() normal case', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          2    -> {round(2)}\n          2.3  -> {round(2.3)}\n          2.5  -> {round(2.5)}\n          2.99 -> {round(2.99)}\n          3.5  -> {round(3.5)}\n          -0.3 -> {round(-0.3)}\n          -0.5 -> {round(-0.5)}\n          ===\n        ''',\n        testPlan: '''\n          line: 2    -> 2\n          line: 2.3  -> 2\n          line: 2.5  -> 3\n          line: 2.99 -> 3\n          line: 3.5  -> 4\n          line: -0.3 -> 0\n          line: -0.5 -> -1\n        ''',\n      );\n    });\n\n    test('too few arguments', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{round()}\\n===\\n'),\n        hasTypeError(\n          'TypeError: function round() requires a single argument\\n'\n          '>  at line 3 column 8:\\n'\n          '>  {round()}\\n'\n          '>         ^\\n',\n        ),\n      );\n    });\n\n    test('too many arguments', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{round(3, 0)}\\n===\\n'),\n        hasTypeError(\n          'TypeError: function round() requires a single argument\\n'\n          '>  at line 3 column 11:\\n'\n          '>  {round(3, 0)}\\n'\n          '>            ^\\n',\n        ),\n      );\n    });\n\n    test('non-numeric argument', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{round(\"o\")}\\n===\\n'),\n        hasTypeError(\n          'TypeError: the argument in round() should be numeric\\n'\n          '>  at line 3 column 8:\\n'\n          '>  {round(\"o\")}\\n'\n          '>         ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/string_test.dart",
    "content": "import 'package:jenny/src/yarn_project.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('StringFn', () {\n    test('string() normal case', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          {string(2 + 2)}\n          {string(true)}\n          {string(false)}\n          {string(\"Jenny\")}\n          {string(12345678900000000000000000)}\n          {string(0.000000001)}\n          ===\n        ''',\n        testPlan: '''\n          line: 4\n          line: true\n          line: false\n          line: Jenny\n          line: 1.23456789e+25\n          line: 1e-9\n        ''',\n      );\n    });\n\n    test('too few arguments', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{string()}\\n===\\n'),\n        hasTypeError(\n          'TypeError: function string() requires a single argument\\n'\n          '>  at line 3 column 9:\\n'\n          '>  {string()}\\n'\n          '>          ^\\n',\n        ),\n      );\n    });\n\n    test('too many arguments', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{string(3, 6)}\\n===\\n'),\n        hasTypeError(\n          'TypeError: function string() requires a single argument\\n'\n          '>  at line 3 column 12:\\n'\n          '>  {string(3, 6)}\\n'\n          '>             ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/user_defined_function_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\n// See also: function_storage_test.dart\nvoid main() {\n  group('NumericUserDefinedFn', () {\n    test('simple udf with numeric return type', () {\n      num myFunction() {\n        return 42;\n      }\n\n      final yarn = YarnProject()\n        ..functions.addFunction0('answer', myFunction)\n        ..parse(\n          'title: A\\n---\\n'\n          'The answer to life, Universe, and everything: { answer() }\\n'\n          '===\\n',\n        );\n      final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n      line.evaluate();\n      expect(line.text, 'The answer to life, Universe, and everything: 42');\n    });\n  });\n\n  group('BooleanUserDefinedFn', () {\n    test('simple udf with boolean return type', () {\n      bool myFunction() {\n        return false;\n      }\n\n      final yarn = YarnProject()\n        ..functions.addFunction0('answer', myFunction)\n        ..parse(\n          'title: A\\n---\\n'\n          'The Earth is flat -- true or false? { answer() }\\n'\n          '===\\n',\n        );\n      final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n      line.evaluate();\n      expect(line.text, 'The Earth is flat -- true or false? false');\n    });\n  });\n\n  group('StringUserDefinedFn', () {\n    test('simple udf with string return type', () {\n      String myFunction() {\n        return 'secret';\n      }\n\n      final yarn = YarnProject()\n        ..functions.addFunction0('answer', myFunction)\n        ..parse(\n          'title: A\\n---\\n'\n          'Your favorite color? { answer() }\\n'\n          '===\\n',\n        );\n      final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n      line.evaluate();\n      expect(line.text, 'Your favorite color? secret');\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/visit_count_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('VisitCountFn', () {\n    test('visit_count Start', () async {\n      final yarn = YarnProject();\n      await testScenario(\n        yarn: yarn,\n        input: '''\n          title: Start\n          ---\n          count of Start visits = {visit_count(\"Start\")}\n          <<if not visited(\"Start\")>>\n            jumping...\n            <<jump Start>>\n          <<endif>>\n          // Also let's make sure <<stop>> doesn't prevent calculation\n          // of node visits\n          <<stop>>\n          ===\n        ''',\n        testPlan: '''\n          line: count of Start visits = 0\n          line: jumping...\n          line: count of Start visits = 1\n        ''',\n      );\n      expect(yarn.variables.getNumericValue('@Start'), 2);\n    });\n\n    test('VisitCount.yarn', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          entered start\n          seconds visited count is {visit_count(\"second\")} // default = 0\n          <<jump second>>\n          ===\n\n          title: second\n          ---\n          <<if visit_count(\"second\") < 3>>\n            second visited {visit_count(\"second\")} times // will be 0,1,2\n            <<jump second>>\n          <<else>>\n            <<jump third>>\n          <<endif>>\n          ===\n\n          title: third\n          ---\n          entered third\n          seconds was visited a total of {visit_count(\"second\")} times // 4\n          ===\n        ''',\n        testPlan: '''\n          line: entered start\n          line: seconds visited count is 0\n          line: second visited 0 times\n          line: second visited 1 times\n          line: second visited 2 times\n          line: entered third\n          line: seconds was visited a total of 4 times\n        ''',\n      );\n    });\n\n    test('VisitTracking.yarn', () async {\n      // Note: we ignore the tracking directive and always track all nodes\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          beginning\n          <<jump NoTrack>>\n          ===\n          \n          title: NoTrack\n          tracking: never\n          ---\n          entered NoTrack\n          <<jump Track>>\n          ===\n          \n          title: Track\n          tracking: always\n          ---\n          entered Track\n          <<jump End>>\n          ===\n          \n          title: End\n          ---\n          entered End\n          did we visit NoTrack? {visited(\"No\" + \"Track\")}!\n          did we visit Track? {visited(\"Track\")}!\n          done\n          ===\n        ''',\n        testPlan: '''\n          line: beginning\n          line: entered NoTrack\n          line: entered Track\n          line: entered End\n          line: did we visit NoTrack? true!\n          line: did we visit Track? true!\n          line: done\n        ''',\n      );\n    });\n\n    test('visit_count() with an unknown node', () {\n      final yarn = YarnProject()\n        ..parse(\n          'title:A\\n'\n          '---\\n'\n          '{visit_count(\"Africa\")}\\n'\n          '===\\n',\n        );\n      final line = yarn.nodes['A']!.lines.first as DialogueLine;\n      expect(\n        line.evaluate,\n        hasDialogueError('Unknown node name \"Africa\"'),\n      );\n    });\n\n    test('too few arguments', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{visit_count()}\\n===\\n'),\n        hasTypeError(\n          'TypeError: function visit_count() requires a single argument\\n'\n          '>  at line 3 column 14:\\n'\n          '>  {visit_count()}\\n'\n          '>               ^\\n',\n        ),\n      );\n    });\n\n    test('too many arguments', () {\n      expect(\n        () =>\n            YarnProject()\n              ..parse('title:A\\n---\\n{visit_count(\"Start\", \"Finish\")}\\n===\\n'),\n        hasTypeError(\n          'TypeError: function visit_count() requires a single argument\\n'\n          '>  at line 3 column 23:\\n'\n          '>  {visit_count(\"Start\", \"Finish\")}\\n'\n          '>                        ^\\n',\n        ),\n      );\n    });\n\n    test('invalid argument type', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{visit_count(1)}\\n===\\n'),\n        hasTypeError(\n          'TypeError: the argument should be a string\\n'\n          '>  at line 3 column 14:\\n'\n          '>  {visit_count(1)}\\n'\n          '>               ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/functions/visited_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('VisitedFn', () {\n    test('visit Start', () async {\n      final yarn = YarnProject();\n      await testScenario(\n        yarn: yarn,\n        input: '''\n          title: Start\n          ---\n          visited Start = {visited(\"Start\")}\n          <<if not visited(\"Start\")>>\n            jumping...\n            <<jump Start>>\n          <<endif>>\n          ===\n        ''',\n        testPlan: '''\n          line: visited Start = false\n          line: jumping...\n          line: visited Start = true\n        ''',\n      );\n      expect(yarn.variables.getNumericValue('@Start'), 2);\n    });\n\n    test('Visited.yarn', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          entered start\n          <<if visited(\"Start\")>>\n            we have visited Start before\n          <<else>>\n            we have not visited Start\n            <<jump Start>>\n          <<endif>>\n          ===\n        ''',\n        testPlan: '''\n          line: entered start\n          line: we have not visited Start\n          line: entered start\n          line: we have visited Start before\n        ''',\n      );\n    });\n\n    test('visited() with an unknown node', () {\n      final yarn = YarnProject()\n        ..parse(\n          'title:A\\n'\n          '---\\n'\n          '{visited(\"Africa\")}\\n'\n          '===\\n',\n        );\n      final line = yarn.nodes['A']!.lines.first as DialogueLine;\n      expect(\n        line.evaluate,\n        hasDialogueError('Unknown node name \"Africa\"'),\n      );\n    });\n\n    test('too few arguments', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{visited()}\\n===\\n'),\n        hasTypeError(\n          'TypeError: function visited() requires a single argument\\n'\n          '>  at line 3 column 10:\\n'\n          '>  {visited()}\\n'\n          '>           ^\\n',\n        ),\n      );\n    });\n\n    test('too many arguments', () {\n      expect(\n        () =>\n            YarnProject()\n              ..parse('title:A\\n---\\n{visited(\"Start\", \"Finish\")}\\n===\\n'),\n        hasTypeError(\n          'TypeError: function visited() requires a single argument\\n'\n          '>  at line 3 column 19:\\n'\n          '>  {visited(\"Start\", \"Finish\")}\\n'\n          '>                    ^\\n',\n        ),\n      );\n    });\n\n    test('invalid argument type', () {\n      expect(\n        () => YarnProject()..parse('title:A\\n---\\n{visited(1)}\\n===\\n'),\n        hasTypeError(\n          'TypeError: the argument should be a string\\n'\n          '>  at line 3 column 10:\\n'\n          '>  {visited(1)}\\n'\n          '>           ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/literal_test.dart",
    "content": "import 'package:jenny/src/structure/expressions/literal.dart';\nimport 'package:test/test.dart';\n\nimport '../../test_scenario.dart';\n\nvoid main() {\n  group('NumLiteral', () {\n    test('simple literal', () {\n      const literal = NumLiteral(3.14);\n      expect(literal.value, 3.14);\n    });\n\n    test('constZero', () {\n      expect(constZero.value, 0);\n    });\n\n    test('various numeric literals', () async {\n      await testScenario(\n        input: r'''\n          title: Start\n          ---\n          Integers\\: {5} {0} {-0} {777} {1000000000}\n          Decimals\\: {3.5} {0.0} {-0.0} {16.99} {-7.00000}\n          // Scientific\\: {7e5} {1.6e-2} {2e+100}\n          // Hexadecimal: {0x100}\n          ===\n        ''',\n        testPlan: '''\n          line: Integers: 5 0 0 777 1000000000\n          line: Decimals: 3.5 0 0 16.99 -7\n          // line: Scientific: 700000 0.16 2.0e100\n          // line: Hexadecimal: 256\n        ''',\n      );\n    });\n\n    testScenario(\n      testName: 'DecimalNumbers.yarn',\n      input: r'''\n        // Declarations (i.e. constant values)\n        <<declare $myInteger = 1 as Number>>\n        <<declare $myFloat = 1.2 as Number>>\n\n        title: Start\n        ---\n        // Expressions\n        <<if 1.2 >= 1.2>>\n            Success\n        <<endif>>\n\n        // Inline expressions\n        Here's a number: {45.1}\n        ===\n      ''',\n      testPlan: '''\n        line: Success\n        line: Here's a number: 45.1\n      ''',\n    );\n  });\n\n  group('StringLiteral', () {\n    test('simple literal', () {\n      const literal = StringLiteral('Jenny');\n      expect(literal.value, 'Jenny');\n    });\n\n    test('constEmptyString', () {\n      expect(constEmptyString.value, '');\n    });\n\n    test('various string literals', () async {\n      await testScenario(\n        input: r'''\n          title: Start\n          ---\n          Double quoted: { \"one two three\" }\n          Single quoted: { 'four five six' }\n          Escapes\\: { '12 o\\'clock' }\n          No interpolation: { \"Hello, {$world}\" }\n          ===\n        ''',\n        testPlan: r'''\n          line: Double quoted: one two three\n          line: Single quoted: four five six\n          line: Escapes: 12 o'clock\n          line: No interpolation: Hello, {$world}\n        ''',\n      );\n    });\n  });\n\n  group('BoolLiteral', () {\n    test('simple literal', () {\n      const literal = BoolLiteral(true);\n      expect(literal.value, true);\n    });\n\n    test('consts', () {\n      expect(constTrue.value, true);\n      expect(constFalse.value, false);\n    });\n  });\n\n  group('VoidLiteral', () {\n    test('constVoid', () {\n      expect(constVoid.value, null);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/operators/add_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('Add', () {\n    test('x + y', () async {\n      await testScenario(\n        input: r'''\n          title: Start\n          ---\n          <<local $world = \"World\">>\n          {5 + 7}\n          {3.5 + 2}\n          {1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9}\n          {'Yarn' + 'Spinner'}\n          { 'Hello' + ', ' + $world + '!' }\n          ===\n        ''',\n        testPlan: '''\n          line: 12\n          line: 5.5\n          line: 45\n          line: YarnSpinner\n          line: Hello, World!\n        ''',\n      );\n    });\n\n    test('x += y', () async {\n      await testScenario(\n        input: r'''\n          title: Start\n          ---\n          <<local $x = 7>>\n          <<local $world = \"World\">>\n          <<set $x += 3>>\n          <<set $world += '!'>>\n          {$x}\n          {$world}\n          ===\n        ''',\n        testPlan: '''\n          line: 10\n          line: World!\n        ''',\n      );\n    });\n\n    test('wrong argument types 1', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{2 + false}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: both left and right sides of `+` must be numeric or '\n          'strings, instead the types are (numeric, boolean)\\n'\n          '>  at line 3 column 4:\\n'\n          '>  {2 + false}\\n'\n          '>     ^\\n',\n        ),\n      );\n    });\n\n    test('wrong argument types 2', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{\"Hey\" + 3}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: both left and right sides of `+` must be numeric or '\n          'strings, instead the types are (string, numeric)\\n'\n          '>  at line 3 column 8:\\n'\n          '>  {\"Hey\" + 3}\\n'\n          '>         ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/operators/and_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('And', () {\n    test('x && y', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          {true && true} {true and true}\n          {true && false} {true and false}\n          {false && true} {false and true}\n          {false && false} {false and false}\n          ===\n        ''',\n        testPlan: '''\n          line: true true\n          line: false false\n          line: false false\n          line: false false\n        ''',\n      );\n    });\n\n    test('wrong argument types', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{true && 0}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: both left and right sides of `&&` must be boolean, '\n          'instead the types are (boolean, numeric)\\n'\n          '>  at line 3 column 7:\\n'\n          '>  {true && 0}\\n'\n          '>        ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/operators/divide_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('Divide', () {\n    test('x / y', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          { 48 / 2 / 3 }\n          ===\n        ''',\n        testPlan: '''\n          line: 8\n        ''',\n      );\n    });\n\n    test('x /= y', () async {\n      await testScenario(\n        input: r'''\n          title: Start\n          ---\n          <<local $x = 8>>\n          <<set $x /= 2>>\n          {$x}\n          ===\n        ''',\n        testPlan: '''\n          line: 4\n        ''',\n      );\n    });\n\n    test('division by zero', () {\n      final yarn = YarnProject()\n        ..parse(\n          'title:A\\n---\\n'\n          '{ 4.0 / 0.0 }\\n'\n          '===\\n',\n        );\n      final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n      expect(\n        line.evaluate,\n        hasDialogueError('Division by zero'),\n      );\n    });\n\n    test('wrong argument types', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{\"X\" / \"y\"}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: both left and right sides of `/` must be numeric, '\n          'instead the types are (string, string)\\n'\n          '>  at line 3 column 6:\\n'\n          '>  {\"X\" / \"y\"}\\n'\n          '>       ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/operators/equal_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('Equals', () {\n    test('x == y', () async {\n      await testScenario(\n        input: r'''\n          title: Start\n          ---\n          <<local $famous = true>>\n          <<local $name = \"Mr.Bronze\">>\n          { $famous == true }\n          { 8 % 3 == 2 }\n          { $name == \"monkey\" }\n          ===\n        ''',\n        testPlan: '''\n          line: true\n          line: true\n          line: false\n        ''',\n      );\n    });\n\n    test('wrong argument types', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{\"flame\" == 7}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: equality operator between operands of unrelated types '\n          'string and numeric\\n'\n          '>  at line 3 column 10:\\n'\n          '>  {\"flame\" == 7}\\n'\n          '>           ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/operators/greater_or_equal_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('GreaterOrEqual', () {\n    test('x >= y', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          { 7 >= 5 + 2 }\n          { 2.3 + 1 >= 4 }\n          { 4 >= 3 }\n          { 4 >= 4.0 }\n          { 4 >= 4.0001 }\n          ===\n        ''',\n        testPlan: '''\n          line: true\n          line: false\n          line: true\n          line: true\n          line: false\n        ''',\n      );\n    });\n\n    test('wrong argument types', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{true >= false}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: both left and right sides of `>=` must be numeric, '\n          'instead the types are (boolean, boolean)\\n'\n          '>  at line 3 column 7:\\n'\n          '>  {true >= false}\\n'\n          '>        ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/operators/greater_than_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('GreaterThan', () {\n    test('x > y', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          { 7 > 5 + 1 }\n          { 2.3 + 1 > 4 }\n          ===\n        ''',\n        testPlan: '''\n          line: true\n          line: false\n        ''',\n      );\n    });\n\n    test('wrong argument types', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{true > false}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: both left and right sides of `>` must be numeric, '\n          'instead the types are (boolean, boolean)\\n'\n          '>  at line 3 column 7:\\n'\n          '>  {true > false}\\n'\n          '>        ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/operators/less_or_equal_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('LessOrEqual', () {\n    test('x <= y', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          { 7 <= 5 + 2 }\n          { 2.3 + 1 <= 4 }\n          { 4 <= 3 }\n          { 4 <= 4.0 }\n          { 4 <= 4.0001 }\n          ===\n        ''',\n        testPlan: '''\n          line: true\n          line: true\n          line: false\n          line: true\n          line: true\n        ''',\n      );\n    });\n\n    test('wrong argument types', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{\"He\" <= \"She\"}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: both left and right sides of `<=` must be numeric, '\n          'instead the types are (string, string)\\n'\n          '>  at line 3 column 7:\\n'\n          '>  {\"He\" <= \"She\"}\\n'\n          '>        ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/operators/less_than_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('LessThan', () {\n    test('x < y', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          { 7 < 5 + 1 }\n          { 2.3 + 1 < 4 }\n          ===\n        ''',\n        testPlan: '''\n          line: false\n          line: true\n        ''',\n      );\n    });\n\n    test('wrong argument types', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{\"jenny\" < 7}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: both left and right sides of `<` must be numeric, '\n          'instead the types are (string, numeric)\\n'\n          '>  at line 3 column 10:\\n'\n          '>  {\"jenny\" < 7}\\n'\n          '>           ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/operators/modulo_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('Modulo', () {\n    test('x % y', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          { 48 % 5 }\n          { 4 % 1.2 }\n          { -7 % 5 }\n          { -3.14 % 1.5 }\n          ===\n        ''',\n        testPlan: '''\n          line: 3\n          line: 0.40000000000000013\n          line: 3\n          line: 1.3599999999999999\n        ''',\n      );\n    });\n\n    test('x %= y', () async {\n      await testScenario(\n        input: r'''\n          title: Start\n          ---\n          <<local $x = 10>>\n          <<set $x %= 7>>\n          { $x }\n          ===\n        ''',\n        testPlan: '''\n          line: 3\n        ''',\n      );\n    });\n\n    test('negative divisor', () {\n      final yarn = YarnProject()\n        ..parse(\n          'title:A\\n---\\n'\n          '{ 4 % -5 }\\n'\n          '===\\n',\n        );\n      final line = yarn.nodes['A']!.lines[0] as DialogueLine;\n      expect(\n        line.evaluate,\n        hasDialogueError(\n          'The divisor of a modulo is not a positive number: -5',\n        ),\n      );\n    });\n\n    test('wrong argument types', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{\"X\" % \"y\"}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: both left and right sides of `%` must be numeric, '\n          'instead the types are (string, string)\\n'\n          '>  at line 3 column 6:\\n'\n          '>  {\"X\" % \"y\"}\\n'\n          '>       ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/operators/multiply_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('Multiply', () {\n    test('x * y', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          { 11 * 8 * 0.5 }\n          { 2 * -3 }\n          ===\n        ''',\n        testPlan: '''\n          line: 44\n          line: -6\n        ''',\n      );\n    });\n\n    test('x *= y', () async {\n      await testScenario(\n        input: r'''\n          title: Start\n          ---\n          <<local $x = 2.5>>\n          <<set $x *= 4>>\n          { $x }\n          ===\n        ''',\n        testPlan: '''\n          line: 10\n        ''',\n      );\n    });\n\n    test('wrong argument types', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{\"X\" * \"zero\"}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: both left and right sides of `*` must be numeric, '\n          'instead the types are (string, string)\\n'\n          '>  at line 3 column 6:\\n'\n          '>  {\"X\" * \"zero\"}\\n'\n          '>       ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/operators/negate_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('Negate', () {\n    test('unary minus', () async {\n      await testScenario(\n        input: r'''\n          title: Start\n          ---\n          <<local $x = 42>>\n          { -7 + 1 }\n          { 2 * -7 }\n          { -$x }\n          { -(3 + 111) }\n          { --5 }\n          ===\n        ''',\n        testPlan: '''\n          line: -6\n          line: -14\n          line: -42\n          line: -114\n          line: 5\n        ''',\n      );\n    });\n\n    test('unary minus with wrong type', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{-\"banana\"}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: unary minus can only be applied to numbers\\n'\n          '>  at line 3 column 3:\\n'\n          '>  {-\"banana\"}\\n'\n          '>    ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/operators/not_equal_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('NotEquals', () {\n    test('x != y', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          { false != true }\n          { 7 != 2 }\n          { \"m\" + \"on\" + \"key\" != \"monkey\" }\n          ===\n        ''',\n        testPlan: '''\n          line: true\n          line: true\n          line: false\n        ''',\n      );\n    });\n\n    test('wrong argument types', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{\"flame\" != 7}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: inequality operator between operands of unrelated types '\n          'string and numeric\\n'\n          '>  at line 3 column 10:\\n'\n          '>  {\"flame\" != 7}\\n'\n          '>           ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/operators/not_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('Not', () {\n    test('not', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          {! true} {! false}\n          {not true} {not false}\n          {! 5==5}\n          {! (false || true)}\n          ===\n        ''',\n        testPlan: '''\n          line: false true\n          line: false true\n          line: false\n          line: false\n        ''',\n      );\n    });\n\n    test('not with wrong type', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{! 5}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: operator `not` can only be applied to booleans\\n'\n          '>  at line 3 column 4:\\n'\n          '>  {! 5}\\n'\n          '>     ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/operators/or_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('Or', () {\n    test('x || y', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          {true || true}\n          {true || false}\n          {false || true}\n          {false || false}\n          {true or true}\n          {true or false}\n          {false or true}\n          {false or false}\n          ===\n        ''',\n        testPlan: '''\n          line: true\n          line: true\n          line: true\n          line: false\n          line: true\n          line: true\n          line: true\n          line: false\n        ''',\n      );\n    });\n\n    test('wrong argument types', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{true || \"false\"}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: both left and right sides of `||` must be boolean, '\n          'instead the types are (boolean, string)\\n'\n          '>  at line 3 column 7:\\n'\n          '>  {true || \"false\"}\\n'\n          '>        ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/operators/subtract_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('Subtract', () {\n    test('x - y', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          {5 - 7}\n          {3.5 - 2}\n          {'Yarn' - 'Spinner'}\n          { 'Hello' - 'Hell' - 'Paradise' }\n          ===\n        ''',\n        testPlan: '''\n          line: -2\n          line: 1.5\n          line: Yarn\n          line: o\n        ''',\n      );\n    });\n\n    test('x -= y', () async {\n      await testScenario(\n        input: r'''\n          title: Start\n          ---\n          <<local $x = 7>>\n          <<local $world = \"World\">>\n          <<set $x -= 3>>\n          <<set $world -= 'l'>>\n          {$x}\n          {$world}\n          ===\n        ''',\n        testPlan: '''\n          line: 4\n          line: Word\n        ''',\n      );\n    });\n\n    test('wrong argument types 1', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{2 - false}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: both left and right sides of `-` must be numeric or '\n          'strings, instead the types are (numeric, boolean)\\n'\n          '>  at line 3 column 4:\\n'\n          '>  {2 - false}\\n'\n          '>     ^\\n',\n        ),\n      );\n    });\n\n    test('wrong argument types 2', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{\"Hey\" - 3}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: both left and right sides of `-` must be numeric or '\n          'strings, instead the types are (string, numeric)\\n'\n          '>  at line 3 column 8:\\n'\n          '>  {\"Hey\" - 3}\\n'\n          '>         ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/operators/xor_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport '../../../test_scenario.dart';\nimport '../../../utils.dart';\n\nvoid main() {\n  group('Xor', () {\n    test('x ^ y', () async {\n      await testScenario(\n        input: '''\n          title: Start\n          ---\n          {true ^ true}\n          {true ^ false}\n          {false ^ true}\n          {false ^ false}\n          {true xor true}\n          {true xor false}\n          {false xor true}\n          {false xor false}\n          ===\n        ''',\n        testPlan: '''\n          line: false\n          line: true\n          line: true\n          line: false\n          line: false\n          line: true\n          line: true\n          line: false\n        ''',\n      );\n    });\n\n    test('wrong argument types', () {\n      expect(\n        () => YarnProject()\n          ..parse(\n            'title:A\\n---\\n'\n            '{\"false\" ^ \"true\"}\\n'\n            '===\\n',\n          ),\n        hasTypeError(\n          'TypeError: both left and right sides of `^` must be boolean, '\n          'instead the types are (string, string)\\n'\n          '>  at line 3 column 10:\\n'\n          '>  {\"false\" ^ \"true\"}\\n'\n          '>           ^\\n',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/expressions/variables_test.dart",
    "content": "import 'package:test/test.dart';\n\nvoid main() {\n  group('NumericVariable', () {});\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/structure/node_test.dart",
    "content": "import 'package:jenny/src/structure/block.dart';\nimport 'package:jenny/src/structure/dialogue_choice.dart';\nimport 'package:jenny/src/structure/dialogue_entry.dart';\nimport 'package:jenny/src/structure/dialogue_line.dart';\nimport 'package:jenny/src/structure/dialogue_option.dart';\nimport 'package:jenny/src/structure/line_content.dart';\nimport 'package:jenny/src/structure/node.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('Node', () {\n    test('simple node', () {\n      const node = Node(\n        title: 'Introduction',\n        content: Block.empty(),\n      );\n\n      expect(node.title, 'Introduction');\n      expect(node.lines, <DialogueEntry>[]);\n      expect(node.tags, isEmpty);\n      expect('$node', 'Node(Introduction)');\n    });\n\n    test('node with tags', () {\n      const node = Node(\n        title: 'Conclusion',\n        content: Block.empty(),\n        tags: {'line': 'af451', 'characters': 'Alice, Bob'},\n      );\n      expect(node.title, 'Conclusion');\n      expect(node.tags, isNotEmpty);\n      expect(node.tags.length, 2);\n      expect(node.tags['line'], 'af451');\n      expect(node.tags['characters'], 'Alice, Bob');\n    });\n\n    group('iterators', () {\n      test('iterating an empty node', () {\n        const node = Node(title: 'X', content: Block.empty());\n        final statements = List<DialogueEntry>.from(node);\n        expect(statements, isEmpty);\n      });\n\n      test('iterating node with one line', () {\n        final line0 = DialogueLine(content: LineContent(''));\n        final node = Node(title: 'X', content: Block([line0]));\n        expect(node.toList(), [line0]);\n      });\n\n      test('iterating multi-line node', () {\n        final line1 = DialogueLine(content: LineContent('one'));\n        final line2 = DialogueLine(content: LineContent('two'));\n        final line3 = DialogueLine(content: LineContent('three'));\n        final line4 = DialogueLine(content: LineContent('four'));\n        final node = Node(\n          title: 'quadruple',\n          content: Block([line1, line2, line3, line4]),\n        );\n        expect(List<DialogueEntry>.from(node), [line1, line2, line3, line4]);\n      });\n\n      test('iterating deep node', () {\n        final node = Node(\n          title: 'complicated',\n          content: Block([\n            DialogueLine(content: LineContent('one')),\n            DialogueLine(content: LineContent('two')),\n            DialogueChoice([\n              DialogueOption(\n                content: LineContent('select 1'),\n                block: Block([\n                  DialogueLine(content: LineContent('so one it is')),\n                  DialogueLine(content: LineContent('good choice!')),\n                ]),\n              ),\n              DialogueOption(\n                content: LineContent('select 2'),\n                block: Block([\n                  DialogueLine(content: LineContent('oops!')),\n                ]),\n              ),\n            ]),\n            DialogueLine(content: LineContent('bye!')),\n          ]),\n        );\n\n        final lines0 = <DialogueEntry>[];\n        final it0 = node.iterator;\n        while (it0.moveNext()) {\n          lines0.add(it0.current);\n        }\n        expect(lines0, node.lines);\n\n        final lines1 = <DialogueEntry>[];\n        final it1 = node.iterator;\n        while (it1.moveNext()) {\n          final nextLine = it1.current;\n          lines1.add(nextLine);\n          if (nextLine is DialogueChoice) {\n            it1.diveInto(nextLine.options[0].block);\n          }\n        }\n        expect(\n          lines1,\n          [\n            DialogueLine(content: LineContent('one')),\n            DialogueLine(content: LineContent('two')),\n            node.lines[2] as DialogueChoice,\n            DialogueLine(content: LineContent('so one it is')),\n            DialogueLine(content: LineContent('good choice!')),\n            DialogueLine(content: LineContent('bye!')),\n          ],\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/test_scenario.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:jenny/jenny.dart';\nimport 'package:meta/meta.dart';\nimport 'package:test/test.dart';\n\n/// Verifies that the [input] can be parsed and then executed according to\n/// the [testPlan].\n///\n/// Common indents will be removed from both the [input] and the [testPlan],\n/// for convenience.\n@isTest\nFuture<void> testScenario({\n  required String input,\n  required String testPlan,\n  String? testName,\n  bool skip = false,\n  List<String>? commands,\n  YarnProject? yarn,\n}) async {\n  final yarnProject = (yarn ?? YarnProject())..strictCharacterNames = false;\n  commands?.forEach(yarnProject.commands.addOrphanedCommand);\n\n  Future<void> testBody() async {\n    yarnProject.parse(dedent(input));\n    final plan = _TestPlan(dedent(testPlan));\n    final dialogue = DialogueRunner(\n      yarnProject: yarnProject,\n      dialogueViews: [plan],\n    );\n    await dialogue.startDialogue(plan.startNode);\n    assert(\n      plan.done,\n      '\\n'\n      'Expected: ${plan.nextEntry}\\n'\n      'Actual  : END OF DIALOGUE\\n',\n    );\n  }\n\n  if (testName == null) {\n    return testBody();\n  } else {\n    test(testName, testBody, skip: skip);\n  }\n}\n\n/// Removes common indent from a multi-line [input] string.\nString dedent(String input) {\n  var commonIndent = 1000;\n  final lines = const LineSplitter().convert(input);\n  for (final line in lines) {\n    final indent = _calculateIndent(line);\n    if (indent < line.length && indent < commonIndent) {\n      commonIndent = indent;\n    }\n  }\n  if (commonIndent == 0) {\n    return input;\n  } else {\n    for (var i = 0; i < lines.length; i++) {\n      if (lines[i].length > commonIndent) {\n        lines[i] = lines[i].substring(commonIndent);\n      }\n    }\n    return lines.join('\\n');\n  }\n}\n\nint _calculateIndent(String line) {\n  for (var i = 0; i < line.length; i++) {\n    if (line[i] != ' ') {\n      return i;\n    }\n  }\n  return line.length;\n}\n\nclass _TestPlan extends DialogueView {\n  _TestPlan(String input) {\n    _parse(input);\n  }\n\n  final List<dynamic> _expected = <dynamic>[];\n  String startNode = 'Start';\n  int _currentIndex = 0;\n\n  bool get done => _currentIndex == _expected.length;\n  dynamic get nextEntry => done ? null : _expected[_currentIndex];\n\n  @override\n  FutureOr<bool> onLineStart(DialogueLine line) {\n    assert(\n      !done,\n      'Expected: END OF DIALOGUE\\n'\n      'Actual  : $line',\n    );\n    assert(\n      nextEntry is _Line,\n      'Wrong event at test plan index $_currentIndex\\n'\n      'Expected: \"$nextEntry\"\\n'\n      'Actual  : \"$line\"\\n',\n    );\n    final expected = nextEntry as _Line;\n    final text1 = (expected.character == null)\n        ? expected.text\n        : '${expected.character}: ${expected.text}';\n    final text2 = (line.character == null)\n        ? line.text\n        : '${line.character!.name}: ${line.text}';\n    assert(\n      text1 == text2,\n      'Expected line: \"$text1\"\\n'\n      'Actual line  : \"$text2\"\\n',\n    );\n    _currentIndex++;\n    return true;\n  }\n\n  @override\n  Future<int> onChoiceStart(DialogueChoice choice) async {\n    assert(\n      !done,\n      'Expected: END OF DIALOGUE\\n'\n      'Actual  : $choice',\n    );\n    assert(\n      nextEntry is _Choice,\n      'Wrong event at test plan index $_currentIndex\\n'\n      'Expected: $nextEntry\\n'\n      'Actual  : $choice\\n',\n    );\n    final expected = nextEntry as _Choice;\n    assert(\n      expected.options.length == choice.options.length,\n      'Expected: a choice of ${expected.options.length} options\\n'\n      'Actual  : a choice of ${choice.options.length} options\\n',\n    );\n    for (var i = 0; i < choice.options.length; i++) {\n      final option1 = expected.options[i];\n      final option2 = choice.options[i];\n      final text1 =\n          (option1.character == null ? '' : '${option1.character}: ') +\n          option1.text +\n          (option1.enabled ? '' : ' [disabled]');\n      final text2 =\n          (option2.character == null ? '' : '${option2.character!.name}: ') +\n          option2.text +\n          (option2.isAvailable ? '' : ' [disabled]');\n      assert(\n        text1 == text2,\n        '\\n'\n        'Expected ($i): $text1\\n'\n        'Actual   ($i): $text2\\n',\n      );\n      assert(\n        option1.enabled == option2.isAvailable,\n        '\\n'\n        'Expected option($i): $option1; available=${option1.enabled}\\n'\n        'Actual   option($i): $option2; available=${option2.isAvailable}\\n',\n      );\n    }\n    _currentIndex++;\n    return expected.selectionIndex - 1;\n  }\n\n  @override\n  void onCommand(UserDefinedCommand command) {\n    assert(\n      !done,\n      'Expected: END OF DIALOGUE\\n'\n      'Actual  : $command',\n    );\n    assert(\n      nextEntry is _Command,\n      'Wrong event at test plan index $_currentIndex\\n'\n      'Expected: \"$nextEntry\"\\n'\n      'Actual  : \"$command\"\\n',\n    );\n    final expected = nextEntry as _Command;\n    final text1 = '<<${expected.name} ${expected.content}>>';\n    final text2 = '<<${command.name} ${command.argumentString}>>';\n    assert(\n      text1 == text2,\n      'Expected line: \"$text1\"\\n'\n      'Actual line  : \"$text2\"\\n',\n    );\n    _currentIndex++;\n  }\n\n  void _parse(String input) {\n    final rxEmpty = RegExp(r'^\\s*(//.*)?$');\n    final rxLine = RegExp(r'^line:\\s+((\\w+):\\s+)?(.*)$');\n    final rxOption = RegExp(r'^option:\\s+((\\w+):\\s+)?(.*?)\\s*(\\[disabled\\])?$');\n    final rxSelect = RegExp(r'^select:\\s+(\\d+)$');\n    final rxRun = RegExp(r'^run: (.*)$');\n    final rxCommand = RegExp(r'command: (\\w+)(?:\\s+(.*))?$');\n\n    final lines = const LineSplitter().convert(input);\n    for (var i = 0; i < lines.length; i++) {\n      final line = lines[i];\n      final match0 = rxEmpty.firstMatch(line);\n      final match1 = rxLine.firstMatch(line);\n      final match2 = rxOption.firstMatch(line);\n      final match3 = rxSelect.firstMatch(line);\n      final match4 = rxRun.firstMatch(line);\n      final match5 = rxCommand.firstMatch(line);\n      if (match0 != null) {\n        continue;\n      } else if (match1 != null) {\n        final name = match1.group(2);\n        final text = match1.group(3)!;\n        _expected.add(_Line(name, text));\n      } else if (match2 != null) {\n        final name = match2.group(2);\n        final text = match2.group(3)!;\n        final enabled = match2.group(4) == null;\n        _expected.add(_Option(name, text, enabled: enabled));\n      } else if (match3 != null) {\n        final index = int.parse(match3.group(1)!);\n        final options = <_Option>[];\n        while (_expected.isNotEmpty && _expected.last is _Option) {\n          options.insert(0, _expected.removeLast() as _Option);\n        }\n        _expected.add(_Choice(options, index));\n      } else if (match4 != null) {\n        startNode = match4.group(1)!;\n      } else if (match5 != null) {\n        _expected.add(_Command(match5.group(1)!, match5.group(2) ?? ''));\n      } else {\n        throw 'Unrecognized test plan line $i: \"$line\"';\n      }\n    }\n  }\n}\n\nclass _Line {\n  const _Line(this.character, this.text);\n  final String? character;\n  final String text;\n\n  @override\n  String toString() => 'Line($character: $text)';\n}\n\nclass _Choice {\n  const _Choice(this.options, this.selectionIndex);\n  final List<_Option> options;\n  final int selectionIndex;\n\n  @override\n  String toString() => 'Choice($options)';\n}\n\nclass _Option {\n  const _Option(this.character, this.text, {required this.enabled});\n  final String? character;\n  final String text;\n  final bool enabled;\n\n  @override\n  String toString() => 'Option($character: $text [$enabled])';\n}\n\nclass _Command {\n  const _Command(this.name, this.content);\n  final String name;\n  final String content;\n  @override\n  String toString() => 'Command($name, \"$content\")';\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/utils.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nMatcher hasSyntaxError(String message) {\n  return throwsA(\n    isA<SyntaxError>().having((e) => e.toString(), 'toString', message),\n  );\n}\n\nMatcher hasNameError(String message) {\n  return throwsA(\n    isA<NameError>().having((e) => e.toString(), 'toString', message),\n  );\n}\n\nMatcher hasTypeError(String message) {\n  return throwsA(\n    isA<TypeError>().having((e) => e.toString(), 'toString', message),\n  );\n}\n\nMatcher hasDialogueError(String message) {\n  return throwsA(\n    isA<DialogueError>().having(\n      (e) => e.toString(),\n      'toString',\n      'DialogueError: $message',\n    ),\n  );\n}\n\nMatcher throwsAssertionError(String message) {\n  return throwsA(\n    isA<AssertionError>().having((e) => e.message, 'message', message),\n  );\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/variable_storage_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:jenny/src/structure/expressions/expression.dart';\nimport 'package:test/test.dart';\n\nimport 'utils.dart';\n\nvoid main() {\n  group('VariableStorage', () {\n    test('empty', () {\n      final storage = VariableStorage();\n      expect(storage.length, 0);\n      expect(storage.isEmpty, true);\n      expect(storage.isNotEmpty, false);\n      expect(storage.hasVariable('x'), false);\n    });\n\n    test('store numeric variable', () {\n      final storage = VariableStorage();\n      storage.setVariable('x', 42);\n      expect(storage.hasVariable('x'), true);\n      expect(storage.getVariable('x'), 42);\n      expect(storage.getVariableType('x'), ExpressionType.numeric);\n      expect(storage.getNumericValue('x'), 42);\n    });\n\n    test('store string variable', () {\n      final storage = VariableStorage();\n      storage.setVariable('s', 'Flame');\n      expect(storage.hasVariable('s'), true);\n      expect(storage.getVariable('s'), 'Flame');\n      expect(storage.getVariableType('s'), ExpressionType.string);\n      expect(storage.getStringValue('s'), 'Flame');\n    });\n\n    test('store boolean variable', () {\n      final storage = VariableStorage();\n      storage.setVariable('b', true);\n      expect(storage.hasVariable('b'), true);\n      expect(storage.getVariable('b'), true);\n      expect(storage.getVariableType('b'), ExpressionType.boolean);\n      expect(storage.getBooleanValue('b'), true);\n    });\n\n    test('unknown variable type', () {\n      final storage = _BadVariableStorage();\n      expect(storage.hasVariable('x'), true);\n      expect(storage.getVariableType('x'), ExpressionType.unknown);\n    });\n\n    test('setVariable to unknown type', () {\n      final storage = VariableStorage();\n      expect(\n        () => storage.setVariable('xyz', [1, 2, 3]),\n        hasDialogueError(\n          'Cannot set variable xyz to a value with type List<int>',\n        ),\n      );\n    });\n\n    test('setVariable to a different type', () {\n      final storage = VariableStorage();\n      storage.setVariable('xyz', true);\n      expect(\n        () => storage.setVariable('xyz', 7),\n        hasDialogueError(\n          'Redefinition of variable xyz from type bool to int is not allowed',\n        ),\n      );\n    });\n\n    test('remove a variable', () {\n      final storage = VariableStorage();\n      storage.setVariable('x', 42);\n      expect(storage.hasVariable('x'), true);\n\n      storage.remove('x');\n\n      expect(storage.hasVariable('x'), false);\n    });\n\n    test('clear variables except node visits', () {\n      final storage = VariableStorage();\n      storage.setVariable('x', 42);\n      storage.setVariable('@node_name1', 1);\n\n      storage.clear();\n\n      expect(storage.hasVariable('x'), false);\n      expect(storage.hasVariable('@node_name1'), true);\n    });\n\n    test('clear variables including node visits', () {\n      final storage = VariableStorage();\n      storage.setVariable('x', 42);\n      storage.setVariable('@node_name1', 1);\n\n      storage.clear(clearNodeVisits: true);\n\n      expect(storage.isEmpty, true);\n    });\n  });\n}\n\nclass _BadVariableStorage extends VariableStorage {\n  _BadVariableStorage() {\n    variables['x'] = null;\n  }\n}\n"
  },
  {
    "path": "packages/flame_jenny/jenny/test/yarn_project_test.dart",
    "content": "import 'package:jenny/jenny.dart';\nimport 'package:test/test.dart';\n\nimport 'utils.dart';\n\nvoid main() {\n  group('YarnProject', () {\n    test('set a locale', () {\n      final yarn = YarnProject();\n      expect(yarn.locale, 'en');\n      yarn.locale = 'uk';\n      expect(yarn.locale, 'uk');\n    });\n\n    test('set an unknown locale', () {\n      final yarn = YarnProject();\n      expect(\n        () => yarn.locale = 'Klingon',\n        hasDialogueError('Unknown locale \"Klingon\"'),\n      );\n    });\n\n    test('set locale after parsing', () {\n      final yarn = YarnProject()..parse('title:A\\n---\\n===\\n');\n      expect(\n        () => yarn.locale = 'sv',\n        hasDialogueError(\n          'The locale cannot be changed after nodes have been added',\n        ),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_jenny/pubspec.yaml",
    "content": "name: flame_jenny\nresolution: workspace\ndescription: Flame bridge package to Jenny (a Dart port of Yarn Spinner).\nversion: 1.0.0\npublish_to: none\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_jenny\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  collection: ^1.17.1\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  jenny: ^1.5.1\n  meta: ^1.12.0\n\ndev_dependencies:\n  dartdoc: ^9.0.0\n  flame_lint: ^1.4.3\n  test: any\n"
  },
  {
    "path": "packages/flame_kenney_xml/CHANGELOG.md",
    "content": "## 0.1.2\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n - **FEAT**: Support package argument in asset loading methods and widgets ([#3835](https://github.com/flame-engine/flame/issues/3835)). ([3f6f95b0](https://github.com/flame-engine/flame/commit/3f6f95b0225df9e4035c74de6cfad501e8c45167))\n\n## 0.1.1+20\n\n - Update a dependency to the latest release.\n\n## 0.1.1+19\n\n - Update a dependency to the latest release.\n\n## 0.1.1+18\n\n - Update a dependency to the latest release.\n\n## 0.1.1+17\n\n - Update a dependency to the latest release.\n\n## 0.1.1+16\n\n - Update a dependency to the latest release.\n\n## 0.1.1+15\n\n - Update a dependency to the latest release.\n\n## 0.1.1+14\n\n - Update a dependency to the latest release.\n\n## 0.1.1+13\n\n - Update a dependency to the latest release.\n\n## 0.1.1+12\n\n - Update a dependency to the latest release.\n\n## 0.1.1+11\n\n - Update a dependency to the latest release.\n\n## 0.1.1+10\n\n - Update a dependency to the latest release.\n\n## 0.1.1+9\n\n - Update a dependency to the latest release.\n\n## 0.1.1+8\n\n - Update a dependency to the latest release.\n\n## 0.1.1+7\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 0.1.1+6\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 0.1.1+5\n\n - Update a dependency to the latest release.\n\n## 0.1.1+4\n\n - Update a dependency to the latest release.\n\n## 0.1.1+3\n\n - Update a dependency to the latest release.\n\n## 0.1.1+2\n\n - Update a dependency to the latest release.\n\n## 0.1.1+1\n\n - Update a dependency to the latest release.\n\n## 0.1.1\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 0.1.0\n\n - **FEAT**: Add initial version of `flame_kenney_xml`.\n\n"
  },
  {
    "path": "packages/flame_kenney_xml/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/flame_kenney_xml/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nAdds support for parsing XML sprite sheets from https://kenney.nl, and other sprite sheets on the same format.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_kenney_xml\" ><img src=\"https://img.shields.io/pub/v/flame_kenney_xml.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n\n## Getting started\n\nTo get started, first add `flame_kenney_xml` as a dependency in your flutter project.\n\n```bash\nflutter pub add flame_kenney_xml\n```\n\nThen place the `spritesheet.json` in `assets/` and `spritesheet.png` in `assets/images/`\n(or whatever the names of the files are).\n\nThen load the image and the spritesheet using:\n\n```dart\nfinal spritesheet = await XmlSpriteSheet.load(\n  image: 'spritesheet.png',\n  xml: 'spritesheet.xml`,\n);\n```\n"
  },
  {
    "path": "packages/flame_kenney_xml/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options.yaml\n"
  },
  {
    "path": "packages/flame_kenney_xml/example/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\nmigrate_working_dir/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.pub-cache/\n.pub/\n/build/\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Android Studio will place build artifacts here\n/android/app/debug\n/android/app/profile\n/android/app/release\n"
  },
  {
    "path": "packages/flame_kenney_xml/example/README.md",
    "content": "# flame_kenney_xml example\n\nAn example of how to use the flame_kenney_xml package.\n"
  },
  {
    "path": "packages/flame_kenney_xml/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options.yaml\n"
  },
  {
    "path": "packages/flame_kenney_xml/example/assets/license.txt",
    "content": "\r\n###############################################################################\r\n\r\n\tPhysics asset pack by Kenney Vleugels (www.kenney.nl)\r\n\r\n\t\t\t------------------------------\r\n\r\n\t\t\t        License (CC0)\r\n\t       http://creativecommons.org/publicdomain/zero/1.0/\r\n\r\n\tYou may use these graphics in personal and commercial projects.\r\n\tCredit (Kenney or www.kenney.nl) would be nice but is not mandatory.\r\n\r\n###############################################################################"
  },
  {
    "path": "packages/flame_kenney_xml/example/assets/spritesheet_stone.xml",
    "content": "<TextureAtlas imagePath=\"sheet.png\">\n\t<SubTexture name=\"elementStone000.png\" x=\"220\" y=\"720\" width=\"140\" height=\"70\"/>\n\t<SubTexture name=\"elementStone001.png\" x=\"570\" y=\"640\" width=\"70\" height=\"70\"/>\n\t<SubTexture name=\"elementStone002.png\" x=\"710\" y=\"930\" width=\"70\" height=\"70\"/>\n\t<SubTexture name=\"elementStone003.png\" x=\"500\" y=\"70\" width=\"140\" height=\"70\"/>\n\t<SubTexture name=\"elementStone004.png\" x=\"710\" y=\"640\" width=\"70\" height=\"70\"/>\n\t<SubTexture name=\"elementStone005.png\" x=\"710\" y=\"570\" width=\"70\" height=\"70\"/>\n\t<SubTexture name=\"elementStone006.png\" x=\"360\" y=\"500\" width=\"140\" height=\"70\"/>\n\t<SubTexture name=\"elementStone007.png\" x=\"710\" y=\"360\" width=\"70\" height=\"70\"/>\n\t<SubTexture name=\"elementStone008.png\" x=\"710\" y=\"290\" width=\"70\" height=\"70\"/>\n\t<SubTexture name=\"elementStone009.png\" x=\"360\" y=\"710\" width=\"140\" height=\"70\"/>\n\t<SubTexture name=\"elementStone010.png\" x=\"710\" y=\"0\" width=\"70\" height=\"70\"/>\n\t<SubTexture name=\"elementStone011.png\" x=\"640\" y=\"940\" width=\"70\" height=\"70\"/>\n\t<SubTexture name=\"elementStone012.png\" x=\"500\" y=\"570\" width=\"140\" height=\"70\"/>\n\t<SubTexture name=\"elementStone013.png\" x=\"0\" y=\"70\" width=\"220\" height=\"70\"/>\n\t<SubTexture name=\"elementStone014.png\" x=\"640\" y=\"580\" width=\"70\" height=\"70\"/>\n\t<SubTexture name=\"elementStone015.png\" x=\"500\" y=\"280\" width=\"140\" height=\"70\"/>\n\t<SubTexture name=\"elementStone016.png\" x=\"0\" y=\"280\" width=\"220\" height=\"70\"/>\n\t<SubTexture name=\"elementStone017.png\" x=\"640\" y=\"220\" width=\"70\" height=\"140\"/>\n\t<SubTexture name=\"elementStone018.png\" x=\"500\" y=\"140\" width=\"140\" height=\"140\"/>\n\t<SubTexture name=\"elementStone019.png\" x=\"0\" y=\"630\" width=\"220\" height=\"140\"/>\n\t<SubTexture name=\"elementStone020.png\" x=\"710\" y=\"710\" width=\"70\" height=\"220\"/>\n\t<SubTexture name=\"elementStone021.png\" x=\"500\" y=\"350\" width=\"140\" height=\"220\"/>\n\t<SubTexture name=\"elementStone022.png\" x=\"710\" y=\"430\" width=\"70\" height=\"140\"/>\n\t<SubTexture name=\"elementStone023.png\" x=\"360\" y=\"360\" width=\"140\" height=\"140\"/>\n\t<SubTexture name=\"elementStone024.png\" x=\"0\" y=\"840\" width=\"220\" height=\"140\"/>\n\t<SubTexture name=\"elementStone025.png\" x=\"640\" y=\"0\" width=\"70\" height=\"220\"/>\n\t<SubTexture name=\"elementStone026.png\" x=\"360\" y=\"780\" width=\"140\" height=\"220\"/>\n\t<SubTexture name=\"elementStone027.png\" x=\"500\" y=\"640\" width=\"70\" height=\"70\"/>\n\t<SubTexture name=\"elementStone028.png\" x=\"360\" y=\"70\" width=\"140\" height=\"70\"/>\n\t<SubTexture name=\"elementStone029.png\" x=\"220\" y=\"0\" width=\"220\" height=\"70\"/>\n\t<SubTexture name=\"elementStone030.png\" x=\"500\" y=\"780\" width=\"70\" height=\"70\"/>\n\t<SubTexture name=\"elementStone031.png\" x=\"440\" y=\"0\" width=\"140\" height=\"70\"/>\n\t<SubTexture name=\"elementStone032.png\" x=\"0\" y=\"770\" width=\"220\" height=\"70\"/>\n\t<SubTexture name=\"elementStone033.png\" x=\"500\" y=\"850\" width=\"70\" height=\"140\"/>\n\t<SubTexture name=\"elementStone034.png\" x=\"360\" y=\"570\" width=\"140\" height=\"140\"/>\n\t<SubTexture name=\"elementStone035.png\" x=\"0\" y=\"490\" width=\"220\" height=\"140\"/>\n\t<SubTexture name=\"elementStone036.png\" x=\"710\" y=\"70\" width=\"70\" height=\"220\"/>\n\t<SubTexture name=\"elementStone037.png\" x=\"360\" y=\"140\" width=\"140\" height=\"220\"/>\n\t<SubTexture name=\"elementStone038.png\" x=\"570\" y=\"780\" width=\"70\" height=\"140\"/>\n\t<SubTexture name=\"elementStone039.png\" x=\"220\" y=\"860\" width=\"140\" height=\"140\"/>\n\t<SubTexture name=\"elementStone040.png\" x=\"0\" y=\"140\" width=\"220\" height=\"140\"/>\n\t<SubTexture name=\"elementStone041.png\" x=\"640\" y=\"720\" width=\"70\" height=\"220\"/>\n\t<SubTexture name=\"elementStone042.png\" x=\"220\" y=\"500\" width=\"140\" height=\"220\"/>\n\t<SubTexture name=\"elementStone043.png\" x=\"500\" y=\"710\" width=\"70\" height=\"70\"/>\n\t<SubTexture name=\"elementStone044.png\" x=\"220\" y=\"290\" width=\"140\" height=\"70\"/>\n\t<SubTexture name=\"elementStone045.png\" x=\"570\" y=\"710\" width=\"70\" height=\"70\"/>\n\t<SubTexture name=\"elementStone046.png\" x=\"570\" y=\"920\" width=\"70\" height=\"70\"/>\n\t<SubTexture name=\"elementStone047.png\" x=\"220\" y=\"790\" width=\"140\" height=\"70\"/>\n\t<SubTexture name=\"elementStone048.png\" x=\"0\" y=\"0\" width=\"220\" height=\"70\"/>\n\t<SubTexture name=\"elementStone049.png\" x=\"780\" y=\"0\" width=\"70\" height=\"140\"/>\n\t<SubTexture name=\"elementStone050.png\" x=\"220\" y=\"360\" width=\"140\" height=\"140\"/>\n\t<SubTexture name=\"elementStone051.png\" x=\"0\" y=\"350\" width=\"220\" height=\"140\"/>\n\t<SubTexture name=\"elementStone052.png\" x=\"640\" y=\"360\" width=\"70\" height=\"220\"/>\n\t<SubTexture name=\"elementStone053.png\" x=\"220\" y=\"70\" width=\"140\" height=\"220\"/>\n\t<SubTexture name=\"elementStone054.png\" x=\"640\" y=\"650\" width=\"70\" height=\"70\"/>\n</TextureAtlas>"
  },
  {
    "path": "packages/flame_kenney_xml/example/lib/main.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_kenney_xml/flame_kenney_xml.dart';\nimport 'package:flutter/material.dart';\n\n/// A simple game that adds a random sprite component created from a kenney.nl\n/// sprite sheet to the screen when tapped.\nvoid main() {\n  runApp(\n    GameWidget.controlled(\n      gameFactory: () => FlameGame(world: KenneyWorld()),\n    ),\n  );\n}\n\nclass KenneyWorld extends World with TapCallbacks {\n  late final XmlSpriteSheet spritesheet;\n\n  @override\n  Future<void> onLoad() async {\n    spritesheet = await XmlSpriteSheet.load(\n      imagePath: 'spritesheet_stone.png',\n      xmlPath: 'spritesheet_stone.xml',\n    );\n    add(randomSpriteComponent());\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) {\n    add(randomSpriteComponent(position: event.localPosition));\n  }\n\n  SpriteComponent randomSpriteComponent({Vector2? position}) {\n    final name = spritesheet.spriteNames.random();\n    return SpriteComponent(\n      sprite: spritesheet.getSprite(name),\n      position: position,\n      anchor: Anchor.center,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_kenney_xml/example/pubspec.yaml",
    "content": "name: flame_kenney_xml_example\nresolution: workspace\ndescription: \"An example for the `XmlSpriteSheet` used to load kenney.nl assets.\"\n\npublish_to: \"none\"\n\nversion: 1.0.0\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_kenney_xml: ^0.1.2\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n\nflutter:\n  assets:\n    - assets/\n    - assets/images/\n"
  },
  {
    "path": "packages/flame_kenney_xml/lib/flame_kenney_xml.dart",
    "content": "import 'package:flame/cache.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/flame.dart';\nimport 'package:xml/xml.dart';\nimport 'package:xml/xpath.dart';\n\n/// A sprite sheet loaded from an XML file and an image.\n///\n/// The XML file must be in the format of a ShoeBox XML file, formatted in the\n/// same way as the Kenney.nl sprite sheets.\n/// https://twitter.com/KenneyNL/status/1777429120936202344\nclass XmlSpriteSheet {\n  XmlSpriteSheet({\n    required this.image,\n    required String xml,\n  }) {\n    final document = XmlDocument.parse(xml);\n    // ignore: experimental_member_use\n    for (final node in document.xpath('//TextureAtlas/SubTexture')) {\n      final name = node.getAttribute('name')!;\n      final x = double.parse(node.getAttribute('x')!);\n      final y = double.parse(node.getAttribute('y')!);\n      final width = double.parse(node.getAttribute('width')!);\n      final height = double.parse(node.getAttribute('height')!);\n      _spriteBoundaries[name] = Rect.fromLTWH(x, y, width, height);\n    }\n  }\n\n  /// Load an [XmlSpriteSheet] from an image and an XML file.\n  ///\n  /// The [imagePath] should be in relation to `assets/images/`.\n  /// The [xmlPath] should be in relation to `assets/`.\n  static Future<XmlSpriteSheet> load({\n    required String imagePath,\n    required String xmlPath,\n    Images? imageCache,\n    AssetsCache? assetsCache,\n    String? package,\n  }) async {\n    final image = await (imageCache ?? Flame.images).load(\n      imagePath,\n      package: package,\n    );\n    final xml = await (assetsCache ?? Flame.assets).readFile(\n      xmlPath,\n      package: package,\n    );\n    return XmlSpriteSheet(image: image, xml: xml);\n  }\n\n  final Image image;\n  final _spriteBoundaries = <String, Rect>{};\n\n  late final List<String> spriteNames = _spriteBoundaries.keys.toList();\n\n  /// Get a sprite from the sprite sheet by its name.\n  ///\n  /// Throws an [ArgumentError] if the sprite is not found.\n  Sprite getSprite(String name) {\n    final rect = _spriteBoundaries[name];\n    if (rect == null) {\n      throw ArgumentError('Sprite $name not found');\n    }\n    return Sprite(\n      image,\n      srcPosition: rect.topLeft.toVector2(),\n      srcSize: rect.size.toVector2(),\n    );\n  }\n\n  /// Get a random sprite from the sprite sheet.\n  Sprite getRandomSprite() => getSprite(spriteNames.random());\n}\n"
  },
  {
    "path": "packages/flame_kenney_xml/pubspec.yaml",
    "content": "name: flame_kenney_xml\nresolution: workspace\ndescription: \"Support for Kenney XML spritesheets for the Flame game engine. This package parses XML files produced by Kenney.\"\nversion: 0.1.2\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_kenney_xml\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - spritesheet\n  - kenney\n  - tilemap\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  xml: ^6.5.0\n\ndev_dependencies:\n  build_runner: ^2.4.11\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n  mockito: ^5.4.4\n"
  },
  {
    "path": "packages/flame_kenney_xml/test/flame_kenney_xml_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/cache.dart';\nimport 'package:flame_kenney_xml/flame_kenney_xml.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mockito/mockito.dart';\n\nclass _MockImage extends Mock implements Image {\n  @override\n  int get width => 100;\n\n  @override\n  int get height => 100;\n}\n\nclass _MockImages extends Mock implements Images {\n  @override\n  Future<Image> load(String fileName, {String? key, String? package}) async {\n    return _MockImage();\n  }\n}\n\nclass _MockAssetsCache extends Mock implements AssetsCache {\n  @override\n  Future<String> readFile(String fileName, {String? package}) async {\n    return '''\n    <TextureAtlas imagePath=\"spritesheet.png\">\n      <SubTexture name=\"sprite1\" x=\"0\" y=\"0\" width=\"32\" height=\"32\"/>\n      <SubTexture name=\"sprite2\" x=\"32\" y=\"0\" width=\"32\" height=\"32\"/>\n    </TextureAtlas>\n    ''';\n  }\n}\n\nvoid main() {\n  group('XmlSpriteSheet', () {\n    test('creation from constructor', () {\n      final spritesheet = XmlSpriteSheet(\n        image: _MockImage(),\n        xml: '''\n        <TextureAtlas imagePath=\"spritesheet.png\">\n          <SubTexture name=\"sprite1\" x=\"0\" y=\"0\" width=\"32\" height=\"32\"/>\n          <SubTexture name=\"sprite2\" x=\"32\" y=\"0\" width=\"32\" height=\"32\"/>\n        </TextureAtlas>\n        ''',\n      );\n\n      expect(spritesheet.spriteNames, equals(['sprite1', 'sprite2']));\n      final sprite1 = spritesheet.getSprite('sprite1');\n      expect(sprite1.src, equals(const Rect.fromLTWH(0, 0, 32, 32)));\n      final sprite2 = spritesheet.getSprite('sprite2');\n      expect(sprite2.src, equals(const Rect.fromLTWH(32, 0, 32, 32)));\n    });\n\n    test('creation from load method', () async {\n      final mockImages = _MockImages();\n      final mockAssetsCache = _MockAssetsCache();\n\n      final spritesheet = await XmlSpriteSheet.load(\n        imagePath: 'spritesheet_stone.png',\n        xmlPath: 'spritesheet_stone.xml',\n        imageCache: mockImages,\n        assetsCache: mockAssetsCache,\n      );\n\n      expect(spritesheet.spriteNames, equals(['sprite1', 'sprite2']));\n      final sprite1 = spritesheet.getSprite('sprite1');\n      expect(sprite1.src, equals(const Rect.fromLTWH(0, 0, 32, 32)));\n      final sprite2 = spritesheet.getSprite('sprite2');\n      expect(sprite2.src, equals(const Rect.fromLTWH(32, 0, 32, 32)));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_lint/CHANGELOG.md",
    "content": "## 1.4.3\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 1.4.2\n\n - **FIX**: Update flame_lint to use lints 6.0.0 ([#3612](https://github.com/flame-engine/flame/issues/3612)). ([ba5f6789](https://github.com/flame-engine/flame/commit/ba5f6789bed68e4cc7ca95584e35ed62d0111da2))\n\n## 1.4.1\n\n - **FIX**: Remove unnecessary breaks ([#3638](https://github.com/flame-engine/flame/issues/3638)). ([ea29929c](https://github.com/flame-engine/flame/commit/ea29929cd86ed00407f2d2aa69dcf6f34ffc5bbd))\n\n## 1.4.0\n\n - **FEAT**: Preserve trailing commas in Dart ^3.8.0 ([#3607](https://github.com/flame-engine/flame/issues/3607)). ([433829cb](https://github.com/flame-engine/flame/commit/433829cbdaafa9b1e9f0250b68f5143ec1a4d562))\n\n## 1.3.0\n\n - **FEAT**: Bump to new lint package ([#3545](https://github.com/flame-engine/flame/issues/3545)). ([bf6ee518](https://github.com/flame-engine/flame/commit/bf6ee51897591b7ad6e5f9da2193b1eeeaf026f4))\n\n## 1.2.3\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 1.2.2\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 1.2.1\n\n - **FIX**: Update version of lints to comply with new pub requirements ([#3223](https://github.com/flame-engine/flame/issues/3223)). ([1b0bee72](https://github.com/flame-engine/flame/commit/1b0bee726b5937f73d4be5e304bc8780aa3ca6f0))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 1.2.0\n\n - **FEAT**: Expand flame_lint to respect required pub.dev checks ([#3139](https://github.com/flame-engine/flame/issues/3139)). ([6e80bf5e](https://github.com/flame-engine/flame/commit/6e80bf5e679d1cdeeb9362d4103690b0b381161d))\n\n## 1.1.2\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n## 1.1.1\n\n - **REFACTOR**: Enable new DCM rule: avoid-cascade-after-if-null ([#2676](https://github.com/flame-engine/flame/issues/2676)). ([158fc34c](https://github.com/flame-engine/flame/commit/158fc34cae858cf8d0b5d3b5155763e02454779a))\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n\n## 1.1.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **PERF**: Pool `CollisionProspect`s and remove some list creations from the collision detection ([#2625](https://github.com/flame-engine/flame/issues/2625)). ([e430b6cd](https://github.com/flame-engine/flame/commit/e430b6cdf2e6be52bf384efb3428bcb41ae13d30))\n\n## 1.0.0\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n## 0.2.0+2\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n## 0.2.0+1\n\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n\n## 0.2.0\n\n - Removed invariant_booleans\n\n## 0.1.3\n\n - **FEAT**: Add avoid_final_parameters, depend_on_referenced_packages, unnecessary_to_list_in_spreads ([#1927](https://github.com/flame-engine/flame/issues/1927)). ([deccb434](https://github.com/flame-engine/flame/commit/deccb4349d38b6a91ccf5bdf229980b2a3296ce5))\n\n## 0.1.2\n\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **REFACTOR**: Add a few more rules to flame_lint, including use_key_in_widget_constructors ([#1248](https://github.com/flame-engine/flame/issues/1248)). ([bac6c8a4](https://github.com/flame-engine/flame/commit/bac6c8a4469f2c5c2926335f2f589eec9b1a5b5b))\n - **FIX**: Upgrade dartdoc (upgrade analyzer transitive dependency) ([#1630](https://github.com/flame-engine/flame/issues/1630)). ([6da8adb2](https://github.com/flame-engine/flame/commit/6da8adb28cffd8fcb43e6bf8a33aae22578f1b40))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Add non_constant_identifier_names rule ([#1656](https://github.com/flame-engine/flame/issues/1656)). ([1b40de09](https://github.com/flame-engine/flame/commit/1b40de094f4e66be7622d077a6e18cecf1964dde))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n - **DOCS**: Fix various dartdoc warnings ([#1353](https://github.com/flame-engine/flame/issues/1353)). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n\n## 0.1.1\n\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **REFACTOR**: Add a few more rules to flame_lint, including use_key_in_widget_constructors ([#1248](https://github.com/flame-engine/flame/issues/1248)). ([bac6c8a4](https://github.com/flame-engine/flame/commit/bac6c8a4469f2c5c2926335f2f589eec9b1a5b5b))\n - **FIX**: Upgrade dartdoc (upgrade analyzer transitive dependency) ([#1630](https://github.com/flame-engine/flame/issues/1630)). ([6da8adb2](https://github.com/flame-engine/flame/commit/6da8adb28cffd8fcb43e6bf8a33aae22578f1b40))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Add non_constant_identifier_names rule ([#1656](https://github.com/flame-engine/flame/issues/1656)). ([1b40de09](https://github.com/flame-engine/flame/commit/1b40de094f4e66be7622d077a6e18cecf1964dde))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n - **DOCS**: Fix various dartdoc warnings ([#1353](https://github.com/flame-engine/flame/issues/1353)). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n\n# CHANGELOG\n\n## [0.1.0]\n - Made the package publishable\n\n## [0.0.1]\n - Initial release\n"
  },
  {
    "path": "packages/flame_lint/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_lint/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nA strict and satisfying lint for any <a href=\"https://github.com/flame-engine/flame\">Flame</a>, Flutter or Dart project.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_lint\" ><img src=\"https://img.shields.io/pub/v/flame_lint.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n\n# flame_lint\n\nPackage with the lint rules used in all Flame Engine and Blue Fire projects.\n\n\n## Install\n\n- Add `flame_lint` as a dependency in your `pubspec.yaml`\n(check on [pub](https://pub.dev/packages/flame_lint/install) if you don't know how).\n\n- Then include it on your `analysis_options.yaml` file:\n\n```yaml\ninclude: package:flame_lint/analysis_options.yaml\n```\n"
  },
  {
    "path": "packages/flame_lint/analysis_options.yaml",
    "content": "include: lib/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_lint/lib/analysis_options.yaml",
    "content": "# Source of linter options:\n# https://dart-lang.github.io/linter/lints/options/options.html\n\ninclude: package:lints/core.yaml\n\nformatter:\n  trailing_commas: preserve\n\nanalyzer:\n  exclude:\n    - \"**/*.g.dart\"\n  language:\n    strict-casts: true\n    strict-raw-types: false\n\nlinter:\n  rules:\n    - always_declare_return_types\n    - always_put_control_body_on_new_line\n    - always_put_required_named_parameters_first\n    - always_use_package_imports\n    - annotate_overrides\n    - avoid_bool_literals_in_conditional_expressions\n    - avoid_double_and_int_checks\n    - avoid_catches_without_on_clauses\n    - avoid_dynamic_calls\n    - avoid_empty_else\n    - avoid_equals_and_hash_code_on_mutable_classes\n    - avoid_escaping_inner_quotes\n    - avoid_field_initializers_in_const_classes\n    - avoid_final_parameters\n    - avoid_init_to_null\n    - avoid_js_rounded_ints\n    - avoid_multiple_declarations_per_line\n    - avoid_positional_boolean_parameters\n    - avoid_print\n    - avoid_private_typedef_functions\n    - avoid_redundant_argument_values\n    - avoid_relative_lib_imports\n    - avoid_return_types_on_setters\n    - avoid_returning_null_for_void\n    - avoid_returning_this\n    - avoid_shadowing_type_parameters\n    - avoid_single_cascade_in_expression_statements\n    - avoid_slow_async_io\n    - avoid_type_to_string\n    - avoid_types_as_parameter_names\n    - avoid_unnecessary_containers\n    - avoid_unused_constructor_parameters\n    - avoid_void_async\n    - await_only_futures\n    - camel_case_extensions\n    - camel_case_types\n    - cancel_subscriptions\n    - cast_nullable_to_non_nullable\n    - close_sinks\n    - collection_methods_unrelated_type\n    - comment_references\n    - constant_identifier_names\n    - control_flow_in_finally\n    - curly_braces_in_flow_control_structures\n    - dangling_library_doc_comments\n    - depend_on_referenced_packages\n    - deprecated_consistency\n    - directives_ordering\n    - do_not_use_environment\n    - empty_catches\n    - empty_constructor_bodies\n    - empty_statements\n    - exhaustive_cases\n    - file_names\n    - flutter_style_todos\n    - hash_and_equals\n    - implementation_imports\n    - join_return_with_assignment\n    - library_names\n    - library_prefixes\n    - library_private_types_in_public_api\n    - lines_longer_than_80_chars\n    - literal_only_boolean_expressions\n    - missing_whitespace_between_adjacent_strings\n    - no_adjacent_strings_in_list\n    - no_duplicate_case_values\n    - no_leading_underscores_for_library_prefixes\n    - no_leading_underscores_for_local_identifiers\n    - no_runtimeType_toString\n    - non_constant_identifier_names\n    - noop_primitive_operations\n    - null_closures\n    - omit_local_variable_types\n    - package_names\n    - package_prefixed_library_names\n    - parameter_assignments\n    - prefer_adjacent_string_concatenation\n    - prefer_asserts_in_initializer_lists\n    - prefer_collection_literals\n    - prefer_conditional_assignment\n    - prefer_const_constructors\n    - prefer_const_constructors_in_immutables\n    - prefer_const_declarations\n    - prefer_const_literals_to_create_immutables\n    - prefer_constructors_over_static_methods\n    - prefer_contains\n    - prefer_final_fields\n    - prefer_final_in_for_each\n    - prefer_final_locals\n    - prefer_for_elements_to_map_fromIterable\n    - prefer_function_declarations_over_variables\n    - prefer_generic_function_type_aliases\n    - prefer_if_elements_to_conditional_expressions\n    - prefer_if_null_operators\n    - prefer_initializing_formals\n    - prefer_inlined_adds\n    - prefer_interpolation_to_compose_strings\n    - prefer_is_empty\n    - prefer_is_not_empty\n    - prefer_is_not_operator\n    - prefer_iterable_whereType\n    - prefer_null_aware_method_calls\n    - prefer_null_aware_operators\n    - prefer_single_quotes\n    - prefer_spread_collections\n    - prefer_typing_uninitialized_variables\n    - prefer_void_to_null\n    - provide_deprecation_message\n    - recursive_getters\n    - require_trailing_commas\n    - slash_for_doc_comments\n    - sort_child_properties_last\n    - sort_pub_dependencies\n    - sort_unnamed_constructors_first\n    - test_types_in_equals\n    - throw_in_finally\n    - tighten_type_of_initializing_formals\n    - type_annotate_public_apis\n    - type_init_formals\n    - unnecessary_await_in_return\n    - unnecessary_brace_in_string_interps\n    - unnecessary_breaks\n    - unnecessary_const\n    - unnecessary_constructor_name\n    - unnecessary_getters_setters\n    - unnecessary_lambdas\n    - unnecessary_late\n    - unnecessary_new\n    - unnecessary_null_aware_assignments\n    - unnecessary_null_checks\n    - unnecessary_null_in_if_null_operators\n    - unnecessary_nullable_for_final_variable_declarations\n    - unnecessary_overrides\n    - unnecessary_parenthesis\n    - unnecessary_raw_strings\n    - unnecessary_statements\n    - unnecessary_string_escapes\n    - unnecessary_string_interpolations\n    - unnecessary_this\n    - unnecessary_to_list_in_spreads\n    - unrelated_type_equality_checks\n    - use_enums\n    - use_full_hex_values_for_flutter_colors\n    - use_function_type_syntax_for_parameters\n    - use_is_even_rather_than_modulo\n    - use_key_in_widget_constructors\n    - use_late_for_private_fields_and_variables\n    - use_named_constants\n    - use_raw_strings\n    - use_rethrow_when_possible\n    - use_setters_to_change_properties\n    - use_super_parameters\n    - use_test_throws_matchers\n    - valid_regexps\n    - void_checks\n"
  },
  {
    "path": "packages/flame_lint/lib/analysis_options_with_dcm.yaml",
    "content": "include: analysis_options.yaml\n\n# DCM rules defined here: https://dcm.dev/docs/rules/\n\ndart_code_metrics:\n  rules:\n  # dart rules\n    - avoid-cascade-after-if-null\n    - avoid-collection-methods-with-unrelated-types\n    - avoid-throw-in-catch-block\n    - avoid-top-level-members-in-tests:\n        include:\n          - 'test/**/*_test.dart'\n    - avoid-unrelated-type-assertions:\n      ignore-mixins: true\n    - avoid-unnecessary-type-assertions:\n      ignore-mixins: true\n    - double-literal-format\n  # flutter rules\n    - prefer-define-hero-tag"
  },
  {
    "path": "packages/flame_lint/lib/flame_lint.dart",
    "content": "/// The flame_lint package doesn't ship any dart source code.\n///\n/// To enable `flame_lint`,\n/// 1. Add it to your dev_dependencies\n/// ```yaml\n/// dev_dependencies:\n///   flame_lint: VERSION\n/// ```\n///\n/// 2. Include the rules into your `analysis_options.yaml`\n/// ```yaml\n/// include: package:flame_lint/analysis_options.yaml\n/// ```\nlibrary flame_lint;\n"
  },
  {
    "path": "packages/flame_lint/pubspec.yaml",
    "content": "name: flame_lint\nresolution: workspace\ndescription: Flame lint package\nversion: 1.4.3\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_lint\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - lints\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  lints: ^6.0.0\n\ndev_dependencies:\n  dartdoc: ^9.0.0\n"
  },
  {
    "path": "packages/flame_lottie/CHANGELOG.md",
    "content": "## 0.4.2+21\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 0.4.2+20\n\n - Update a dependency to the latest release.\n\n## 0.4.2+19\n\n - Update a dependency to the latest release.\n\n## 0.4.2+18\n\n - Update a dependency to the latest release.\n\n## 0.4.2+17\n\n - Update a dependency to the latest release.\n\n## 0.4.2+16\n\n - Update a dependency to the latest release.\n\n## 0.4.2+15\n\n - Update a dependency to the latest release.\n\n## 0.4.2+14\n\n - Update a dependency to the latest release.\n\n## 0.4.2+13\n\n - Update a dependency to the latest release.\n\n## 0.4.2+12\n\n - Update a dependency to the latest release.\n\n## 0.4.2+11\n\n - Update a dependency to the latest release.\n\n## 0.4.2+10\n\n - Update a dependency to the latest release.\n\n## 0.4.2+9\n\n - Update a dependency to the latest release.\n\n## 0.4.2+8\n\n - Update a dependency to the latest release.\n\n## 0.4.2+7\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 0.4.2+6\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 0.4.2+5\n\n - Update a dependency to the latest release.\n\n## 0.4.2+4\n\n - Update a dependency to the latest release.\n\n## 0.4.2+3\n\n - Update a dependency to the latest release.\n\n## 0.4.2+2\n\n - Update a dependency to the latest release.\n\n## 0.4.2+1\n\n - Update a dependency to the latest release.\n\n## 0.4.2\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 0.4.1\n\n## 0.4.0+1\n\n - Update a dependency to the latest release.\n\n## 0.4.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 0.3.0+8\n\n - Update a dependency to the latest release.\n\n## 0.3.0+7\n\n - Update a dependency to the latest release.\n\n## 0.3.0+6\n\n - Update a dependency to the latest release.\n\n## 0.3.0+5\n\n - Update a dependency to the latest release.\n\n## 0.3.0+4\n\n - Update a dependency to the latest release.\n\n## 0.3.0+3\n\n - Update a dependency to the latest release.\n\n## 0.3.0+2\n\n - **REFACTOR**: Remove unnecessary 'async' keyword across the codebase [DCM] ([#2803](https://github.com/flame-engine/flame/issues/2803)). ([2dfe0e5a](https://github.com/flame-engine/flame/commit/2dfe0e5a431213c7148ab6389e3e8c8dc49fbf3d))\n - **FIX**: Duration in `LottieRenderer` rounds down milliseconds ([#2808](https://github.com/flame-engine/flame/issues/2808)). ([cccae2e1](https://github.com/flame-engine/flame/commit/cccae2e1476de456c15ee3779b746f5fe6dadee2))\n\n## 0.3.0+1\n\n - Update a dependency to the latest release.\n\n## 0.3.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **REFACTOR**: Fix lint issues across the codebase ([#2672](https://github.com/flame-engine/flame/issues/2672)). ([6fe9a247](https://github.com/flame-engine/flame/commit/6fe9a24778fbe1e9cb74ec0d50d71eae7b1a048e))\n - **BREAKING** **FEAT**: Add CameraComponent to FlameGame ([#2740](https://github.com/flame-engine/flame/issues/2740)). ([7c2f4000](https://github.com/flame-engine/flame/commit/7c2f4000761580dbabb5d73b27f64d5819b34e8d))\n\n## 0.2.1+1\n\n - Update a dependency to the latest release.\n\n## 0.2.1\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FEAT**: ComponentKey API ([#2566](https://github.com/flame-engine/flame/issues/2566)). ([b3efb612](https://github.com/flame-engine/flame/commit/b3efb612cb3cb77f69bc030e9ba71516348035d2))\n\n## 0.2.0+3\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n## 0.2.0+2\n\n - Update a dependency to the latest release.\n\n## 0.2.0+1\n\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n\n## 0.2.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Depend on test: any for flame_test ([#2207](https://github.com/flame-engine/flame/issues/2207)). ([acfd418d](https://github.com/flame-engine/flame/commit/acfd418d882ee6872f3aa9961c39680ec123c2e6))\n - **BREAKING** **REFACTOR**: The method `onLoad()` now returns `FutureOr<void>` ([#2228](https://github.com/flame-engine/flame/issues/2228)). ([d898b539](https://github.com/flame-engine/flame/commit/d898b539f734d3e14c47990ef0727043a0e32efb))\n\n## 0.1.1\n\n - **FEAT**: Lottie bridge package ([#2157](https://github.com/flame-engine/flame/issues/2157)). ([3a73d145](https://github.com/flame-engine/flame/commit/3a73d1456c01937234f0503fd077193884912fbb))\n\n"
  },
  {
    "path": "packages/flame_lottie/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_lottie/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nAdds support for <a href=\"https://github.com/airbnb/lottie-android\">Lottie animations</a> to your <a href=\"https://github.com/flame-engine/flame\">Flame</a> games.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_lottie\" ><img src=\"https://img.shields.io/pub/v/flame_lottie.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n\n# flame_lottie\n\nThis package allows you to load and add Lottie animations to your Flame game.\n\n> Lottie is a mobile library for Android and iOS that parses Adobe After Effects animations\nexported as json with Bodymovin and renders them natively on mobile!\n\nSource: [lottie-android](https://github.com/airbnb/lottie-android) on Github\n\n\nThe native Lottie libraries (such as [lottie-android](https://github.com/airbnb/lottie-android))\nare maintained by **Airbnb**.\n\nThe Flutter package ``lottie``, on which this wrapper is based on, is by **xaha.dev** and can be\nfound on [pub dev](https://pub.dev/packages/lottie).\n\n\n## Usage\n\nTo use it in your game you just need to add `flame_lottie` to your pubspec.yaml.\n\nSimply load the Lottie animation using the **loadLottie** method and the\n[LottieBuilder](https://pub.dev/documentation/lottie/latest/lottie/LottieBuilder-class.html). It\nallows all the various ways of loading a Lottie file:\n\n- [Lottie.asset](https://pub.dev/documentation/lottie/latest/lottie/Lottie/asset.html), for\nobtaining a Lottie file from an AssetBundle using a key.\n- [Lottie.network](https://pub.dev/documentation/lottie/latest/lottie/Lottie/network.html), for\nobtaining a lottie file from a URL.\n- [Lottie.file](https://pub.dev/documentation/lottie/latest/lottie/Lottie/file.html), for obtaining\n a lottie file from a File.\n- [Lottie.memory](https://pub.dev/documentation/lottie/latest/lottie/Lottie/memory.html), for\nobtaining a lottie file from a Uint8List.\n\n... and add it as `LottieComponent` to your flame 🔥 game.\n\nExample:\n\n```dart\nclass MyGame extends FlameGame {\n  ...\n  @override\n  Future<void> onLoad() async {\n    final asset = Lottie.asset('assets/LottieLogo1.json');\n    final animation = await loadLottie(asset);\n    add(\n        LottieComponent(\n            composition: animation,\n            repeating: true, // continuously loop the animation\n        ),\n    );\n  }\n  ...\n}\n```\n"
  },
  {
    "path": "packages/flame_lottie/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_lottie/example/README.md",
    "content": "# flame_lottie example\n\nAn example for using the `flame_lottie` package, a bridge between\n[Flame](https://flame-engine.org/) 🔥 and *Lottie*.\n\nThe original, native **lottie** library is developed and maintained by [Airbnb](https://github.com/airbnb/lottie-android).\n\nThe Flutter package `lottie` is maintained by **xaha.dev** and is available on pub dev ([link](https://pub.dev/packages/lottie))\n"
  },
  {
    "path": "packages/flame_lottie/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_lottie/example/assets/LottieLogo1.json",
    "content": "{\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":0,\"ty\":1,\"nm\":\"MASTER\",\"ks\":{\"o\":{\"k\":0},\"r\":{\"k\":0},\"p\":{\"k\":[214.457,347.822,0]},\"a\":{\"k\":[60,60,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"sw\":120,\"sh\":120,\"sc\":\"#ffffff\",\"ip\":12,\"op\":179,\"st\":0,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"S5-Y 4\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":-89.1},\"p\":{\"k\":[53.205,131.606,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[142.038,29.278],[131.282,21.807]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":76,\"s\":[87],\"e\":[50.633]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":79,\"s\":[50.633],\"e\":[0]},{\"t\":83}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":76,\"s\":[100],\"e\":[75.856]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":79,\"s\":[75.856],\"e\":[0]},{\"t\":83}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":76,\"op\":84,\"st\":40,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"S4-Y 4\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":-89.1},\"p\":{\"k\":[53.205,131.606,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[142.183,-5.112],[130.029,5.016]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":76,\"s\":[87],\"e\":[43.833]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":79,\"s\":[43.833],\"e\":[0]},{\"t\":83}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":76,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":79,\"s\":[66.356],\"e\":[0]},{\"t\":83}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":76,\"op\":84,\"st\":40,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"S3-Y 4\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":-89.1},\"p\":{\"k\":[53.205,131.606,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[147.699,13.025],[133.195,13.21]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":76,\"s\":[87],\"e\":[42.133]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":79,\"s\":[42.133],\"e\":[0]},{\"t\":83}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":76,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":79,\"s\":[66.356],\"e\":[0]},{\"t\":83}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":76,\"op\":84,\"st\":40,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"S5-Y 3\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":97.9},\"p\":{\"k\":[58.205,-39.394,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[145.677,22.22],[134.922,14.749]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":75,\"s\":[87],\"e\":[50.633]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":78,\"s\":[50.633],\"e\":[0]},{\"t\":82}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":75,\"s\":[100],\"e\":[75.856]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":78,\"s\":[75.856],\"e\":[0]},{\"t\":82}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":75,\"op\":83,\"st\":39,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"S4-Y 3\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":97.9},\"p\":{\"k\":[58.205,-39.394,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[144.429,-5.397],[132.275,4.731]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":75,\"s\":[87],\"e\":[43.833]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":78,\"s\":[43.833],\"e\":[0]},{\"t\":82}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":75,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":78,\"s\":[66.356],\"e\":[0]},{\"t\":82}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":75,\"op\":83,\"st\":39,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"S3-Y 3\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":97.9},\"p\":{\"k\":[58.205,-39.394,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[149.624,8.244],[136.648,10.156]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":75,\"s\":[87],\"e\":[42.133]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":78,\"s\":[42.133],\"e\":[0]},{\"t\":82}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":75,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":78,\"s\":[66.356],\"e\":[0]},{\"t\":82}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":75,\"op\":83,\"st\":39,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":7,\"ty\":4,\"nm\":\"S13\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[128,3.65],[78.25,3.5]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":85,\"s\":[87],\"e\":[21.233]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":90,\"s\":[21.233],\"e\":[0]},{\"t\":94}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":85,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":90,\"s\":[66.356],\"e\":[0]},{\"t\":94}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":1.5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":85,\"op\":95,\"st\":49,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":8,\"ty\":4,\"nm\":\"S12\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[119.25,-20.05],[63.5,-20.5]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":84,\"s\":[87],\"e\":[21.233]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":87,\"s\":[21.233],\"e\":[0]},{\"t\":91}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":84,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":87,\"s\":[66.356],\"e\":[0]},{\"t\":91}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":1.5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":84,\"op\":94,\"st\":48,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":9,\"ty\":4,\"nm\":\"S11\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[119.5,-45.05],[82.75,-44.75]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":80,\"s\":[87],\"e\":[21.233]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":83,\"s\":[21.233],\"e\":[0]},{\"t\":87}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":80,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":83,\"s\":[66.356],\"e\":[0]},{\"t\":87}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":1.5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":80,\"op\":90,\"st\":44,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":10,\"ty\":4,\"nm\":\"S5-Y 2\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[169.5,18.073],[137.481,11.365]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":97,\"s\":[87],\"e\":[50.633]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":100,\"s\":[50.633],\"e\":[0]},{\"t\":107}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":97,\"s\":[100],\"e\":[75.856]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":100,\"s\":[75.856],\"e\":[0]},{\"t\":107}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":97,\"op\":107,\"st\":61,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":11,\"ty\":4,\"nm\":\"S4-Y 2\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[156.45,-23.05],[132,2.75]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":97,\"s\":[87],\"e\":[43.833]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":100,\"s\":[43.833],\"e\":[0]},{\"t\":107}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":97,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":100,\"s\":[66.356],\"e\":[0]},{\"t\":107}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":97,\"op\":107,\"st\":61,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":12,\"ty\":4,\"nm\":\"S3-Y 2\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[166.731,-7.927],[136.731,7.115]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":97,\"s\":[87],\"e\":[42.133]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":100,\"s\":[42.133],\"e\":[0]},{\"t\":107}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":97,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":100,\"s\":[66.356],\"e\":[0]},{\"t\":107}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":97,\"op\":107,\"st\":61,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":13,\"ty\":4,\"nm\":\"S6-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-87.5,20.95],[-48.75,54.75]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[87],\"e\":[43.933]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":57,\"s\":[43.933],\"e\":[0]},{\"t\":64}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[100],\"e\":[70.456]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":57,\"s\":[70.456],\"e\":[0]},{\"t\":64}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":54,\"op\":64,\"st\":18,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":14,\"ty\":4,\"nm\":\"S5-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-94.5,37.073],[-48.769,55.365]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[87],\"e\":[50.633]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":57,\"s\":[50.633],\"e\":[0]},{\"t\":64}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[100],\"e\":[75.856]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":57,\"s\":[75.856],\"e\":[0]},{\"t\":64}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":54,\"op\":64,\"st\":18,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":15,\"ty\":4,\"nm\":\"S4-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[7.45,21.95],[-32.75,55.75]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[87],\"e\":[43.833]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":57,\"s\":[43.833],\"e\":[0]},{\"t\":64}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":57,\"s\":[66.356],\"e\":[0]},{\"t\":64}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":54,\"op\":64,\"st\":18,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":16,\"ty\":4,\"nm\":\"S3-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[16.231,39.073],[-32.769,57.365]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[87],\"e\":[42.133]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":57,\"s\":[42.133],\"e\":[0]},{\"t\":64}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":57,\"s\":[66.356],\"e\":[0]},{\"t\":64}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":2},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":54,\"op\":64,\"st\":18,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":17,\"ty\":4,\"nm\":\"S8\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[-0.148,14.256],[10.476,0],[0,0]],\"o\":[[0,0],[-8.551,-8.263],[-21.454,0],[0,0]],\"v\":[[-3,35.95],[-1.352,-6.756],[-32.046,-20.579],[-42.25,4.25]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":65,\"s\":[87],\"e\":[21.233]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":70,\"s\":[21.233],\"e\":[0]},{\"t\":75}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":65,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":70,\"s\":[66.356],\"e\":[0]},{\"t\":75}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":1.5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":65,\"op\":75,\"st\":29,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":18,\"ty\":4,\"nm\":\"S7\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[27,1.45],[31.046,-1.421],[0,0]],\"o\":[[-27,-1.45],[-26.426,1.21],[0,0]],\"v\":[[34.5,-13.05],[-35.046,-35.579],[-62.25,-5.75]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":65,\"s\":[87],\"e\":[21.233]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":70,\"s\":[21.233],\"e\":[0]},{\"t\":75}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":65,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":70,\"s\":[66.356],\"e\":[0]},{\"t\":75}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":1.5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":65,\"op\":75,\"st\":29,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":19,\"ty\":4,\"nm\":\"S2-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[1.9,-10.768],[1,-19]],\"o\":[[0,0],[-3.167,17.951],[-1,19]],\"v\":[[-67.25,-105.5],[-72.333,-84.201],[-76.5,-37.75]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":29,\"s\":[87],\"e\":[25.333]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":33,\"s\":[25.333],\"e\":[0]},{\"t\":36}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":29,\"s\":[100],\"e\":[69.056]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":33,\"s\":[69.056],\"e\":[0]},{\"t\":36}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":1.5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":30,\"op\":37,\"st\":-7,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":20,\"ty\":4,\"nm\":\"S1-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[25.043,45.678,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[1.9,-10.768],[1,-19]],\"o\":[[0,0],[-3.167,17.951],[-1,19]],\"v\":[[-67.125,-112],[-75.458,-89.951],[-80.375,-39.25]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":29,\"s\":[87],\"e\":[37.533]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":33,\"s\":[37.533],\"e\":[0]},{\"t\":36}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":29,\"s\":[100],\"e\":[66.356]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":33,\"s\":[66.356],\"e\":[0]},{\"t\":36}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":1.5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Shape 1\"}],\"ip\":30,\"op\":37,\"st\":-7,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":21,\"ty\":4,\"nm\":\"Dot1\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.823,\"y\":0},\"n\":\"0p833_0p833_0p823_0\",\"t\":-3,\"s\":[295.771,108.994,0],\"e\":[35.771,108.994,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":16}]},\"a\":{\"k\":[196.791,266.504,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"k\":[9.4,9.4]},\"p\":{\"k\":[0.8,-0.5]},\"nm\":\"Ellipse Path 1\"},{\"ty\":\"fl\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"nm\":\"Fill 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[196,267],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\"}],\"ip\":-5,\"op\":17,\"st\":-36,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":22,\"ty\":4,\"nm\":\"L-B\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[39.043,45.678,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[25.671,-4.167],[1.456,6.902],[-8.481,1.863],[-47.562,13.01],[-0.501,0.133],[-71.423,-2.315]],\"o\":[[0,0],[-8.224,1.335],[-1.456,-6.903],[23.817,-5.233],[0.16,-0.044],[0.501,-0.133],[0,0]],\"v\":[[-8.837,-58.229],[-35.834,33.662],[-51.688,23.148],[-41.174,7.293],[51.797,44.178],[53.188,43.741],[140.394,43.672]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[166.029,270.643],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 8\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.703],\"y\":[0.821]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p703_0p821_0p167_0p167\"],\"t\":18,\"s\":[80],\"e\":[50]},{\"i\":{\"x\":[0.263],\"y\":[1]},\"o\":{\"x\":[0.037],\"y\":[0.168]},\"n\":[\"0p263_1_0p037_0p168\"],\"t\":23,\"s\":[50],\"e\":[30]},{\"t\":55}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.337],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p337_1_0p167_0p167\"],\"t\":18,\"s\":[81],\"e\":[73.4]},{\"t\":29}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"}],\"ip\":18,\"op\":179,\"st\":8,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":23,\"ty\":4,\"nm\":\"L-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[39.043,45.678,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[25.671,-4.167],[1.456,6.902],[-8.481,1.863],[-47.562,13.01],[-0.501,0.133],[-71.423,-2.315]],\"o\":[[0,0],[-8.224,1.335],[-1.456,-6.903],[23.817,-5.233],[0.16,-0.044],[0.501,-0.133],[0,0]],\"v\":[[-8.837,-58.229],[-35.834,33.662],[-51.688,23.148],[-41.174,7.293],[51.797,44.178],[53.188,43.741],[140.394,43.672]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":8.4},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[166.029,270.643],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 8\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.703],\"y\":[0.857]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p703_0p857_0p167_0p167\"],\"t\":16,\"s\":[80],\"e\":[50]},{\"i\":{\"x\":[0.938],\"y\":[1]},\"o\":{\"x\":[0.333],\"y\":[0.202]},\"n\":[\"0p938_1_0p333_0p202\"],\"t\":20,\"s\":[50],\"e\":[0]},{\"t\":28}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.337],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p337_1_0p167_0p167\"],\"t\":16,\"s\":[81],\"e\":[73.4]},{\"t\":27}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"}],\"ip\":16,\"op\":179,\"st\":8,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":24,\"ty\":1,\"nm\":\"N\",\"parent\":0,\"ks\":{\"o\":{\"k\":0},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.26,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p26_1_0p167_0p167\",\"t\":28,\"s\":[-33.667,8.182,0],\"e\":[-33.667,-72.818,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.74,\"y\":0},\"n\":\"0p833_0p833_0p74_0\",\"t\":40,\"s\":[-33.667,-72.818,0],\"e\":[-33.667,102.057,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":54}]},\"a\":{\"k\":[60,60,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"sw\":120,\"sh\":120,\"sc\":\"#ffffff\",\"ip\":28,\"op\":54,\"st\":0,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":25,\"ty\":4,\"nm\":\"Dot-Y\",\"parent\":24,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p833_0p833_0p167_0p167\",\"t\":28,\"s\":[39.875,60,0],\"e\":[79.375,60,0],\"to\":[6.58333349227905,0,0],\"ti\":[-6.58333349227905,0,0]},{\"t\":54}]},\"a\":{\"k\":[196.791,266.504,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"k\":[9.4,9.4]},\"p\":{\"k\":[0.8,-0.5]},\"nm\":\"Ellipse Path 1\"},{\"ty\":\"fl\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"nm\":\"Fill 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[196,267],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\"}],\"ip\":28,\"op\":54,\"st\":4,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":26,\"ty\":4,\"nm\":\"T1a-B\",\"parent\":36,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[250,250,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[-0.5,9.501],[-0.048,5.655],[0.054,0.06],[0.946,1.486],[-9.967,8.05],[-40.546,0]],\"o\":[[0.031,-0.594],[0.076,-8.978],[-1.161,-1.3],[-5.939,-9.327],[24.677,-19.929],[0,0]],\"v\":[[-30.72,63.761],[-30.741,45.192],[-37.397,27.014],[-40.698,22.661],[-37.873,-7.117],[49.506,11.559]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":24.9,\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.673],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p673_1_0p167_0p167\"],\"t\":70,\"s\":[24.9],\"e\":[89.1]},{\"t\":84}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[227.677,234.375],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 9\"}],\"ip\":70,\"op\":179,\"st\":17,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":27,\"ty\":4,\"nm\":\"T2a-B\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[39.043,45.678,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[1.681,-29.992],[-1.681,29.992]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.06],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p06_1_0p167_0p167\"],\"t\":75,\"s\":[50],\"e\":[0]},{\"t\":85}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.06],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p06_1_0p167_0p167\"],\"t\":75,\"s\":[50],\"e\":[100]},{\"t\":85}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":3,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[277.698,247.258],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 7\"}],\"ip\":75,\"op\":179,\"st\":15,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":28,\"ty\":4,\"nm\":\"T1a-Y 2\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p667_1_0p167_0p167\",\"t\":56,\"s\":[39.043,48.678,0],\"e\":[39.043,45.678,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":64}]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[-0.5,9.501],[-0.048,5.655],[0.054,0.06],[0.946,1.486],[-9.967,8.05],[-40.546,0]],\"o\":[[0.031,-0.594],[0.076,-8.978],[-1.161,-1.3],[-5.939,-9.327],[24.677,-19.929],[0,0]],\"v\":[[-30.72,63.761],[-30.741,45.192],[-37.397,27.014],[-40.698,22.661],[-37.873,-7.117],[49.506,11.559]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[1]},\"o\":{\"x\":[0.301],\"y\":[0]},\"n\":[\"0p833_1_0p301_0\"],\"t\":54,\"s\":[0],\"e\":[24.9]},{\"t\":70}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.301],\"y\":[0]},\"n\":[\"0p667_1_0p301_0\"],\"t\":54,\"s\":[0],\"e\":[100]},{\"t\":78}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":8.4},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[227.677,234.375],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 9\"}],\"ip\":59,\"op\":179,\"st\":12,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":29,\"ty\":4,\"nm\":\"O-B\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p833_0p833_0p167_0p167\",\"t\":31,\"s\":[-62.792,73.057,0],\"e\":[-53.792,7.557,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.638,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.198},\"n\":\"0p638_1_0p167_0p198\",\"t\":35.257,\"s\":[-53.792,7.557,0],\"e\":[-33.667,-72.818,0],\"to\":[0,0,0],\"ti\":[-19.1562919616699,1.73831975460052,0]},{\"i\":{\"x\":0.795,\"y\":1},\"o\":{\"x\":0.523,\"y\":0},\"n\":\"0p795_1_0p523_0\",\"t\":44,\"s\":[-33.667,-72.818,0],\"e\":[-14.167,102.182,0],\"to\":[16.2075271606445,-1.47073686122894,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.348,\"y\":1},\"o\":{\"x\":0.18,\"y\":0},\"n\":\"0p348_1_0p18_0\",\"t\":54,\"s\":[-14.167,102.182,0],\"e\":[-14.167,59.182,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.27,\"y\":1},\"o\":{\"x\":0.693,\"y\":0},\"n\":\"0p27_1_0p693_0\",\"t\":63,\"s\":[-14.167,59.182,0],\"e\":[-14.167,62.182,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":73}]},\"a\":{\"k\":[196.791,266.504,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"k\":[{\"i\":{\"x\":[0.667,0.667],\"y\":[1,1]},\"o\":{\"x\":[0.333,0.333],\"y\":[0,0]},\"n\":[\"0p667_1_0p333_0\",\"0p667_1_0p333_0\"],\"t\":54,\"s\":[3,3],\"e\":[44.6,44.6]},{\"t\":61}]},\"p\":{\"k\":[0.8,-0.5]},\"nm\":\"Ellipse Path 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[196,267],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[0],\"e\":[30]},{\"i\":{\"x\":[0.432],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[1.124]},\"n\":[\"0p432_1_0p167_1p124\"],\"t\":63,\"s\":[30],\"e\":[39.9]},{\"t\":91}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":54,\"s\":[100],\"e\":[88]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":63,\"s\":[88],\"e\":[88]},{\"t\":91}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"}],\"ip\":54,\"op\":179,\"st\":4,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":30,\"ty\":4,\"nm\":\"O-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p833_0p833_0p167_0p167\",\"t\":31,\"s\":[-62.792,73.057,0],\"e\":[-53.792,7.557,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.638,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.198},\"n\":\"0p638_1_0p167_0p198\",\"t\":35.257,\"s\":[-53.792,7.557,0],\"e\":[-33.667,-72.818,0],\"to\":[0,0,0],\"ti\":[-19.1562919616699,1.73831975460052,0]},{\"i\":{\"x\":0.795,\"y\":1},\"o\":{\"x\":0.523,\"y\":0},\"n\":\"0p795_1_0p523_0\",\"t\":44,\"s\":[-33.667,-72.818,0],\"e\":[-14.167,102.182,0],\"to\":[16.2075271606445,-1.47073686122894,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.348,\"y\":1},\"o\":{\"x\":0.18,\"y\":0},\"n\":\"0p348_1_0p18_0\",\"t\":54,\"s\":[-14.167,102.182,0],\"e\":[-14.167,59.182,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.27,\"y\":1},\"o\":{\"x\":0.693,\"y\":0},\"n\":\"0p27_1_0p693_0\",\"t\":63,\"s\":[-14.167,59.182,0],\"e\":[-14.167,62.182,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":73}]},\"a\":{\"k\":[196.791,266.504,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"k\":[{\"i\":{\"x\":[0.667,0.667],\"y\":[1,1]},\"o\":{\"x\":[0.333,0.333],\"y\":[0,0]},\"n\":[\"0p667_1_0p333_0\",\"0p667_1_0p333_0\"],\"t\":54,\"s\":[3,3],\"e\":[44.6,44.6]},{\"t\":61}]},\"p\":{\"k\":[0.8,-0.5]},\"nm\":\"Ellipse Path 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":8.8},\"lc\":1,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[196,267],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\"}],\"ip\":54,\"op\":179,\"st\":4,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":31,\"ty\":4,\"nm\":\"T1b-B\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[39.043,45.678,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[1.768,-25.966],[-1.768,25.966]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":0,\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.21],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p21_1_0p167_0p167\"],\"t\":81,\"s\":[11.7],\"e\":[100]},{\"t\":88}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":2,\"lj\":2,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[242.756,265.581],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 10\"}],\"ip\":81,\"op\":179,\"st\":26,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":32,\"ty\":4,\"nm\":\"T1b-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[39.043,45.678,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[1.768,-25.966],[-1.768,25.966]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":70,\"s\":[0],\"e\":[0]},{\"t\":75}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":70,\"s\":[11.7],\"e\":[100]},{\"t\":75}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":8.4},\"lc\":2,\"lj\":2,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[242.756,265.581],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 10\"}],\"ip\":70,\"op\":161,\"st\":15,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":33,\"ty\":4,\"nm\":\"T2b-B\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[39.043,45.678,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[246.65,213.814],[340.956,213.628]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":82,\"s\":[29],\"e\":[0]},{\"t\":91}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":82,\"s\":[41.1],\"e\":[66.5]},{\"t\":91}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 5\"}],\"ip\":82,\"op\":179,\"st\":-17,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":34,\"ty\":4,\"nm\":\"T2a-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[39.043,45.678,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[1.681,-29.992],[-1.681,29.992]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.06],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p06_1_0p167_0p167\"],\"t\":72,\"s\":[50],\"e\":[0]},{\"t\":82}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.06],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p06_1_0p167_0p167\"],\"t\":72,\"s\":[50],\"e\":[100]},{\"t\":82}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":3,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[277.698,247.258],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 7\"}],\"ip\":72,\"op\":89,\"st\":12,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":35,\"ty\":4,\"nm\":\"T2b-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[39.043,45.678,0]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[246.65,213.814],[340.956,213.628]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":76,\"s\":[29],\"e\":[0]},{\"t\":85}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":76,\"s\":[41.1],\"e\":[66.5]},{\"t\":85}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 5\"}],\"ip\":76,\"op\":92,\"st\":-23,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":36,\"ty\":4,\"nm\":\"T1a-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p667_1_0p167_0p167\",\"t\":56,\"s\":[39.043,48.678,0],\"e\":[39.043,45.678,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":64}]},\"a\":{\"k\":[250,250,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[-0.5,9.501],[-0.048,5.655],[0.054,0.06],[0.946,1.486],[-9.967,8.05],[-40.546,0]],\"o\":[[0.031,-0.594],[0.076,-8.978],[-1.161,-1.3],[-5.939,-9.327],[24.677,-19.929],[0,0]],\"v\":[[-30.72,63.761],[-30.741,45.192],[-37.397,27.014],[-40.698,22.661],[-37.873,-7.117],[49.506,11.559]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[1]},\"o\":{\"x\":[0.301],\"y\":[0]},\"n\":[\"0p833_1_0p301_0\"],\"t\":54,\"s\":[0],\"e\":[24.9]},{\"t\":70}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.667],\"y\":[1]},\"o\":{\"x\":[0.301],\"y\":[0]},\"n\":[\"0p667_1_0p301_0\"],\"t\":54,\"s\":[0],\"e\":[100]},{\"t\":74}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":8.4},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[227.677,234.375],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 9\"}],\"ip\":59,\"op\":156,\"st\":12,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":37,\"ty\":4,\"nm\":\"E1-B\",\"parent\":38,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[344.672,214.842,0]},\"a\":{\"k\":[344.672,214.842,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-13.664,-0.145],[62.163,0.29]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.562},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[344.672,214.842],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[0.12]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_0p12_0p167_0p167\"],\"t\":84,\"s\":[0],\"e\":[0]},{\"t\":93}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":84,\"s\":[0],\"e\":[37.5]},{\"t\":93}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"}],\"ip\":84,\"op\":179,\"st\":84,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":38,\"ty\":4,\"nm\":\"E1-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.12,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p12_1_0p167_0p167\",\"t\":79,\"s\":[113.715,9.146,0],\"e\":[137.715,9.146,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.12,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"n\":\"0p12_1_0p167_0\",\"t\":88,\"s\":[137.715,9.146,0],\"e\":[133.715,9.146,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":92}]},\"a\":{\"k\":[344.672,214.842,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-13.664,-0.145],[62.163,0.29]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":8.4},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[344.672,214.842],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 2\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[0.12]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_0p12_0p167_0p167\"],\"t\":79,\"s\":[0],\"e\":[0]},{\"t\":88}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":79,\"s\":[0],\"e\":[37.5]},{\"t\":88}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"}],\"ip\":79,\"op\":94,\"st\":79,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":39,\"ty\":4,\"nm\":\"E2-B\",\"parent\":40,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[332.05,237.932,0]},\"a\":{\"k\":[332.05,237.932,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-26.67,-0.283],[99.171,0.066]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[0.12]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_0p12_0p167_0p167\"],\"t\":86,\"s\":[0],\"e\":[0]},{\"t\":95}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":86,\"s\":[0],\"e\":[43]},{\"t\":95}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.562},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[331.664,238.14],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 3\"}],\"ip\":86,\"op\":179,\"st\":86,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":40,\"ty\":4,\"nm\":\"E2-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.12,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p12_1_0p167_0p167\",\"t\":83,\"s\":[109.092,33.61,0],\"e\":[121.092,33.61,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.12,\"y\":0.12},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p12_0p12_0p167_0p167\",\"t\":92,\"s\":[121.092,33.61,0],\"e\":[121.092,33.61,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":96}]},\"a\":{\"k\":[332.05,237.932,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-26.67,-0.283],[99.171,0.066]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[0.12]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_0p12_0p167_0p167\"],\"t\":83,\"s\":[0],\"e\":[0]},{\"t\":92}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":83,\"s\":[0],\"e\":[43]},{\"t\":92}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":8.4},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[331.664,238.14],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 3\"}],\"ip\":83,\"op\":96,\"st\":83,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":41,\"ty\":4,\"nm\":\"I-B\",\"parent\":42,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[303.802,282.182,0]},\"a\":{\"k\":[303.802,282.182,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[0.859,-21.143],[-4.359,70.392]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[0.12]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_0p12_0p167_0p167\"],\"t\":81,\"s\":[0],\"e\":[0]},{\"t\":91}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":81,\"s\":[0],\"e\":[45.7]},{\"t\":91}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.194},\"lc\":3,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[304.135,282.409],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 6\"}],\"ip\":81,\"op\":179,\"st\":18,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":42,\"ty\":4,\"nm\":\"I-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.12,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p12_1_0p167_0p167\",\"t\":78,\"s\":[93.594,62.861,0],\"e\":[92.626,82.829,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.12,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"n\":\"0p12_1_0p167_0\",\"t\":88,\"s\":[92.626,82.829,0],\"e\":[92.844,77.861,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":92}]},\"a\":{\"k\":[303.802,282.182,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[0.859,-21.143],[-4.359,70.392]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[0.12]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_0p12_0p167_0p167\"],\"t\":78,\"s\":[0],\"e\":[0]},{\"t\":88}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.12],\"y\":[1]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p12_1_0p167_0p167\"],\"t\":78,\"s\":[0],\"e\":[45.7]},{\"t\":88}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 1\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":8.4},\"lc\":3,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[304.135,282.409],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 6\"}],\"ip\":78,\"op\":93,\"st\":15,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":43,\"ty\":4,\"nm\":\"E3-B\",\"parent\":44,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[345.189,261.801,0]},\"a\":{\"k\":[345.124,261.801,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-13.664,-0.145],[75.663,0.29]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":92,\"s\":[0],\"e\":[0]},{\"t\":97}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":92,\"s\":[0],\"e\":[31.6]},{\"t\":97}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 2\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.562},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[344.674,261.877],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\"}],\"ip\":92,\"op\":179,\"st\":29,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":44,\"ty\":4,\"nm\":\"E3-Y\",\"parent\":0,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p667_1_0p167_0p167\",\"t\":84,\"s\":[119.167,57.479,0],\"e\":[137.167,57.479,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.167,\"y\":0},\"n\":\"0p667_1_0p167_0\",\"t\":92,\"s\":[137.167,57.479,0],\"e\":[134.167,57.479,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":96}]},\"a\":{\"k\":[345.124,261.801,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ks\":{\"k\":{\"i\":[[0,0],[0,0]],\"o\":[[0,0],[0,0]],\"v\":[[-13.664,-0.145],[75.663,0.29]],\"c\":false}},\"nm\":\"Path 1\"},{\"ty\":\"tm\",\"s\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":84,\"s\":[0],\"e\":[0]},{\"t\":92}],\"ix\":1},\"e\":{\"k\":[{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.167],\"y\":[0.167]},\"n\":[\"0p833_0p833_0p167_0p167\"],\"t\":84,\"s\":[0],\"e\":[31.6]},{\"t\":92}],\"ix\":2},\"o\":{\"k\":0,\"ix\":3},\"m\":1,\"ix\":2,\"nm\":\"Trim Paths 2\"},{\"ty\":\"st\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.48,0.53,1]},\"o\":{\"k\":100},\"w\":{\"k\":9.562},\"lc\":2,\"lj\":1,\"ml\":10,\"nm\":\"Stroke 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[344.674,261.877],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Group 1\"}],\"ip\":84,\"op\":102,\"st\":21,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":45,\"ty\":4,\"nm\":\"Dot-Y\",\"parent\":46,\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0,\"y\":0.812},\"o\":{\"x\":0,\"y\":0},\"n\":\"0_0p812_0_0\",\"t\":96,\"s\":[43.263,59.75,0],\"e\":[62.513,59.75,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.708,\"y\":1},\"o\":{\"x\":0.39,\"y\":0.707},\"n\":\"0p708_1_0p39_0p707\",\"t\":108,\"s\":[62.513,59.75,0],\"e\":[63.763,59.75,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":115}]},\"a\":{\"k\":[196.791,266.504,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":1,\"ty\":\"el\",\"s\":{\"k\":[9.2,9.2]},\"p\":{\"k\":[0.8,-0.5]},\"nm\":\"Ellipse Path 1\"},{\"ty\":\"fl\",\"fillEnabled\":true,\"c\":{\"k\":[1,1,1,1]},\"o\":{\"k\":100},\"nm\":\"Fill 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[196,267],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\"}],\"ip\":96,\"op\":182,\"st\":65,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":46,\"ty\":1,\"nm\":\"Bncr\",\"parent\":0,\"ks\":{\"o\":{\"k\":0},\"r\":{\"k\":0},\"p\":{\"k\":[{\"i\":{\"x\":0.18,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p18_1_0p167_0p167\",\"t\":96,\"s\":[164.782,57.473,0],\"e\":[164.782,55.473,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.82,\"y\":0},\"n\":\"0p833_0p833_0p82_0\",\"t\":99,\"s\":[164.782,55.473,0],\"e\":[164.782,57.473,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.18,\"y\":1},\"o\":{\"x\":0.167,\"y\":0.167},\"n\":\"0p18_1_0p167_0p167\",\"t\":102,\"s\":[164.782,57.473,0],\"e\":[164.782,56.909,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.833,\"y\":0.833},\"o\":{\"x\":0.82,\"y\":0},\"n\":\"0p833_0p833_0p82_0\",\"t\":105,\"s\":[164.782,56.909,0],\"e\":[164.782,57.473,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":108}]},\"a\":{\"k\":[60,60,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"sw\":120,\"sh\":120,\"sc\":\"#ffffff\",\"ip\":96,\"op\":182,\"st\":15,\"bm\":0,\"sr\":1},{\"ddd\":0,\"ind\":47,\"ty\":4,\"nm\":\"BG\",\"ks\":{\"o\":{\"k\":100},\"r\":{\"k\":0},\"p\":{\"k\":[187.5,333.5,0]},\"a\":{\"k\":[0,0,0]},\"s\":{\"k\":[100,100,100]}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"k\":[375,667]},\"p\":{\"k\":[0,0]},\"r\":{\"k\":0},\"nm\":\"Rectangle Path 1\"},{\"ty\":\"fl\",\"fillEnabled\":true,\"c\":{\"k\":[0,0.82,0.76,1]},\"o\":{\"k\":100},\"nm\":\"Fill 1\"},{\"ty\":\"tr\",\"p\":{\"k\":[0,0],\"ix\":2},\"a\":{\"k\":[0,0],\"ix\":1},\"s\":{\"k\":[100,100],\"ix\":3},\"r\":{\"k\":0,\"ix\":6},\"o\":{\"k\":100,\"ix\":7},\"sk\":{\"k\":0,\"ix\":4},\"sa\":{\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle 1\"}],\"ip\":0,\"op\":179,\"st\":0,\"bm\":0,\"sr\":1}],\"v\":\"4.4.26\",\"ddd\":0,\"ip\":0,\"op\":179,\"fr\":30,\"w\":375,\"h\":667}"
  },
  {
    "path": "packages/flame_lottie/example/lib/main.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame_lottie/flame_lottie.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(GameWidget(game: LottieExampleGame()));\n}\n\nclass LottieExampleGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    final asset = await loadLottie(Lottie.asset('assets/LottieLogo1.json'));\n    add(\n      LottieComponent(\n        asset,\n        size: Vector2.all(400),\n        repeating: true,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_lottie/example/pubspec.yaml",
    "content": "name: flame_lottie_example\nresolution: workspace\ndescription: An example for flame_lottie. The example shows how load a Lottie animation and add it to a flame game.\npublish_to: 'none'\nversion: 0.0.1+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_lottie: ^0.4.2+21\n  flutter:\n    sdk: flutter\n  lottie:\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n\nflutter:\n  uses-material-design: true\n  assets:\n    - assets/\n"
  },
  {
    "path": "packages/flame_lottie/lib/flame_lottie.dart",
    "content": "library flame_lottie;\n\nexport 'package:lottie/lottie.dart';\nexport 'src/lottie_component.dart';\nexport 'src/lottie_renderer.dart';\n"
  },
  {
    "path": "packages/flame_lottie/lib/src/lottie_component.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame_lottie/src/lottie_renderer.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:lottie/lottie.dart';\n\n/// A Flame [Component] which renders a [Lottie] animation using the already\n/// existing Flutter library [lottie](https://pub.dev/packages/lottie).\nclass LottieComponent extends PositionComponent with HasPaint {\n  late final LottieRenderer _renderer;\n\n  /// The [controller] drives the [Lottie] animation. In case none is specified\n  /// it will be created implicitly in the [LottieRenderer].\n  LottieComponent(\n    LottieComposition composition, {\n    EffectController? controller,\n    double? progress,\n    LottieDelegates? delegates,\n    bool? enableMergePaths,\n    FrameRate? frameRate,\n    double? duration,\n    bool? repeating,\n    Alignment alignment = Alignment.center,\n    BoxFit? fit = BoxFit.contain,\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.key,\n  }) {\n    _renderer = LottieRenderer(\n      composition: composition,\n      progress: progress ?? 0.0,\n      size: size,\n      controller: controller,\n      duration: duration,\n      repeating: repeating,\n      alignment: alignment,\n      fit: fit,\n      delegates: delegates,\n      enableMergePaths: enableMergePaths,\n      frameRate: frameRate,\n    );\n  }\n\n  @mustCallSuper\n  @override\n  void render(Canvas canvas) {\n    _renderer.render(canvas);\n  }\n\n  @mustCallSuper\n  @override\n  void update(double dt) {\n    _renderer.update(dt);\n  }\n}\n\n/// Loads the Lottie animation from the specified Lottie file.\nFuture<LottieComposition> loadLottie(\n  FutureOr<LottieBuilder> file,\n) async {\n  final loaded = await file;\n  final composition = await loaded.lottie.load();\n\n  return composition;\n}\n"
  },
  {
    "path": "packages/flame_lottie/lib/src/lottie_renderer.dart",
    "content": "import 'package:flame/effects.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:lottie/lottie.dart';\n\nclass LottieRenderer {\n  final LottieDrawable drawable;\n  final EffectController _controller;\n\n  final BoxFit? fit;\n  final Alignment? alignment;\n\n  Rect boundingRect;\n\n  LottieRenderer({\n    required LottieComposition composition,\n    required double progress,\n    required NotifyingVector2 size,\n    EffectController? controller,\n    double? duration,\n    bool? repeating,\n    this.alignment,\n    this.fit,\n    LottieDelegates? delegates,\n    bool? enableMergePaths,\n    FrameRate? frameRate,\n  }) : assert(progress >= 0.0 && progress <= 1.0),\n       boundingRect = size.toRect(),\n       drawable = LottieDrawable(composition, frameRate: frameRate)\n         ..setProgress(progress)\n         ..delegates = delegates\n         ..enableMergePaths = enableMergePaths ?? false,\n       _controller =\n           controller ??\n           EffectController(\n             duration: duration ?? composition.duration.inMilliseconds / 1000,\n             infinite: repeating ?? false,\n           ) {\n    size.addListener(() {\n      boundingRect = size.toRect();\n    });\n  }\n\n  /// Renders the current frame of the Lottie animation onto the canvas.\n  void render(Canvas canvas) {\n    drawable.draw(canvas, boundingRect, fit: fit, alignment: alignment);\n  }\n\n  void update(double dt) {\n    _controller.advance(dt);\n    drawable.setProgress(_controller.progress);\n  }\n}\n"
  },
  {
    "path": "packages/flame_lottie/pubspec.yaml",
    "content": "name: flame_lottie\nresolution: workspace\ndescription: Flame wrapper for Lottie by AirBnB. This package implements a bridge between Lottie and Flame, allowing to load and display Lottie animations.\nversion: 0.4.2+21\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_lottie\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - lottie\n  - animations\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  lottie: ^3.1.2\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flame_test: ^2.2.3\n  flutter_test:\n    sdk: flutter\n"
  },
  {
    "path": "packages/flame_lottie/test/flame_lottie_test.dart",
    "content": "import 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:flame_lottie/flame_lottie.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  testWithFlameGame(\n    'Game holds LottieComponent',\n    (game) async {\n      // When running tests we have to sync read the file and can not use the\n      // recommended [loadLottie] function.\n      final data = File('example/assets/LottieLogo1.json').readAsBytesSync();\n      final composition = await LottieComposition.fromBytes(data);\n\n      final lottieComponent = LottieComponent(composition);\n\n      await game.world.add(lottieComponent);\n      await game.ready();\n\n      expect(game.world.children, [lottieComponent]);\n    },\n  );\n\n  testWithFlameGame(\n    'Load composition as AssetBundle and use loadLottie function by library',\n    (game) async {\n      final logoData = Future.value(\n        _bytesForFile('example/assets/LottieLogo1.json'),\n      );\n\n      final mockAsset = _FakeAssetBundle({'logo.json': logoData});\n\n      final asset = Lottie.asset(\n        'logo.json',\n        bundle: mockAsset,\n      );\n\n      final composition = await loadLottie(asset);\n\n      await game.ready();\n\n      expect(\n        composition.duration,\n        const Duration(seconds: 5, milliseconds: 966),\n      );\n    },\n  );\n}\n\nByteData _bytesForFile(String path) =>\n    File(path).readAsBytesSync().buffer.asByteData();\n\nclass _FakeAssetBundle extends Fake implements AssetBundle {\n  final Map<String, Future<ByteData>> data;\n\n  _FakeAssetBundle(this.data);\n\n  @override\n  Future<ByteData> load(String key) {\n    return data[key] ?? (Future.error('Asset $key not found'));\n  }\n}\n"
  },
  {
    "path": "packages/flame_markdown/CHANGELOG.md",
    "content": "## 0.2.4+14\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 0.2.4+13\n\n - Update a dependency to the latest release.\n\n## 0.2.4+12\n\n - Update a dependency to the latest release.\n\n## 0.2.4+11\n\n - Update a dependency to the latest release.\n\n## 0.2.4+10\n\n - Update a dependency to the latest release.\n\n## 0.2.4+9\n\n - Update a dependency to the latest release.\n\n## 0.2.4+8\n\n - Update a dependency to the latest release.\n\n## 0.2.4+7\n\n - Update a dependency to the latest release.\n\n## 0.2.4+6\n\n - Update a dependency to the latest release.\n\n## 0.2.4+5\n\n - Update a dependency to the latest release.\n\n## 0.2.4+4\n\n - Update a dependency to the latest release.\n\n## 0.2.4+3\n\n - Update a dependency to the latest release.\n\n## 0.2.4+2\n\n - Update a dependency to the latest release.\n\n## 0.2.4+1\n\n - Update a dependency to the latest release.\n\n## 0.2.4\n\n - **FEAT**: Support custom attributes syntax to allow for multiple styles in the text rendering pipeline ([#3519](https://github.com/flame-engine/flame/issues/3519)). ([fbc58053](https://github.com/flame-engine/flame/commit/fbc58053dd12e6dc62b09cb14e4b438ef7b7f1b2))\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 0.2.3+2\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 0.2.3+1\n\n - Update a dependency to the latest release.\n\n## 0.2.3\n\n - **FIX**: Do not encode HTML by default when parsing markdown [flame_markdown] ([#3425](https://github.com/flame-engine/flame/issues/3425)). ([3067da94](https://github.com/flame-engine/flame/commit/3067da94fbc6df2da5197771cb9617588006a9b9))\n - **FEAT**: Add support for strike-through text for flame_markdown ([#3426](https://github.com/flame-engine/flame/issues/3426)). ([1f9b0ea9](https://github.com/flame-engine/flame/commit/1f9b0ea9f35a7180725ec7f8f79a561c5f544bb7))\n\n## 0.2.2+3\n\n - Update a dependency to the latest release.\n\n## 0.2.2+2\n\n - Update a dependency to the latest release.\n\n## 0.2.2+1\n\n - Update a dependency to the latest release.\n\n## 0.2.2\n\n - **FEAT**: Support inline code blocks on markdown rich text ([#3186](https://github.com/flame-engine/flame/issues/3186)). ([67e069c0](https://github.com/flame-engine/flame/commit/67e069c00dcb32c258231a326b0918739c6f80e6))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 0.2.1\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n\n## 0.2.0+1\n\n - Update a dependency to the latest release.\n\n## 0.2.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 0.1.1+8\n\n - Update a dependency to the latest release.\n\n## 0.1.1+7\n\n - Update a dependency to the latest release.\n\n## 0.1.1+6\n\n - Update a dependency to the latest release.\n\n## 0.1.1+5\n\n - Update a dependency to the latest release.\n\n## 0.1.1+4\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n## 0.1.1+3\n\n - Update a dependency to the latest release.\n\n## 0.1.1+2\n\n - Update a dependency to the latest release.\n\n## 0.1.1+1\n\n - Update a dependency to the latest release.\n\n## 0.1.1\n\n - **FEAT**: Create flame_markdown ([#2703](https://github.com/flame-engine/flame/issues/2703)). ([b77c2373](https://github.com/flame-engine/flame/commit/b77c23737104260aea2483c38ec3bef999975e7d))\n\n"
  },
  {
    "path": "packages/flame_markdown/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_markdown/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nAdds markdown support for <a href=\"https://github.com/flame-engine/flame\">Flame</a> using the <a href=\"https://github.com/dart-lang/markdown\">markdown</a> package.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_markdown\" ><img src=\"https://img.shields.io/pub/v/flame_markdown.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n# flame_markdown\n\nThis package facilitates the creation of Flame's TextNode hierarchies by leveraging markdown\nstrings.\n\nIt integrates with the `markdown` package to parse markdown strings into an AST and uses that AST to\ncreate a `DocumentRoot` containing the equivalent list of nodes from Flame's text rendering\npipeline.\n\nAdd this as a dependency to your Flame game if you want to easily apply simple inline formatting to\ntext in your game (bold, italics), or if you want to create complex text documents with headings and\nparagraphs and have them formatted by Flame's text layout system, without needing to manually\nspecify the `TextNode` tree structure.\n"
  },
  {
    "path": "packages/flame_markdown/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    - public_member_api_docs\n"
  },
  {
    "path": "packages/flame_markdown/example/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\nmigrate_working_dir/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\n/build/\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Android Studio will place build artifacts here\n/android/app/debug\n/android/app/profile\n/android/app/release\n"
  },
  {
    "path": "packages/flame_markdown/example/README.md",
    "content": "# flame markdown example\n\nSimple project to showcase the usage of flame_markdown\n"
  },
  {
    "path": "packages/flame_markdown/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    public_member_api_docs: false\n"
  },
  {
    "path": "packages/flame_markdown/example/assets/fire_and_ice.md",
    "content": "# Fire & Ice\n\nSome say the world will ~~end~~ in **fire**,\n\nSome say in *ice*.\n\nFrom what I've tasted of >desire<,\n\nI hold with those who favor **fire**.\n\n[- by Robert Frost]{.author}\n"
  },
  {
    "path": "packages/flame_markdown/example/lib/main.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/text.dart';\nimport 'package:flame_markdown/custom_attribute_syntax.dart';\nimport 'package:flame_markdown/flame_markdown.dart';\nimport 'package:flutter/widgets.dart' hide Animation;\nimport 'package:markdown/markdown.dart';\n\nvoid main() {\n  runApp(GameWidget(game: MarkdownGame()));\n}\n\n/// This example game showcases the use of the FlameMarkdown package\n/// to render rich-text components using a simple markdown syntax.\nclass MarkdownGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    final markdown = await Flame.assets.readFile('fire_and_ice.md');\n    await add(\n      TextElementComponent.fromDocument(\n        document: FlameMarkdown.toDocument(\n          markdown,\n          document: Document(\n            encodeHtml: false,\n            inlineSyntaxes: [\n              StrikethroughSyntax(),\n              CustomAttributeSyntax(),\n            ],\n          ),\n        ),\n        style: DocumentStyle(\n          padding: const EdgeInsets.all(16),\n          customStyles: {\n            'author': InlineTextStyle(\n              color: const Color(0xFF888888),\n              fontSize: 16,\n              fontStyle: FontStyle.italic,\n            ),\n          },\n        ),\n        size: size,\n      ),\n    );\n    await super.onLoad();\n  }\n}\n"
  },
  {
    "path": "packages/flame_markdown/example/pubspec.yaml",
    "content": "name: flame_markdown_example\nresolution: workspace\ndescription: Simple project to showcase the usage of flame_markdown\npublish_to: 'none'\nversion: 0.1.0\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_markdown: ^0.2.4+14\n  flutter:\n    sdk: flutter\n  markdown: ^7.3.0\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n\nflutter:\n  uses-material-design: false\n  assets:\n    - assets/"
  },
  {
    "path": "packages/flame_markdown/lib/custom_attribute_syntax.dart",
    "content": "import 'package:markdown/markdown.dart';\n\n// cSpell:ignore charcode.dart (file name from another package)\n// NOTE: values obtained from file `charcode.dart` from the markdown package\nclass _Chars {\n  /// Character `[`.\n  static const int leftBracket = 0x5B;\n\n  /// Character `{`.\n  static const int leftBrace = 0x7B;\n\n  /// Character `}`.\n  static const int rightBrace = 0x7D;\n}\n\n/// Allows for a toned-down version of custom attributes extension for markdown,\n/// inspired by the markdown-it-attrs package.\n///\n/// This allows users to specify a custom class name to a span of text:\n///\n/// ```markdown\n/// [This is a custom class]{.my-custom-class}\n/// This word will be [red]{.red} and this one will be [blue]{.blue}.\n/// ```\n///\n/// This is based on the standard Link markdown parser (which matches the\n/// `[text](url)` and `[text][ref]` syntaxes).\nclass CustomAttributeSyntax extends LinkSyntax {\n  /// Creates a new custom attribute syntax.\n  CustomAttributeSyntax()\n    : super(\n        pattern: r'\\[',\n        startCharacter: _Chars.leftBracket,\n      );\n\n  @override\n  Iterable<Node>? close(\n    InlineParser parser,\n    covariant SimpleDelimiter opener,\n    Delimiter? closer, {\n    required List<Node> Function() getChildren,\n    String? tag,\n  }) {\n    final text = parser.source.substring(opener.endPos, parser.pos);\n\n    // The current character is the `]` that closed the span text.\n    // The next character must be a `{`:\n    parser.advanceBy(1);\n    if (parser.isDone) {\n      return null; // not valid syntax - skip\n    }\n    final char = parser.charAt(parser.pos);\n    if (char != _Chars.leftBrace) {\n      return null; // not valid syntax - skip\n    }\n\n    final attributes = _parseAttributes(parser) ?? {};\n    final node = _createNode(text, attributes, getChildren: getChildren);\n    return [node];\n  }\n\n  /// Create this node represented by a span with custom attributes.\n  Node _createNode(\n    String text,\n    Map<String, String> attributes, {\n    required List<Node> Function() getChildren,\n  }) {\n    final children = getChildren();\n    final element = Element('span', children);\n    for (final attr in attributes.entries) {\n      element.attributes[attr.key] = attr.value;\n    }\n    return element;\n  }\n\n  /// At this point, we have parsed a custom tag opening `[`, and then a\n  /// matching closing `]`, and now [parser] is pointing at an opening `{`.\n  Map<String, String>? _parseAttributes(InlineParser parser) {\n    // Start walking to the character just after the opening `{`.\n    parser.advanceBy(1);\n\n    final buffer = StringBuffer();\n\n    while (true) {\n      final char = parser.charAt(parser.pos);\n      if (char == _Chars.rightBrace) {\n        final attributes = buffer.toString();\n        return _parseAttributeList(attributes);\n      }\n\n      buffer.writeCharCode(char);\n      parser.advanceBy(1);\n      if (parser.isDone) {\n        return null; // not valid syntax - skip\n      }\n    }\n  }\n\n  Map<String, String>? _parseAttributeList(String attributes) {\n    // Currently we only support one attribute being the class name.\n    // More support can be added in the future following the syntax from\n    // the markdown-it library.\n    final regex = RegExp(r'\\.([a-zA-Z0-9_-]+)'); // matches `.class-name`\n    final content = attributes.trim();\n    final className = regex.firstMatch(content)?.group(1);\n    if (className == null) {\n      return null; // not valid syntax - skip\n    }\n    return {'class': className};\n  }\n}\n"
  },
  {
    "path": "packages/flame_markdown/lib/flame_markdown.dart",
    "content": "import 'package:flame/text.dart';\nimport 'package:markdown/markdown.dart';\n\n/// Helper to parse markdown strings into an AST structure provided by the\n/// `markdown` package, and convert that structure into an equivalent\n/// [DocumentRoot] from Flame.\n///\n/// This allows for the creation of rich-text components on Flame using a\n/// very simple and easy-to-write markdown syntax.\n///\n/// Note more advanced markdown features are not supported, such as tables,\n/// code blocks, images, and inline HTML.\n/// It is also possible that some otherwise valid markdown nestings of\n/// block and inline-type elements are not currently supported.\nclass FlameMarkdown {\n  /// Converts a markdown string to a [DocumentRoot] from Flame.\n  ///\n  /// This uses the `markdown` package to parse the markdown string\n  /// into an AST structure, and then converts that structure into\n  /// a [DocumentRoot] from Flame.\n  static DocumentRoot toDocument(String markdown, {Document? document}) {\n    final nodes = _parse(markdown, document: document);\n    return DocumentRoot(\n      nodes.map(_convertNode).map(_castCheck<BlockNode>).toList(),\n    );\n  }\n\n  static List<Node> _parse(String markdown, {Document? document}) {\n    return (document ?? _defaultDocument()).parse(markdown);\n  }\n\n  static Document _defaultDocument() {\n    return Document(\n      encodeHtml: false,\n    );\n  }\n\n  static TextNode _convertNode(Node node) {\n    if (node is Element) {\n      return _convertElement(node);\n    } else if (node is Text) {\n      return _convertText(node);\n    } else {\n      throw Exception('Unknown node type: ${node.runtimeType}');\n    }\n  }\n\n  static TextNode _convertElement(Element element) {\n    final children = (element.children ?? [])\n        .map(_convertNode)\n        .map(_castCheck<InlineTextNode>)\n        .toList();\n    final child = _groupInlineChildren(children);\n\n    final customClassName = element.attributes['class'];\n    if (customClassName != null) {\n      if (element.tag != 'span') {\n        throw Exception(\n          'Invalid markdown structure: '\n          'Only <span> elements can have custom classes',\n        );\n      }\n      return CustomInlineTextNode(child, styleName: customClassName);\n    }\n\n    return switch (element.tag) {\n          'span' => child,\n          'h1' => HeaderNode(child, level: 1),\n          'h2' => HeaderNode(child, level: 2),\n          'h3' => HeaderNode(child, level: 3),\n          'h4' => HeaderNode(child, level: 4),\n          'h5' => HeaderNode(child, level: 5),\n          'h6' => HeaderNode(child, level: 6),\n          'p' => ParagraphNode(child),\n          'em' || 'i' => ItalicTextNode(child),\n          'strong' || 'b' => BoldTextNode(child),\n          'code' => CodeTextNode(child),\n          'del' => StrikethroughTextNode(child),\n          _ => throw Exception('Unknown element tag: ${element.tag}'),\n        }\n        as TextNode;\n  }\n\n  static PlainTextNode _convertText(Text text) {\n    return PlainTextNode(text.text);\n  }\n\n  static InlineTextNode _groupInlineChildren(List<InlineTextNode> children) {\n    if (children.isEmpty) {\n      throw 'Invalid markdown structure: Found block element with no children';\n    } else if (children.length == 1) {\n      return children.single;\n    } else {\n      return GroupTextNode(children);\n    }\n  }\n\n  static T _castCheck<T extends TextNode>(TextNode node) {\n    if (node is T) {\n      return node;\n    } else {\n      throw 'Invalid markdown structure: '\n          'Expected $T but got ${node.runtimeType}';\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_markdown/pubspec.yaml",
    "content": "name: flame_markdown\nresolution: workspace\ndescription: |\n  Markdown support for the Flame game engine, bridging the markdown package into Flame's text rendering pipeline.\nversion: 0.2.4+14\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_markdown\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - markdown\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  markdown: ^7.1.1\n\ndev_dependencies:\n  dartdoc: ^9.0.0\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter"
  },
  {
    "path": "packages/flame_markdown/test/flame_markdown_test.dart",
    "content": "import 'dart:io';\nimport 'dart:ui';\n\nimport 'package:flame/text.dart';\nimport 'package:flame_markdown/custom_attribute_syntax.dart';\nimport 'package:flame_markdown/flame_markdown.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:markdown/markdown.dart';\n\nvoid main() {\n  group('FlameMarkdown#toDocument', () {\n    test('just plain text', () {\n      final doc = FlameMarkdown.toDocument('Hello world!');\n\n      _expectDocument(doc, [\n        (node) => _expectSimpleParagraph(node, 'Hello world!'),\n      ]);\n\n      final element = doc.format(\n        DocumentStyle(\n          width: 1000,\n          text: InlineTextStyle(\n            fontSize: 12,\n          ),\n        ),\n      );\n\n      _expectElementGroup(element, [\n        (el) => _expectElementGroup(el, [\n          (el) => _expectElementTextPainter(\n            el,\n            'Hello world!',\n            const TextStyle(\n              fontSize: 12,\n            ),\n          ),\n        ]),\n      ]);\n    });\n\n    test('rich text', () {\n      final doc = FlameMarkdown.toDocument('**Flame**: Hello, _world_!');\n\n      _expectDocument(doc, [\n        (node) => _expectParagraph(node, (p) {\n          _expectGroup(p, [\n            (node) => _expectBold(node, 'Flame'),\n            (node) => _expectPlain(node, ': Hello, '),\n            (node) => _expectItalic(node, 'world'),\n            (node) => _expectPlain(node, '!'),\n          ]);\n        }),\n      ]);\n\n      final element = doc.format(\n        DocumentStyle(\n          width: 1000,\n          text: InlineTextStyle(\n            fontSize: 12,\n          ),\n          boldText: InlineTextStyle(\n            fontWeight: FontWeight.bold,\n          ),\n          italicText: InlineTextStyle(\n            fontStyle: FontStyle.italic,\n          ),\n        ),\n      );\n\n      _expectElementGroup(element, [\n        (el) => _expectElementGroup(el, [\n          (el) => _expectElementGroupText(el, [\n            (el) => _expectElementTextPainter(\n              el,\n              'Flame',\n              const TextStyle(\n                fontSize: 12,\n                fontWeight: FontWeight.bold,\n              ),\n            ),\n            (el) => _expectElementTextPainter(\n              el,\n              ': Hello, ',\n              const TextStyle(\n                fontSize: 12,\n              ),\n            ),\n            (el) => _expectElementTextPainter(\n              el,\n              'world',\n              const TextStyle(\n                fontSize: 12,\n                fontStyle: FontStyle.italic,\n              ),\n            ),\n            (el) => _expectElementTextPainter(\n              el,\n              '!',\n              const TextStyle(\n                fontSize: 12,\n              ),\n            ),\n          ]),\n        ]),\n      ]);\n    });\n\n    test('inline code block', () {\n      final doc = FlameMarkdown.toDocument('Flame: `var game = FlameGame();`');\n\n      _expectDocument(doc, [\n        (node) => _expectParagraph(node, (p) {\n          _expectGroup(p, [\n            (node) => _expectPlain(node, 'Flame: '),\n            (node) => _expectCode(node, 'var game = FlameGame();'),\n          ]);\n        }),\n      ]);\n\n      final element = doc.format(\n        DocumentStyle(\n          width: 1000,\n          text: InlineTextStyle(\n            fontSize: 12,\n          ),\n          codeText: InlineTextStyle(\n            fontFamily: 'monospace',\n          ),\n        ),\n      );\n\n      _expectElementGroup(element, [\n        (el) => _expectElementGroup(el, [\n          (el) => _expectElementGroupText(el, [\n            (el) => _expectElementTextPainter(\n              el,\n              'Flame: ',\n              const TextStyle(\n                fontSize: 12,\n              ),\n            ),\n            (el) => _expectElementTextPainter(\n              el,\n              'var game = FlameGame();',\n              const TextStyle(\n                fontSize: 12,\n                fontFamily: 'monospace',\n              ),\n            ),\n          ]),\n        ]),\n      ]);\n    });\n\n    test('nested inline blocks', () {\n      final doc = FlameMarkdown.toDocument(\n        '**This _is `code` inside italics_ inside bold.**',\n      );\n\n      _expectDocument(doc, [\n        (node) => _expectParagraph(node, (p) {\n          _expectBoldGroup(p, [\n            (node) => _expectPlain(node, 'This '),\n            (node) => _expectItalicGroup(node, [\n              (node) => _expectPlain(node, 'is '),\n              (node) => _expectCode(node, 'code'),\n              (node) => _expectPlain(node, ' inside italics'),\n            ]),\n            (node) => _expectPlain(node, ' inside bold.'),\n          ]);\n        }),\n      ]);\n\n      final element = doc.format(\n        DocumentStyle(\n          width: 1000,\n          text: InlineTextStyle(\n            fontSize: 12,\n          ),\n          boldText: InlineTextStyle(\n            fontWeight: FontWeight.bold,\n          ),\n          italicText: InlineTextStyle(\n            fontStyle: FontStyle.italic,\n          ),\n          codeText: InlineTextStyle(\n            fontFamily: 'monospace',\n          ),\n        ),\n      );\n\n      _expectElementGroup(element, [\n        (el) => _expectElementGroup(el, [\n          (el) => _expectElementGroupText(el, [\n            (el) => _expectElementTextPainter(\n              el,\n              'This ',\n              const TextStyle(\n                fontSize: 12,\n                fontWeight: FontWeight.bold,\n              ),\n            ),\n            (el) => _expectElementGroupText(el, [\n              (el) => _expectElementTextPainter(\n                el,\n                'is ',\n                const TextStyle(\n                  fontSize: 12,\n                  fontWeight: FontWeight.bold,\n                  fontStyle: FontStyle.italic,\n                ),\n              ),\n              (el) => _expectElementTextPainter(\n                el,\n                'code',\n                const TextStyle(\n                  fontSize: 12,\n                  fontWeight: FontWeight.bold,\n                  fontStyle: FontStyle.italic,\n                  fontFamily: 'monospace',\n                ),\n              ),\n              (el) => _expectElementTextPainter(\n                el,\n                ' inside italics',\n                const TextStyle(\n                  fontSize: 12,\n                  fontWeight: FontWeight.bold,\n                  fontStyle: FontStyle.italic,\n                ),\n              ),\n            ]),\n            (el) => _expectElementTextPainter(\n              el,\n              ' inside bold.',\n              const TextStyle(\n                fontSize: 12,\n                fontWeight: FontWeight.bold,\n              ),\n            ),\n          ]),\n        ]),\n      ]);\n    });\n\n    test('all header levels', () {\n      final doc = FlameMarkdown.toDocument(\n        '# h1\\n'\n        '## h2\\n'\n        '### h3\\n'\n        '#### h4\\n'\n        '##### h5\\n'\n        '###### h6\\n',\n      );\n\n      _expectDocument(doc, [\n        (node) => _expectHeader(node, 1, 'h1'),\n        (node) => _expectHeader(node, 2, 'h2'),\n        (node) => _expectHeader(node, 3, 'h3'),\n        (node) => _expectHeader(node, 4, 'h4'),\n        (node) => _expectHeader(node, 5, 'h5'),\n        (node) => _expectHeader(node, 6, 'h6'),\n      ]);\n    });\n\n    test('several paragraphs with header', () {\n      final markdown = File(\n        'example/assets/fire_and_ice.md',\n      ).readAsStringSync();\n      final doc = FlameMarkdown.toDocument(markdown);\n\n      _expectDocument(doc, [\n        (node) => _expectHeader(node, 1, 'Fire & Ice'),\n        (node) => _expectParagraph(node, (p) {\n          _expectGroup(p, [\n            (node) => _expectPlain(\n              node,\n              // note: strike-trough is only parsed if enabled\n              'Some say the world will ~~end~~ in ',\n            ),\n            (node) => _expectBold(node, 'fire'),\n            (node) => _expectPlain(node, ','),\n          ]);\n        }),\n        (node) => _expectParagraph(\n          node,\n          (p) => _expectGroup(p, [\n            (node) => _expectPlain(node, 'Some say in '),\n            (node) => _expectItalic(node, 'ice'),\n            (node) => _expectPlain(node, '.'),\n          ]),\n        ),\n        (node) => _expectSimpleParagraph(\n          node,\n          \"From what I've tasted of >desire<,\",\n        ),\n        (node) => _expectParagraph(node, (p) {\n          _expectGroup(p, [\n            (node) => _expectPlain(node, 'I hold with those who favor '),\n            (node) => _expectBold(node, 'fire'),\n            (node) => _expectPlain(node, '.'),\n          ]);\n        }),\n        // note: custom attribute is only parsed if enabled\n        (node) => _expectParagraph(node, (p) {\n          _expectPlain(p, '[- by Robert Frost]{.author}');\n        }),\n      ]);\n    });\n\n    test('strikethrough can be enabled', () {\n      const markdown = 'Flame ~~will be~~ is a great game engine!';\n      final doc = FlameMarkdown.toDocument(\n        markdown,\n        document: Document(\n          encodeHtml: false,\n          inlineSyntaxes: [\n            StrikethroughSyntax(),\n          ],\n        ),\n      );\n\n      _expectDocument(doc, [\n        (node) => _expectParagraph(node, (p) {\n          _expectGroup(p, [\n            (node) => _expectPlain(node, 'Flame '),\n            (node) => _expectStrikethrough(node, 'will be'),\n            (node) => _expectPlain(node, ' is a great game engine!'),\n          ]);\n        }),\n      ]);\n    });\n\n    test('custom attributes can be enabled', () {\n      const markdown =\n          'This one will be [red]{.red} and this one will be [blue]{.blue}.';\n      final doc = FlameMarkdown.toDocument(\n        markdown,\n        document: Document(\n          encodeHtml: false,\n          inlineSyntaxes: [\n            CustomAttributeSyntax(),\n          ],\n        ),\n      );\n\n      _expectDocument(doc, [\n        (node) => _expectParagraph(node, (p) {\n          _expectGroup(p, [\n            (node) => _expectPlain(node, 'This one will be '),\n            (node) => _expectCustom(node, 'red', styleName: 'red'),\n            (node) => _expectPlain(node, ' and this one will be '),\n            (node) => _expectCustom(node, 'blue', styleName: 'blue'),\n            (node) => _expectPlain(node, '.'),\n          ]);\n        }),\n      ]);\n\n      final element = doc.format(\n        DocumentStyle(\n          width: 1000,\n          text: InlineTextStyle(\n            fontSize: 12,\n          ),\n          customStyles: {\n            'red': InlineTextStyle(\n              color: const Color(0xFFFF0000),\n            ),\n            'blue': InlineTextStyle(\n              color: const Color(0xFF0000FF),\n            ),\n          },\n        ),\n      );\n\n      _expectElementGroup(element, [\n        (el) => _expectElementGroup(el, [\n          (el) => _expectElementGroupText(el, [\n            (el) => _expectElementTextPainter(\n              el,\n              'This one will be ',\n              const TextStyle(\n                fontSize: 12,\n              ),\n            ),\n            (el) => _expectElementTextPainter(\n              el,\n              'red',\n              const TextStyle(\n                fontSize: 12,\n                color: Color(0xFFFF0000),\n              ),\n            ),\n            (el) => _expectElementTextPainter(\n              el,\n              ' and this one will be ',\n              const TextStyle(\n                fontSize: 12,\n              ),\n            ),\n            (el) => _expectElementTextPainter(\n              el,\n              'blue',\n              const TextStyle(\n                fontSize: 12,\n                color: Color(0xFF0000FF),\n              ),\n            ),\n            (el) => _expectElementTextPainter(\n              el,\n              '.',\n              const TextStyle(\n                fontSize: 12,\n              ),\n            ),\n          ]),\n        ]),\n      ]);\n    });\n  });\n}\n\n// node expects\n\nvoid _expectStrikethrough(InlineTextNode node, String text) {\n  expect(node, isA<StrikethroughTextNode>());\n  final content = (node as StrikethroughTextNode).child;\n  expect(content, isA<PlainTextNode>());\n  expect((content as PlainTextNode).text, text);\n}\n\nvoid _expectBold(InlineTextNode node, String text) {\n  expect(node, isA<BoldTextNode>());\n  final content = (node as BoldTextNode).child;\n  expect(content, isA<PlainTextNode>());\n  expect((content as PlainTextNode).text, text);\n}\n\nvoid _expectBoldGroup(\n  InlineTextNode node,\n  List<void Function(InlineTextNode)> expectChildren,\n) {\n  expect(node, isA<BoldTextNode>());\n  final content = (node as BoldTextNode).child;\n  _expectGroup(content, expectChildren);\n}\n\nvoid _expectItalicGroup(\n  InlineTextNode node,\n  List<void Function(InlineTextNode)> expectChildren,\n) {\n  expect(node, isA<ItalicTextNode>());\n  final content = (node as ItalicTextNode).child;\n  _expectGroup(content, expectChildren);\n}\n\nvoid _expectItalic(InlineTextNode node, String text) {\n  expect(node, isA<ItalicTextNode>());\n  final content = (node as ItalicTextNode).child;\n  expect(content, isA<PlainTextNode>());\n  expect((content as PlainTextNode).text, text);\n}\n\nvoid _expectPlain(InlineTextNode node, String text) {\n  expect(node, isA<PlainTextNode>());\n  final span = node as PlainTextNode;\n  expect(span.text, text);\n}\n\nvoid _expectCustom(\n  InlineTextNode node,\n  String text, {\n  required String styleName,\n}) {\n  expect(node, isA<CustomInlineTextNode>());\n  final custom = node as CustomInlineTextNode;\n  expect(custom.child, isA<PlainTextNode>());\n  expect((custom.child as PlainTextNode).text, text);\n  expect(custom.styleName, styleName);\n}\n\nvoid _expectCode(InlineTextNode node, String text) {\n  expect(node, isA<CodeTextNode>());\n  final content = (node as CodeTextNode).child;\n  expect(content, isA<PlainTextNode>());\n  expect((content as PlainTextNode).text, text);\n}\n\nvoid _expectParagraph(\n  BlockNode node,\n  void Function(InlineTextNode) expectChild,\n) {\n  expect(node, isA<ParagraphNode>());\n  final p = node as ParagraphNode;\n  expectChild(p.child);\n}\n\nvoid _expectSimpleParagraph(BlockNode node, String text) {\n  _expectParagraph(node, (child) => _expectPlain(child, text));\n}\n\nvoid _expectHeader(BlockNode node, int level, String text) {\n  expect(node, isA<HeaderNode>());\n  final header = node as HeaderNode;\n  expect(header.level, level);\n  expect(header.child, isA<PlainTextNode>());\n  expect((header.child as PlainTextNode).text, text);\n}\n\nvoid _expectGroup(\n  InlineTextNode node,\n  List<void Function(InlineTextNode)> expectChildren,\n) {\n  expect(node, isA<GroupTextNode>());\n  final group = node as GroupTextNode;\n  expect(group.children, hasLength(expectChildren.length));\n  for (final (index, expectChild) in expectChildren.indexed) {\n    expectChild(group.children[index]);\n  }\n}\n\nvoid _expectDocument(\n  DocumentRoot root,\n  List<void Function(BlockNode)> expectChildren,\n) {\n  expect(root.children, hasLength(expectChildren.length));\n  for (final (index, expectChild) in expectChildren.indexed) {\n    expectChild(root.children[index]);\n  }\n}\n\n// element expects\n\nvoid _expectElementGroup(\n  TextElement element,\n  List<void Function(TextElement)> expectChildren,\n) {\n  expect(element, isA<GroupElement>());\n  final group = element as GroupElement;\n  expect(group.children, hasLength(expectChildren.length));\n  for (final (index, expectChild) in expectChildren.indexed) {\n    expectChild(group.children[index]);\n  }\n}\n\nvoid _expectElementGroupText(\n  TextElement element,\n  List<void Function(TextElement)> expectChildren,\n) {\n  expect(element, isA<GroupTextElement>());\n  final group = element as GroupTextElement;\n  expect(group.children, hasLength(expectChildren.length));\n  for (final (index, expectChild) in expectChildren.indexed) {\n    expectChild(group.children[index]);\n  }\n}\n\nvoid _expectElementTextPainter(\n  TextElement element,\n  String text,\n  TextStyle style,\n) {\n  expect(element, isA<TextPainterTextElement>());\n  final textPainterElement = element as TextPainterTextElement;\n  expect(textPainterElement.textPainter.text!.toPlainText(), text);\n  expect(textPainterElement.textPainter.text!.style, style);\n}\n"
  },
  {
    "path": "packages/flame_network_assets/CHANGELOG.md",
    "content": "## 0.3.3+21\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 0.3.3+20\n\n - Update a dependency to the latest release.\n\n## 0.3.3+19\n\n - Update a dependency to the latest release.\n\n## 0.3.3+18\n\n - **FIX**: Not re-encode image when saving ([#3780](https://github.com/flame-engine/flame/issues/3780)). ([30a344cf](https://github.com/flame-engine/flame/commit/30a344cfae78c6608713d4b194f3112e47068adf))\n\n## 0.3.3+17\n\n - **DOCS**: Deprecate TapDetector in favour of TapCallbacks ([#2886](https://github.com/flame-engine/flame/issues/2886)). ([b173697b](https://github.com/flame-engine/flame/commit/b173697bfb7ea61287251c43cd3c9d2fdb448fe3))\n\n## 0.3.3+16\n\n - Update a dependency to the latest release.\n\n## 0.3.3+15\n\n - Update a dependency to the latest release.\n\n## 0.3.3+14\n\n - Update a dependency to the latest release.\n\n## 0.3.3+13\n\n - Update a dependency to the latest release.\n\n## 0.3.3+12\n\n - Update a dependency to the latest release.\n\n## 0.3.3+11\n\n - Update a dependency to the latest release.\n\n## 0.3.3+10\n\n - Update a dependency to the latest release.\n\n## 0.3.3+9\n\n - Update a dependency to the latest release.\n\n## 0.3.3+8\n\n - Update a dependency to the latest release.\n\n## 0.3.3+7\n\n - Update a dependency to the latest release.\n\n## 0.3.3+6\n\n - Update a dependency to the latest release.\n\n## 0.3.3+5\n\n - Update a dependency to the latest release.\n\n## 0.3.3+4\n\n - Update a dependency to the latest release.\n\n## 0.3.3+3\n\n - Update a dependency to the latest release.\n\n## 0.3.3+2\n\n - Update a dependency to the latest release.\n\n## 0.3.3+1\n\n - Update a dependency to the latest release.\n\n## 0.3.3\n\n## 0.3.2\n\n## 0.3.1\n\n - **FEAT**: Update http dependency on flame_network_assets ([#3084](https://github.com/flame-engine/flame/issues/3084)). ([e3e755c6](https://github.com/flame-engine/flame/commit/e3e755c6dec35f36b4a42893afeea5f64ff025b7))\n\n## 0.3.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 0.2.0+13\n\n - Update a dependency to the latest release.\n\n## 0.2.0+12\n\n - Update a dependency to the latest release.\n\n## 0.2.0+11\n\n - Update a dependency to the latest release.\n\n## 0.2.0+10\n\n - Update a dependency to the latest release.\n\n## 0.2.0+9\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n## 0.2.0+8\n\n - Update a dependency to the latest release.\n\n## 0.2.0+7\n\n - **FIX**: Remove deprecations for 1.10.0 ([#2809](https://github.com/flame-engine/flame/issues/2809)). ([5b67b8f1](https://github.com/flame-engine/flame/commit/5b67b8f14ad4fdb38a249d0a41ecba49ba2fcc44))\n\n## 0.2.0+6\n\n - Update a dependency to the latest release.\n\n## 0.2.0+5\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n\n## 0.2.0+4\n\n - Update a dependency to the latest release.\n\n## 0.2.0+3\n\n - Update a dependency to the latest release.\n\n## 0.2.0+2\n\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n## 0.2.0+1\n\n - Update a dependency to the latest release.\n\n## 0.2.0\n\n> Note: This release has breaking changes.\n\n - **FEAT**: Add network assets package. ([#2314](https://github.com/flame-engine/flame/issues/2314)). ([61d69656](https://github.com/flame-engine/flame/commit/61d69656de2cede71cd4f1b4c469ebb4904c4ce8))\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **BREAKING** **FEAT**: The `HasTappableComponents` mixin is no longer needed ([#2450](https://github.com/flame-engine/flame/issues/2450)). ([b5bdf4ec](https://github.com/flame-engine/flame/commit/b5bdf4ec173e87907a59a9f62fcdf35cc968af2a))\n\n"
  },
  {
    "path": "packages/flame_network_assets/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/flame_network_assets/README.md",
    "content": "# flame_network_assets\n\n<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nAdds network images support to <a href=\"https://github.com/flame-engine/flame\">Flame</a>.\n</p>\n\n<p align=\"center\">\n  <img src=\"https://github.com/flame-engine/flame_network_image/workflows/Lint/badge.svg?branch=master&event=push\" alt=\"Test\" />\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\" ><img src=\"https://img.shields.io/discord/509714518008528896.svg\" /></a>\n</p>\n<!-- markdownlint-enable MD013 -->\n\n---\n\nThis package makes it easy to use and cache assets from the network inside a Flame game.\n\nFor instructions on how to use this package to load images,\ncheck [Flame docs](https://docs.flame-engine.org/1.6.0/bridge_packages/flame_network_assets/flame_network_assets.html).\n"
  },
  {
    "path": "packages/flame_network_assets/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    - public_member_api_docs\n"
  },
  {
    "path": "packages/flame_network_assets/example/README.md",
    "content": "# flame_network_images example\n\nAn example app that shows how to use the `flame_network_images` package.\n"
  },
  {
    "path": "packages/flame_network_assets/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_network_assets/example/lib/main.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_network_assets/flame_network_assets.dart';\nimport 'package:flutter/material.dart' hide Image;\n\nvoid main() {\n  runApp(const GameWidget.controlled(gameFactory: MyGame.new));\n}\n\nclass MyGame extends FlameGame with TapCallbacks {\n  final networkImages = FlameNetworkImages();\n  late Image playerSprite;\n\n  @override\n  Future<void> onLoad() async {\n    playerSprite = await networkImages.load(\n      'https://examples.flame-engine.org/assets/assets/images/bomb_ptero.png',\n    );\n  }\n\n  @override\n  void onTapUp(TapUpEvent event) {\n    add(\n      SpriteAnimationComponent.fromFrameData(\n        playerSprite,\n        SpriteAnimationData.sequenced(\n          textureSize: Vector2(48, 32),\n          amount: 4,\n          stepTime: 0.2,\n        ),\n        size: Vector2(100, 50),\n        anchor: Anchor.center,\n        position: event.canvasPosition,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_network_assets/example/pubspec.yaml",
    "content": "name: flame_network_assets_example\nresolution: workspace\ndescription: A flame network assets example.\npublish_to: 'none'\n\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_network_assets: ^0.3.3+21\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n"
  },
  {
    "path": "packages/flame_network_assets/lib/flame_network_assets.dart",
    "content": "library flame_network_assets;\n\nexport 'src/flame_asset_response.dart';\nexport 'src/flame_network_assets.dart';\nexport 'src/flame_network_images.dart';\n"
  },
  {
    "path": "packages/flame_network_assets/lib/src/flame_asset_response.dart",
    "content": "import 'dart:typed_data';\n\n/// {@template flame_assets_response}\n/// A class containing the relevant http attributes to\n/// Flame Assets Network package.\n/// {@endtemplate}\nclass FlameAssetResponse {\n  /// {@macro flame_assets_response}\n  const FlameAssetResponse({\n    required this.statusCode,\n    required this.bytes,\n  });\n\n  /// Http status code.\n  final int statusCode;\n\n  /// response bytes.\n  final Uint8List bytes;\n}\n"
  },
  {
    "path": "packages/flame_network_assets/lib/src/flame_network_assets.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:flame/cache.dart';\nimport 'package:flame_network_assets/flame_network_assets.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as path;\nimport 'package:path_provider/path_provider.dart';\n\n/// Function signature used by Flame Network Assets to fetch assets.\ntypedef GetAssetFunction =\n    Future<FlameAssetResponse> Function(\n      String url, {\n      Map<String, String>? headers,\n    });\n\n/// Function signature used by Flame Network Assets to decode assets from a\n/// raw format.\ntypedef DecodeAssetFunction<T> = Future<T> Function(Uint8List);\n\n/// Function signature used by Flame Network Assets to encode assets to a\n/// raw format.\ntypedef EncodeAssetFunction<T> = Future<Uint8List> Function(T);\n\n/// Function signature by Flame Network Assets to get the app directory\n/// which is used for the local storage caching.\ntypedef GetAppDirectoryFunction = Future<Directory> Function();\n\n/// {@template flame_network_assets}\n///\n/// [FlameNetworkAssets] is a class similar to Flame's assets classes (like\n/// [Images] for example), but instead of loading assets from the assets bundle,\n/// it loads from networks urls.\n///\n/// By default, [FlameNetworkAssets] uses the [http.get] method to make the\n/// requests. It can be customized by passing a different [GetAssetFunction] to\n/// the `get` argument on the constructor.\n///\n/// [FlameNetworkAssets] also will automatically cache files in a two layer\n/// system.\n///\n/// The first layer is an in-memory cache, handled by an internal [MemoryCache],\n/// while the second one is the device's own file system, where images are\n/// cached in the application document directory, which by default is provided\n/// by path_providers' [getApplicationDocumentsDirectory] method, and can\n/// be customized using the `getAppDirectory` argument in the constructor.\n///\n/// When an asset is requested, [FlameAssetResponse] will first check on its\n/// cache layers before making the http request, if both layer are cache miss,\n/// then the request is made and both layers set with the response.\n///\n/// Another important note about the cache layers is that the first layer, is a\n/// per instance cache, while the local storage is an app global cache. This\n/// means that two different [FlameAssetResponse] instances will have the same\n/// local storage cache, but not the same memory cache.\n///\n/// Note that the local storage layer is not present when running on web since\n/// that platform doesn't really have a file system. The browser caching will\n/// work as a similar replacement for this layer, though that can't be\n/// controlled by this package, make sure that the server where the images\n/// are being fetched returns the correct cache header to make the browser\n/// cache the assets.\n///\n/// Each cache layer can be disabled by the [cacheInMemory] or [cacheInStorage]\n/// argument on the constructor.\n///\n/// {@endtemplate}\nabstract class FlameNetworkAssets<T> {\n  /// {@macro flame_network_assets}\n  ///\n  /// - [decodeAsset] a [DecodeAssetFunction] responsible for decoding the asset\n  /// from its raw format.\n  /// - [encodeAsset] is an optional [EncodeAssetFunction] responsible for\n  /// encoding the asset to its raw format, if omitted the raw bytes from the\n  /// response will be cached.\n  /// - [get] is an optional [GetAssetFunction], if omitted [http.get] is used\n  /// by default.\n  /// - [getAppDirectory] is an optional [GetAppDirectoryFunction], if omitted\n  /// [getApplicationDocumentsDirectory] is used by default.\n  /// - [cacheInMemory] will not cache assets in the memory when false,\n  /// (true by default).\n  /// - [cacheInStorage] will not cache assets in the file system when false,\n  /// (true by default).\n  FlameNetworkAssets({\n    required DecodeAssetFunction<T> decodeAsset,\n    EncodeAssetFunction<T>? encodeAsset,\n    GetAssetFunction? get,\n    GetAppDirectoryFunction? getAppDirectory,\n    this.cacheInMemory = true,\n    this.cacheInStorage = true,\n  }) : _isWeb = kIsWeb,\n       _decode = decodeAsset,\n       _encode = encodeAsset {\n    _get =\n        get ??\n        (\n          String url, {\n          Map<String, String>? headers,\n        }) => http.get(Uri.parse(url), headers: headers).then((response) {\n          return FlameAssetResponse(\n            statusCode: response.statusCode,\n            bytes: response.bodyBytes,\n          );\n        });\n\n    _getAppDirectory = getAppDirectory ?? getApplicationDocumentsDirectory;\n  }\n\n  late final GetAssetFunction _get;\n  late final GetAppDirectoryFunction _getAppDirectory;\n  final DecodeAssetFunction<T> _decode;\n  final EncodeAssetFunction<T>? _encode;\n\n  /// Flag indicating if files will be cached in memory.\n  final bool cacheInMemory;\n\n  /// Flag indicating if files will be cached in the local storage.\n  final bool cacheInStorage;\n\n  final bool _isWeb;\n\n  final _memoryCache = MemoryCache<String, T>();\n\n  String _urlToId(String url) {\n    final bytes = utf8.encode(url);\n    return base64.encode(bytes);\n  }\n\n  /// Loads the asset from the given url.\n  Future<T> load(\n    String url, {\n    Map<String, String>? headers,\n  }) async {\n    final id = _urlToId(url);\n\n    final memoryCacheValue = _memoryCache.getValue(id);\n    if (memoryCacheValue != null) {\n      return memoryCacheValue;\n    }\n\n    if (!_isWeb && cacheInStorage) {\n      final storageAsset = await _fetchAssetFromStorageCache(id);\n      if (storageAsset != null) {\n        if (cacheInMemory) {\n          _memoryCache.setValue(id, storageAsset);\n        }\n        return storageAsset;\n      }\n    }\n\n    final response = await _get(url, headers: headers);\n    if (response.statusCode >= 200 && response.statusCode < 400) {\n      final image = await _decode(response.bytes);\n\n      if (cacheInMemory) {\n        _memoryCache.setValue(id, image);\n      }\n\n      if (!_isWeb && cacheInStorage) {\n        if (_encode == null) {\n          unawaited(_saveBytesInLocalStorage(id, response.bytes));\n        } else {\n          unawaited(_saveAssetInLocalStorage(id, image));\n        }\n      }\n\n      return image;\n    } else {\n      throw Exception(\n        'Error fetching asset from $url, response return status code '\n        '${response.statusCode}',\n      );\n    }\n  }\n\n  Future<T?> _fetchAssetFromStorageCache(String id) async {\n    try {\n      final appDir = await _getAppDirectory();\n      final file = File(path.join(appDir.path, id));\n\n      if (file.existsSync()) {\n        final bytes = await file.readAsBytes();\n        return await _decode(bytes);\n      }\n    } on Exception catch (_) {\n      return null;\n    }\n    return null;\n  }\n\n  Future<void> _saveAssetInLocalStorage(String id, T asset) async {\n    try {\n      final appDir = await _getAppDirectory();\n      final file = File(path.join(appDir.path, id));\n\n      await file.writeAsBytes(await _encode!(asset));\n    } on Exception catch (_) {}\n  }\n\n  Future<void> _saveBytesInLocalStorage(String id, Uint8List bytesData) async {\n    try {\n      final appDir = await _getAppDirectory();\n      final file = File(path.join(appDir.path, id));\n\n      await file.writeAsBytes(bytesData);\n    } on Exception catch (_) {}\n  }\n}\n"
  },
  {
    "path": "packages/flame_network_assets/lib/src/flame_network_images.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame_network_assets/flame_network_assets.dart';\nimport 'package:flutter/rendering.dart';\n\n/// {@template flame_network_images}\n/// A specialized [FlameAssetResponse] that can be used to load [Image]s.\n///\n/// {@macro flame_network_assets}\n///\n/// {@endtemplate}\nclass FlameNetworkImages extends FlameNetworkAssets<Image> {\n  /// {@macro flame_network_images}\n  FlameNetworkImages({\n    super.get,\n    super.getAppDirectory,\n    super.cacheInMemory,\n    super.cacheInStorage,\n  }) : super(decodeAsset: decodeImageFromList);\n}\n"
  },
  {
    "path": "packages/flame_network_assets/pubspec.yaml",
    "content": "name: flame_network_assets\nresolution: workspace\ndescription: Network assets support for Flame.\nversion: 0.3.3+21\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_network_assets\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - assets\n  - network-assets\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  dev: ^1.0.0\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  http: ^1.2.1\n  path: ^1.8.3\n  path_provider: ^2.0.15\n\ndev_dependencies:\n  dartdoc: ^9.0.0\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n  mocktail: ^1.0.4\n"
  },
  {
    "path": "packages/flame_network_assets/test/flame_network_image_test.dart",
    "content": "import 'dart:io';\nimport 'dart:ui';\n\nimport 'package:flame_network_assets/flame_network_assets.dart';\nimport 'package:flutter/material.dart' hide Image;\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:path/path.dart' as path;\n\nabstract class __MockHttpClient {\n  Future<FlameAssetResponse> get(\n    String url, {\n    Map<String, String>? headers,\n  });\n}\n\nclass _MockHttpClient extends Mock implements __MockHttpClient {}\n\nabstract class __MockPathProvider {\n  Future<Directory> getAppDirectory();\n}\n\nclass _MockPathProvider extends Mock implements __MockPathProvider {}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('FlameNetworkAssets', () {\n    late __MockHttpClient httpClient;\n    late __MockPathProvider pathProvider;\n    late Directory testDirectory;\n    late FlameNetworkImages networkAssets;\n    late Image image;\n\n    setUpAll(() {\n      testDirectory = Directory(\n        path.join(\n          Directory.systemTemp.path,\n          'flame_network_assets_test',\n        ),\n      )..createSync();\n    });\n\n    tearDownAll(() {\n      testDirectory.deleteSync(recursive: true);\n    });\n\n    setUp(() async {\n      httpClient = _MockHttpClient();\n      pathProvider = _MockPathProvider();\n\n      when(pathProvider.getAppDirectory).thenAnswer((_) async => testDirectory);\n\n      networkAssets = FlameNetworkImages(\n        get: httpClient.get,\n        getAppDirectory: pathProvider.getAppDirectory,\n      );\n\n      final recorder = PictureRecorder();\n      final canvas = Canvas(recorder);\n      canvas.drawRect(\n        const Rect.fromLTWH(0, 0, 50, 50),\n        Paint()..color = Colors.pink,\n      );\n      final picture = recorder.endRecording();\n      image = await picture.toImage(50, 50);\n\n      final pngImage = await image.toByteData(format: ImageByteFormat.png);\n\n      when(\n        () => httpClient.get(any(), headers: any(named: 'headers')),\n      ).thenAnswer(\n        (_) async => FlameAssetResponse(\n          statusCode: 200,\n          bytes: pngImage!.buffer.asUint8List(),\n        ),\n      );\n    });\n\n    test('can be instantiated', () {\n      expect(\n        FlameNetworkImages(),\n        isNotNull,\n      );\n    });\n\n    test('returns the image', () async {\n      const url = 'https://image1.com';\n      final loadedImage = await networkAssets.load(url);\n      expect(loadedImage, isA<Image>());\n    });\n\n    test('fetches the image in the network', () async {\n      const url = 'https://image2.com';\n      await networkAssets.load(url);\n      verify(() => httpClient.get(url)).called(1);\n    });\n\n    test('returns the image from memory once it is cached', () async {\n      const url = 'https://image3.com';\n      final image1 = await networkAssets.load(url);\n      final image2 = await networkAssets.load(url);\n\n      verify(() => httpClient.get(url)).called(1);\n      verify(pathProvider.getAppDirectory).called(2);\n\n      expect(image1, equals(image2));\n    });\n\n    test('returns the image from local storage', () async {\n      const url = 'https://image4.com';\n\n      final image1 = await networkAssets.load(url);\n\n      final secondNetworkAssets = FlameNetworkImages(\n        getAppDirectory: pathProvider.getAppDirectory,\n        get: httpClient.get,\n      );\n\n      await Future<void>.delayed(const Duration(milliseconds: 100));\n\n      final image2 = await secondNetworkAssets.load(url);\n\n      verify(() => httpClient.get(url)).called(1);\n      verify(pathProvider.getAppDirectory).called(3);\n\n      expect(image1.width, equals(image2.width));\n      expect(image1.height, equals(image2.height));\n    });\n\n    test('can still get the image if the local storage breaks', () async {\n      const url = 'https://image5.com';\n\n      final image1 = await networkAssets.load(url);\n\n      final brokenPathProvider = _MockPathProvider();\n      when(brokenPathProvider.getAppDirectory).thenThrow(Exception());\n      final secondNetworkAssets = FlameNetworkImages(\n        getAppDirectory: brokenPathProvider.getAppDirectory,\n        get: httpClient.get,\n      );\n\n      await Future<void>.delayed(const Duration(milliseconds: 100));\n\n      final image2 = await secondNetworkAssets.load(url);\n\n      verify(() => httpClient.get(url)).called(2);\n\n      expect(image1.width, equals(image2.width));\n      expect(image1.height, equals(image2.height));\n    });\n\n    test('does not cache in memory when cacheInMemory is false', () async {\n      final secondNetworkAssets = FlameNetworkImages(\n        getAppDirectory: pathProvider.getAppDirectory,\n        get: httpClient.get,\n        cacheInMemory: false,\n      );\n      const url = 'https://image6.com';\n      final image1 = await secondNetworkAssets.load(url);\n      await Future<void>.delayed(const Duration(milliseconds: 100));\n      final image2 = await secondNetworkAssets.load(url);\n\n      verify(() => httpClient.get(url)).called(1);\n      verify(pathProvider.getAppDirectory).called(3);\n\n      expect(image1.width, equals(image2.width));\n      expect(image1.height, equals(image2.height));\n    });\n\n    test(\n      'does not cache in local storage when cacheInMemory is false',\n      () async {\n        final secondNetworkAssets = FlameNetworkImages(\n          getAppDirectory: pathProvider.getAppDirectory,\n          get: httpClient.get,\n          cacheInMemory: false,\n          cacheInStorage: false,\n        );\n        const url = 'https://image7.com';\n        final image1 = await secondNetworkAssets.load(url);\n        await Future<void>.delayed(const Duration(milliseconds: 100));\n        final image2 = await secondNetworkAssets.load(url);\n\n        verify(() => httpClient.get(url)).called(2);\n        verifyNever(pathProvider.getAppDirectory);\n\n        expect(image1.width, equals(image2.width));\n        expect(image1.height, equals(image2.height));\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_noise/CHANGELOG.md",
    "content": "## 0.3.2+21\n\n - **FIX**: `NoiseEffectController` producing zero progress on some platforms ([#3831](https://github.com/flame-engine/flame/issues/3831)). ([5d88832f](https://github.com/flame-engine/flame/commit/5d88832fa522a6880beeecb617b565aebcb703e7))\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 0.3.2+20\n\n - Update a dependency to the latest release.\n\n## 0.3.2+19\n\n - Update a dependency to the latest release.\n\n## 0.3.2+18\n\n - Update a dependency to the latest release.\n\n## 0.3.2+17\n\n - Update a dependency to the latest release.\n\n## 0.3.2+16\n\n - Update a dependency to the latest release.\n\n## 0.3.2+15\n\n - Update a dependency to the latest release.\n\n## 0.3.2+14\n\n - Update a dependency to the latest release.\n\n## 0.3.2+13\n\n - Update a dependency to the latest release.\n\n## 0.3.2+12\n\n - Update a dependency to the latest release.\n\n## 0.3.2+11\n\n - Update a dependency to the latest release.\n\n## 0.3.2+10\n\n - Update a dependency to the latest release.\n\n## 0.3.2+9\n\n - Update a dependency to the latest release.\n\n## 0.3.2+8\n\n - Update a dependency to the latest release.\n\n## 0.3.2+7\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 0.3.2+6\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 0.3.2+5\n\n - Update a dependency to the latest release.\n\n## 0.3.2+4\n\n - Update a dependency to the latest release.\n\n## 0.3.2+3\n\n - Update a dependency to the latest release.\n\n## 0.3.2+2\n\n - Update a dependency to the latest release.\n\n## 0.3.2+1\n\n - Update a dependency to the latest release.\n\n## 0.3.2\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 0.3.1\n\n## 0.3.0+1\n\n - Update a dependency to the latest release.\n\n## 0.3.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 0.2.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FEAT**: Update flame_noise to use latest version of fast_noise ([#3015](https://github.com/flame-engine/flame/issues/3015)). ([2fd84c84](https://github.com/flame-engine/flame/commit/2fd84c846f808bf593ef568150ffb49eecaebf30))\n\n## 0.1.1+12\n\n - Update a dependency to the latest release.\n\n## 0.1.1+11\n\n - Update a dependency to the latest release.\n\n## 0.1.1+10\n\n - Update a dependency to the latest release.\n\n## 0.1.1+9\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n## 0.1.1+8\n\n - Update a dependency to the latest release.\n\n## 0.1.1+7\n\n - Update a dependency to the latest release.\n\n## 0.1.1+6\n\n - Update a dependency to the latest release.\n\n## 0.1.1+5\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n\n## 0.1.1+4\n\n - Update a dependency to the latest release.\n\n## 0.1.1+3\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n\n## 0.1.1+2\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n## 0.1.1+1\n\n - Update a dependency to the latest release.\n\n## 0.1.1\n\n - **FEAT**: Introduce flame_noise, deprecate NoiseEffectController ([#2393](https://github.com/flame-engine/flame/issues/2393)). ([b2fdf06a](https://github.com/flame-engine/flame/commit/b2fdf06a79520c2b556c1c83de0b0f24df80cfd2))\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n\n"
  },
  {
    "path": "packages/flame_noise/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_noise/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nIntegrates the different noises from the <a href=\"https://github.com/frankpepermans/fast_noise\">fast_noise</a> packages into useful <a href=\"https://github.com/flame-engine/flame\">Flame</a> components and effects.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_noise\" ><img src=\"https://img.shields.io/pub/v/flame_noise.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n\n# flame_noise\n\n> :warning: This package is experimental. Use it at your own risk!\n\nPackage to bridge the `fast_noise` library into easy-to-use Flame components\n -- in particular, noise-based Effects.\n"
  },
  {
    "path": "packages/flame_noise/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_noise/lib/flame_noise.dart",
    "content": "export 'src/effects.dart';\n"
  },
  {
    "path": "packages/flame_noise/lib/src/effects/noise_effect_controller.dart",
    "content": "import 'dart:math';\n\nimport 'package:fast_noise/fast_noise.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flutter/animation.dart' show Curve, Curves;\n\n/// Effect controller that oscillates around following a noise curve.\n///\n/// The [taperingCurve] describes how the effect fades out over time. The\n/// curve that you supply will be flipped along the X axis, so that the effect\n/// starts at full force, and gradually reduces to zero towards the end.\n///\n/// This effect controller can be used to implement various shake effects. For\n/// example, putting into a `MoveEffect.by` will create a shake motion, where\n/// the magnitude and the direction of shaking is controlled by the effect's\n/// `offset`.\nclass NoiseEffectController extends DurationEffectController {\n  /// Square root of 2 is used as the y-offset for noise sampling to avoid\n  /// landing on integer lattice points where Perlin noise returns 0.\n  static const _noiseYOffset = sqrt2;\n\n  final Curve taperingCurve;\n  final Noise2 noise;\n\n  NoiseEffectController({\n    required double duration,\n    this.taperingCurve = Curves.easeInOutCubic,\n    Noise2? noise,\n  }) : noise = noise ?? PerlinNoise(),\n       super(duration);\n\n  @override\n  double get progress {\n    final x = timer / duration;\n    final amplitude = taperingCurve.transform(1 - x);\n    return noise.getNoise2(timer, _noiseYOffset) * amplitude;\n  }\n}\n"
  },
  {
    "path": "packages/flame_noise/lib/src/effects.dart",
    "content": "export 'package:fast_noise/fast_noise.dart';\n\nexport 'effects/noise_effect_controller.dart';\n"
  },
  {
    "path": "packages/flame_noise/pubspec.yaml",
    "content": "name: flame_noise\nresolution: workspace\ndescription: Integrate the fast_noise package into Flame\nversion: 0.3.2+21\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_noise\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - noise\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  fast_noise: ^2.0.0\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  dartdoc: ^9.0.0\n  flame_lint: ^1.4.3\n  flame_test: ^2.2.3\n  test: any"
  },
  {
    "path": "packages/flame_noise/test/noise_effect_controller_test.dart",
    "content": "import 'package:flame_noise/flame_noise.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/animation.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('PerlinNoiseEffectController', () {\n    test('general properties', () {\n      final ec = NoiseEffectController(\n        duration: 1,\n        noise: PerlinNoise(frequency: 12),\n      );\n      expect(ec.duration, 1.0);\n      expect(ec.taperingCurve, Curves.easeInOutCubic);\n      expect(ec.started, true);\n      expect(ec.completed, false);\n      expect(ec.progress, isNot(equals(0)));\n      expect(ec.isRandom, false);\n    });\n\n    test('progression', () {\n      final ec = NoiseEffectController(\n        duration: 1,\n        noise: PerlinNoise(frequency: 0.05),\n      );\n      final observed = <double>[];\n      for (var t = 0.0; t < 1.0; t += 0.1) {\n        observed.add(ec.progress);\n        ec.advance(0.1);\n      }\n      expect(observed, [\n        0.0734333516348943,\n        0.06784203703768521,\n        0.060923283194411315,\n        0.05202093691489379,\n        0.04032939038279296,\n        0.02500062595136621,\n        0.011900109469461025,\n        0.005054520292697595,\n        0.001797664482472573,\n        0.00044657726207562496,\n        0.0000015006292153437839,\n      ]);\n    });\n\n    test('errors', () {\n      expect(\n        () => NoiseEffectController(duration: -1),\n        failsAssert('Duration cannot be negative: -1.0'),\n      );\n    });\n\n    // Regression test: high integer frequencies (like 400) used to produce\n    // near-zero values on some platforms (e.g. Firefox) because the noise was\n    // sampled at y=1, which when multiplied by an integer frequency landed on\n    // an integer lattice point where Perlin noise returns 0.\n    test('non-zero progress with high integer frequency', () {\n      final ec = NoiseEffectController(\n        duration: 0.2,\n        noise: PerlinNoise(frequency: 400),\n      );\n      // Advance a small amount so timer > 0, then verify progress is non-zero\n      ec.advance(0.016); // ~1 frame at 60fps\n      expect(ec.progress, isNot(equals(0)));\n      expect(ec.progress.abs(), greaterThan(0.001));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_oxygen/CHANGELOG.md",
    "content": "## 0.2.3+21\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 0.2.3+20\n\n - Update a dependency to the latest release.\n\n## 0.2.3+19\n\n - Update a dependency to the latest release.\n\n## 0.2.3+18\n\n - Update a dependency to the latest release.\n\n## 0.2.3+17\n\n - Update a dependency to the latest release.\n\n## 0.2.3+16\n\n - Update a dependency to the latest release.\n\n## 0.2.3+15\n\n - Update a dependency to the latest release.\n\n## 0.2.3+14\n\n - Update a dependency to the latest release.\n\n## 0.2.3+13\n\n - Update a dependency to the latest release.\n\n## 0.2.3+12\n\n - Update a dependency to the latest release.\n\n## 0.2.3+11\n\n - Update a dependency to the latest release.\n\n## 0.2.3+10\n\n - Update a dependency to the latest release.\n\n## 0.2.3+9\n\n - Update a dependency to the latest release.\n\n## 0.2.3+8\n\n - Update a dependency to the latest release.\n\n## 0.2.3+7\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 0.2.3+6\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 0.2.3+5\n\n - Update a dependency to the latest release.\n\n## 0.2.3+4\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n\n## 0.2.3+3\n\n - Update a dependency to the latest release.\n\n## 0.2.3+2\n\n - Update a dependency to the latest release.\n\n## 0.2.3+1\n\n - Update a dependency to the latest release.\n\n## 0.2.3\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 0.2.2\n\n## 0.2.1\n\n - **FIX**: Updated oxygen dep to v0.3.1 and added removing components ([#3087](https://github.com/flame-engine/flame/issues/3087)). ([8f50c927](https://github.com/flame-engine/flame/commit/8f50c9279581999b4ff7f506682148425b248e28))\n\n## 0.2.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 0.1.9+8\n\n - Update a dependency to the latest release.\n\n## 0.1.9+7\n\n - Update a dependency to the latest release.\n\n## 0.1.9+6\n\n - Update a dependency to the latest release.\n\n## 0.1.9+5\n\n - Update a dependency to the latest release.\n\n## 0.1.9+4\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n## 0.1.9+3\n\n - Update a dependency to the latest release.\n\n## 0.1.9+2\n\n - Update a dependency to the latest release.\n\n## 0.1.9+1\n\n - Update a dependency to the latest release.\n\n## 0.1.9\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **BREAKING** **REFACTOR**: Simplify text rendering pipeline ([#2663](https://github.com/flame-engine/flame/issues/2663)). ([34f69b95](https://github.com/flame-engine/flame/commit/34f69b953c137fbf0168aebec3860c6abc888594))\n\n## 0.1.8+5\n\n - Update a dependency to the latest release.\n\n## 0.1.8+4\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n\n## 0.1.8+3\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n## 0.1.8+2\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n## 0.1.8+1\n\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n\n## 0.1.8\n\n## 0.1.7\n\n## 0.1.6\n\n## 0.1.5\n\n - **REFACTOR**: Game is now a class, not a mixin ([#1751](https://github.com/flame-engine/flame/issues/1751)). ([5225a4eb](https://github.com/flame-engine/flame/commit/5225a4ebd55a21f5709ccab9a1e24c728b2747ed))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n## 0.1.4\n\n - **REFACTOR**: Game is now a class, not a mixin ([#1751](https://github.com/flame-engine/flame/issues/1751)). ([5225a4eb](https://github.com/flame-engine/flame/commit/5225a4ebd55a21f5709ccab9a1e24c728b2747ed))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n## 0.1.3\n\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **FIX**: Fix setter in Oxygen's SizeComponent ([#1557](https://github.com/flame-engine/flame/issues/1557)). ([b1fae297](https://github.com/flame-engine/flame/commit/b1fae2976ef5445a52c99399dd8cc284fc272684))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n - **FEAT**: Add `FpsComponent` and `FpsTextComponent` ([#1595](https://github.com/flame-engine/flame/issues/1595)). ([4c68c2b0](https://github.com/flame-engine/flame/commit/4c68c2b0a2660e705b30099234da4ab1eb4616d0))\n\n## 0.1.2\n\n## 0.1.1\n\n - Graduate package to a stable release. See pre-releases prior to this version for changelog entries.\n\n## 0.1.1-releasecandidate.1\n\n# CHANGELOG\n\n## [0.1.1]\n - Updated to flame 1.0.0\n\n## [0.1.0-releasecandidate.17]\n - Updated to flame 1.0.0-releasecandidate.17\n\n## [0.1.0-releasecandidate.13]\n - Initial release of `flame_oxygen`\n"
  },
  {
    "path": "packages/flame_oxygen/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_oxygen/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nAdds support for <a href=\"https://github.com/flame-engine/oxygen\">Oxygen</a>, an alternative ECS written by the Flame Team, to your <a href=\"https://github.com/flame-engine/flame\">Flame</a> games.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_oxygen\" ><img src=\"https://img.shields.io/pub/v/flame_oxygen.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n\n# flame_oxygen\n\n\nThis library acts as a bridge between [Oxygen](https://github.com/flame-engine/oxygen)\n(an ECS written by the Flame Team) and the Flame Engine.\n"
  },
  {
    "path": "packages/flame_oxygen/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_oxygen/example/README.md",
    "content": "# Oxygen Samples\n\nAn example showcasing the bridge between [Flame](https://flame-engine.org) and [Oxygen](https://pub.dev/packages/oxygen).\n"
  },
  {
    "path": "packages/flame_oxygen/example/lib/component/timer_component.dart",
    "content": "import 'package:flame_oxygen/flame_oxygen.dart';\n\nclass TimerComponent extends Component<double> {\n  late double _maxTime;\n\n  /// Max time in seconds.\n  double get maxTime => _maxTime;\n\n  late double _timePassed;\n\n  /// Passed time in seconds.\n  double get timePassed => _timePassed;\n\n  set timePassed(double time) {\n    _timePassed = time.clamp(0, maxTime);\n  }\n\n  bool get done => _timePassed >= _maxTime;\n\n  double get percentage => _timePassed / _maxTime;\n\n  @override\n  void init([double? maxTime]) {\n    _maxTime = maxTime ?? 0;\n    _timePassed = 0;\n  }\n\n  @override\n  void reset() {\n    _maxTime = 0;\n    _timePassed = 0;\n  }\n}\n"
  },
  {
    "path": "packages/flame_oxygen/example/lib/component/velocity_component.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_oxygen/flame_oxygen.dart';\n\nclass VelocityComponent extends Component<Vector2> {\n  late Vector2 _velocity;\n\n  Vector2 get velocity => _velocity;\n  set velocity(Vector2 position) => _velocity.setFrom(position);\n\n  @override\n  void init([Vector2? velocity]) => _velocity = velocity ?? Vector2.zero();\n\n  @override\n  void reset() => _velocity.setZero();\n}\n"
  },
  {
    "path": "packages/flame_oxygen/example/lib/main.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/game.dart';\nimport 'package:flame_oxygen/flame_oxygen.dart';\nimport 'package:flame_oxygen_example/component/timer_component.dart';\nimport 'package:flame_oxygen_example/component/velocity_component.dart';\nimport 'package:flame_oxygen_example/system/debug_system.dart';\nimport 'package:flame_oxygen_example/system/kawabunga_system.dart';\nimport 'package:flame_oxygen_example/system/move_system.dart';\nimport 'package:flame_oxygen_example/system/sprite_system.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(GameWidget(game: ExampleGame()));\n}\n\nclass ExampleGame extends OxygenGame {\n  @override\n  Future<void> init() async {\n    if (kDebugMode) {\n      world.registerSystem(DebugSystem());\n    }\n    world.registerSystem(MoveSystem());\n    world.registerSystem(SpriteSystem());\n    world.registerSystem(KawabungaSystem());\n\n    world.registerComponent<TimerComponent, double>(TimerComponent.new);\n    world.registerComponent<VelocityComponent, Vector2>(\n      VelocityComponent.new,\n    );\n\n    final random = Random();\n    for (var i = 0; i < 10; i++) {\n      createEntity(\n          name: 'Entity $i',\n          position: size / 2,\n          size: Vector2.all(64),\n          angle: 0,\n        )\n        ..add<SpriteComponent, SpriteInit>(\n          SpriteInit(await loadSprite('pizza.png')),\n        )\n        ..add<VelocityComponent, Vector2>(\n          Vector2(\n            random.nextDouble() * 100 * (random.nextBool() ? 1 : -1),\n            random.nextDouble() * 100 * (random.nextBool() ? 1 : -1),\n          ),\n        );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_oxygen/example/lib/system/debug_system.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame_oxygen/flame_oxygen.dart';\nimport 'package:flutter/material.dart';\n\nclass DebugSystem extends BaseSystem {\n  final debugPaint = Paint()\n    ..color = Colors.green\n    ..style = PaintingStyle.stroke;\n\n  final textPainter = TextPaint(\n    style: const TextStyle(color: Colors.green, fontSize: 10),\n  );\n\n  final statusPainter = TextPaint(\n    style: const TextStyle(color: Colors.green, fontSize: 16),\n  );\n\n  @override\n  List<Filter<Component>> get filters => [];\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    statusPainter.render(\n      canvas,\n      [\n        'Entities: ${world!.entities.length}',\n      ].join('\\n'),\n      Vector2.zero(),\n    );\n  }\n\n  @override\n  void renderEntity(Canvas canvas, Entity entity) {\n    final size = entity.get<SizeComponent>()!.size;\n\n    canvas.drawRect(Vector2.zero() & size, debugPaint);\n\n    textPainter.render(\n      canvas,\n      [\n        'position: ${entity.get<PositionComponent>()!.position}',\n        'size: $size',\n        'angle: ${entity.get<AngleComponent>()?.radians ?? 0}',\n        'anchor: ${entity.get<AnchorComponent>()?.anchor ?? Anchor.topLeft}',\n      ].join('\\n'),\n      Vector2(size.x + 2, 0),\n    );\n    textPainter.render(\n      canvas,\n      entity.name ?? '',\n      Vector2(size.x / 2, size.y + 2),\n      anchor: Anchor.topCenter,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_oxygen/example/lib/system/kawabunga_system.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame_oxygen/flame_oxygen.dart';\nimport 'package:flame_oxygen_example/component/timer_component.dart';\nimport 'package:flutter/material.dart';\n\nclass KawabungaSystem extends BaseSystem with UpdateSystem {\n  @override\n  List<Filter<Component>> get filters => [\n    Has<TextComponent>(),\n    Has<TimerComponent>(),\n  ];\n\n  @override\n  void renderEntity(Canvas canvas, Entity entity) {\n    final timer = entity.get<TimerComponent>()!;\n    final textComponent = entity.get<TextComponent>()!;\n    final textRenderer = TextPaint(\n      style: textComponent.style.copyWith(\n        color: textComponent.style.color!.withValues(\n          alpha: 1 - timer.percentage,\n        ),\n      ),\n    );\n\n    textRenderer.render(\n      canvas,\n      textComponent.text,\n      Vector2.zero(),\n    );\n  }\n\n  @override\n  void update(double delta) {\n    for (final entity in entities) {\n      final textComponent = entity.get<TextComponent>()!;\n      final size = entity.get<SizeComponent>()!.size;\n      final textRenderer = TextPaint(style: textComponent.style);\n      size.setFrom(textRenderer.getLineMetrics(textComponent.text).size);\n\n      final timer = entity.get<TimerComponent>()!;\n      timer.timePassed = timer.timePassed + delta;\n      if (timer.done) {\n        entity.dispose();\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_oxygen/example/lib/system/move_system.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_oxygen/flame_oxygen.dart';\nimport 'package:flame_oxygen_example/component/timer_component.dart';\nimport 'package:flame_oxygen_example/component/velocity_component.dart';\nimport 'package:flame_oxygen_example/main.dart';\nimport 'package:flutter/material.dart';\n\nclass MoveSystem extends System with UpdateSystem, GameRef<ExampleGame> {\n  Query? _query;\n\n  @override\n  void init() {\n    _query = createQuery([\n      Has<PositionComponent>(),\n      Has<VelocityComponent>(),\n    ]);\n  }\n\n  @override\n  void dispose() {\n    _query = null;\n    super.dispose();\n  }\n\n  @override\n  void update(double delta) {\n    for (final entity in _query?.entities ?? <Entity>[]) {\n      final velocity = entity.get<VelocityComponent>()!.velocity;\n      final size = entity.get<SizeComponent>()!.size;\n      final position = entity.get<PositionComponent>()!.position\n        ..add(velocity * delta);\n\n      final screenSize = Vector2.zero() & game!.size;\n      if (!screenSize.containsPoint(position) ||\n          !screenSize.containsPoint(position + size)) {\n        velocity.setFrom(-velocity);\n\n        game!.createEntity(\n            name: '${entity.name} says',\n            position: position + size / 2,\n            size: Vector2.zero(),\n            anchor: Anchor.topCenter,\n          )\n          ..add<TextComponent, TextInit>(\n            TextInit(\n              'Kawabunga',\n              style: const TextStyle(color: Colors.blue, fontSize: 12),\n            ),\n          )\n          ..add<TimerComponent, double>(3);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_oxygen/example/lib/system/sprite_system.dart",
    "content": "import 'package:flame_oxygen/flame_oxygen.dart';\nimport 'package:flutter/material.dart';\n\nclass SpriteSystem extends BaseSystem {\n  @override\n  List<Filter<Component>> get filters => [Has<SpriteComponent>()];\n\n  @override\n  void renderEntity(Canvas canvas, Entity entity) {\n    final size = entity.get<SizeComponent>()!.size;\n    final sprite = entity.get<SpriteComponent>()?.sprite;\n\n    sprite?.render(canvas, size: size);\n  }\n}\n"
  },
  {
    "path": "packages/flame_oxygen/example/pubspec.yaml",
    "content": "name: flame_oxygen_example\nresolution: workspace\ndescription: Flame Oxygen example\n\npublish_to: 'none'\n\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_oxygen: ^0.2.3+21\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\nflutter:\n  uses-material-design: true\n  assets:\n    - assets/images/pizza.png\n    - assets/images/chopper.png\n"
  },
  {
    "path": "packages/flame_oxygen/lib/flame_oxygen.dart",
    "content": "export 'package:oxygen/oxygen.dart';\n\nexport 'src/component.dart';\nexport 'src/flame_system_manager.dart';\nexport 'src/flame_world.dart';\nexport 'src/oxygen_game.dart';\nexport 'src/system.dart';\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/component/anchor_component.dart",
    "content": "import 'package:flame/particles.dart';\nimport 'package:oxygen/oxygen.dart';\n\nexport 'package:flame/particles.dart' show Anchor;\n\nclass AnchorComponent extends Component<Anchor> {\n  late Anchor anchor;\n\n  @override\n  void init([Anchor? anchor]) => this.anchor = anchor ?? Anchor.topLeft;\n\n  @override\n  void reset() => anchor = Anchor.topLeft;\n}\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/component/angle_component.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:oxygen/oxygen.dart';\n\nclass AngleComponent extends Component<double> {\n  late double radians;\n\n  double get degrees => radians * radians2Degrees;\n  set degrees(double degrees) => radians = degrees * degrees2Radians;\n\n  @override\n  void init([double? radians]) => this.radians = radians ?? 0;\n\n  @override\n  void reset() => radians = 0;\n}\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/component/flip_component.dart",
    "content": "import 'package:oxygen/oxygen.dart';\n\nclass FlipInit {\n  final bool flipX;\n\n  final bool flipY;\n\n  FlipInit({\n    this.flipX = false,\n    this.flipY = false,\n  });\n}\n\nclass FlipComponent extends Component<FlipInit> {\n  late bool flipX;\n\n  late bool flipY;\n\n  @override\n  void init([FlipInit? initValue]) {\n    flipX = initValue?.flipX ?? false;\n    flipY = initValue?.flipY ?? false;\n  }\n\n  @override\n  void reset() {\n    flipX = false;\n    flipY = false;\n  }\n}\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/component/particle_component.dart",
    "content": "import 'package:flame/particles.dart';\nimport 'package:oxygen/oxygen.dart';\n\nexport 'package:flame/particles.dart' show Particle;\n\nclass ParticleComponent extends Component<Particle> {\n  Particle? particle;\n\n  /// Returns progress of the [particle].\n  double? get progress => particle?.progress;\n\n  @override\n  void init([Particle? data]) => particle = data;\n\n  @override\n  void reset() => particle = null;\n}\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/component/position_component.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:oxygen/oxygen.dart';\n\nclass PositionComponent extends Component<Vector2> {\n  late Vector2 _position;\n\n  Vector2 get position => _position;\n  set position(Vector2 position) => _position.setFrom(position);\n\n  double get x => _position.x;\n  set x(double x) => _position.x = x;\n\n  double get y => _position.y;\n  set y(double y) => _position.y = y;\n\n  @override\n  void init([Vector2? position]) {\n    _position = position?.clone() ?? Vector2.zero();\n  }\n\n  @override\n  void reset() => _position.setZero();\n}\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/component/size_component.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:oxygen/oxygen.dart';\n\nclass SizeComponent extends Component<Vector2> {\n  late Vector2 _size;\n\n  Vector2 get size => _size;\n  set size(Vector2 position) => _size.setFrom(position);\n\n  double get width => _size.x;\n  set width(double width) => _size.x = width;\n\n  double get height => _size.y;\n  set height(double height) => _size.y = height;\n\n  @override\n  void init([Vector2? size]) => _size = size?.clone() ?? Vector2.zero();\n\n  @override\n  void reset() => _size.setZero();\n}\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/component/sprite_component.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/sprite.dart';\nimport 'package:oxygen/oxygen.dart';\n\nexport 'package:flame/sprite.dart';\n\nclass SpriteInit {\n  final Sprite sprite;\n\n  const SpriteInit(this.sprite);\n\n  factory SpriteInit.fromImage(\n    Image image, {\n    Vector2? srcPosition,\n    Vector2? srcSize,\n  }) {\n    return SpriteInit(\n      Sprite(\n        image,\n        srcPosition: srcPosition,\n        srcSize: srcSize,\n      ),\n    );\n  }\n}\n\nclass SpriteComponent extends Component<SpriteInit> {\n  Sprite? sprite;\n\n  @override\n  void init([SpriteInit? initValue]) => sprite = initValue?.sprite;\n\n  @override\n  void reset() => sprite = null;\n}\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/component/text_component.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:oxygen/oxygen.dart';\n\nclass TextInit {\n  final String text;\n\n  TextStyle? style;\n\n  TextInit(\n    this.text, {\n    this.style,\n  });\n}\n\nclass TextComponent extends Component<TextInit> {\n  late String text;\n\n  late TextStyle style;\n\n  @override\n  void init([TextInit? initValue]) {\n    style = initValue?.style ?? const TextStyle();\n    text = initValue?.text ?? '';\n  }\n\n  @override\n  void reset() {\n    style = const TextStyle();\n    text = '';\n  }\n}\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/component.dart",
    "content": "export 'component/anchor_component.dart';\nexport 'component/angle_component.dart';\nexport 'component/flip_component.dart';\nexport 'component/particle_component.dart';\nexport 'component/position_component.dart';\nexport 'component/size_component.dart';\nexport 'component/sprite_component.dart';\nexport 'component/text_component.dart';\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/flame_system_manager.dart",
    "content": "import 'package:flame_oxygen/src/system.dart';\nimport 'package:oxygen/oxygen.dart';\n\n/// Extension class for adding Flame specific system filters.\nextension FlameSystemManager on SystemManager {\n  /// List of all systems that can render.\n  Iterable<RenderSystem> get renderSystems => systems.whereType<RenderSystem>();\n\n  /// List of all systems that can update.\n  Iterable<UpdateSystem> get updateSystems => systems.whereType<UpdateSystem>();\n}\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/flame_world.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_oxygen/src/flame_system_manager.dart';\nimport 'package:flame_oxygen/src/oxygen_game.dart';\nimport 'package:flame_oxygen/src/system.dart';\nimport 'package:oxygen/oxygen.dart';\n\nclass FlameWorld extends World {\n  /// The game this world belongs to.\n  final OxygenGame game;\n\n  FlameWorld(this.game) : super();\n\n  /// Render all the [RenderSystem]s.\n  void render(Canvas canvas) {\n    for (final system in systemManager.renderSystems) {\n      system.render(canvas);\n    }\n  }\n\n  /// Render all the [UpdateSystem]s.\n  void update(double delta) {\n    for (final system in systemManager.updateSystems) {\n      system.update(delta);\n    }\n    entityManager.processRemovedEntities();\n    entityManager.processRemovedComponents();\n  }\n\n  @override\n  void execute(double delta) => throw Exception(\n    'FlameWorld.execute is not supported in flame_oxygen',\n  );\n}\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/oxygen_game.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_oxygen/src/component.dart';\nimport 'package:flame_oxygen/src/flame_world.dart';\nimport 'package:flutter/material.dart';\nimport 'package:oxygen/oxygen.dart';\n\n/// This is an Oxygen based implementation of [Game].\n///\n/// [OxygenGame] should be extended to add your own game logic.\n///\n/// It is based on the Oxygen package.\nabstract class OxygenGame extends Game {\n  late final FlameWorld world;\n\n  OxygenGame() {\n    world = FlameWorld(this);\n  }\n\n  /// Create a new [Entity].\n  Entity createEntity({\n    required Vector2 position,\n    required Vector2 size,\n    String? name,\n    double? angle,\n    Anchor? anchor,\n    bool flipX = false,\n    bool flipY = false,\n  }) {\n    final entity = world.entityManager.createEntity(name)\n      ..add<PositionComponent, Vector2>(position)\n      ..add<SizeComponent, Vector2>(size)\n      ..add<AnchorComponent, Anchor>(anchor)\n      ..add<AngleComponent, double>(angle)\n      ..add<FlipComponent, FlipInit>(FlipInit(flipX: flipX, flipY: flipY));\n    return entity;\n  }\n\n  @override\n  @mustCallSuper\n  Future<void> onLoad() async {\n    // Registering default components.\n    world.registerComponent<SizeComponent, Vector2>(SizeComponent.new);\n    world.registerComponent<PositionComponent, Vector2>(\n      PositionComponent.new,\n    );\n    world.registerComponent<AngleComponent, double>(AngleComponent.new);\n    world.registerComponent<AnchorComponent, Anchor>(AnchorComponent.new);\n    world.registerComponent<SpriteComponent, SpriteInit>(\n      SpriteComponent.new,\n    );\n    world.registerComponent<TextComponent, TextInit>(TextComponent.new);\n    world.registerComponent<FlipComponent, FlipInit>(FlipComponent.new);\n\n    await init();\n    world.init();\n  }\n\n  /// Initialize the game and world.\n  Future<void> init();\n\n  @override\n  @mustCallSuper\n  void render(Canvas canvas) => world.render(canvas);\n\n  @override\n  @mustCallSuper\n  void update(double dt) => world.update(dt);\n}\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/system/base_system.dart",
    "content": "import 'package:flame_oxygen/src/component.dart';\nimport 'package:flame_oxygen/src/system.dart';\nimport 'package:flutter/material.dart';\nimport 'package:oxygen/oxygen.dart';\n\n/// System that provides base rendering for default components.\n///\n/// Based on the PositionComponent logic from Flame.\nabstract class BaseSystem extends System with RenderSystem {\n  Query? _query;\n\n  /// List of all the entities found using the [filters].\n  List<Entity> get entities => _query?.entities ?? [];\n\n  /// Filters used for querying entities.\n  ///\n  /// The [PositionComponent] and [SizeComponent] will be added automatically.\n  List<Filter<Component>> get filters;\n\n  @override\n  @mustCallSuper\n  void init() {\n    _query = createQuery([\n      Has<PositionComponent>(),\n      Has<SizeComponent>(),\n      ...filters,\n    ]);\n  }\n\n  @override\n  void dispose() {\n    _query = null;\n    super.dispose();\n  }\n\n  @override\n  void render(Canvas canvas) {\n    for (final entity in entities) {\n      final position = entity.get<PositionComponent>()!;\n      final size = entity.get<SizeComponent>()!.size;\n      final anchor = entity.get<AnchorComponent>()?.anchor ?? Anchor.topLeft;\n      final angle = entity.get<AngleComponent>()?.radians ?? 0;\n      final flip = entity.get<FlipComponent>();\n\n      canvas\n        ..save()\n        ..translate(position.x, position.y)\n        ..rotate(angle);\n\n      final delta = -anchor.toVector2()\n        ..multiply(size);\n      canvas.translate(delta.x, delta.y);\n\n      // Handle inverted rendering by moving center and flipping.\n      if (flip != null && (flip.flipX || flip.flipY)) {\n        canvas.translate(size.x / 2, size.y / 2);\n        canvas.scale(flip.flipX ? -1.0 : 1.0, flip.flipY ? -1.0 : 1.0);\n        canvas.translate(-size.x / 2, -size.y / 2);\n      }\n      renderEntity(canvas, entity);\n\n      canvas.restore();\n    }\n  }\n\n  /// Render given entity.\n  ///\n  /// The canvas is already prepared for this entity.\n  void renderEntity(Canvas canvas, Entity entity);\n}\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/system/game_ref.dart",
    "content": "import 'package:flame_oxygen/src/flame_world.dart';\nimport 'package:flame_oxygen/src/oxygen_game.dart';\nimport 'package:oxygen/oxygen.dart';\n\nmixin GameRef<T extends OxygenGame> on System {\n  /// The world this system belongs to.\n  @override\n  FlameWorld? get world => super.world as FlameWorld?;\n\n  /// The [T] this system belongs to.\n  T? get game => world?.game as T?;\n}\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/system/particle_system.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_oxygen/src/component.dart';\nimport 'package:flame_oxygen/src/system/render_system.dart';\nimport 'package:flame_oxygen/src/system/update_system.dart';\nimport 'package:oxygen/oxygen.dart';\n\n/// Allows Particles from Flame to be rendered.\nclass ParticleSystem extends System with RenderSystem, UpdateSystem {\n  Query? _query;\n\n  @override\n  void init() => _query = createQuery([Has<ParticleComponent>()]);\n\n  @override\n  void dispose() {\n    _query = null;\n    super.dispose();\n  }\n\n  @override\n  void render(Canvas canvas) {\n    for (final entity in _query?.entities ?? <Entity>[]) {\n      final particle = entity.get<ParticleComponent>()!.particle;\n      particle?.render(canvas);\n    }\n  }\n\n  @override\n  void update(double delta) {\n    for (final entity in _query?.entities ?? <Entity>[]) {\n      final particle = entity.get<ParticleComponent>()!.particle;\n      particle?.update(delta);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/system/render_system.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_oxygen/src/flame_world.dart';\nimport 'package:oxygen/oxygen.dart';\n\n/// Allow a [System] to be part of the render loop from Flame.\nmixin RenderSystem on System {\n  /// The world this system belongs to.\n  @override\n  FlameWorld? get world => super.world as FlameWorld?;\n\n  /// Implement this method to render the current game state in the [canvas].\n  void render(Canvas canvas);\n\n  @override\n  void execute(double delta) {\n    throw Exception('RenderSystem.execute is not supported in flame_oxygen');\n  }\n}\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/system/update_system.dart",
    "content": "import 'package:flame_oxygen/src/flame_world.dart';\nimport 'package:oxygen/oxygen.dart';\n\n/// Allow a [System] to be part of the update loop from Flame.\nmixin UpdateSystem on System {\n  /// The world this system belongs to.\n  @override\n  FlameWorld? get world => super.world as FlameWorld?;\n\n  /// Implement this method to update the game state, given the time [delta]\n  /// that has passed since the last update.\n  void update(double delta);\n\n  @override\n  void execute(double delta) {\n    throw Exception('UpdateSystem.execute is not supported in flame_oxygen');\n  }\n}\n"
  },
  {
    "path": "packages/flame_oxygen/lib/src/system.dart",
    "content": "export 'system/base_system.dart';\nexport 'system/game_ref.dart';\nexport 'system/particle_system.dart';\nexport 'system/render_system.dart';\nexport 'system/update_system.dart';\n"
  },
  {
    "path": "packages/flame_oxygen/pubspec.yaml",
    "content": "name: flame_oxygen\nresolution: workspace\ndescription: Integrate the Oxygen ECS with the Flame Engine.\nversion: 0.2.3+21\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_oxygen\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  oxygen: ^0.3.1\n\ndev_dependencies:\n  dartdoc: ^9.0.0\n  flame_lint: ^1.4.3"
  },
  {
    "path": "packages/flame_rive/CHANGELOG.md",
    "content": "## 1.11.0\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n - **FEAT**: Rive 0.14 support ([#3839](https://github.com/flame-engine/flame/issues/3839)). ([8b245181](https://github.com/flame-engine/flame/commit/8b2451813672c25d1783447b966fd2f739492b65))\n\n## 1.10.23\n\n - Update a dependency to the latest release.\n\n## 1.10.22\n\n - Update a dependency to the latest release.\n\n## 1.10.21\n\n - Update a dependency to the latest release.\n\n## 1.10.20\n\n - Update a dependency to the latest release.\n\n## 1.10.19\n\n - Update a dependency to the latest release.\n\n## 1.10.18\n\n - Update a dependency to the latest release.\n\n## 1.10.17\n\n - Update a dependency to the latest release.\n\n## 1.10.16\n\n - Update a dependency to the latest release.\n\n## 1.10.15\n\n - Update a dependency to the latest release.\n\n## 1.10.14\n\n - Update a dependency to the latest release.\n\n## 1.10.13\n\n - Update a dependency to the latest release.\n\n## 1.10.12\n\n - **FIX**: Bump Rive version and skip tests ([#3544](https://github.com/flame-engine/flame/issues/3544)). ([a3a7dd51](https://github.com/flame-engine/flame/commit/a3a7dd51faee57d74e89fbc29e7581ed44459832))\n\n## 1.10.11\n\n - Update a dependency to the latest release.\n\n## 1.10.10\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 1.10.9\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 1.10.8\n\n - Update a dependency to the latest release.\n\n## 1.10.7\n\n - Update a dependency to the latest release.\n\n## 1.10.6\n\n - Update a dependency to the latest release.\n\n## 1.10.5\n\n - Update a dependency to the latest release.\n\n## 1.10.4\n\n - Update a dependency to the latest release.\n\n## 1.10.3\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 1.10.2\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n\n## 1.10.1\n\n - Update a dependency to the latest release.\n\n## 1.10.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 1.9.11\n\n - Update a dependency to the latest release.\n\n## 1.9.10\n\n - Update a dependency to the latest release.\n\n## 1.9.9\n\n - **DOCS**: Remove references to Tappable and Draggable ([#2912](https://github.com/flame-engine/flame/issues/2912)). ([d12e4544](https://github.com/flame-engine/flame/commit/d12e45444e49bbe0b24a7acbd24f0cda20a13755))\n\n## 1.9.8\n\n - Update a dependency to the latest release.\n\n## 1.9.7\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n## 1.9.6\n\n - Bump to Rive 0.12.3\n\n## 1.9.5\n\n - Update a dependency to the latest release.\n\n## 1.9.4\n\n - **REFACTOR**: Remove unnecessary 'async' keyword across the codebase [DCM] ([#2803](https://github.com/flame-engine/flame/issues/2803)). ([2dfe0e5a](https://github.com/flame-engine/flame/commit/2dfe0e5a431213c7148ab6389e3e8c8dc49fbf3d))\n\n## 1.9.3\n\n - Update a dependency to the latest release.\n\n## 1.9.2\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **DOCS**: Enable CSpell on tests ([#2723](https://github.com/flame-engine/flame/issues/2723)). ([e051298c](https://github.com/flame-engine/flame/commit/e051298cba76550229780438b1a589557c7b488d))\n\n## 1.9.1\n\n - **FIX**: Respect artboard clip value ([#2639](https://github.com/flame-engine/flame/issues/2639)). ([4e664245](https://github.com/flame-engine/flame/commit/4e6642458494b4d4544bcc03b568476faeb0a71f))\n\n## 1.9.0\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Avoid creation of unnecessary objects for RiveComponent ([#2553](https://github.com/flame-engine/flame/issues/2553)). ([52b35fbf](https://github.com/flame-engine/flame/commit/52b35fbf56a551a7585c493e2de51473266bf759))\n - **FEAT**: ComponentKey API ([#2566](https://github.com/flame-engine/flame/issues/2566)). ([b3efb612](https://github.com/flame-engine/flame/commit/b3efb612cb3cb77f69bc030e9ba71516348035d2))\n\n## 1.8.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Avoid creation of unnecessary objects for RiveComponent ([#2553](https://github.com/flame-engine/flame/issues/2553)). ([52b35fbf](https://github.com/flame-engine/flame/commit/52b35fbf56a551a7585c493e2de51473266bf759))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n - **BREAKING** **REFACTOR**: Move `CameraComponent` and events out of experimental ([#2505](https://github.com/flame-engine/flame/issues/2505)). ([87b8a067](https://github.com/flame-engine/flame/commit/87b8a067f3e0096cebff3db4f5767e68616928fd))\n\n## 1.7.1\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n## 1.7.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Added useArtboardSize functionality ([#2294](https://github.com/flame-engine/flame/issues/2294)). ([00b0dbef](https://github.com/flame-engine/flame/commit/00b0dbef0df80433eaa78fe3cc68de867d5ca4f5))\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n - **BREAKING** **FEAT**: The `HasTappableComponents` mixin is no longer needed ([#2450](https://github.com/flame-engine/flame/issues/2450)). ([b5bdf4ec](https://github.com/flame-engine/flame/commit/b5bdf4ec173e87907a59a9f62fcdf35cc968af2a))\n\n## 1.6.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Depend on test: any for flame_test ([#2207](https://github.com/flame-engine/flame/issues/2207)). ([acfd418d](https://github.com/flame-engine/flame/commit/acfd418d882ee6872f3aa9961c39680ec123c2e6))\n - **BREAKING** **REFACTOR**: The method `onLoad()` now returns `FutureOr<void>` ([#2228](https://github.com/flame-engine/flame/issues/2228)). ([d898b539](https://github.com/flame-engine/flame/commit/d898b539f734d3e14c47990ef0727043a0e32efb))\n\n## 1.5.3\n\n - **FIX**: Export rive from flame_rive ([#2130](https://github.com/flame-engine/flame/issues/2130)). ([d1833329](https://github.com/flame-engine/flame/commit/d1833329028d1d8483faa049c6e1ad478ba9ca49))\n - **FIX**: antialiasing should change the artboard([#2076](https://github.com/flame-engine/flame/issues/2076)). ([47970224](https://github.com/flame-engine/flame/commit/47970224f8c9c90718c54301ee69d9cddcced87b))\n - **FIX**: Fixed null exception when no artboard with specified name is exists ([#2069](https://github.com/flame-engine/flame/issues/2069)). ([a3a65f30](https://github.com/flame-engine/flame/commit/a3a65f30ab64c029da66f9ded08eaf730d760336))\n\n## 1.5.2\n\n - Update a dependency to the latest release.\n\n## 1.5.1\n\n - Update a dependency to the latest release.\n\n## 1.5.0\n\n - **FIX**: Flame_rive now can load Nested Artboards and update to 0.9.0 rive package  ([#1741](https://github.com/flame-engine/flame/issues/1741)). ([82e4be96](https://github.com/flame-engine/flame/commit/82e4be96f3090908e95659a96006bf50fbb5b08c))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n## 1.4.0\n\n - **FIX**: Flame_rive now can load Nested Artboards and update to 0.9.0 rive package  ([#1741](https://github.com/flame-engine/flame/issues/1741)). ([82e4be96](https://github.com/flame-engine/flame/commit/82e4be96f3090908e95659a96006bf50fbb5b08c))\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n## 1.3.0\n\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n - **FEAT**: update to Rive 0.8.4 ([#1542](https://github.com/flame-engine/flame/issues/1542)). ([ac3d4bf6](https://github.com/flame-engine/flame/commit/ac3d4bf61b1386df555de4673e2bb6da1f0edd50))\n\n## 1.2.0\n\n - **FEAT**: Added children parameter to Component constructor (#1525). ([f0b31fcf](https://github.com/flame-engine/flame/commit/f0b31fcfc0fc6b0f8f96895ef6a68fd5a30a3159))\n\n## 1.1.0\n\n - Graduate package to a stable release. See pre-releases prior to this version for changelog entries.\n\n## 1.1.0-releasecandidate.6\n\n - Update a dependency to the latest release.\n\n## 1.1.0-releasecandidate.5\n\n - Update a dependency to the latest release.\n\n## 1.1.0-releasecandidate.4\n\n - Update a dependency to the latest release.\n\n## 1.1.0-releasecandidate.3\n\n - Update a dependency to the latest release.\n\n## 1.1.0-releasecandidate.2\n\n - **REFACTOR**: Remove Loadable, optional onLoads (#1333). ([05f7a4c3](https://github.com/flame-engine/flame/commit/05f7a4c3d6b1e3b67575c4ec920cf270691bbab4))\n - **FEAT**: Components are now always added in the correct order (#1337). ([c753fc46](https://github.com/flame-engine/flame/commit/c753fc4636d337d850a5a5cc684be8155f08b214))\n - **FEAT**: update rive package to 0.8.1 (now support raster graphics) (#1343). ([062962de](https://github.com/flame-engine/flame/commit/062962de087cd2a8107b1ae27472095e72bdf847))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n\n## 1.1.0-releasecandidate.1\n\n - **REFACTOR**: Remove Loadable, optional onLoads (#1333). ([05f7a4c3](https://github.com/flame-engine/flame/commit/05f7a4c3d6b1e3b67575c4ec920cf270691bbab4))\n - **FEAT**: Components are now always added in the correct order (#1337). ([c753fc46](https://github.com/flame-engine/flame/commit/c753fc4636d337d850a5a5cc684be8155f08b214))\n - **FEAT**: update rive package to 0.8.1 (now support raster graphics) (#1343). ([062962de](https://github.com/flame-engine/flame/commit/062962de087cd2a8107b1ae27472095e72bdf847))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n\n# CHANGELOG\n\n## [1.0.0]\n* Update to Flame 1.0.0\n\n## [1.0.0-releasecandidate.2]\n* Lower minimum sdk to 2.14.0\n\n## [1.0.0-releasecandidate.1]\n\n* Add Basic rive support to component mode\n\n## [0.0.1]\n\n* Empty release; in the future all flame rive related code will live here."
  },
  {
    "path": "packages/flame_rive/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_rive/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nAdds support for <a href=\"https://github.com/rive-app/rive-flutter\">Rive animations</a> to your <a href=\"https://github.com/flame-engine/flame\">Flame</a> games.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_rive\" ><img src=\"https://img.shields.io/pub/v/flame_rive.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n\n# flame_rive\n\nPackage to add Rive support for the Flame Engine.\n\n"
  },
  {
    "path": "packages/flame_rive/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_rive/example/README.md",
    "content": "# Flame Rive example\n\nProject to showcase the usage of flame_rive\n"
  },
  {
    "path": "packages/flame_rive/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml"
  },
  {
    "path": "packages/flame_rive/example/lib/main.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_rive/flame_rive.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  await RiveNative.init();\n  runApp(const GameWidget.controlled(gameFactory: RiveExampleGame.new));\n}\n\nclass RiveExampleGame extends FlameGame {\n  @override\n  Color backgroundColor() => const Color(0xFF444444);\n\n  @override\n  Future<void> onLoad() async {\n    final file = await File.asset(\n      'assets/rewards.riv',\n      riveFactory: Factory.flutter,\n    ).then((file) => file!);\n\n    final artboard = await loadArtboard(file);\n    final stateMachine = artboard.stateMachine('State Machine 1');\n\n    if (stateMachine != null) {\n      final viewModel = file.defaultArtboardViewModel(artboard);\n      if (viewModel != null) {\n        final viewModelInstance = viewModel.createDefaultInstance();\n        if (viewModelInstance != null) {\n          stateMachine.bindViewModelInstance(viewModelInstance);\n        }\n      }\n    }\n\n    add(RewardsComponent(artboard, stateMachine));\n  }\n}\n\nclass RewardsComponent extends RiveComponent {\n  RewardsComponent(Artboard artboard, StateMachine? stateMachine)\n    : super(\n        artboard: artboard,\n        stateMachine: stateMachine,\n      );\n\n  ViewModelInstanceNumber? _coinInput;\n  ViewModelInstanceNumber? _gemInput;\n  ViewModelInstanceNumber? _livesInput;\n\n  late final RewardsArea _livesArea;\n  late final RewardsArea _coinArea;\n  late final RewardsArea _gemArea;\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    this.size = size;\n    if (isLoaded) {\n      _updateAreas();\n    }\n  }\n\n  void _updateAreas() {\n    _livesArea.position = Vector2(size.x / 2, 0);\n    _livesArea.size = Vector2(size.x / 2, size.y);\n\n    _coinArea.position = Vector2.zero();\n    _coinArea.size = Vector2(size.x / 2, size.y / 2);\n\n    _gemArea.position = Vector2(0, size.y / 2);\n    _gemArea.size = Vector2(size.x / 2, size.y / 2);\n  }\n\n  @override\n  Future<void> onLoad() async {\n    if (stateMachine != null) {\n      final viewModelInstance = stateMachine!.boundRuntimeViewModelInstance;\n      if (viewModelInstance != null) {\n        _coinInput = viewModelInstance.viewModel('Coin')?.number('Item_Value');\n        _gemInput = viewModelInstance.viewModel('Gem')?.number('Item_Value');\n        _livesInput = viewModelInstance\n            .viewModel('Energy_Bar')\n            ?.number('Lives');\n      }\n    }\n\n    add(\n      _livesArea = RewardsArea(\n        onTap: () {\n          if (_livesInput != null) {\n            _livesInput!.value = (_livesInput!.value - 10) % 101;\n          }\n        },\n      ),\n    );\n    add(\n      _coinArea = RewardsArea(\n        onTap: () {\n          if (_coinInput != null) {\n            _coinInput!.value = (_coinInput!.value + 10) % 1001;\n          }\n        },\n      ),\n    );\n    add(\n      _gemArea = RewardsArea(\n        onTap: () {\n          if (_gemInput != null) {\n            _gemInput!.value = (_gemInput!.value + 1) % 1001;\n          }\n        },\n      ),\n    );\n    _updateAreas();\n  }\n}\n\nclass RewardsArea extends PositionComponent with TapCallbacks, GestureHitboxes {\n  RewardsArea({\n    required this.onTap,\n  });\n\n  final VoidCallback onTap;\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    add(RectangleHitbox());\n  }\n\n  @override\n  void onTapDown(TapDownEvent event) => onTap();\n}\n"
  },
  {
    "path": "packages/flame_rive/example/pubspec.yaml",
    "content": "name: flame_rive_example\nresolution: workspace\ndescription: A testbed for flame_rive\npublish_to: 'none'\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_rive: ^1.11.0\n  flutter:\n    sdk: flutter\n  rive: ^0.14.0\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n\nflutter:\n  uses-material-design: true\n  assets:\n    - assets/\n"
  },
  {
    "path": "packages/flame_rive/lib/flame_rive.dart",
    "content": "export 'package:rive/rive.dart';\n\nexport 'src/rive_component.dart';\n"
  },
  {
    "path": "packages/flame_rive/lib/src/rive_component.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:rive/rive.dart';\n\nclass RiveComponent extends PositionComponent {\n  final Artboard artboard;\n  final StateMachine? stateMachine;\n  final Alignment _alignment;\n  final bool _clipToBounds;\n  final Fit _riveFit;\n  final Paint? _layerPaint;\n\n  late Size _renderSize;\n  AABB _frame = AABB();\n  Size _frameSize = Size.zero;\n\n  RiveComponent({\n    required this.artboard,\n    this.stateMachine,\n    bool antialiasing = true,\n    BoxFit fit = BoxFit.contain,\n    Alignment alignment = Alignment.center,\n    bool clipToBounds = false,\n    super.position,\n\n    /// The logical size of the component.\n    /// Default value is ArtboardSize\n    Vector2? size,\n    super.scale,\n    super.angle = 0.0,\n    super.anchor = Anchor.topLeft,\n    super.children,\n    super.priority,\n    super.key,\n  }) : _alignment = alignment,\n       _clipToBounds = clipToBounds,\n       _riveFit = _toRiveFit(fit),\n       _layerPaint = antialiasing ? null : (Paint()..isAntiAlias = false),\n       super(size: size ?? Vector2(artboard.width, artboard.height)) {\n    void updateRenderSize() {\n      _renderSize = this.size.toSize();\n    }\n\n    this.size.addListener(updateRenderSize);\n    updateRenderSize();\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.save();\n\n    if (_clipToBounds) {\n      canvas.clipRect(Offset.zero & _renderSize);\n    }\n\n    if (_layerPaint != null) {\n      canvas.saveLayer(Offset.zero & _renderSize, _layerPaint);\n    }\n\n    final renderer = Renderer.make(canvas);\n    try {\n      if (_frameSize != _renderSize) {\n        _frameSize = _renderSize;\n        _frame = AABB.fromValues(\n          0,\n          0,\n          _renderSize.width,\n          _renderSize.height,\n        );\n      }\n\n      renderer.align(\n        _riveFit,\n        _alignment,\n        _frame,\n        artboard.bounds,\n        1.0,\n      );\n      artboard.draw(renderer);\n    } finally {\n      renderer.dispose();\n      if (_layerPaint != null) {\n        canvas.restore();\n      }\n      canvas.restore();\n    }\n  }\n\n  @override\n  void update(double dt) {\n    if (stateMachine != null) {\n      stateMachine!.advanceAndApply(dt);\n    } else {\n      artboard.advance(dt);\n    }\n  }\n\n  static Fit _toRiveFit(BoxFit fit) => switch (fit) {\n    BoxFit.fill => Fit.fill,\n    BoxFit.contain => Fit.contain,\n    BoxFit.cover => Fit.cover,\n    BoxFit.fitHeight => Fit.fitHeight,\n    BoxFit.fitWidth => Fit.fitWidth,\n    BoxFit.none => Fit.none,\n    BoxFit.scaleDown => Fit.scaleDown,\n  };\n}\n\n/// Loads the Artboard from the specified Rive File.\n///\n/// When [artboardName] is not null it returns the artboard with the specified\n/// name, an assertion is triggered if no artboard with that name exists in the\n/// file.\nFuture<Artboard> loadArtboard(\n  FutureOr<File> file, {\n  String? artboardName,\n}) async {\n  final loaded = await file;\n  if (artboardName == null) {\n    return loaded.defaultArtboard()!;\n  } else {\n    final artboard = loaded.artboard(artboardName);\n    assert(\n      artboard != null,\n      'No artboard with the specified name exists in the RiveFile',\n    );\n    return artboard!;\n  }\n}\n"
  },
  {
    "path": "packages/flame_rive/pubspec.yaml",
    "content": "name: flame_rive\nresolution: workspace\ndescription: Rive support for the Flame game engine. This uses the rive package and provides wrappers and components to be used inside Flame.\nhomepage: https://github.com/flame-engine/flame\nversion: 1.11.0\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - rive\n  - animations\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  rive: ^0.14.0\n\ndev_dependencies:\n  dartdoc: ^9.0.0\n  flame_lint: ^1.4.3\n  flame_test: ^2.2.3\n  flutter_test:\n    sdk: flutter\n"
  },
  {
    "path": "packages/flame_rive/test/flame_rive_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_rive/flame_rive.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'utils.dart';\n\nvoid main() {\n  group(\n    'RiveComponents',\n    // Skip tests until https://github.com/rive-app/rive-flutter/issues/354 is solved\n    skip: true,\n    () {\n      late Future<File> riveFile;\n\n      setUpAll(() async {\n        riveFile = File.decode(\n          loadFile('assets/skills.riv').buffer.asUint8List(),\n          riveFactory: Factory.flutter,\n        ).then((file) => file!);\n      });\n\n      group('loadArtboard', () {\n        test('Load mainArtboard by default', () async {\n          final artboard = await loadArtboard(riveFile);\n          final tempFile = await riveFile;\n          expect(artboard.name, tempFile.defaultArtboard()!.name);\n        });\n\n        test('Load the Specified Artboard', () async {\n          const artboardName = 'New Artboard';\n          final artboard = await loadArtboard(\n            riveFile,\n            artboardName: artboardName,\n          );\n          expect(artboard.name, artboardName);\n        });\n\n        test('Load an artboard that does not exist', () {\n          expect(\n            () => loadArtboard(\n              riveFile,\n              artboardName: 'Empty',\n            ),\n            throwsA(isA<AssertionError>()),\n          );\n        });\n      });\n\n      group('RiveComponent', () {\n        testWithFlameGame('Can Add to FlameGame', (game) async {\n          final skillsArtboard = await loadArtboard(riveFile);\n          final riveComponent = _RiveComponent(artboard: skillsArtboard);\n\n          game.add(riveComponent);\n          await game.ready();\n\n          expect(riveComponent.parent, game);\n        });\n\n        testWithFlameGame(\n          'Can Add with TapCallbacks',\n          (game) async {\n            final child = _RiveComponentWithTappable(\n              artboard: await loadArtboard(riveFile),\n            );\n            await game.ensureAdd(child);\n            await game.ready();\n\n            expect(child.parent, game);\n          },\n        );\n      });\n\n      group('RiveAnimation', () {\n        testWithFlameGame('Does not Animate when no controller is attached', (\n          game,\n        ) async {\n          final skillsArtboard = await loadArtboard(riveFile);\n          final riveComponent = _RiveComponent(artboard: skillsArtboard);\n\n          game.add(riveComponent);\n          await game.ready();\n        });\n\n        testWithFlameGame('Animate when controller is attached', (game) async {\n          final skillsArtboard = await loadArtboard(riveFile);\n          final riveComponent = _RiveComponentWithAnimation(\n            artboard: skillsArtboard,\n          );\n\n          game.add(riveComponent);\n          await game.ready();\n        });\n      });\n\n      group('Component size', () {\n        test('use specific size', () async {\n          final skillsArtboard = await loadArtboard(riveFile);\n          final riveComponent = RiveComponent(\n            artboard: skillsArtboard,\n            size: Vector2.all(250.0),\n          );\n\n          expect(riveComponent.size, Vector2.all(250.0));\n        });\n\n        test('default value (ArtboardSize)', () async {\n          final skillsArtboard = await loadArtboard(riveFile);\n          final riveComponent = RiveComponent(artboard: skillsArtboard);\n\n          expect(\n            riveComponent.size,\n            Vector2(skillsArtboard.width, skillsArtboard.height),\n          );\n        });\n      });\n    },\n  );\n}\n\nclass _RiveComponent extends RiveComponent {\n  _RiveComponent({required super.artboard});\n}\n\nclass _RiveComponentWithAnimation extends RiveComponent {\n  _RiveComponentWithAnimation({required super.artboard});\n}\n\nclass _RiveComponentWithTappable extends RiveComponent with TapCallbacks {\n  _RiveComponentWithTappable({required super.artboard});\n}\n"
  },
  {
    "path": "packages/flame_rive/test/utils.dart",
    "content": "import 'dart:io';\nimport 'dart:typed_data';\n\nByteData loadFile(String filename) {\n  final file = File('./test/$filename');\n  return ByteData.sublistView(file.readAsBytesSync());\n}\n"
  },
  {
    "path": "packages/flame_riverpod/CHANGELOG.md",
    "content": "## 5.5.3\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 5.5.2\n\n - Update a dependency to the latest release.\n\n## 5.5.1\n\n - Update a dependency to the latest release.\n\n## 5.5.0\n\n - **FEAT**: Riverpod 3.0 in flame_riverpod ([#3789](https://github.com/flame-engine/flame/issues/3789)). ([57f7f9d8](https://github.com/flame-engine/flame/commit/57f7f9d8b30632717fdf894321758a26d4985907))\n\n## 5.4.21\n\n - Update a dependency to the latest release.\n\n## 5.4.20\n\n - Update a dependency to the latest release.\n\n## 5.4.19\n\n - Update a dependency to the latest release.\n\n## 5.4.18\n\n - Update a dependency to the latest release.\n\n## 5.4.17\n\n - Update a dependency to the latest release.\n\n## 5.4.16\n\n - Update a dependency to the latest release.\n\n## 5.4.15\n\n - Update a dependency to the latest release.\n\n## 5.4.14\n\n - Update a dependency to the latest release.\n\n## 5.4.13\n\n - Update a dependency to the latest release.\n\n## 5.4.12\n\n - Update a dependency to the latest release.\n\n## 5.4.11\n\n - Update a dependency to the latest release.\n\n## 5.4.10\n\n - Update a dependency to the latest release.\n\n## 5.4.9\n\n - Update a dependency to the latest release.\n\n## 5.4.8\n\n - Update a dependency to the latest release.\n\n## 5.4.7\n\n - Update a dependency to the latest release.\n\n## 5.4.6\n\n - Update a dependency to the latest release.\n\n## 5.4.5\n\n - Update a dependency to the latest release.\n\n## 5.4.4\n\n - Update a dependency to the latest release.\n\n## 5.4.3\n\n - Bump \"flame_riverpod\" to `5.4.3`.\n\n## 5.4.2\n\n - Update a dependency to the latest release.\n\n## 5.4.1\n\n - Update a dependency to the latest release.\n\n## 5.4.0\n\n - **FIX**: Resolve logic error with assignment of ComponentRef's game property in flame_riverpod ([#3082](https://github.com/flame-engine/flame/issues/3082)). ([b44011fd](https://github.com/flame-engine/flame/commit/b44011fd714ec5919de5407f53d0772f31ed1a13))\n - **FIX**: Resolve breaking changes from Riverpod affecting flame_riverpod ([#3080](https://github.com/flame-engine/flame/issues/3080)). ([e3aaa7c2](https://github.com/flame-engine/flame/commit/e3aaa7c21d89a6679c3ae70de6e676d1f11501fa))\n - **FIX**: Implement necessary `ProviderSubscription` getters ([#3075](https://github.com/flame-engine/flame/issues/3075)). ([17da92b2](https://github.com/flame-engine/flame/commit/17da92b2d1c527162106778f459d72f19a5c5607))\n - **FEAT**: Allow ComponentRef access in RiverpodGameMixin ([#3010](https://github.com/flame-engine/flame/issues/3010)). ([44b10fd6](https://github.com/flame-engine/flame/commit/44b10fd60c61392d449a8d12020c45724ad19625))\n\n## 5.3.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 5.2.0\n\n - **FIX**: Add Template param to RiverpodGameMixin ([#2972](https://github.com/flame-engine/flame/issues/2972)). ([622c8553](https://github.com/flame-engine/flame/commit/622c855318b6c1731891b023ddc6429ba1f32329))\n - **FEAT**: Make `Component.key` public ([#2988](https://github.com/flame-engine/flame/issues/2988)). ([7fbd5af9](https://github.com/flame-engine/flame/commit/7fbd5af935211264822f89bc1beb4062d3efdf7a))\n\n## 5.1.5\n\n - **FIX**: Change return type of RiverpodComponentMixin.onLoad to FutureOr<void> ([#2964](https://github.com/flame-engine/flame/issues/2964)). ([7ac80a78](https://github.com/flame-engine/flame/commit/7ac80a78e95b06bb1287fb74773634483d80b1c9))\n\n## 5.1.4\n\n - Update a dependency to the latest release.\n\n## 5.1.3\n\n - **FIX**: Fix logic inside flame_riverpod persistent frame callback. ([#2950](https://github.com/flame-engine/flame/issues/2950)). ([230fb88f](https://github.com/flame-engine/flame/commit/230fb88fa9f9d82711461d10fe4aff9f8520cd29))\n\n## 5.1.2\n\n - **FIX**: Package flame_riverpod, setState() or markNeedsBuild() called during build. ([#2943](https://github.com/flame-engine/flame/issues/2943)). ([54d0e95d](https://github.com/flame-engine/flame/commit/54d0e95d863cc40e95f0310b4964343085f422e9))\n\n## 5.1.1\n\n - **FIX**: Add super constructor fields to RiverpodAwareGameWidget ([#2932](https://github.com/flame-engine/flame/issues/2932)). ([c2e6ea71](https://github.com/flame-engine/flame/commit/c2e6ea71e5c3c5f0d7ae6bc01a6c2f1f4d4d563b))\n\n## 5.1.0\n\n - **FIX**: SpriteAnimationWidget was resetting the ticker even when the playing didn't changed ([#2891](https://github.com/flame-engine/flame/issues/2891)). ([9aed8b4d](https://github.com/flame-engine/flame/commit/9aed8b4dea3074c9ca708ad991cdc90b12707fbe))\n - **FEAT**: Integration of flame_riverpod ([#2367](https://github.com/flame-engine/flame/issues/2367)). ([0c74560b](https://github.com/flame-engine/flame/commit/0c74560b2e25e86163c6c678ef6515bc11f9c3e7))\n\n## 5.0.0\n\n* New API with breaking changes. Added [RiverpodAwareGameWidget], [RiverpodGameMixin], [RiverpodComponentMixin]. See the example for details.\n\n## 4.0.0+2\n\n* Miscellaneous format post-processing on the files. \n\n## 4.0.0+1\n\n* Miscellaneous tidy-up of package internals.\n\n## 4.0.0\n\n* Made [WidgetRef] property on [ComponentRef] private. It should not be accessed directly. \n* Removed the [riverpodAwar`eGameProvider]. If required, this is better handled at the application-level.\n\n## 3.0.0\n\n* Changes to focus on [FlameGame].\n  * [riverpodAwareGameProvider] now expects a [FlameGame].\n  * Removed the [HasComponentRef] on Game.\n  * Renamed [RiverpodComponentMixin] to [HasComponentRef]\n* [HasComponentRef] now has a static setter for a WidgetRef. Components that use the new [HasComponentRef] mixin no\nlonger need to explicitly provide a [ComponentRef].\n* Renamed the [WidgetRef] property on the [ComponentRef] to [widgetRef].\n* Updated Example to reflect changes.\n* Updated README to reflect changes.\n\n## 2.0.0\n\n* Pruned the public API, removing custom widget definitions (these have now been defined inside the example for \nreference)\n* Renamed [RiverpodAwareGameMixin] -> [HasComponentRef] to bring closer to the Flame 'house-style' for mixins.\n\n## 1.1.0+2\n\n* Another correction to README and example code. onMount should not call super.onLoad.\n\n## 1.1.0+1\n\n* Correction to README to reflect API change.\n\n## 1.1.0\n\n* Added [RiverpodComponentMixin] to handle disposing of [ProviderSubscription]s.\n* Correction to the [RiverpodGameWidget] initializeGame constructor - param is now \n [RiverpodAwareGameMixin Function (ref)] as originally intended.\n\n## 1.0.0+1\n\n* Reduced package description length.\n* Ran dart format.\n\n## 1.0.0\n\n* Initial release.\n  * ComponentRef\n  * riverpodAwareGameProvider\n  * RiverpodAwareFlameGame\n  * RiverpodAwareGame\n  * RiverpodGameWidget\n"
  },
  {
    "path": "packages/flame_riverpod/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/flame_riverpod/README.md",
    "content": "# flame_riverpod\n\n[Riverpod](https://pub.dev/packages/flutter_riverpod) is a reactive caching and data-binding\nframework for Dart & Flutter.\n\nIn `flutter_riverpod`, widgets can be configured to rebuild when the state\nof a provider changes.\n\nWhen using Flame, we are interacting with components, which are *not* Widgets.\n\n`flame_riverpod` provides the `RiverpodAwareGameWidget`, `RiverpodGameMixin`, and\n`RiverpodComponentMixin` to facilitate managing state from Providers in your Flame Game.\n\n\n## Usage\n\nYou should use the `RiverpodAwareGameWidget` as your Flame `GameWidget`, the `RiverpodGameMixin`\nmixin on your game that extends `FlameGame`, and the `RiverpodComponentMixin` on any components\ninteracting with Riverpod providers.\n\nThe full range of operations defined in Riverpod's `WidgetRef` definition are accessible from\ncomponents.\n\nSubscriptions to a provider are managed in accordance with the lifecycle\nof a Flame Component: initialization occurs when a Component is mounted, and disposal\noccurs when a Component is removed. By default, the `RiverpodAwareGameWidget` is rebuilt when\nRiverpod-aware (i.e. using the `RiverpodComponentMixin`) components are mounted and when they are\nremoved.\n\n```dart\n\n/// An excerpt from the Example. Check it out!\nclass RefExampleGame extends FlameGame with RiverpodGameMixin {\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    add(TextComponent(text: 'Flame'));\n    add(RiverpodAwareTextComponent());\n  }\n}\n\nclass RiverpodAwareTextComponent extends PositionComponent\n    with RiverpodComponentMixin {\n  late TextComponent textComponent;\n  int currentValue = 0;\n\n  /// [onMount] should be used over [onLoad] to initialize subscriptions,\n  /// cancellation is handled for the user inside [onRemove],\n  /// which is only called if the [Component] was mounted.\n  /// \n  /// [RiverpodComponentMixin.addToGameWidgetBuild] **must** be invoked in \n  /// your Component **before** [RiverpodComponentMixin.onMount] in order to \n  /// have the provided function invoked on \n  /// [RiverpodAwareGameWidgetState.build].\n  /// \n  /// From `flame_riverpod` 5.0.0, [WidgetRef.watch], is also accessible from \n  /// components.\n  @override\n  void onMount() {\n    addToGameWidgetBuild(() {\n      ref.listen(countingStreamProvider, (p0, p1) {\n        if (p1.hasValue) {\n          currentValue = p1.value!;\n          textComponent.text = '$currentValue';\n        }\n      });\n    });\n    super.onMount();\n    add(textComponent = TextComponent(position: position + Vector2(0, 27)));\n  }\n}\n\n```\n\n\n## Credits\n\n[Mark Videon](https://markvideon.dev) for the initial groundwork and implementation.\n"
  },
  {
    "path": "packages/flame_riverpod/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options.yaml\n"
  },
  {
    "path": "packages/flame_riverpod/example/README.md",
    "content": "# flame_riverpod_example\n\nThe example consists of a very simple FlameGame with a custom\nComponent, updated alongside a comparable Flutter widget.\n\nBoth the Component and the Widget depend on a `StreamProvider`\nthat counts upwards indefinitely.\n\nBoth a Flame Component and a Flutter Text Widget update in real-time from\nthe same data source.\n"
  },
  {
    "path": "packages/flame_riverpod/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options.yaml\n"
  },
  {
    "path": "packages/flame_riverpod/example/lib/main.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart' hide Timer;\nimport 'package:flame/game.dart';\nimport 'package:flame_riverpod/flame_riverpod.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nfinal countingStreamProvider = StreamProvider<int>((ref) {\n  return Stream.periodic(const Duration(seconds: 1), (inc) => inc);\n});\n\nvoid main() {\n  runApp(const ProviderScope(child: MyApp()));\n}\n\nfinal gameInstance = RefExampleGame();\nfinal GlobalKey<RiverpodAwareGameWidgetState> gameWidgetKey =\n    GlobalKey<RiverpodAwareGameWidgetState>();\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      title: 'Flutter Demo',\n      home: Row(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          const Expanded(child: FlutterCountingComponent()),\n          Expanded(\n            child: RiverpodAwareGameWidget(\n              key: gameWidgetKey,\n              game: gameInstance,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass FlutterCountingComponent extends ConsumerWidget {\n  const FlutterCountingComponent({super.key});\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final textStyle = Theme.of(\n      context,\n    ).textTheme.headlineSmall?.copyWith(color: Colors.white);\n\n    final stream = ref.watch(countingStreamProvider);\n    return Material(\n      color: Colors.transparent,\n      child: Column(\n        children: [\n          Text('Flutter', style: textStyle),\n          stream.when(\n            data: (value) => Text('$value', style: textStyle),\n            error: (error, stackTrace) => Text('$error', style: textStyle),\n            loading: () => Text('Loading...', style: textStyle),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass RefExampleGame extends FlameGame with RiverpodGameMixin {\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    add(TextComponent(text: 'Flame'));\n    add(RiverpodAwareTextComponent());\n  }\n}\n\nclass RiverpodAwareTextComponent extends PositionComponent\n    with RiverpodComponentMixin {\n  late TextComponent textComponent;\n  int currentValue = 0;\n\n  /// [onMount] should be used over [onLoad] to initialize subscriptions,\n  /// which is only called if the [Component] was mounted.\n  /// Cancellation is handled for the user automatically inside [onRemove].\n  ///\n  /// [RiverpodComponentMixin.addToGameWidgetBuild] **must** be invoked in\n  /// your Component **before** [RiverpodComponentMixin.onMount] in order to\n  /// have the provided function invoked on\n  /// [RiverpodAwareGameWidgetState.build].\n  ///\n  /// From `flame_riverpod` 5.0.0, [WidgetRef.watch], is also accessible from\n  /// components.\n  @override\n  void onMount() {\n    addToGameWidgetBuild(() {\n      ref.listen(countingStreamProvider, (p0, p1) {\n        if (p1.hasValue) {\n          currentValue = p1.value!;\n          textComponent.text = '$currentValue';\n        }\n      });\n    });\n    super.onMount();\n    add(textComponent = TextComponent(position: position + Vector2(0, 27)));\n  }\n}\n"
  },
  {
    "path": "packages/flame_riverpod/example/pubspec.yaml",
    "content": "name: flame_riverpod_example\nresolution: workspace\ndescription: Showcasing the flame_riverpod functionality.\npublish_to: 'none' # Remove this line if you wish to publish to pub.dev\nversion: 1.0.0+1\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\ndependencies:\n  flame: ^1.36.0\n  flame_riverpod: ^5.5.3\n  flutter:\n    sdk: flutter\n  flutter_riverpod: ^3.0.3\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n\nflutter:\n  uses-material-design: true\n"
  },
  {
    "path": "packages/flame_riverpod/example/test/widget_test.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame_riverpod/flame_riverpod.dart';\nimport 'package:flame_riverpod_example/main.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  testWidgets('Test equality of Flutter Text Widget and Flame Text Component', (\n    widgetTester,\n  ) async {\n    await widgetTester.pumpWidget(\n      const ProviderScope(child: MyApp()),\n    );\n\n    await widgetTester.pump(const Duration(seconds: 5));\n\n    // Expect FlutterCountingComponent to exist on the page\n    final flutterCounterFinder = find.byType(FlutterCountingComponent);\n    expect(flutterCounterFinder, findsOneWidget);\n    final flutterCounterTextFinder = find.descendant(\n      of: flutterCounterFinder,\n      matching: find.byType(Text),\n    );\n\n    // Expect a title 'e.g. Flutter' and the current count of the stream as\n    // separate [Text] widget.\n    expect(flutterCounterTextFinder, findsNWidgets(2));\n\n    final flutterCounterTextWidgets = widgetTester.widgetList(\n      flutterCounterTextFinder,\n    );\n\n    // Expect RiverpodAwareGameWidget to exist\n    final riverpodGameWidgetFinder = find.byType(RiverpodAwareGameWidget);\n    expect(riverpodGameWidgetFinder, findsOneWidget);\n\n    final gameWidget =\n        widgetTester.widget(riverpodGameWidgetFinder)\n            as RiverpodAwareGameWidget;\n\n    // GameWidget contains a FutureBuilder, which calls setState when a Future\n    // completes. We therefore need to pump / re-render the widget to ensure\n    // that the game mounts properly. Alternatively, we could manually trigger\n    // lifecycle events.\n    await widgetTester.pump(const Duration(seconds: 1));\n\n    final flameGame = gameWidget.game as FlameGame?;\n    expect(flameGame?.isAttached, true);\n    expect(flameGame?.isLoaded, true);\n    expect(flameGame?.isMounted, true);\n\n    // Pump again to provide the gameRenderBox with a [BuildContext].\n    await widgetTester.pump(const Duration(seconds: 1));\n\n    // Check components are mounted as expected.\n    expect(flameGame?.children.isNotEmpty ?? false, true);\n\n    final riverpodAwareTextComponent =\n        flameGame?.children.elementAt(2) as RiverpodAwareTextComponent?;\n    expect(riverpodAwareTextComponent is RiverpodAwareTextComponent, true);\n\n    // Current count of the stream from the [Text] widget. This is best\n    // retrieved after all pumps.\n    final flutterCounterTextWidgetOfInterest = flutterCounterTextWidgets\n        .elementAt(1);\n\n    final currentCount = int.parse(\n      (flutterCounterTextWidgetOfInterest as Text).data!,\n    );\n\n    // Expect equality (in the presented string value)\n    // of the Text Component and the Text Widget\n    expect(\n      riverpodAwareTextComponent?.textComponent.text == currentCount.toString(),\n      true,\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_riverpod/lib/flame_riverpod.dart",
    "content": "/// Helpers for using Riverpod in conjunction with Flame, to share state from\n/// the game into other parts of your application, or from other parts of your\n/// application into your game.\nlibrary flame_riverpod;\n\nexport 'src/consumer.dart';\nexport 'src/widget.dart';\n"
  },
  {
    "path": "packages/flame_riverpod/lib/src/consumer.dart",
    "content": "import 'dart:async';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_riverpod/src/widget.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:flutter_riverpod/misc.dart';\n\nclass ComponentRef {\n  ComponentRef({required this.game});\n\n  RiverpodGameMixin? game;\n\n  BuildContext get context => game!.buildContext!;\n\n  RiverpodAwareGameWidgetState? get _container {\n    return game?.widgetKey?.currentState;\n  }\n\n  Res watch<Res>(ProviderListenable<Res> target) {\n    return _container!.watch(target);\n  }\n\n  void listen<T>(\n    ProviderListenable<T> provider,\n    void Function(T? previous, T value) listener, {\n    void Function(Object error, StackTrace stackTrace)? onError,\n  }) {\n    _container!.listen(provider, listener, onError: onError);\n  }\n\n  bool exists(ProviderBase<Object?> provider) {\n    return _container!.exists(provider);\n  }\n\n  T read<T>(ProviderListenable<T> provider) {\n    return _container!.read(provider);\n  }\n\n  T refresh<T>(Refreshable<T> provider) {\n    return _container!.refresh(provider);\n  }\n\n  void invalidate(ProviderOrFamily provider) {\n    _container!.invalidate(provider);\n  }\n\n  ProviderSubscription<T> listenManual<T>(\n    ProviderListenable<T> provider,\n    void Function(T? previous, T next) listener, {\n    void Function(Object error, StackTrace stackTrace)? onError,\n    bool fireImmediately = false,\n  }) {\n    return _container!.listenManual(\n      provider,\n      listener,\n      onError: onError,\n      fireImmediately: fireImmediately,\n    );\n  }\n}\n\nmixin RiverpodComponentMixin on Component {\n  final ComponentRef ref = ComponentRef(game: null);\n  final List<Function()> _onBuildCallbacks = [];\n\n  /// Whether to immediately call [RiverpodAwareGameWidgetState.build] when\n  /// this component is mounted.\n  bool rebuildOnMountWhen(ComponentRef ref) => true;\n\n  /// Whether to immediately call [RiverpodAwareGameWidgetState.build] when\n  /// this component is removed.\n  bool rebuildOnRemoveWhen(ComponentRef ref) => true;\n\n  /// Adds a callback method to be invoked in the build method of\n  /// [RiverpodAwareGameWidgetState].\n  void addToGameWidgetBuild(Function() cb) {\n    _onBuildCallbacks.add(cb);\n  }\n\n  @mustCallSuper\n  @override\n  void onMount() {\n    super.onMount();\n    ref.game = findGame()! as RiverpodGameMixin;\n    ref.game!._onBuildCallbacks.addAll(_onBuildCallbacks);\n\n    if (rebuildOnMountWhen(ref) == true) {\n      rebuildGameWidget();\n    }\n  }\n\n  @mustCallSuper\n  @override\n  void onRemove() {\n    // Remove this component's onBuild callbacks from the GameWidget\n    _onBuildCallbacks.forEach(ref.game!._onBuildCallbacks.remove);\n\n    // Clear the local store of build callbacks - if the component is\n    // re-mounted, it would be undesirable to double-up.\n    _onBuildCallbacks.clear();\n\n    // Force build to flush dependencies\n    if (rebuildOnRemoveWhen(ref) == true) {\n      rebuildGameWidget();\n    }\n\n    // Clear game reference as the component is no longer mounted to this game\n    ref.game = null;\n\n    super.onRemove();\n  }\n\n  void rebuildGameWidget() {\n    assert(ref.game!.isMounted == true);\n    if (ref.game!.isMounted) {\n      ref.game!.widgetKey!.currentState!.forceBuild();\n    }\n  }\n}\n\nmixin RiverpodGameMixin<W extends World> on FlameGame<W> {\n  /// [GlobalKey] associated with the [RiverpodAwareGameWidget] that this game\n  /// was provided to.\n  ///\n  /// Used to facilitate [Component] access to the [ProviderContainer].\n  GlobalKey<RiverpodAwareGameWidgetState>? widgetKey;\n\n  final ComponentRef ref = ComponentRef(game: null);\n  final List<void Function()> _onBuildCallbacks = [];\n\n  /// Adds a callback method to be invoked in the build method of\n  /// [RiverpodAwareGameWidgetState].\n  void addToGameWidgetBuild(Function() cb) {\n    _onBuildCallbacks.add(cb);\n  }\n\n  @override\n  void onMount() {\n    super.onMount();\n    mounted.whenComplete(() {\n      widgetKey!.currentState!.forceBuild();\n    });\n  }\n\n  @mustCallSuper\n  @override\n  FutureOr<void> onLoad() {\n    ref.game = this;\n    return super.onLoad();\n  }\n\n  /// Invoked in [RiverpodAwareGameWidgetState.build]. Each callback is\n  /// expected to consist of calls to methods implemented in [WidgetRef].\n  /// E.g. [WidgetRef.watch], [WidgetRef.listen], etc.\n  void onBuild() {\n    for (final callback in _onBuildCallbacks) {\n      callback.call();\n    }\n  }\n\n  bool get hasBuildCallbacks => _onBuildCallbacks.isNotEmpty;\n}\n"
  },
  {
    "path": "packages/flame_riverpod/lib/src/widget.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_riverpod/flame_riverpod.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n// ignore: depend_on_referenced_packages, implementation_imports\nimport 'package:riverpod/src/framework.dart';\n\n/// A [GameWidget] that provides access to [Component]s using\n/// [RiverpodComponentMixin] attached to [FlameGame]s using [RiverpodGameMixin]\n/// access to Riverpod [Provider]s.\n///\n/// The corresponding [State] object ([RiverpodAwareGameWidgetState]) assumes\n/// responsibilities associated with ConsumerStatefulElement in\n/// `flutter_riverpod`.\nclass RiverpodAwareGameWidget<T extends Game> extends GameWidget<T> {\n  RiverpodAwareGameWidget({\n    required super.game,\n    required this.key,\n    super.textDirection,\n    super.loadingBuilder,\n    super.errorBuilder,\n    super.backgroundBuilder,\n    super.overlayBuilderMap,\n    super.initialActiveOverlays,\n    super.focusNode,\n    super.autofocus,\n    super.mouseCursor,\n    super.addRepaintBoundary,\n  }) : super(key: key);\n\n  @override\n  final GlobalKey<RiverpodAwareGameWidgetState<T>> key;\n\n  @override\n  GameWidgetState<T> createState() => RiverpodAwareGameWidgetState<T>();\n}\n\nclass RiverpodAwareGameWidgetState<T extends Game> extends GameWidgetState<T> {\n  RiverpodGameMixin get game => widget.game! as RiverpodGameMixin;\n\n  bool _isForceBuilding = false;\n  bool _hasQueuedBuild = false;\n\n  late ProviderContainer _container = ProviderScope.containerOf(context);\n  var _dependencies =\n      <ProviderListenable<Object?>, ProviderSubscription<Object?>>{};\n  Map<ProviderListenable<Object?>, ProviderSubscription<Object?>>?\n  _oldDependencies;\n  final _listeners = <ProviderSubscription<Object?>>[];\n  List<ProviderSubscription<Object?>>? _manualListeners;\n\n  /// Rebuilds the [RiverpodAwareGameWidget] by calling [setState].\n  /// As it is undesirable to call [setState] while the widget may be building,\n  /// this function honours (potential) subsequent requests to rebuild by\n  /// setting a flag.\n  void forceBuild() {\n    if (_isForceBuilding) {\n      _hasQueuedBuild = true;\n      return;\n    }\n    _isForceBuilding = true;\n    setState(() {});\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    game.widgetKey = (widget as RiverpodAwareGameWidget<T>).key;\n\n    WidgetsBinding.instance.addPersistentFrameCallback((_) {\n      _isForceBuilding = false;\n\n      if (_hasQueuedBuild) {\n        _hasQueuedBuild = false;\n        forceBuild();\n      }\n    });\n  }\n\n  @override\n  void didUpdateWidget(covariant GameWidget<T> oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    game.widgetKey = (widget as RiverpodAwareGameWidget<T>).key;\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    final newContainer = ProviderScope.containerOf(context);\n    if (_container != newContainer) {\n      _container = newContainer;\n      for (final dependency in _dependencies.values) {\n        dependency.close();\n      }\n      _dependencies.clear();\n    }\n  }\n\n  @override\n  void dispose() {\n    // Below comments are from the implementation of ConsumerStatefulWidget:\n\n    // Calling `super.unmount()` will call `dispose` on the state\n    // And [ListenManual] subscriptions should be closed after `dispose`\n    super.dispose();\n\n    for (final dependency in _dependencies.values) {\n      dependency.close();\n    }\n    for (var i = 0; i < _listeners.length; i++) {\n      _listeners[i].close();\n    }\n    final manualListeners = _manualListeners?.toList();\n    if (manualListeners != null) {\n      for (final listener in manualListeners) {\n        listener.close();\n      }\n      _manualListeners = null;\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    try {\n      _oldDependencies = _dependencies;\n      for (var i = 0; i < _listeners.length; i++) {\n        _listeners[i].close();\n      }\n      _listeners.clear();\n      _dependencies = {};\n      game.onBuild();\n      return super.build(context);\n    } finally {\n      for (final dep in _oldDependencies!.values) {\n        dep.close();\n      }\n      _oldDependencies = null;\n    }\n  }\n\n  void _assertNotDisposed() {\n    if (!context.mounted) {\n      throw StateError('Cannot use \"ref\" after the widget was disposed.');\n    }\n  }\n\n  Res watch<Res>(ProviderListenable<Res> target) {\n    _assertNotDisposed();\n    return _dependencies.putIfAbsent(target, () {\n          final oldDependency = _oldDependencies?.remove(target);\n\n          if (oldDependency != null) {\n            return oldDependency;\n          }\n\n          return _container.listen<Res>(\n            target,\n            // setState call has been replaced with forceBuild,\n            // to prevent setState calls while the widget is\n            // building, which throws a framework error.\n            (_, __) => forceBuild(),\n          );\n        }).read()\n        as Res;\n  }\n\n  void listen<U>(\n    ProviderListenable<U> provider,\n    void Function(U? previous, U value) listener, {\n    void Function(Object error, StackTrace stackTrace)? onError,\n  }) {\n    _assertNotDisposed();\n    assert(\n      context.debugDoingBuild,\n      'ref.listen can only be used within the build method of a ConsumerWidget',\n    );\n\n    // We can't implement a fireImmediately flag because we wouldn't know\n    // which listen call was preserved between widget rebuild, and we wouldn't\n    // want to call the listener on every rebuild.\n    final sub = _container.listen<U>(provider, listener, onError: onError);\n    _listeners.add(sub);\n  }\n\n  bool exists(ProviderBase<Object?> provider) {\n    _assertNotDisposed();\n    return ProviderScope.containerOf(context, listen: false).exists(provider);\n  }\n\n  Res read<Res>(ProviderListenable<Res> provider) {\n    _assertNotDisposed();\n    return ProviderScope.containerOf(context, listen: false).read(provider);\n  }\n\n  S refresh<S>(Refreshable<S> provider) {\n    _assertNotDisposed();\n    return ProviderScope.containerOf(context, listen: false).refresh(provider);\n  }\n\n  void invalidate(ProviderOrFamily provider) {\n    _assertNotDisposed();\n    _container.invalidate(provider);\n  }\n\n  ProviderSubscription<Res> listenManual<Res>(\n    ProviderListenable<Res> provider,\n    void Function(Res? previous, Res next) listener, {\n    void Function(Object error, StackTrace stackTrace)? onError,\n    bool fireImmediately = false,\n  }) {\n    _assertNotDisposed();\n    final listeners = _manualListeners ??= [];\n\n    // Reading the container using \"listen:false\" to guarantee that this can\n    // be used inside initState.\n    final container = ProviderScope.containerOf(context, listen: false);\n\n    final sub = container.listen<Res>(\n      provider,\n      listener,\n      onError: onError,\n      fireImmediately: fireImmediately,\n      // ignore: invalid_use_of_internal_member, from riverpod\n    );\n\n    // Hook-up on onClose to avoid memory leaks.\n    final previousOnClose = sub.impl.onClose;\n    sub.impl.onClose = () {\n      previousOnClose?.call();\n      // If the subscription is closed, we remove it from the manual listeners\n      // so that it doesn't leak.\n      _manualListeners?.remove(sub);\n    };\n\n    listeners.add(sub);\n\n    return sub;\n  }\n}\n"
  },
  {
    "path": "packages/flame_riverpod/pubspec.yaml",
    "content": "name: flame_riverpod\nresolution: workspace\ndescription: Helpers for using Riverpod - a reactive caching and data-binding framework, in conjunction with Flame.\nversion: 5.5.3\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_riverpod\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - state-management\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  flutter_riverpod: ^3.0.3\n  riverpod: ^3.0.3\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n"
  },
  {
    "path": "packages/flame_riverpod/test/widget_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_riverpod/flame_riverpod.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nfinal numberProvider = Provider.autoDispose((ref) {\n  return 1;\n});\n\nclass MyGame extends FlameGame with RiverpodGameMixin {}\n\nclass MyGameWithRefAccess extends FlameGame with RiverpodGameMixin {\n  @override\n  void onMount() {\n    addToGameWidgetBuild(() {\n      ref.watch(numberProvider);\n    });\n    super.onMount();\n  }\n}\n\nclass EmptyComponent extends Component with RiverpodComponentMixin {\n  @override\n  void onLoad() {\n    super.onLoad();\n    addToGameWidgetBuild(() {\n      // do nothing\n    });\n  }\n}\n\nclass WatchingComponent extends Component with RiverpodComponentMixin {\n  @override\n  void onLoad() {\n    super.onLoad();\n    addToGameWidgetBuild(() {\n      ref.watch(numberProvider);\n    });\n  }\n}\n\nvoid main() {\n  testWidgets(\n    'Test registration and de-registration of GameWidget build callbacks',\n    (widgetTester) async {\n      final game = MyGame();\n      final component = EmptyComponent();\n      final key = GlobalKey<RiverpodAwareGameWidgetState>();\n\n      await widgetTester.pumpWidget(\n        ProviderScope(\n          child: MaterialApp(\n            home: RiverpodAwareGameWidget(\n              game: game,\n              key: key,\n            ),\n          ),\n        ),\n      );\n      await widgetTester.pump(const Duration(seconds: 5));\n\n      expect(game.hasBuildCallbacks, false);\n\n      // Add the custom component\n      game.add(component);\n\n      // Expect the game is ready to play\n      expect(game.isAttached, true);\n      expect(game.isMounted, true);\n      expect(game.isLoaded, true);\n\n      // Pump to ensure the custom component's lifecycle events are handled\n      await widgetTester.pump(const Duration(seconds: 1));\n\n      // Expect the component has added a callback for the game widget's build\n      // method.\n      expect(game.hasBuildCallbacks, true);\n\n      // Remove the custom component.\n      game.remove(component);\n\n      // Pump to ensure the component has been removed.\n      await widgetTester.pump(Duration.zero);\n\n      // When the component is removed there should be no onBuild callbacks\n      // remaining.\n      expect(game.hasBuildCallbacks, false);\n\n      // When the component is removed, there should be no game reference on the\n      // component.\n      expect(component.ref.game == null, true);\n    },\n  );\n\n  testWidgets('Test registration and de-registration of Provider listeners', (\n    widgetTester,\n  ) async {\n    final game = MyGame();\n    final component = WatchingComponent();\n    final key = GlobalKey<RiverpodAwareGameWidgetState>();\n\n    await widgetTester.pumpWidget(\n      ProviderScope(\n        child: MaterialApp(\n          home: RiverpodAwareGameWidget(\n            game: game,\n            key: key,\n          ),\n        ),\n      ),\n    );\n    await widgetTester.pump(const Duration(seconds: 5));\n\n    // Expect that the GameWidget is not initially listening to\n    // numberProvider\n    expect(key.currentState?.exists(numberProvider), false);\n\n    // Add the custom component\n    game.add(component);\n\n    // Expect the game is ready to play\n    expect(game.isAttached, true);\n    expect(game.isMounted, true);\n    expect(game.isLoaded, true);\n\n    // Pump to ensure the custom component's lifecycle events are handled\n    await widgetTester.pump(Duration.zero);\n\n    // Expect that the GameWidget is now listening to\n    // numberProvider as the watching component has been added.\n    expect(key.currentState?.exists(numberProvider), true);\n\n    // Remove the custom component from the game.\n    game.remove(component);\n\n    // Pump to ensure the component has been removed.\n    await widgetTester.pump(Duration.zero);\n\n    // Expect the component has been removed from the game.\n    expect(component.isRemoved, true);\n\n    // Pump to ensure the listener has been cancelled by ProviderScope.\n    await widgetTester.pump(const Duration(seconds: 5));\n\n    // Expect that the GameWidget is no longer listening to\n    // numberProvider as the watching component has been removed.\n    expect(key.currentState?.exists(numberProvider), false);\n  });\n\n  testWidgets(\n    'Test registration and de-registration of Game Provider listeners',\n    (widgetTester) async {\n      final game = MyGameWithRefAccess();\n      final key = GlobalKey<RiverpodAwareGameWidgetState>();\n\n      await widgetTester.pumpWidget(\n        ProviderScope(\n          child: MaterialApp(\n            home: RiverpodAwareGameWidget(\n              game: game,\n              key: key,\n            ),\n          ),\n        ),\n      );\n      await widgetTester.pump(Duration.zero);\n\n      // Expect the game is ready to play\n      expect(game.isAttached, true);\n      expect(game.isMounted, true);\n      expect(game.isLoaded, true);\n\n      // Pump to ensure the custom component's lifecycle events are handled\n      await widgetTester.pump(Duration.zero);\n\n      // Expect that the GameWidget is initially listening to\n      // numberProvider\n      expect(key.currentState?.exists(numberProvider), true);\n\n      // Replace the widget tree so that the GameWidget gets disposed\n      await widgetTester.pumpWidget(Container());\n      await widgetTester.pumpAndSettle();\n\n      // Expect that the component has been removed from the game.\n      expect(game.isAttached, false);\n\n      // Expect that the key no longer has access to a state.\n      expect(key.currentState, null);\n    },\n  );\n}\n"
  },
  {
    "path": "packages/flame_spine/CHANGELOG.md",
    "content": "## 0.3.0+4\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 0.3.0+3\n\n - Update a dependency to the latest release.\n\n## 0.3.0+2\n\n - Update a dependency to the latest release.\n\n## 0.3.0+1\n\n - **DOCS**: Updated flame_spine documentation ([#3763](https://github.com/flame-engine/flame/issues/3763)). ([3c721d91](https://github.com/flame-engine/flame/commit/3c721d91f3e3514c79e77b8e2e37a641305a2e04))\n\n## 0.3.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FEAT**: Support Spine 4.3 ([#3760](https://github.com/flame-engine/flame/issues/3760)). ([17bc40c3](https://github.com/flame-engine/flame/commit/17bc40c361471fde35b360671cccef115ed0b4bc))\n\n## 0.2.2+17\n\n - **DOCS**: Deprecate TapDetector in favour of TapCallbacks ([#2886](https://github.com/flame-engine/flame/issues/2886)). ([b173697b](https://github.com/flame-engine/flame/commit/b173697bfb7ea61287251c43cd3c9d2fdb448fe3))\n\n## 0.2.2+16\n\n - Update a dependency to the latest release.\n\n## 0.2.2+15\n\n - Update a dependency to the latest release.\n\n## 0.2.2+14\n\n - Update a dependency to the latest release.\n\n## 0.2.2+13\n\n - Update a dependency to the latest release.\n\n## 0.2.2+12\n\n - Update a dependency to the latest release.\n\n## 0.2.2+11\n\n - Update a dependency to the latest release.\n\n## 0.2.2+10\n\n - Update a dependency to the latest release.\n\n## 0.2.2+9\n\n - Update a dependency to the latest release.\n\n## 0.2.2+8\n\n - **FIX**: Bump spine version and update example files ([#3534](https://github.com/flame-engine/flame/issues/3534)). ([f346e3f6](https://github.com/flame-engine/flame/commit/f346e3f67793f2ebece3f11c1f440c5d485bf959))\n\n## 0.2.2+7\n\n - Update a dependency to the latest release.\n\n## 0.2.2+6\n\n - Update a dependency to the latest release.\n\n## 0.2.2+5\n\n - Update a dependency to the latest release.\n\n## 0.2.2+4\n\n - Update a dependency to the latest release.\n\n## 0.2.2+3\n\n - Update a dependency to the latest release.\n\n## 0.2.2+2\n\n - Update a dependency to the latest release.\n\n## 0.2.2+1\n\n - Update a dependency to the latest release.\n\n## 0.2.2\n\n - **DOCS**: Homepage link typo for flame_spine ([#3277](https://github.com/flame-engine/flame/issues/3277)). ([f76355f1](https://github.com/flame-engine/flame/commit/f76355f151a61fa0eddb5356b7e2a7c27b96c221))\n\n## 0.2.1\n\n## 0.2.0+1\n\n - Update a dependency to the latest release.\n\n## 0.2.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 0.1.1+10\n\n - Update a dependency to the latest release.\n\n## 0.1.1+9\n\n - Update a dependency to the latest release.\n\n## 0.1.1+8\n\n - Update a dependency to the latest release.\n\n## 0.1.1+7\n\n - Update a dependency to the latest release.\n\n## 0.1.1+6\n\n - **FIX**: Removing spine flutter overriding ([#2877](https://github.com/flame-engine/flame/issues/2877)). ([f4ff3117](https://github.com/flame-engine/flame/commit/f4ff31174a0498dd8af90f815ad9c098df3b30b7))\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n## 0.1.1+5\n\n - Update a dependency to the latest release.\n\n## 0.1.1+4\n\n - Update a dependency to the latest release.\n\n## 0.1.1+3\n\n - Update a dependency to the latest release.\n\n## 0.1.1+2\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n\n## 0.1.1+1\n\n - Update a dependency to the latest release.\n\n## 0.1.1\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FEAT**: ComponentKey API ([#2566](https://github.com/flame-engine/flame/issues/2566)). ([b3efb612](https://github.com/flame-engine/flame/commit/b3efb612cb3cb77f69bc030e9ba71516348035d2))\n\n## 0.1.0+1\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n## 0.1.0\n\n - **FEAT**: Spine bridge package ([#2530](https://github.com/flame-engine/flame/issues/2530)). ([5d1a6fd1](https://github.com/flame-engine/flame/commit/5d1a6fd1679c5690685e5a5f9b695a0ab6699bca))\n\n"
  },
  {
    "path": "packages/flame_spine/LICENSE",
    "content": "\nflame_spine License\n-------------------\n\nMIT License\n\nCopyright (c) 2022 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n\nSpine Runtimes License Agreement\n--------------------------------\n\nCopyright (c) 2013-2021, Esoteric Software LLC\n\nIntegration of the Spine Runtimes into software or otherwise creating\nderivative works of the Spine Runtimes is permitted under the terms and\nconditions of Section 2 of the Spine Editor License Agreement:\nhttp://esotericsoftware.com/spine-editor-license\n\nOtherwise, it is permitted to integrate the Spine Runtimes into software\nor otherwise create derivative works of the Spine Runtimes (collectively,\n\"Products\"), provided that each user of the Products must obtain their own\nSpine Editor license and redistribution of the Products in any form must\ninclude this license and copyright notice.\n\nTHE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC \"AS IS\" AND ANY\nEXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,\nBUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\nTHE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "packages/flame_spine/README.md",
    "content": "# Flame Spine\n\nPackage to add [Spine](http://esotericsoftware.com/) support for the Flame game engine.\n\nYou can read more about the spine-flutter runtime and how to use it in the docs at their\n[site](https://esotericsoftware.com/spine-flutter).\n\n\n## License\n\nThe code and all associated assets of the `flame_spine` package is distributed under the\nMIT License (see the LICENSE file). **HOWEVER**, as `flame_spine` includes Spine Runtime libraries,\nyour use of `flame_spine` in your games or other products is also subject to the terms and\nconditions of the Spine Runtime License Agreement (see the LICENSE file).\n\n**In particular, in order to use `flame_spine` you are required to obtain a valid Spine Editor\nLicense.**\n\n\n## Spine 4.2 vs Spine 4.3\n\nMake sure that you are using the right version of `flame_spine` for your current version of the\nSpine Editor.\n\nFor Spine 4.2, `v0.2.2` is the last release. Spine 4.3 support begins with `v0.3.0`.\n\nTo migrate from 4.2 to 4.3 models, make sure to re-export any existing 4.2 models through the 4.3\nSpine Editor, and follow the `spine_flutter` [migration guide](https://pub.dev/packages/spine_flutter/changelog).\n\nThere is a [Spine CLI](https://en.esotericsoftware.com/spine-command-line-interface) available to\nassist with the re-exporting process.\n\nFeel free to reach out on the [official forums](https://esotericsoftware.com/forum/) if you need\nmore specific assistance.\n"
  },
  {
    "path": "packages/flame_spine/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_spine/example/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\nmigrate_working_dir/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\n/build/\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Android Studio will place build artifacts here\n/android/app/debug\n/android/app/profile\n/android/app/release\n"
  },
  {
    "path": "packages/flame_spine/example/README.md",
    "content": "# Flame Spine example\n\nProject to showcase the usage of flame_spine\n"
  },
  {
    "path": "packages/flame_spine/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml"
  },
  {
    "path": "packages/flame_spine/example/assets/LICENSE",
    "content": "Copyright (c) 2013, Esoteric Software\n\nThe images in this project may be redistributed as long as they are accompanied\nby this license file. The images may not be used for commercial use of any\nkind.\n\nThe project file is released into the public domain. It may be used as the basis\nfor derivative work."
  },
  {
    "path": "packages/flame_spine/example/assets/spineboy.atlas",
    "content": "spineboy.png\n\tsize: 1024, 256\n\tfilter: Linear, Linear\n\tscale: 0.5\ncrosshair\n\tbounds: 263, 11, 45, 45\neye-indifferent\n\tbounds: 214, 11, 47, 45\neye-surprised\n\tbounds: 965, 33, 47, 45\n\trotate: 90\nfront-bracer\n\tbounds: 2, 5, 29, 40\n\trotate: 90\nfront-fist-closed\n\tbounds: 505, 3, 38, 41\n\trotate: 90\nfront-fist-open\n\tbounds: 790, 9, 43, 44\n\trotate: 90\nfront-foot\n\tbounds: 149, 21, 63, 35\nfront-shin\n\tbounds: 505, 43, 41, 92\n\trotate: 90\nfront-thigh\n\tbounds: 359, 14, 23, 56\n\trotate: 90\nfront-upper-arm\n\tbounds: 955, 8, 23, 49\n\trotate: 90\ngoggles\n\tbounds: 180, 58, 131, 83\ngun\n\tbounds: 313, 39, 105, 102\nhead\n\tbounds: 29, 83, 136, 149\n\trotate: 90\nhoverboard-board\n\tbounds: 180, 143, 246, 76\nhoverboard-thruster\n\tbounds: 790, 57, 30, 32\nhoverglow-small\n\tbounds: 826, 54, 137, 38\nmouth-grind\n\tbounds: 707, 8, 47, 30\nmouth-oooo\n\tbounds: 658, 8, 47, 30\nmouth-smile\n\tbounds: 548, 11, 47, 30\nmuzzle-glow\n\tbounds: 997, 194, 25, 25\nmuzzle-ring\n\tbounds: 2, 114, 25, 105\nmuzzle01\n\tbounds: 965, 82, 67, 40\n\trotate: 90\nmuzzle02\n\tbounds: 953, 151, 68, 42\n\trotate: 90\nmuzzle03\n\tbounds: 420, 31, 83, 53\nmuzzle04\n\tbounds: 2, 36, 75, 45\nmuzzle05\n\tbounds: 79, 43, 68, 38\nneck\n\tbounds: 997, 171, 18, 21\nportal-bg\n\tbounds: 563, 86, 133, 133\nportal-flare1\n\tbounds: 79, 11, 56, 30\nportal-flare2\n\tbounds: 836, 21, 57, 31\nportal-flare3\n\tbounds: 895, 22, 58, 30\nportal-shade\n\tbounds: 428, 86, 133, 133\nportal-streaks1\n\tbounds: 698, 91, 126, 128\nportal-streaks2\n\tbounds: 826, 94, 125, 125\nrear-bracer\n\tbounds: 756, 2, 28, 36\nrear-foot\n\tbounds: 599, 14, 57, 30\nrear-shin\n\tbounds: 599, 46, 38, 89\n\trotate: 90\nrear-thigh\n\tbounds: 310, 9, 28, 47\n\trotate: 90\nrear-upper-arm\n\tbounds: 417, 9, 20, 44\n\trotate: 90\ntorso\n\tbounds: 698, 40, 49, 90\n\trotate: 90\n"
  },
  {
    "path": "packages/flame_spine/example/lib/main.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_spine/flame_spine.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  await initSpineFlutter();\n  runApp(const GameWidget.controlled(gameFactory: SpineExample.new));\n}\n\nclass SpineExample extends FlameGame with TapCallbacks {\n  late final SpineComponent spineboy;\n\n  final states = [\n    'walk',\n    'aim',\n    'death',\n    'hoverboard',\n    'idle',\n    'jump',\n    'portal',\n    'run',\n    'shoot',\n  ];\n\n  int _stateIndex = 0;\n\n  @override\n  Future<void> onLoad() async {\n    // Load the Spineboy atlas and skeleton data from asset files\n    // and create a SpineComponent from them, scaled down and\n    // centered on the screen\n    spineboy = await SpineComponent.fromAssets(\n      atlasFile: 'assets/spineboy.atlas',\n      skeletonFile: 'assets/spineboy-pro.skel',\n      scale: Vector2(0.4, 0.4),\n      anchor: Anchor.center,\n      position: Vector2(size.x / 2, size.y / 2),\n    );\n\n    // Set the \"walk\" animation on track 0 in looping mode\n    spineboy.animationState.setAnimation(0, 'walk', true);\n    await add(spineboy);\n  }\n\n  @override\n  void onTapDown(_) {\n    _stateIndex = (_stateIndex + 1) % states.length;\n    spineboy.animationState.setAnimation(0, states[_stateIndex], true);\n  }\n\n  @override\n  void onDetach() {\n    // Dispose the native resources that have been loaded for spineboy.\n    spineboy.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/flame_spine/example/pubspec.yaml",
    "content": "name: flame_spine_example\nresolution: workspace\ndescription: An example of using a flame_spine component\npublish_to: 'none' # Remove this line if you wish to publish to pub.dev\nversion: 1.0.0\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_spine: ^0.3.0+4\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n\nflutter:\n  uses-material-design: true\n  assets:\n    - assets/\n"
  },
  {
    "path": "packages/flame_spine/lib/flame_spine.dart",
    "content": "export 'package:spine_flutter/spine_flutter.dart';\nexport 'src/spine_component.dart';\n"
  },
  {
    "path": "packages/flame_spine/lib/src/spine_component.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flutter/services.dart';\nimport 'package:spine_flutter/spine_flutter.dart';\n\nclass SpineComponent extends PositionComponent {\n  final BoundsProvider _boundsProvider;\n  final SkeletonDrawableFlutter _drawable;\n  late final Bounds _bounds;\n  final bool _ownsDrawable;\n\n  SpineComponent(\n    this._drawable, {\n    bool ownsDrawable = true,\n    BoundsProvider boundsProvider = const SetupPoseBounds(),\n    super.position,\n    super.scale,\n    double super.angle = 0.0,\n    Anchor super.anchor = Anchor.topLeft,\n    super.children,\n    super.priority,\n    super.key,\n  }) : _ownsDrawable = ownsDrawable,\n       _boundsProvider = boundsProvider {\n    _drawable.update(0);\n    _bounds = _boundsProvider.computeBounds(_drawable);\n    size = Vector2(_bounds.width, _bounds.height);\n  }\n\n  static Future<SpineComponent> fromAssets({\n    required String atlasFile,\n    required String skeletonFile,\n    AssetBundle? bundle,\n    BoundsProvider boundsProvider = const SetupPoseBounds(),\n    Vector2? position,\n    Vector2? scale,\n    double angle = 0.0,\n    Anchor anchor = Anchor.topLeft,\n    Iterable<Component>? children,\n    int? priority,\n  }) async {\n    return SpineComponent(\n      await SkeletonDrawableFlutter.fromAsset(\n        atlasFile,\n        skeletonFile,\n        bundle: bundle,\n      ),\n      boundsProvider: boundsProvider,\n      position: position,\n      scale: scale,\n      angle: angle,\n      anchor: anchor,\n      children: children,\n      priority: priority,\n    );\n  }\n\n  void dispose() {\n    if (_ownsDrawable) {\n      _drawable.dispose();\n    }\n  }\n\n  @override\n  void update(double dt) {\n    _drawable.update(dt);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    canvas.save();\n    canvas.translate(-_bounds.x, -_bounds.y);\n    _drawable.renderToCanvas(canvas);\n    canvas.restore();\n  }\n\n  AnimationState get animationState => _drawable.animationState;\n\n  AnimationStateData get animationStateData => _drawable.animationStateData;\n\n  Skeleton get skeleton => _drawable.skeleton;\n}\n"
  },
  {
    "path": "packages/flame_spine/pubspec.yaml",
    "content": "name: flame_spine\nresolution: workspace\ndescription: Spine support for the Flame game engine. This uses the spine_flutter package and provides wrappers and components to be used inside Flame.\nversion: 0.3.0+4\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_spine\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - animations\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  collection: ^1.17.1\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  spine_flutter: ^4.3.0\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n  test: any\n\nassets:\n  assets/logo_flame.png\n"
  },
  {
    "path": "packages/flame_spine/test/flame_spine_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  test('flame_spine', () {});\n}\n"
  },
  {
    "path": "packages/flame_splash_screen/CHANGELOG.md",
    "content": "## 0.3.1+3\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 0.3.1+2\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 0.3.1+1\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 0.3.1\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 0.3.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 0.2.0\n\n- Added linter\n- Updated dependencies\n- Improved README\n- Updated to Flutter 3.13.0\n\n## 0.1.0\n\n- Added non nullability support\n- Update to new logo\n\n## 0.0.1\n\n- Initial implementation with theme, controller and of course, the splash screen widget.\n"
  },
  {
    "path": "packages/flame_splash_screen/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_splash_screen/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\nStyle your [Flame](https://github.com/flame-engine/flame) game with a beautiful splash screen.\n\nThis package includes a `FlameSplashScreen` widget.\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_splash_screen\" ><img src=\"https://img.shields.io/pub/v/flame_splash_screen.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n<p align=\"center\">\n  <img src=\"demo.gif\" />\n</p>\n\n<!-- markdownlint-disable-next-line MD002 -->\n## Install\n\nAdd `flame_splash_screen` as a dependency to\n[your pubspec.yaml file](https://pub.dev/packages/flame_splash_screen/install).\n\nImport the widget:\n\n```dart\nimport 'package:flame_splash_screen/flame_splash_screen.dart';\n```\n\n\n## Usage\n\nThe splash screen is a widget that can be used to show the splash screen.\n\n\n### Simple usage\n\nThere is just two required params:\n\n- `onFinish`, a callback that is executed when all animations from the splash screen is over.\n- `theme`, than can be either `FlameSplashTheme.dark` or `FlameSplashTheme.white`.\n\n```dart\nFlameSplashScreen(\n  theme: FlameSplashTheme.dark,\n  onFinish: (BuildContext context) => Navigator.pushNamed(context, '/your-game-initial-screen'),\n)\n```\n\n\n#### Adding your own content\n\nYou can pass your own logo (or/and anything else) to be shown before or after the Flame's logo.\n\n```dart\nFlameSplashScreen(\n  theme: FlameSplashTheme.dark,\n  showBefore: (BuildContext context) {\n    return Text(\"To be shown before flame animation\");\n  },\n  onFinish: (BuildContext context) => Navigator.pushNamed(context, '/your-game-initial-screen'),\n)\n```\n\n```dart\nFlameSplashScreen(\n  theme: FlameSplashTheme.dark,\n  showAfter: (BuildContext context) {\n    return Text(\"To be shown after flame animation\");\n  },\n  onFinish: (BuildContext context) => Navigator.pushNamed(context, '/your-game-initial-screen'),\n)\n```\n\nRemember: you can also specify both `showBefore` and `showAfter` at the same time.\n\n\n#### Changing theme\n\nBy default the splash screen has a dark background. You can change it by specifying the `white`\ntheme.\n\nAside from `FlameSplashTheme.dark`, you can pass `FlameSplashTheme.white` for a white background.\n\n```dart\nFlameSplashScreen(\n  theme: FlameSplashTheme.white,\n  onFinish: (BuildContext context) => Navigator.pushNamed(context, '/your-game-initial-screen'),\n)\n```\n\nYou can create your own theme passing a custom logo builder (changing flames logo for another one)\nand a background decoration\n\n\n### Usage with controller\n\nController enables `FlameSplashScreen` to be customized regarding animation duration and when it\nstarts.\n\nThere is duration params and `autoStart` (which is true by default).\n\nTo use it, make the controller lives as much as a widget state:\n\n```dart\nclass SplashScreenGameState extends State<SplashScreenGame> {\n  FlameSplashController controller;\n  @override\n  void initState() {\n      super.initState();\n      controller = FlameSplashController(\n        fadeInDuration: Duration(seconds: 1),\n        fadeOutDuration: Duration(milliseconds: 250),\n        waitDuration: Duration(seconds: 2),\n        autoStart: false,\n      );\n  }\n  \n  @override\n  void dispose() {\n    controller.dispose(); // dispose it when necessary\n    super.dispose();\n  }\n  \n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: FlameSplashScreen(\n        showBefore: (BuildContext context) {\n          return Text(\"Before the logo\");\n        },\n        showAfter: (BuildContext context) {\n          return Text(\"After the logo\");\n       },\n       theme: FlameSplashTheme.white,\n       onFinish: (context) => Navigator.pushNamed(context, '/the-game-initial-screen'),\n        controller: controller,\n      ),\n    );\n  }\n}\n```\n"
  },
  {
    "path": "packages/flame_splash_screen/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options.yaml"
  },
  {
    "path": "packages/flame_splash_screen/example/README.md",
    "content": "# Splash screen example app\n\nExample of usage\n"
  },
  {
    "path": "packages/flame_splash_screen/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options.yaml"
  },
  {
    "path": "packages/flame_splash_screen/example/lib/main.dart",
    "content": "import 'package:flame_splash_screen/flame_splash_screen.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(const MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: const SplashScreenGame(),\n      theme: ThemeData.dark(),\n      debugShowCheckedModeBanner: false,\n    );\n  }\n}\n\nclass OtherScreen extends StatelessWidget {\n  const OtherScreen({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Center(\n        child: ElevatedButton(\n          child: const Text('Come again'),\n          onPressed: () {\n            Navigator.push<void>(\n              context,\n              MaterialPageRoute(builder: (context) => const SplashScreenGame()),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass SplashScreenGame extends StatefulWidget {\n  const SplashScreenGame({super.key});\n\n  @override\n  SplashScreenGameState createState() => SplashScreenGameState();\n}\n\nclass SplashScreenGameState extends State<SplashScreenGame> {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: FlameSplashScreen(\n        showBefore: (BuildContext context) {\n          return const Text('Before logo');\n        },\n        showAfter: (BuildContext context) {\n          return const Text('After logo');\n        },\n        theme: FlameSplashTheme.dark,\n        onFinish: (context) => Navigator.pushReplacement<void, void>(\n          context,\n          MaterialPageRoute(builder: (context) => const OtherScreen()),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_splash_screen/example/pubspec.yaml",
    "content": "name: flame_splash_screen_example\nresolution: workspace\ndescription: Flame Splash Screen Example\npublish_to: none\n\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame_splash_screen: ^0.3.1+3\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n\n"
  },
  {
    "path": "packages/flame_splash_screen/lib/flame_splash_screen.dart",
    "content": "export 'src/controller.dart';\nexport 'src/splash.dart';\nexport 'src/theme.dart';\n"
  },
  {
    "path": "packages/flame_splash_screen/lib/src/controller.dart",
    "content": "import 'package:flutter/widgets.dart';\nimport 'package:meta/meta.dart';\n\n/// Controller enables you to start the animation whenever you want with\n/// [autoStart] option and customize animation duration as well.\nclass FlameSplashController {\n  FlameSplashController({\n    Duration fadeInDuration = const Duration(milliseconds: 750),\n    Duration waitDuration = const Duration(seconds: 2),\n    Duration fadeOutDuration = const Duration(milliseconds: 450),\n    this.autoStart = true,\n  }) : stepController = FlameSplashControllerStep(0),\n       durations = FlameSplashDurations(\n         fadeInDuration,\n         waitDuration,\n         fadeOutDuration,\n       );\n\n  /// Defines if you want to start the animations right after widget mount.\n  final bool autoStart;\n\n  @internal\n  final FlameSplashDurations durations;\n\n  @internal\n  final FlameSplashControllerStep stepController;\n\n  FlameSplashControllerState _state = FlameSplashControllerState.idle;\n  bool _hasSetup = false;\n  int _stepsAmount = 0;\n  late void Function() _onFinish;\n\n  /// Displays the actual state of the controller regarding the animation.\n  FlameSplashControllerState get state => _state;\n  @internal\n  set state(FlameSplashControllerState newState) => _state = newState;\n\n  /// Method used to start the animation, do not call if you set [autoStart] to\n  /// true.\n  void start() {\n    assert(\n      _hasSetup,\n      'This controller is not being used by any FlameSplashScreen widget. '\n      'Start it only after widget mount.',\n    );\n    assert(\n      _state != FlameSplashControllerState.started,\n      'This controller has been already started, verify if autoStart has been '\n      'specified',\n    );\n\n    _state = FlameSplashControllerState.started;\n    _tickStep(0);\n  }\n\n  /// Called by the [start] method; this is only exposed for testing purposes.\n  @internal\n  void setup(\n    int steps,\n    void Function() onFinish,\n  ) {\n    _onFinish = onFinish;\n    _stepsAmount = steps;\n    _hasSetup = true;\n    if (autoStart) {\n      start();\n    }\n  }\n\n  Future<void> _tickStep(int index) async {\n    stepController.value = index;\n    await Future<void>.delayed(durations.total);\n    final finished = index >= _stepsAmount - 1;\n    if (finished) {\n      _state = FlameSplashControllerState.finished;\n      _onFinish();\n      return;\n    }\n    _tickStep(index + 1);\n  }\n\n  /// Properly disposes of this controller.\n  /// Must be called after no longer used.\n  void dispose() {\n    stepController.dispose();\n  }\n}\n\n/// Represents the state of the splash screen.\nenum FlameSplashControllerState {\n  /// Not started yet, but ready to start.\n  /// Note that if autoStart is set, this stage will be skipped.\n  idle,\n\n  /// Started and currently running through the steps.\n  started,\n\n  /// Finished to run through all steps.\n  finished,\n}\n\nclass FlameSplashControllerStep extends ValueNotifier<int> {\n  FlameSplashControllerStep(super.value);\n}\n\nclass FlameSplashDurations {\n  const FlameSplashDurations(\n    this.fadeInDuration,\n    this.waitDuration,\n    this.fadeOutDuration,\n  );\n\n  final Duration fadeInDuration;\n  final Duration waitDuration;\n  final Duration fadeOutDuration;\n\n  Duration get total {\n    return fadeInDuration + fadeOutDuration + waitDuration;\n  }\n}\n"
  },
  {
    "path": "packages/flame_splash_screen/lib/src/splash.dart",
    "content": "import 'package:flame_splash_screen/flame_splash_screen.dart';\nimport 'package:flutter/widgets.dart';\n\n/// A stateful widget to show a splash screen animation for flame games\nclass FlameSplashScreen extends StatefulWidget {\n  /// Creates a [FlameSplashScreen].\n  const FlameSplashScreen({\n    required this.onFinish,\n    required this.theme,\n    this.showBefore,\n    this.showAfter,\n    this.controller,\n    super.key,\n  });\n\n  /// Gives extra controller over the splash animation.\n  final FlameSplashController? controller;\n\n  /// Enables to set a different theme other than the default.\n  final FlameSplashTheme theme;\n\n  /// The only required option, callback to be invoked when animation finished\n  final ValueChanged<BuildContext> onFinish;\n\n  /// Adds an extra step to the animation showing a widget, can be other logo.\n  ///\n  /// Shown before flame logo.\n  final WidgetBuilder? showBefore;\n\n  /// Adds an extra step to the animation showing a widget, can be other logo.\n  ///\n  /// Shown after flame logo.\n  final WidgetBuilder? showAfter;\n\n  @override\n  FlameSplashScreenState createState() => FlameSplashScreenState();\n}\n\n/// The state for the [FlameSplashScreen] that holds the [controller] for\n/// controlling the animation durations and whether it should automatically\n/// start. Also contains the list of steps that it should animate through.\nclass FlameSplashScreenState extends State<FlameSplashScreen> {\n  /// The [controller] for controlling the animation durations and whether it\n  /// should automatically start.\n  late FlameSplashController controller;\n\n  /// The list of steps that it should animate through.\n  late List<WidgetBuilder> steps;\n  bool _externallyControlled = false;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _externallyControlled = widget.controller != null;\n    controller = widget.controller ?? FlameSplashController();\n\n    _computeSteps();\n    controller.setup(steps.length, () => widget.onFinish(context));\n  }\n\n  void _computeSteps() {\n    setState(() {\n      steps = [\n        if (widget.showBefore != null) widget.showBefore!,\n        widget.theme.logoBuilder,\n        if (widget.showAfter != null) widget.showAfter!,\n      ];\n    });\n  }\n\n  @override\n  void didUpdateWidget(FlameSplashScreen oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    final hasStepsChanged =\n        widget.showBefore != oldWidget.showBefore ||\n        widget.showAfter != oldWidget.showAfter ||\n        widget.theme.logoBuilder != oldWidget.theme.logoBuilder;\n    if (hasStepsChanged &&\n        controller.state != FlameSplashControllerState.started) {\n      _computeSteps();\n      controller.start();\n    }\n  }\n\n  @override\n  void dispose() {\n    if (!_externallyControlled) {\n      controller.dispose();\n    }\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      constraints: widget.theme.constraints,\n      decoration: widget.theme.backgroundDecoration,\n      child: Center(\n        child: ValueListenableBuilder<int>(\n          valueListenable: controller.stepController,\n          builder: (context, currentStep, _) {\n            return _SplashScreenStep(\n              builder: steps[currentStep],\n              durations: controller.durations,\n              key: ObjectKey(currentStep),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass _SplashScreenStep extends StatefulWidget {\n  const _SplashScreenStep({\n    required this.builder,\n    required this.durations,\n    super.key,\n  });\n\n  final WidgetBuilder builder;\n  final FlameSplashDurations durations;\n\n  @override\n  __SplashScreenStepState createState() => __SplashScreenStepState();\n}\n\nclass __SplashScreenStepState extends State<_SplashScreenStep>\n    with TickerProviderStateMixin {\n  late AnimationController controller;\n  late Animation<double> opacityAnimation;\n  double opacity = 0.0;\n\n  @override\n  void initState() {\n    super.initState();\n    controller = AnimationController(\n      vsync: this,\n    )..addListener(handleAnimation);\n    opacityAnimation = Tween<double>(\n      begin: 0.0,\n      end: 1.0,\n    ).animate(controller);\n    startAnimation();\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n\n  void handleAnimation() {\n    setState(() {\n      opacity = opacityAnimation.value;\n    });\n  }\n\n  Future<void> startAnimation() async {\n    // fade in\n    controller\n      ..value = 0.0\n      ..duration = widget.durations.fadeInDuration;\n    await controller.forward();\n    await Future<void>.delayed(widget.durations.waitDuration);\n    controller\n      ..value = 1.0\n      ..duration = widget.durations.fadeOutDuration\n      ..reverse();\n  }\n\n  @override\n  void didUpdateWidget(_SplashScreenStep oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    if (oldWidget.key != widget.key) {\n      startAnimation();\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Opacity(\n      opacity: opacity,\n      child: widget.builder(context),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_splash_screen/lib/src/theme.dart",
    "content": "import 'package:flutter/widgets.dart';\n\n/// This widget builds up the Flame logo composed of 3 layers,\n/// that are rendered via separate PNG files under the assets directory.\nclass AnimatedLogo extends AnimatedWidget {\n  /// Create this widget providing the animation parameter to control\n  /// the opacity of the flame.\n  const AnimatedLogo({\n    required Animation<double> animation,\n    super.key,\n  }) : super(listenable: animation);\n\n  @override\n  Widget build(BuildContext context) {\n    final animation = listenable as Animation<double>;\n    return Stack(\n      fit: StackFit.expand,\n      alignment: Alignment.center,\n      children: [\n        Image.asset(\n          'assets/layer1.png',\n          package: 'flame_splash_screen',\n        ),\n        Opacity(\n          opacity: animation.value,\n          child: Image.asset(\n            'assets/layer2.png',\n            package: 'flame_splash_screen',\n          ),\n        ),\n        Image.asset(\n          'assets/layer3.png',\n          package: 'flame_splash_screen',\n        ),\n      ],\n    );\n  }\n}\n\n/// Creates and controls an [AnimatedLogo], making sure to provide the required\n/// animation and to properly dispose of itself after usage.\nclass LogoComposite extends StatefulWidget {\n  /// Creates a [LogoComposite].\n  const LogoComposite({super.key});\n\n  @override\n  LogoCompositeState createState() => LogoCompositeState();\n}\n\n/// The state holding the state of the animated logo and its controller.\nclass LogoCompositeState extends State<LogoComposite>\n    with SingleTickerProviderStateMixin {\n  /// The state of the animated logo.\n  late Animation<double> animation;\n\n  /// The controller for the animated logo.\n  late AnimationController controller;\n\n  @override\n  void initState() {\n    super.initState();\n    controller = AnimationController(\n      duration: const Duration(milliseconds: 500),\n      vsync: this,\n    );\n    animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller);\n\n    controller.addStatusListener((status) {\n      if (status == AnimationStatus.completed) {\n        controller.reverse();\n      } else if (status == AnimationStatus.dismissed) {\n        controller.forward();\n      }\n    });\n\n    controller.forward();\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) => AnimatedLogo(animation: animation);\n}\n\nWidget _logoBuilder(BuildContext context) {\n  return LayoutBuilder(\n    builder: (context, constraints) {\n      return FractionalTranslation(\n        translation: const Offset(0, -0.25),\n        child: ConstrainedBox(\n          constraints: BoxConstraints.loose(const Size(300, 300)),\n          child: const LogoComposite(),\n        ),\n      );\n    },\n  );\n}\n\n/// Wraps the splash screen layout options.\n/// There is two predefined themes [FlameSplashTheme.dark] and\n/// [FlameSplashTheme.white].\nclass FlameSplashTheme {\n  /// Creates a customized theme. [logoBuilder] returns the widget that will be\n  /// rendered in place of main step of the animation.\n  const FlameSplashTheme({\n    required this.backgroundDecoration,\n    required this.logoBuilder,\n    this.constraints = const BoxConstraints.expand(),\n  });\n\n  /// Decoration to be applied to the widget underneath the Flame logo.\n  /// It can be used to set the background color, among other parameters.\n  final BoxDecoration backgroundDecoration;\n\n  /// A lambda to build the widget representing the logo itself.\n  /// By default this will be wired to use the [LogoComposite] widget.\n  final WidgetBuilder logoBuilder;\n\n  /// Th constraints of the outside box, defaults to [BoxConstraints.expand].\n  final BoxConstraints constraints;\n\n  /// One of the two default themes provided; this is optimal of light mode\n  /// apps.\n  static FlameSplashTheme white = const FlameSplashTheme(\n    backgroundDecoration: BoxDecoration(color: Color(0xFFFFFFFF)),\n    logoBuilder: _logoBuilder,\n  );\n\n  /// One of the two default themes provided; this is optimal of dark mode apps.\n  static FlameSplashTheme dark = const FlameSplashTheme(\n    backgroundDecoration: BoxDecoration(color: Color(0xFF000000)),\n    logoBuilder: _logoBuilder,\n  );\n}\n"
  },
  {
    "path": "packages/flame_splash_screen/pubspec.yaml",
    "content": "name: flame_splash_screen\nresolution: workspace\ndescription: Style your flame game with a beautiful splash screen with logo reveal. Simple to use but still customizable.\nversion: 0.3.1+3\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_splash_screen\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flutter:\n    sdk: flutter\n  meta: ^1.12.0\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n  mockito: ^5.4.2\n\nflutter:\n  assets:\n   - assets/\n"
  },
  {
    "path": "packages/flame_splash_screen/test/controller_test.dart",
    "content": "import 'package:flame_splash_screen/flame_splash_screen.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mockito/mockito.dart';\n\nclass OnFinishContainer {\n  void onFinish() {}\n}\n\nclass MockOnFinish extends Mock implements OnFinishContainer {}\n\nvoid main() {\n  group('Without autostart', () {\n    late FlameSplashController controller;\n    setUp(() {\n      controller = FlameSplashController(\n        fadeInDuration: const Duration(milliseconds: 500),\n        fadeOutDuration: const Duration(milliseconds: 500),\n        waitDuration: const Duration(milliseconds: 500),\n        autoStart: false,\n      );\n    });\n    test('Do not let it start before setup', () async {\n      expect(() => controller.start(), throwsAssertionError);\n      controller.setup(1, () {});\n      controller.start();\n    });\n    test('Idle after setup', () {\n      controller.setup(1, () {});\n      expect(controller.state, FlameSplashControllerState.idle);\n    });\n    test('Started after .start()', () {\n      controller.setup(1, () {});\n      expect(controller.state, FlameSplashControllerState.idle);\n      controller.start();\n      expect(controller.state, FlameSplashControllerState.started);\n    });\n    test('Calls onFinish after steps', () async {\n      final onFinishContainer = MockOnFinish();\n      controller.setup(3, onFinishContainer.onFinish);\n      controller.start();\n      await untilCalled(onFinishContainer.onFinish());\n      expect(controller.state, FlameSplashControllerState.finished);\n    });\n  });\n  group('With autostart', () {\n    late FlameSplashController controller;\n    setUp(() {\n      controller = FlameSplashController(\n        fadeInDuration: const Duration(milliseconds: 500),\n        fadeOutDuration: const Duration(milliseconds: 500),\n        waitDuration: const Duration(milliseconds: 500),\n      );\n    });\n    test('Started automatically', () {\n      controller.setup(1, () {});\n      expect(controller.state, FlameSplashControllerState.started);\n    });\n    test('Calls onFinish after steps', () async {\n      final onFinishContainer = MockOnFinish();\n      controller.setup(3, onFinishContainer.onFinish);\n      await untilCalled(onFinishContainer.onFinish());\n      expect(controller.state, FlameSplashControllerState.finished);\n    });\n\n    test('Do not let it start again', () async {\n      controller.setup(1, () {});\n      expect(() => controller.start(), throwsAssertionError);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_splash_screen/test/theme_test.dart",
    "content": "import 'package:flame_splash_screen/flame_splash_screen.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  test('white', () {\n    final theme = FlameSplashTheme.white;\n    expect(\n      theme.constraints,\n      const BoxConstraints.expand(),\n    );\n    expect(\n      theme.backgroundDecoration,\n      const BoxDecoration(color: Color(0xFFFFFFFF)),\n    );\n  });\n\n  test('dark', () {\n    final theme = FlameSplashTheme.dark;\n    expect(theme.constraints, const BoxConstraints.expand());\n    expect(\n      theme.backgroundDecoration,\n      const BoxDecoration(color: Color(0xFF000000)),\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_sprite_fusion/CHANGELOG.md",
    "content": "## 0.2.3\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n - **FEAT**: Support package argument in asset loading methods and widgets ([#3835](https://github.com/flame-engine/flame/issues/3835)). ([3f6f95b0](https://github.com/flame-engine/flame/commit/3f6f95b0225df9e4035c74de6cfad501e8c45167))\n\n## 0.2.2+3\n\n - Update a dependency to the latest release.\n\n## 0.2.2+2\n\n - Update a dependency to the latest release.\n\n## 0.2.2+1\n\n - Update a dependency to the latest release.\n\n## 0.2.2\n\n - **FEAT**: Add `getLayerByName` to `SpriteFusionTilemapData` ([#3755](https://github.com/flame-engine/flame/issues/3755)). ([9a6ca27b](https://github.com/flame-engine/flame/commit/9a6ca27b42715c0b9ae52885ab8c94bed3819c11))\n\n## 0.2.1\n\n - **FEAT**: Add `hasAttribute` and `getAttribute` methods for `SpriteFusionTileData` ([#3751](https://github.com/flame-engine/flame/issues/3751)). ([17aeb93b](https://github.com/flame-engine/flame/commit/17aeb93b6f0e1bab11f6e237446811824e512b77))\n\n## 0.2.0+3\n\n - Update a dependency to the latest release.\n\n## 0.2.0+2\n\n - Update a dependency to the latest release.\n\n## 0.2.0+1\n\n - Update a dependency to the latest release.\n\n## 0.2.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FEAT**: Pass `WidgetTester` for `testGolden` prepare function ([#3624](https://github.com/flame-engine/flame/issues/3624)). ([10509326](https://github.com/flame-engine/flame/commit/105093266431408db0f9e74042e03e2234d9b22e))\n\n## 0.1.3+12\n\n - Update a dependency to the latest release.\n\n## 0.1.3+11\n\n - Update a dependency to the latest release.\n\n## 0.1.3+10\n\n - Update a dependency to the latest release.\n\n## 0.1.3+9\n\n - Update a dependency to the latest release.\n\n## 0.1.3+8\n\n - Update a dependency to the latest release.\n\n## 0.1.3+7\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 0.1.3+6\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 0.1.3+5\n\n - Update a dependency to the latest release.\n\n## 0.1.3+4\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n\n## 0.1.3+3\n\n - Update a dependency to the latest release.\n\n## 0.1.3+2\n\n - Update a dependency to the latest release.\n\n## 0.1.3+1\n\n - Update a dependency to the latest release.\n\n## 0.1.3\n\n - **FIX**: Add nativeAngle to constructors where it makes sense ([#3197](https://github.com/flame-engine/flame/issues/3197)). ([e8704934](https://github.com/flame-engine/flame/commit/e8704934b19d9ed1982d35ce62819f01ac3de189))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 0.1.2\n\n## 0.1.1\n\n - **FEAT**: Add initial version of `flame_sprite_fusion` package ([#3062](https://github.com/flame-engine/flame/issues/3062)). ([1c51334e](https://github.com/flame-engine/flame/commit/1c51334e865ae7000f93832574e24707e8c9dfa0))\n\n"
  },
  {
    "path": "packages/flame_sprite_fusion/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/flame_sprite_fusion/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nAdds support for parsing and rendering tilemap exported from <a href=\"https://www.spritefusion.com/\">Sprite Fusion</a> directly in <a href=\"https://flame-engine.org/\">Flame engine</a>.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_sprite_fusion\" ><img src=\"https://img.shields.io/pub/v/flame_sprite_fusion.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n\n## Features\n\nProvides a component called `SpriteFusionTilemapComponent` to easily load json exports from Sprite Fusion.\nThis component works exactly like any other Flame component and plays nicely with rest of the Flame\nComponent System (a.k.a FCS).\n\n\n## Getting started\n\n- To get started, first add `flame_sprite_fusion` as a dependency in your flutter project.\n\n  ```bash\n  flutter pub add flame_sprite_fusion\n  ```\n\n- Then place the `map.json` and `spritesheet.png` exported from Sprite Fusion into the `assets/tiles/`\nand `assets/images/` directory of your project respectively.\n\n- Finally load the map and spritesheet using `SpriteFusionTilemapComponent` in your game.\n\n\n## Usage\n\n\n```dart\n// Load the map.\nfinal map = await SpriteFusionTilemapComponent.load(\n  mapJsonFile: 'map.json',\n  spriteSheetFile: 'spritesheet.png'\n);\n\n//Add it to the game world.\nworld.add(map);\n```\n\n\n## Additional information\n\n\n> :warning: Under the current sprite batch implementation, you might experience extra lines while\nrendering due to a bug in Flutter, see [this issue](https://github.com/flame-engine/flame/issues/1152).\n"
  },
  {
    "path": "packages/flame_sprite_fusion/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options.yaml\n"
  },
  {
    "path": "packages/flame_sprite_fusion/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options.yaml\n"
  },
  {
    "path": "packages/flame_sprite_fusion/example/assets/tiles/map.json",
    "content": "{\n    \"tileSize\": 18,\n    \"mapWidth\": 20,\n    \"mapHeight\": 12,\n    \"layers\": [\n        {\n            \"name\": \"Spikes\",\n            \"tiles\": [\n                {\n                    \"id\": \"0\",\n                    \"x\": 0,\n                    \"y\": 0\n                },\n                {\n                    \"id\": \"0\",\n                    \"x\": 17,\n                    \"y\": 1\n                }\n            ],\n            \"collider\": false\n        },\n        {\n            \"name\": \"Water\",\n            \"tiles\": [\n                {\n                    \"id\": \"1\",\n                    \"x\": 13,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"2\",\n                    \"x\": 13,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 0,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 1,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 2,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 3,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 4,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 5,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 6,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 7,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 8,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 9,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 10,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 11,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 12,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 13,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 14,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 15,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 16,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 17,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 18,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 19,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"4\",\n                    \"x\": 13,\n                    \"y\": 10\n                }\n            ],\n            \"collider\": false\n        },\n        {\n            \"name\": \"Grass\",\n            \"tiles\": [\n                {\n                    \"id\": \"5\",\n                    \"x\": 6,\n                    \"y\": 7\n                },\n                {\n                    \"id\": \"6\",\n                    \"x\": 10,\n                    \"y\": 7\n                },\n                {\n                    \"id\": \"7\",\n                    \"x\": 18,\n                    \"y\": 5\n                }\n            ],\n            \"collider\": false\n        },\n        {\n            \"name\": \"Platforms\",\n            \"tiles\": [\n                {\n                    \"id\": \"8\",\n                    \"x\": 16,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"9\",\n                    \"x\": 19,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"10\",\n                    \"x\": 19,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"10\",\n                    \"x\": 19,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"10\",\n                    \"x\": 19,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"10\",\n                    \"x\": 19,\n                    \"y\": 7\n                },\n                {\n                    \"id\": \"11\",\n                    \"x\": 19,\n                    \"y\": 6\n                },\n                {\n                    \"id\": \"12\",\n                    \"x\": 16,\n                    \"y\": 6\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 17,\n                    \"y\": 6\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 18,\n                    \"y\": 6\n                },\n                {\n                    \"id\": \"14\",\n                    \"x\": 16,\n                    \"y\": 7\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 15,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"12\",\n                    \"x\": 14,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 17,\n                    \"y\": 7\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 18,\n                    \"y\": 7\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 18,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 18,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 18,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 17,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 16,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 15,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"14\",\n                    \"x\": 14,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"14\",\n                    \"x\": 14,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 15,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 16,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 17,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 17,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 18,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 17,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 16,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 15,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"16\",\n                    \"x\": 14,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"17\",\n                    \"x\": 11,\n                    \"y\": 4\n                },\n                {\n                    \"id\": \"18\",\n                    \"x\": 12,\n                    \"y\": 4\n                },\n                {\n                    \"id\": \"19\",\n                    \"x\": 13,\n                    \"y\": 4\n                },\n                {\n                    \"id\": \"20\",\n                    \"x\": 6,\n                    \"y\": 3\n                },\n                {\n                    \"id\": \"21\",\n                    \"x\": 7,\n                    \"y\": 3\n                },\n                {\n                    \"id\": \"22\",\n                    \"x\": 8,\n                    \"y\": 3\n                },\n                {\n                    \"id\": \"23\",\n                    \"x\": 10,\n                    \"y\": 1\n                },\n                {\n                    \"id\": \"24\",\n                    \"x\": 1,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"25\",\n                    \"x\": 1,\n                    \"y\": 7\n                },\n                {\n                    \"id\": \"25\",\n                    \"x\": 1,\n                    \"y\": 6\n                },\n                {\n                    \"id\": \"25\",\n                    \"x\": 1,\n                    \"y\": 5\n                },\n                {\n                    \"id\": \"25\",\n                    \"x\": 1,\n                    \"y\": 4\n                },\n                {\n                    \"id\": \"25\",\n                    \"x\": 1,\n                    \"y\": 3\n                },\n                {\n                    \"id\": \"26\",\n                    \"x\": 1,\n                    \"y\": 2\n                },\n                {\n                    \"id\": \"27\",\n                    \"x\": 1,\n                    \"y\": 1\n                },\n                {\n                    \"id\": \"28\",\n                    \"x\": 2,\n                    \"y\": 1\n                },\n                {\n                    \"id\": \"28\",\n                    \"x\": 3,\n                    \"y\": 1\n                },\n                {\n                    \"id\": \"29\",\n                    \"x\": 4,\n                    \"y\": 1\n                },\n                {\n                    \"id\": \"30\",\n                    \"x\": 0,\n                    \"y\": 1\n                },\n                {\n                    \"id\": \"17\",\n                    \"x\": 15,\n                    \"y\": 2\n                },\n                {\n                    \"id\": \"18\",\n                    \"x\": 16,\n                    \"y\": 2\n                },\n                {\n                    \"id\": \"18\",\n                    \"x\": 17,\n                    \"y\": 2\n                },\n                {\n                    \"id\": \"19\",\n                    \"x\": 18,\n                    \"y\": 2\n                },\n                {\n                    \"id\": \"12\",\n                    \"x\": 0,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 1,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"11\",\n                    \"x\": 2,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"14\",\n                    \"x\": 0,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"16\",\n                    \"x\": 0,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 1,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"10\",\n                    \"x\": 2,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"9\",\n                    \"x\": 2,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 1,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"12\",\n                    \"x\": 5,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"11\",\n                    \"x\": 12,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"16\",\n                    \"x\": 5,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"9\",\n                    \"x\": 12,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"14\",\n                    \"x\": 5,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"14\",\n                    \"x\": 5,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"10\",\n                    \"x\": 12,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"10\",\n                    \"x\": 12,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 6,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 7,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 8,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 9,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 10,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 11,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 6,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 7,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 8,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 9,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 10,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 11,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 6,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 7,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 8,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 9,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 10,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 11,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 11,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 10,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 9,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 8,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 7,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 6,\n                    \"y\": 10\n                }\n            ],\n            \"collider\": false\n        }\n    ]\n}"
  },
  {
    "path": "packages/flame_sprite_fusion/example/lib/main.dart",
    "content": "import 'dart:async';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_sprite_fusion/flame_sprite_fusion.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(const MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: GameWidget.controlled(\n        gameFactory: () => PlatformerGame(\n          camera: CameraComponent.withFixedResolution(width: 320, height: 180),\n        ),\n      ),\n    );\n  }\n}\n\nclass PlatformerGame extends FlameGame {\n  PlatformerGame({super.camera});\n\n  @override\n  Color backgroundColor() => Colors.white70;\n\n  @override\n  Future<void> onLoad() async {\n    final map = await SpriteFusionTilemapComponent.load(\n      mapJsonFile: 'map.json',\n      spriteSheetFile: 'spritesheet.png',\n    );\n    await world.add(map);\n\n    camera.moveTo(map.size * 0.5);\n  }\n}\n"
  },
  {
    "path": "packages/flame_sprite_fusion/example/pubspec.yaml",
    "content": "name: flame_sprite_fusion_example\nresolution: workspace\ndescription: \"An example for the SpriteFusionTilemapComponent.\"\n\npublish_to: \"none\"\n\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_sprite_fusion: ^0.2.3\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n\nflutter:\n  uses-material-design: true\n  assets:\n    - assets/images/\n    - assets/tiles/\n"
  },
  {
    "path": "packages/flame_sprite_fusion/lib/flame_sprite_fusion.dart",
    "content": "library flame_sprite_fusion;\n\nexport 'src/sprite_fusion_layer_data.dart';\nexport 'src/sprite_fusion_tile_data.dart';\nexport 'src/sprite_fusion_tilemap_component.dart';\nexport 'src/sprite_fusion_tilemap_data.dart';\n"
  },
  {
    "path": "packages/flame_sprite_fusion/lib/src/sprite_fusion_layer_data.dart",
    "content": "import 'package:flame_sprite_fusion/flame_sprite_fusion.dart';\n\n/// A class that holds the data of a layer from a sprite fusion map.\nclass SpriteFusionLayerData {\n  /// The name of the layer.\n  final String name;\n\n  /// The tiles of the layer.\n  final List<SpriteFusionTileData> tiles;\n\n  /// If the layer is a collider.\n  final bool collider;\n\n  /// Creates a new instance of [SpriteFusionLayerData].\n  SpriteFusionLayerData({\n    required this.name,\n    required this.tiles,\n    required this.collider,\n  });\n\n  /// Creates a new instance of [SpriteFusionLayerData] from a map.\n  factory SpriteFusionLayerData.fromMap(Map<String, dynamic> map) {\n    return SpriteFusionLayerData(\n      name: map['name'] as String,\n      tiles: (map['tiles'] as List<dynamic>)\n          .map(\n            (tile) =>\n                SpriteFusionTileData.fromMap(tile as Map<String, dynamic>),\n          )\n          .toList(growable: false),\n      collider: map['collider'] as bool,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_sprite_fusion/lib/src/sprite_fusion_tile_data.dart",
    "content": "/// A class that holds the data of a tile from a sprite fusion map.\nclass SpriteFusionTileData {\n  /// The id of the tile.\n  ///\n  /// This is also the index position of the tile in the tileset, starting at 0,\n  /// from left to right, top to bottom.\n  final int id;\n\n  /// The x position of the tile in tile units.\n  final int x;\n\n  /// The y position of the tile in tile units.\n  final int y;\n\n  /// The attributes of the tile.\n  final Map<String, dynamic>? attributes;\n\n  /// Creates a new instance of [SpriteFusionTileData].\n  SpriteFusionTileData({\n    required this.id,\n    required this.x,\n    required this.y,\n    this.attributes,\n  });\n\n  /// Creates a new instance of [SpriteFusionTileData] from a map.\n  factory SpriteFusionTileData.fromMap(Map<String, dynamic> map) {\n    return SpriteFusionTileData(\n      id: int.parse(map['id'].toString()),\n      x: int.parse(map['x'].toString()),\n      y: int.parse(map['y'].toString()),\n      attributes: map['attributes'] as Map<String, dynamic>?,\n    );\n  }\n\n  /// Checks if the tile has an attribute with the given [key].\n  bool hasAttribute(String key) {\n    return attributes != null && attributes!.containsKey(key);\n  }\n\n  /// Gets the attribute with the given [key].\n  /// The attribute is casted to the given type [T].\n  ///\n  /// Returns null if the attribute does not exist.\n  T? getAttribute<T>(String key) {\n    return hasAttribute(key) ? attributes![key] as T : null;\n  }\n}\n"
  },
  {
    "path": "packages/flame_sprite_fusion/lib/src/sprite_fusion_tilemap_component.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:flame/cache.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/sprite.dart';\nimport 'package:flame_sprite_fusion/flame_sprite_fusion.dart';\nimport 'package:flutter/widgets.dart';\n\n/// A component that renders a tilemap from a sprite fusion.\nclass SpriteFusionTilemapComponent extends PositionComponent {\n  /// The data of the tilemap.\n  final SpriteFusionTilemapData tilemapData;\n\n  /// The sprite sheet of the tilemap.\n  final SpriteSheet spriteSheet;\n\n  /// The sprite batch of the tilemap.\n  late final SpriteBatch _spriteBatch;\n\n  /// Creates a new [SpriteFusionTilemapComponent] with the given [tilemapData]\n  /// and [spriteSheet].\n  SpriteFusionTilemapComponent({\n    required this.tilemapData,\n    required this.spriteSheet,\n    bool useAtlas = true,\n    super.position,\n    super.scale,\n    super.angle,\n    super.nativeAngle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.key,\n  }) : super(\n         size: Vector2(\n           tilemapData.mapWidth * tilemapData.tileSize,\n           tilemapData.mapHeight * tilemapData.tileSize,\n         ),\n       ) {\n    _spriteBatch = SpriteBatch(spriteSheet.image, useAtlas: useAtlas);\n\n    for (final data in tilemapData.layers.reversed) {\n      for (final tileData in data.tiles) {\n        final sprite = spriteSheet.getSpriteById(tileData.id);\n\n        _spriteBatch.add(\n          source: Rect.fromLTWH(\n            sprite.srcPosition.x,\n            sprite.srcPosition.y,\n            sprite.srcSize.x,\n            sprite.srcSize.y,\n          ),\n          offset: Vector2(\n            tileData.x * tilemapData.tileSize,\n            tileData.y * tilemapData.tileSize,\n          ),\n        );\n      }\n    }\n  }\n\n  @override\n  void render(Canvas canvas) {\n    _spriteBatch.render(canvas);\n  }\n\n  /// Loads a [SpriteFusionTilemapComponent] from the given json file and\n  /// spritesheet file.\n  static Future<SpriteFusionTilemapComponent> load({\n    required String mapJsonFile,\n    required String spriteSheetFile,\n    bool useAtlas = true,\n    String tilemapPrefix = 'assets/tiles/',\n    AssetBundle? assetBundle,\n    Images? images,\n    Vector2? position,\n    Vector2? scale,\n    double? angle,\n    double nativeAngle = 0,\n    Anchor? anchor,\n    Iterable<Component>? children,\n    int? priority,\n    ComponentKey? key,\n    String? package,\n  }) async {\n    final prefix = package == null\n        ? tilemapPrefix\n        : 'packages/$package/$tilemapPrefix';\n    final content = await (assetBundle ?? Flame.bundle).loadString(\n      '$prefix$mapJsonFile',\n    );\n\n    final json = jsonDecode(content) as Map<String, dynamic>;\n\n    return SpriteFusionTilemapComponent(\n      tilemapData: SpriteFusionTilemapData.fromMap(json),\n      spriteSheet: SpriteSheet(\n        image: await (images ?? Flame.images).load(\n          spriteSheetFile,\n          package: package,\n        ),\n        srcSize: Vector2.all(double.parse(json['tileSize'].toString())),\n      ),\n      useAtlas: useAtlas,\n      position: position,\n      scale: scale,\n      angle: angle,\n      nativeAngle: nativeAngle,\n      anchor: anchor,\n      children: children,\n      priority: priority,\n      key: key,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_sprite_fusion/lib/src/sprite_fusion_tilemap_data.dart",
    "content": "import 'package:collection/collection.dart';\nimport 'package:flame_sprite_fusion/flame_sprite_fusion.dart';\n\n/// A class that holds the data of the tilemap from a sprite fusion.\nclass SpriteFusionTilemapData {\n  /// The size of the tiles in the tilemap.\n  final double tileSize;\n\n  /// The width of the tilemap in tile units.\n  final double mapWidth;\n\n  /// The height of the tilemap in tile units.\n  final double mapHeight;\n\n  /// The layers of the tilemap.\n  final List<SpriteFusionLayerData> layers;\n\n  /// Creates a new instance of [SpriteFusionTilemapData].\n  SpriteFusionTilemapData({\n    required this.tileSize,\n    required this.mapWidth,\n    required this.mapHeight,\n    required this.layers,\n  });\n\n  /// Creates a new instance of [SpriteFusionTilemapData] from a map.\n  factory SpriteFusionTilemapData.fromMap(Map<String, dynamic> map) {\n    return SpriteFusionTilemapData(\n      tileSize: double.parse(map['tileSize'].toString()),\n      mapWidth: double.parse(map['mapWidth'].toString()),\n      mapHeight: double.parse(map['mapHeight'].toString()),\n      layers: (map['layers'] as List<dynamic>)\n          .map(\n            (layer) =>\n                SpriteFusionLayerData.fromMap(layer as Map<String, dynamic>),\n          )\n          .toList(growable: false),\n    );\n  }\n\n  /// Gets a layer by its [name].\n  ///\n  /// Returns the first layer found with the given name.\n  /// For multiple layers with the same name, use [layers] property directly.\n  SpriteFusionLayerData? getLayerByName(String name) {\n    return layers.firstWhereOrNull((layer) => layer.name == name);\n  }\n}\n"
  },
  {
    "path": "packages/flame_sprite_fusion/pubspec.yaml",
    "content": "name: flame_sprite_fusion\nresolution: workspace\ndescription: \"Sprite Fusion support for the Flame game engine. This package parses and renders tilemaps exported from Sprite Fusion tool.\"\nversion: 0.2.3\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_sprite_fusion\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - tilemap\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  collection: ^1.19.1\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flame_test: ^2.2.3\n  flutter_test:\n    sdk: flutter\n"
  },
  {
    "path": "packages/flame_sprite_fusion/test/assets/map.json",
    "content": "{\n    \"tileSize\": 18,\n    \"mapWidth\": 20,\n    \"mapHeight\": 12,\n    \"layers\": [\n        {\n            \"name\": \"Spikes\",\n            \"tiles\": [\n                {\n                    \"id\": \"0\",\n                    \"x\": 0,\n                    \"y\": 0\n                },\n                {\n                    \"id\": \"0\",\n                    \"x\": 17,\n                    \"y\": 1\n                }\n            ],\n            \"collider\": false\n        },\n        {\n            \"name\": \"Water\",\n            \"tiles\": [\n                {\n                    \"id\": \"1\",\n                    \"x\": 13,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"2\",\n                    \"x\": 13,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 0,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 1,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 2,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 3,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 4,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 5,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 6,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 7,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 8,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 9,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 10,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 11,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 12,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 13,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 14,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 15,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 16,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 17,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 18,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"3\",\n                    \"x\": 19,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"4\",\n                    \"x\": 13,\n                    \"y\": 10\n                }\n            ],\n            \"collider\": false\n        },\n        {\n            \"name\": \"Grass\",\n            \"tiles\": [\n                {\n                    \"id\": \"5\",\n                    \"x\": 6,\n                    \"y\": 7\n                },\n                {\n                    \"id\": \"6\",\n                    \"x\": 10,\n                    \"y\": 7\n                },\n                {\n                    \"id\": \"7\",\n                    \"x\": 18,\n                    \"y\": 5\n                }\n            ],\n            \"collider\": false\n        },\n        {\n            \"name\": \"Platforms\",\n            \"tiles\": [\n                {\n                    \"id\": \"8\",\n                    \"x\": 16,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"9\",\n                    \"x\": 19,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"10\",\n                    \"x\": 19,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"10\",\n                    \"x\": 19,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"10\",\n                    \"x\": 19,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"10\",\n                    \"x\": 19,\n                    \"y\": 7\n                },\n                {\n                    \"id\": \"11\",\n                    \"x\": 19,\n                    \"y\": 6\n                },\n                {\n                    \"id\": \"12\",\n                    \"x\": 16,\n                    \"y\": 6\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 17,\n                    \"y\": 6\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 18,\n                    \"y\": 6\n                },\n                {\n                    \"id\": \"14\",\n                    \"x\": 16,\n                    \"y\": 7\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 15,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"12\",\n                    \"x\": 14,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 17,\n                    \"y\": 7\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 18,\n                    \"y\": 7\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 18,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 18,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 18,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 17,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 16,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 15,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"14\",\n                    \"x\": 14,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"14\",\n                    \"x\": 14,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 15,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 16,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 17,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 17,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 18,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 17,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 16,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 15,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"16\",\n                    \"x\": 14,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"17\",\n                    \"x\": 11,\n                    \"y\": 4\n                },\n                {\n                    \"id\": \"18\",\n                    \"x\": 12,\n                    \"y\": 4\n                },\n                {\n                    \"id\": \"19\",\n                    \"x\": 13,\n                    \"y\": 4\n                },\n                {\n                    \"id\": \"20\",\n                    \"x\": 6,\n                    \"y\": 3\n                },\n                {\n                    \"id\": \"21\",\n                    \"x\": 7,\n                    \"y\": 3\n                },\n                {\n                    \"id\": \"22\",\n                    \"x\": 8,\n                    \"y\": 3\n                },\n                {\n                    \"id\": \"23\",\n                    \"x\": 10,\n                    \"y\": 1\n                },\n                {\n                    \"id\": \"24\",\n                    \"x\": 1,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"25\",\n                    \"x\": 1,\n                    \"y\": 7\n                },\n                {\n                    \"id\": \"25\",\n                    \"x\": 1,\n                    \"y\": 6\n                },\n                {\n                    \"id\": \"25\",\n                    \"x\": 1,\n                    \"y\": 5\n                },\n                {\n                    \"id\": \"25\",\n                    \"x\": 1,\n                    \"y\": 4\n                },\n                {\n                    \"id\": \"25\",\n                    \"x\": 1,\n                    \"y\": 3\n                },\n                {\n                    \"id\": \"26\",\n                    \"x\": 1,\n                    \"y\": 2\n                },\n                {\n                    \"id\": \"27\",\n                    \"x\": 1,\n                    \"y\": 1\n                },\n                {\n                    \"id\": \"28\",\n                    \"x\": 2,\n                    \"y\": 1\n                },\n                {\n                    \"id\": \"28\",\n                    \"x\": 3,\n                    \"y\": 1\n                },\n                {\n                    \"id\": \"29\",\n                    \"x\": 4,\n                    \"y\": 1\n                },\n                {\n                    \"id\": \"30\",\n                    \"x\": 0,\n                    \"y\": 1\n                },\n                {\n                    \"id\": \"17\",\n                    \"x\": 15,\n                    \"y\": 2\n                },\n                {\n                    \"id\": \"18\",\n                    \"x\": 16,\n                    \"y\": 2\n                },\n                {\n                    \"id\": \"18\",\n                    \"x\": 17,\n                    \"y\": 2\n                },\n                {\n                    \"id\": \"19\",\n                    \"x\": 18,\n                    \"y\": 2\n                },\n                {\n                    \"id\": \"12\",\n                    \"x\": 0,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 1,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"11\",\n                    \"x\": 2,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"14\",\n                    \"x\": 0,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"16\",\n                    \"x\": 0,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 1,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"10\",\n                    \"x\": 2,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"9\",\n                    \"x\": 2,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 1,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"12\",\n                    \"x\": 5,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"11\",\n                    \"x\": 12,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"16\",\n                    \"x\": 5,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"9\",\n                    \"x\": 12,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"14\",\n                    \"x\": 5,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"14\",\n                    \"x\": 5,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"10\",\n                    \"x\": 12,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"10\",\n                    \"x\": 12,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 6,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 7,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 8,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 9,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 10,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"13\",\n                    \"x\": 11,\n                    \"y\": 8\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 6,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 7,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 8,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 9,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 10,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"15\",\n                    \"x\": 11,\n                    \"y\": 11\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 6,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 7,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 8,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 9,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 10,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 11,\n                    \"y\": 9\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 11,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 10,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 9,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 8,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 7,\n                    \"y\": 10\n                },\n                {\n                    \"id\": \"8\",\n                    \"x\": 6,\n                    \"y\": 10\n                }\n            ],\n            \"collider\": false\n        }\n    ]\n}"
  },
  {
    "path": "packages/flame_sprite_fusion/test/sprite_fusion_data_test.dart",
    "content": "import 'package:flame_sprite_fusion/flame_sprite_fusion.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('SpriteFusionTileData', () {\n    test('creation test', () {\n      final tileData = SpriteFusionTileData(id: 50, x: 65, y: 89);\n\n      expect(tileData.id, 50);\n      expect(tileData.x, 65);\n      expect(tileData.y, 89);\n    });\n\n    test('creation test with map', () {\n      final map = <String, dynamic>{\n        'id': 50,\n        'x': 65,\n        'y': 89,\n      };\n      final tileData = SpriteFusionTileData.fromMap(map);\n\n      expect(tileData.id, map['id']);\n      expect(tileData.x, map['x']);\n      expect(tileData.y, map['y']);\n    });\n\n    test('hasAttribute returns false when attributes is null', () {\n      final tileData = SpriteFusionTileData(id: 1, x: 0, y: 0);\n\n      expect(tileData.hasAttribute('anyKey'), false);\n    });\n\n    test('hasAttribute returns false when attribute does not exist', () {\n      final tileData = SpriteFusionTileData(\n        id: 1,\n        x: 0,\n        y: 0,\n        attributes: {'existingKey': 'value'},\n      );\n\n      expect(tileData.hasAttribute('nonExistentKey'), false);\n    });\n\n    test('hasAttribute returns true when attribute exists', () {\n      final tileData = SpriteFusionTileData(\n        id: 1,\n        x: 0,\n        y: 0,\n        attributes: {'type': 'grass', 'walkable': true},\n      );\n\n      expect(tileData.hasAttribute('type'), true);\n      expect(tileData.hasAttribute('walkable'), true);\n    });\n\n    test('getAttribute returns null when attributes is null', () {\n      final tileData = SpriteFusionTileData(id: 1, x: 0, y: 0);\n\n      expect(tileData.getAttribute<String>('anyKey'), null);\n    });\n\n    test('getAttribute returns null when attribute does not exist', () {\n      final tileData = SpriteFusionTileData(\n        id: 1,\n        x: 0,\n        y: 0,\n        attributes: {'existingKey': 'value'},\n      );\n\n      expect(tileData.getAttribute<String>('nonExistentKey'), null);\n    });\n\n    test('getAttribute returns correct value for String type', () {\n      final tileData = SpriteFusionTileData(\n        id: 1,\n        x: 0,\n        y: 0,\n        attributes: {'type': 'grass'},\n      );\n\n      expect(tileData.getAttribute<String>('type'), 'grass');\n    });\n\n    test('getAttribute returns correct value for bool type', () {\n      final tileData = SpriteFusionTileData(\n        id: 1,\n        x: 0,\n        y: 0,\n        attributes: {'walkable': true, 'blocking': false},\n      );\n\n      expect(tileData.getAttribute<bool>('walkable'), true);\n      expect(tileData.getAttribute<bool>('blocking'), false);\n    });\n\n    test('getAttribute returns correct value for int type', () {\n      final tileData = SpriteFusionTileData(\n        id: 1,\n        x: 0,\n        y: 0,\n        attributes: {'damage': 10, 'health': 100},\n      );\n\n      expect(tileData.getAttribute<int>('damage'), 10);\n      expect(tileData.getAttribute<int>('health'), 100);\n    });\n\n    test('getAttribute returns correct value for double type', () {\n      final tileData = SpriteFusionTileData(\n        id: 1,\n        x: 0,\n        y: 0,\n        attributes: {'speed': 1.5, 'multiplier': 2.75},\n      );\n\n      expect(tileData.getAttribute<double>('speed'), 1.5);\n      expect(tileData.getAttribute<double>('multiplier'), 2.75);\n    });\n\n    test('getAttribute with multiple attributes', () {\n      final tileData = SpriteFusionTileData(\n        id: 1,\n        x: 0,\n        y: 0,\n        attributes: {\n          'type': 'grass',\n          'walkable': true,\n          'damage': 0,\n          'speed': 1.0,\n        },\n      );\n\n      expect(tileData.getAttribute<String>('type'), 'grass');\n      expect(tileData.getAttribute<bool>('walkable'), true);\n      expect(tileData.getAttribute<int>('damage'), 0);\n      expect(tileData.getAttribute<double>('speed'), 1.0);\n    });\n  });\n\n  group('SpriteFusionLayerData', () {\n    test('creation test', () {\n      final tileData = SpriteFusionLayerData(\n        name: 'layer1',\n        tiles: [\n          SpriteFusionTileData(id: 50, x: 65, y: 89),\n          SpriteFusionTileData(id: 51, x: 66, y: 90),\n        ],\n        collider: true,\n      );\n\n      expect(tileData.name, 'layer1');\n      expect(tileData.tiles.length, 2);\n      expect(tileData.collider, true);\n    });\n\n    test('creation test with map', () {\n      final map = <String, dynamic>{\n        'name': 'layer4',\n        'tiles': [\n          {'id': 50, 'x': 65, 'y': 89},\n          {'id': 51, 'x': 66, 'y': 90},\n          {'id': 40, 'x': 70, 'y': 95},\n        ],\n        'collider': false,\n      };\n      final tileData = SpriteFusionLayerData.fromMap(map);\n\n      expect(tileData.name, map['name']);\n      expect(tileData.tiles.length, 3);\n      expect(tileData.collider, map['collider']);\n    });\n  });\n\n  group('SpriteFusionTilemapData', () {\n    test('creation test', () {\n      final tileData = SpriteFusionTilemapData(\n        mapWidth: 10,\n        mapHeight: 20,\n        tileSize: 32,\n        layers: [\n          SpriteFusionLayerData(\n            name: 'layer1',\n            tiles: [\n              SpriteFusionTileData(id: 50, x: 65, y: 89),\n              SpriteFusionTileData(id: 51, x: 66, y: 90),\n            ],\n            collider: true,\n          ),\n          SpriteFusionLayerData(\n            name: 'layer2',\n            tiles: [\n              SpriteFusionTileData(id: 50, x: 65, y: 89),\n              SpriteFusionTileData(id: 51, x: 66, y: 90),\n            ],\n            collider: false,\n          ),\n        ],\n      );\n\n      expect(tileData.mapWidth, 10);\n      expect(tileData.mapHeight, 20);\n      expect(tileData.tileSize, 32);\n      expect(tileData.layers.length, 2);\n    });\n\n    test('creation test with map', () {\n      final map = <String, dynamic>{\n        'mapWidth': 10,\n        'mapHeight': 20,\n        'tileSize': 32,\n        'layers': [\n          {\n            'name': 'layer1',\n            'tiles': [\n              {'id': 50, 'x': 65, 'y': 89},\n              {'id': 51, 'x': 66, 'y': 90},\n            ],\n            'collider': true,\n          },\n          {\n            'name': 'layer2',\n            'tiles': [\n              {'id': 50, 'x': 65, 'y': 89},\n              {'id': 51, 'x': 66, 'y': 90},\n            ],\n            'collider': false,\n          },\n        ],\n      };\n      final tileData = SpriteFusionTilemapData.fromMap(map);\n\n      expect(tileData.mapWidth, map['mapWidth']);\n      expect(tileData.mapHeight, map['mapHeight']);\n      expect(tileData.tileSize, map['tileSize']);\n      expect(tileData.layers.length, 2);\n    });\n\n    test('getLayerByName returns the correct layer', () {\n      final tilemapData = SpriteFusionTilemapData(\n        mapWidth: 10,\n        mapHeight: 10,\n        tileSize: 32,\n        layers: [\n          SpriteFusionLayerData(\n            name: 'background',\n            tiles: [],\n            collider: false,\n          ),\n          SpriteFusionLayerData(\n            name: 'foreground',\n            tiles: [],\n            collider: true,\n          ),\n        ],\n      );\n\n      final layer = tilemapData.getLayerByName('foreground');\n\n      expect(layer, isNotNull);\n      expect(layer?.name, 'foreground');\n      expect(layer?.collider, true);\n    });\n\n    test('getLayerByName returns null for non-existent layer', () {\n      final tilemapData = SpriteFusionTilemapData(\n        mapWidth: 10,\n        mapHeight: 10,\n        tileSize: 32,\n        layers: [\n          SpriteFusionLayerData(\n            name: 'background',\n            tiles: [],\n            collider: false,\n          ),\n        ],\n      );\n\n      final layer = tilemapData.getLayerByName('nonExistent');\n\n      expect(layer, isNull);\n    });\n\n    test('getLayerByName returns first layer when multiple have same name', () {\n      final tilemapData = SpriteFusionTilemapData(\n        mapWidth: 10,\n        mapHeight: 10,\n        tileSize: 32,\n        layers: [\n          SpriteFusionLayerData(\n            name: 'duplicate',\n            tiles: [SpriteFusionTileData(id: 1, x: 0, y: 0)],\n            collider: false,\n          ),\n          SpriteFusionLayerData(\n            name: 'duplicate',\n            tiles: [SpriteFusionTileData(id: 2, x: 1, y: 1)],\n            collider: true,\n          ),\n        ],\n      );\n\n      final layer = tilemapData.getLayerByName('duplicate');\n\n      expect(layer, isNotNull);\n      expect(layer?.name, 'duplicate');\n      expect(layer?.collider, false);\n      expect(layer?.tiles.first.id, 1);\n    });\n\n    test('getLayerByName returns null for empty layers list', () {\n      final tilemapData = SpriteFusionTilemapData(\n        mapWidth: 10,\n        mapHeight: 10,\n        tileSize: 32,\n        layers: [],\n      );\n\n      final layer = tilemapData.getLayerByName('anyLayer');\n\n      expect(layer, isNull);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_sprite_fusion/test/sprite_fusion_tilemap_component_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/cache.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/sprite.dart';\nimport 'package:flame_sprite_fusion/flame_sprite_fusion.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'test_asset_bundle.dart';\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('SpriteFusionTilemapComponent', () {\n    late AssetBundle bundle;\n    late Images images;\n\n    setUp(() {\n      bundle = TestAssetBundle(\n        imageNames: ['spritesheet.png'],\n        stringNames: ['map.json'],\n      );\n      images = Images(bundle: bundle);\n    });\n\n    test('creation test', () async {\n      final tilemapData = SpriteFusionTilemapData(\n        mapWidth: 10,\n        mapHeight: 10,\n        tileSize: 16,\n        layers: [],\n      );\n\n      final spriteSheet = SpriteSheet(\n        image: await images.load('spritesheet.png'),\n        srcSize: Vector2.all(tilemapData.tileSize),\n      );\n\n      expect(\n        () => SpriteFusionTilemapComponent(\n          tilemapData: tilemapData,\n          spriteSheet: spriteSheet,\n        ),\n        returnsNormally,\n      );\n    });\n\n    test('loads map from file', () {\n      expect(\n        SpriteFusionTilemapComponent.load(\n          mapJsonFile: 'map.json',\n          spriteSheetFile: 'spritesheet.png',\n          assetBundle: bundle,\n          images: images,\n          tilemapPrefix: '',\n        ),\n        completes,\n      );\n    });\n\n    test('component size matches the map size', () async {\n      final tilemapData = SpriteFusionTilemapData(\n        mapWidth: 20,\n        mapHeight: 40,\n        tileSize: 16,\n        layers: [],\n      );\n\n      final spriteSheet = SpriteSheet(\n        image: await images.load('spritesheet.png'),\n        srcSize: Vector2.all(tilemapData.tileSize),\n      );\n\n      final component = SpriteFusionTilemapComponent(\n        tilemapData: tilemapData,\n        spriteSheet: spriteSheet,\n      );\n\n      expect(\n        component.size,\n        Vector2(\n          tilemapData.mapWidth * tilemapData.tileSize,\n          tilemapData.mapHeight * tilemapData.tileSize,\n        ),\n      );\n    });\n\n    testGolden(\n      'renders the map correctly',\n      (game, tester) async {\n        final map = await SpriteFusionTilemapComponent.load(\n          mapJsonFile: 'map.json',\n          spriteSheetFile: 'spritesheet.png',\n          assetBundle: bundle,\n          images: images,\n          tilemapPrefix: '',\n        );\n        await game.add(map);\n        await game.ready();\n      },\n      size: Vector2(360, 216),\n      goldenFile: 'goldens/sprite_fusion_render_test.png',\n    );\n\n    testGolden(\n      'position is respected when rendering',\n      (game, tester) async {\n        final map = await SpriteFusionTilemapComponent.load(\n          mapJsonFile: 'map.json',\n          spriteSheetFile: 'spritesheet.png',\n          assetBundle: bundle,\n          images: images,\n          tilemapPrefix: '',\n          position: Vector2(100, 100),\n        );\n        await game.add(map);\n        await game.ready();\n      },\n      size: Vector2(360, 216),\n      goldenFile: 'goldens/sprite_fusion_position_test.png',\n    );\n\n    testGolden(\n      'anchor is respected when rendering',\n      (game, tester) async {\n        final map = await SpriteFusionTilemapComponent.load(\n          mapJsonFile: 'map.json',\n          spriteSheetFile: 'spritesheet.png',\n          assetBundle: bundle,\n          images: images,\n          tilemapPrefix: '',\n          anchor: Anchor.center,\n        );\n        await game.add(map);\n        await game.ready();\n      },\n      size: Vector2(360, 216),\n      goldenFile: 'goldens/sprite_fusion_anchor_test.png',\n    );\n\n    testGolden(\n      'scale is respected when rendering',\n      (game, tester) async {\n        final map = await SpriteFusionTilemapComponent.load(\n          mapJsonFile: 'map.json',\n          spriteSheetFile: 'spritesheet.png',\n          assetBundle: bundle,\n          images: images,\n          tilemapPrefix: '',\n          scale: Vector2.all(0.5),\n        );\n        await game.add(map);\n        await game.ready();\n      },\n      size: Vector2(360, 216),\n      goldenFile: 'goldens/sprite_fusion_scale_test.png',\n    );\n\n    testGolden(\n      'angle is respected when rendering',\n      (game, tester) async {\n        final map = await SpriteFusionTilemapComponent.load(\n          mapJsonFile: 'map.json',\n          spriteSheetFile: 'spritesheet.png',\n          assetBundle: bundle,\n          images: images,\n          tilemapPrefix: '',\n          angle: pi * 0.125,\n        );\n        await game.add(map);\n        await game.ready();\n      },\n      size: Vector2(360, 216),\n      goldenFile: 'goldens/sprite_fusion_angle_test.png',\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_sprite_fusion/test/test_asset_bundle.dart",
    "content": "import 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:flutter/services.dart' show CachingAssetBundle;\n\nclass TestAssetBundle extends CachingAssetBundle {\n  TestAssetBundle({\n    required this.imageNames,\n    required this.stringNames,\n  });\n\n  final List<String> imageNames;\n  final List<String> stringNames;\n\n  @override\n  Future<ByteData> load(String key) async {\n    late String imgName;\n    late String fileName;\n    if (key.contains('..')) {\n      final parts = key.split('/');\n\n      final index = parts.indexOf('..');\n\n      imgName = parts.sublist(index + 1).join('/');\n\n      fileName = key.replaceFirst('assets/images/', 'test/assets/');\n    } else {\n      final pattern = RegExp(r'assets/images/(\\.\\./)*');\n      final split = key.split('/');\n      imgName = split.isNotEmpty ? key.replaceFirst(pattern, '') : key;\n\n      final toLoadName = key.replaceFirst(pattern, '');\n      fileName = 'test/assets/$toLoadName';\n    }\n\n    if (!imageNames.contains(imgName)) {\n      throw StateError(\n        'No $fileName found in the TestAssetBundle. Did you forget to add it?',\n      );\n    }\n    return File(fileName).readAsBytes().then(\n      (bytes) => ByteData.view(Uint8List.fromList(bytes).buffer),\n    );\n  }\n\n  @override\n  Future<String> loadString(String key, {bool cache = true}) {\n    final pattern = RegExp(r'assets/tiles/(\\.\\./)*');\n    final split = key.split('/');\n    final mapName = split.isNotEmpty ? key.replaceFirst(pattern, '') : key;\n\n    final toLoadName = key.replaceFirst(pattern, '');\n    final fileName = 'test/assets/$toLoadName';\n\n    if (!stringNames.contains(mapName)) {\n      throw StateError(\n        'No $fileName found in the TestAssetBundle. Did you forget to add it?',\n      );\n    }\n\n    return File(fileName).readAsString();\n  }\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/CHANGELOG.md",
    "content": "## 0.2.1+4\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 0.2.1+3\n\n - Update a dependency to the latest release.\n\n## 0.2.1+2\n\n - Update a dependency to the latest release.\n\n## 0.2.1+1\n\n - Update a dependency to the latest release.\n\n## 0.2.1\n\n - **FEAT**: Add flame_steering_behaviors package ([#3748](https://github.com/flame-engine/flame/issues/3748)). ([2d4f0d43](https://github.com/flame-engine/flame/commit/2d4f0d43ef472b5a473cde3fc97579b8a1c0a9fc))\n\n# 0.2.0\n\n- chore: tighten dependencies ([#64](https://github.com/VeryGoodOpenSource/flame_behaviors/pull/64, [#67](https://github.com/VeryGoodOpenSource/flame_behaviors/pull/67, [#71](https://github.com/VeryGoodOpenSource/flame_behaviors/pull/71)\n\n# 0.1.0 (2023-12-21)\n\n- initial release\n\n# 0.0.1-dev.0\n"
  },
  {
    "path": "packages/flame_steering_behaviors/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_steering_behaviors/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nAn implementation of steering behaviors for Flame Behaviors.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_behaviors\" ><img src=\"https://img.shields.io/pub/v/flame_behaviors.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n# steering_behaviors\n\n\nAn implementation of steering behaviors for Flame Behaviors.\nSee [Steering Behaviors For Autonomous Characters](https://www.red3d.com/cwr/steer/) by\n[Craig Reynolds](https://www.red3d.com/cwr/) for an in-depth explanation\n\nDeveloped with 💙 and 🔥 by [Very Good Ventures][very_good_ventures_link] 🦄\n\n\n---\n\n\n## Installation 💻\n\n```sh\nflutter pub add flame_steering_behaviors\n```\n\n\n## Usage ✨\n\nThis package is built on top of the\n[`flame_behaviors`](https://pub.dev/packages/flame_behaviors), if you are not\nyet familiar with it, we recommend reading up on the documentation of that\npackage first.\n\n\n### Steerable\n\nIf you want to apply steering behaviors to your entities you have to add the\n`Steerable` mixin to your entity class:\n\n```dart\nclass MyEntity extends Entity with Steerable {\n  /// Provide the max velocity this entity can hold.\n  double get maxVelocity => 100;\n\n  ...\n}\n```\n\nThe `Steerable` mixin provides a `velocity` value to your entity, this\nvelocity will then be applied on each update cycle to your entity until the\nvelocity becomes zero.\n\n\n### Steering Behaviors\n\nEach algorithm defined by this project is available as a `Behavior` and you\ncan add them to your [steerable](#steerable) entities as you would with any\nbehavior:\n\n```dart\nclass MyEntity extends Entity with Steerable {\n  MyEntity()\n    : super(\n        behaviors: [\n          WanderBehavior(\n            circleDistance: 200,\n            maximumAngle: 45 * degrees2Radians,\n            startingAngle: 0,\n          ),\n        ],\n      );\n      ...\n}\n```\n\nSome steering behaviors require information that is not always available on\nentity creation, when that happens we recommend using the entity's `onLoad`\nmethod:\n\n```dart\nclass MyEntity extends Entity with Steerable {\n  ...\n\n  @override\n  Future<void> onLoad() async {\n    world.children.register<MyOtherEntity>();\n    await add(\n      SeparationBehavior(\n        world.children.query<MyOtherEntity>(),\n        maxDistance: 25,\n        maxAcceleration: 1000,\n      ),\n    );\n  }\n\n  ...\n}\n```\n\n\n[very_good_ventures_link]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=CLI\n\n"
  },
  {
    "path": "packages/flame_steering_behaviors/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml"
  },
  {
    "path": "packages/flame_steering_behaviors/example/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_steering_behaviors/example/README.md",
    "content": "# Example\n\nAn example of using the `flame_steering_behaviors` package.\n\n"
  },
  {
    "path": "packages/flame_steering_behaviors/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml"
  },
  {
    "path": "packages/flame_steering_behaviors/example/lib/main.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame_steering_behaviors_example/src/example_game.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(GameWidget(game: ExampleGame()));\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/example/lib/src/behaviors/behaviors.dart",
    "content": "export 'screen_collision_behavior.dart';\n"
  },
  {
    "path": "packages/flame_steering_behaviors/example/lib/src/behaviors/screen_collision_behavior.dart",
    "content": "import 'package:flame/collisions.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\n\n/// Simplified \"screen wrapping\" behavior, while not perfect it does showcase\n/// the possibility of acting on collision with non-entities.\nclass ScreenCollisionBehavior\n    extends CollisionBehavior<ScreenHitbox, PositionedEntity> {\n  @override\n  void onCollisionEnd(ScreenHitbox other) {\n    if (parent.position.x < other.position.x) {\n      parent.position.x = other.position.x + other.scaledSize.x;\n    } else if (parent.position.x > other.position.x + other.scaledSize.x) {\n      parent.position.x = other.position.x;\n    }\n\n    if (parent.position.y < other.position.y) {\n      parent.position.y = other.position.y + other.scaledSize.y;\n    } else if (parent.position.y > other.position.y + other.scaledSize.y) {\n      parent.position.y = other.position.y;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/example/lib/src/entities/dot/dot.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/collisions.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\nimport 'package:flame_steering_behaviors_example/src/behaviors/behaviors.dart';\nimport 'package:flame_steering_behaviors_example/src/example_game.dart';\n\nclass Dot extends PositionedEntity with Steerable {\n  Dot({\n    super.position,\n    Random? random,\n  }) : super(\n         size: Vector2.all(relativeValue),\n         children: [\n           CircleComponent.relative(\n             1,\n             parentSize: Vector2.all(relativeValue),\n           ),\n         ],\n         behaviors: [\n           PropagatingCollisionBehavior(CircleHitbox()),\n           ScreenCollisionBehavior(),\n           WanderBehavior(\n             circleDistance: 3 * relativeValue,\n             maximumAngle: 45 * degrees2Radians,\n             startingAngle: 0,\n             random: random,\n           ),\n         ],\n       );\n\n  @override\n  double get maxVelocity => 10 * relativeValue;\n\n  @override\n  Future<void> onLoad() async {\n    parent!.children.register<Dot>();\n    await add(\n      SeparationBehavior(\n        parent!.children.query<Dot>(),\n        maxDistance: 1 * relativeValue,\n        maxAcceleration: 10 * relativeValue,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/example/lib/src/entities/entities.dart",
    "content": "export 'dot/dot.dart';\n"
  },
  {
    "path": "packages/flame_steering_behaviors/example/lib/src/example_game.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_steering_behaviors_example/src/entities/entities.dart';\n\nconst relativeValue = 16.0;\n\nclass ExampleGame extends FlameGame with HasCollisionDetection {\n  ExampleGame()\n    : super(\n        children: [\n          FpsTextComponent(position: Vector2.zero()),\n          ScreenHitbox(),\n        ],\n      );\n\n  @override\n  Future<void> onLoad() async {\n    await addAll([\n      for (var i = 0; i < 100; i++)\n        Dot(position: Vector2.random()..multiply(size)),\n    ]);\n  }\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/example/pubspec.yaml",
    "content": "name: flame_steering_behaviors_example\nresolution: workspace\ndescription: An example of using the flame_steering_behaviors package.\nversion: 1.0.0+1\npublish_to: none\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_behaviors: ^1.3.4\n  flame_steering_behaviors: ^0.2.1+4\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flame_test: ^2.2.3\n  flutter_test:\n    sdk: flutter\n  mocktail: ^1.0.4\n"
  },
  {
    "path": "packages/flame_steering_behaviors/example/test/helpers/helpers.dart",
    "content": "export 'test_game.dart';\n"
  },
  {
    "path": "packages/flame_steering_behaviors/example/test/helpers/test_game.dart",
    "content": "import 'package:flame/game.dart';\n\nclass TestGame extends FlameGame with HasCollisionDetection {}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/example/test/src/behaviors/screen_collision_behavior_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_steering_behaviors_example/src/behaviors/behaviors.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nimport '../../helpers/helpers.dart';\n\nclass _MockScreenHitbox extends Mock implements ScreenHitbox {}\n\nclass _TestEntity extends PositionedEntity {\n  _TestEntity({super.position}) : super(size: Vector2.all(50));\n}\n\nvoid main() {\n  final flameTester = FlameTester(TestGame.new);\n\n  group('ScreenCollisionBehavior', () {\n    late ScreenHitbox screenHitbox;\n\n    setUp(() {\n      screenHitbox = _MockScreenHitbox();\n      when(() => screenHitbox.position).thenReturn(NotifyingVector2.zero());\n      when(() => screenHitbox.scaledSize).thenReturn(Vector2.all(200));\n    });\n\n    flameTester.testGameWidget(\n      'does not move the parent entity',\n      setUp: (game, tester) async {\n        final screenCollisionBehavior = ScreenCollisionBehavior();\n        final entity = _TestEntity();\n\n        await entity.add(screenCollisionBehavior);\n        await game.ensureAdd(entity);\n\n        screenCollisionBehavior.onCollisionEnd(screenHitbox);\n        expect(entity.position, closeToVector(Vector2(0, 0)));\n      },\n    );\n\n    flameTester.testGameWidget(\n      'moves parent entity from top to bottom',\n      setUp: (game, tester) async {\n        final screenCollisionBehavior = ScreenCollisionBehavior();\n        final entity = _TestEntity(position: Vector2(-25, 0));\n\n        await entity.add(screenCollisionBehavior);\n        await game.ensureAdd(entity);\n\n        screenCollisionBehavior.onCollisionEnd(screenHitbox);\n        expect(entity.position, closeToVector(Vector2(200, 0)));\n      },\n    );\n\n    flameTester.testGameWidget(\n      'moves parent entity from bottom to top',\n      setUp: (game, tester) async {\n        final screenCollisionBehavior = ScreenCollisionBehavior();\n        final entity = _TestEntity(position: Vector2(225, 0));\n\n        await entity.add(screenCollisionBehavior);\n        await game.ensureAdd(entity);\n\n        screenCollisionBehavior.onCollisionEnd(screenHitbox);\n        expect(entity.position, closeToVector(Vector2(0, 0)));\n      },\n    );\n\n    flameTester.testGameWidget(\n      'moves parent entity from left to right',\n      setUp: (game, tester) async {\n        final screenCollisionBehavior = ScreenCollisionBehavior();\n        final entity = _TestEntity(position: Vector2(0, -25));\n\n        await entity.add(screenCollisionBehavior);\n        await game.ensureAdd(entity);\n\n        screenCollisionBehavior.onCollisionEnd(screenHitbox);\n        expect(entity.position, closeToVector(Vector2(0, 200)));\n      },\n    );\n\n    flameTester.testGameWidget(\n      'moves parent entity from right to left',\n      setUp: (game, tester) async {\n        final screenCollisionBehavior = ScreenCollisionBehavior();\n        final entity = _TestEntity(position: Vector2(0, 225));\n\n        await entity.add(screenCollisionBehavior);\n        await game.ensureAdd(entity);\n\n        screenCollisionBehavior.onCollisionEnd(screenHitbox);\n        expect(entity.position, closeToVector(Vector2(0, 0)));\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/example/test/src/entities/dot_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/components.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\nimport 'package:flame_steering_behaviors_example/src/entities/entities.dart';\nimport 'package:flame_steering_behaviors_example/src/example_game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nimport '../../helpers/helpers.dart';\n\nclass _MockRandom extends Mock implements Random {}\n\nvoid main() {\n  final flameTester = FlameTester(TestGame.new);\n\n  group('Dot', () {\n    late Random random;\n\n    setUp(() {\n      random = _MockRandom();\n      when(() => random.nextDouble()).thenReturn(0);\n    });\n\n    flameTester.testGameWidget(\n      'loads correctly',\n      setUp: (game, tester) async {\n        final dot = Dot(position: Vector2.zero(), random: random);\n        await game.ensureAdd(dot);\n\n        expect(dot.hasBehavior<WanderBehavior>(), isTrue);\n        expect(dot.hasBehavior<SeparationBehavior>(), isTrue);\n      },\n    );\n\n    test('has correct max velocity', () {\n      final dot = Dot(position: Vector2.zero(), random: random);\n      expect(dot.maxVelocity, equals(10 * relativeValue));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/example/test/src/example_game_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_steering_behaviors_example/src/entities/entities.dart';\nimport 'package:flame_steering_behaviors_example/src/example_game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  final flameTester = FlameTester(ExampleGame.new);\n\n  group('ExampleGame', () {\n    test('can be instantiated', () {\n      expect(ExampleGame(), isA<ExampleGame>());\n    });\n\n    flameTester.testGameWidget(\n      'generates the correct components',\n      verify: (game, tester) async {\n        await tester.pump();\n        expect(game.firstChild<FpsTextComponent>(), isNotNull);\n        expect(game.firstChild<ScreenHitbox>(), isNotNull);\n        expect(game.children.whereType<Dot>().length, equals(100));\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/lib/flame_steering_behaviors.dart",
    "content": "/// Flame Steering Behaviors brings steering algorithms to the behavior pattern,\n/// originally built by Very Good Ventures.\nlibrary;\n\nexport 'src/behaviors/behaviors.dart';\nexport 'src/mixins/mixins.dart';\nexport 'src/steering/steering.dart';\n"
  },
  {
    "path": "packages/flame_steering_behaviors/lib/src/behaviors/behaviors.dart",
    "content": "export 'flee_behavior.dart';\nexport 'pursue_behavior.dart';\nexport 'separation_behavior.dart';\nexport 'steering_behavior.dart';\nexport 'wander_behavior.dart';\n"
  },
  {
    "path": "packages/flame_steering_behaviors/lib/src/behaviors/flee_behavior.dart",
    "content": "import 'package:flame/effects.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\n\n/// {@template flee_behavior}\n/// Flee steering behavior.\n/// {@endtemplate}\nclass FleeBehavior<Parent extends Steerable> extends SteeringBehavior<Parent> {\n  /// {@macro flee_behavior}\n  FleeBehavior(\n    this.target, {\n    required this.maxAcceleration,\n    required this.panicDistance,\n  });\n\n  /// The target to flee from.\n  final ReadOnlyPositionProvider target;\n\n  /// The maximum acceleration of the entity.\n  final double maxAcceleration;\n\n  /// The maximum distance between the target and entity for the entity to\n  /// panic.\n  final double panicDistance;\n\n  @override\n  void update(double dt) {\n    final distanceToTarget = target.position.distanceTo(parent.position);\n\n    if (distanceToTarget < panicDistance) {\n      steer(Flee(target, maxAcceleration: maxAcceleration), dt);\n    }\n  }\n\n  @override\n  void renderDebugMode(Canvas canvas) {\n    canvas.drawCircle((parent.size / 2).toOffset(), panicDistance, debugPaint);\n    super.renderDebugMode(canvas);\n  }\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/lib/src/behaviors/pursue_behavior.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\n\n/// {@template pursue_behavior}\n/// Pursue steering behavior.\n/// {@endtemplate}\nclass PursueBehavior<Parent extends Steerable>\n    extends SteeringBehavior<Parent> {\n  /// {@macro pursue_behavior}\n  PursueBehavior(\n    this.target, {\n    required this.pursueRange,\n    this.maxPrediction = 1,\n  });\n\n  /// The target to pursue.\n  final PositionComponent target;\n\n  /// The maximum prediction time.\n  final double maxPrediction;\n\n  /// The range in which the goblin will pursue the player.\n  final double pursueRange;\n\n  @override\n  void update(double dt) {\n    final distanceToTarget = target.distance(parent);\n\n    if (distanceToTarget < pursueRange) {\n      steer(Pursue(target, maxPrediction: maxPrediction), dt);\n    }\n  }\n\n  @override\n  void renderDebugMode(Canvas canvas) {\n    canvas.drawCircle(\n      (parent.size / 2).toOffset(),\n      pursueRange,\n      debugPaint,\n    );\n    super.renderDebugMode(canvas);\n  }\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/lib/src/behaviors/separation_behavior.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\n\n/// {@template separation_behavior}\n/// Separation steering behavior.\n/// {@endtemplate}\nclass SeparationBehavior<Parent extends Steerable>\n    extends SteeringBehavior<Parent> {\n  /// {@macro separation_behavior}\n  SeparationBehavior(\n    this.entities, {\n    required this.maxDistance,\n    required this.maxAcceleration,\n  });\n\n  /// The maximum distance at which the entity will separate.\n  final double maxDistance;\n\n  /// The maximum acceleration the entity can apply to enable separation.\n  final double maxAcceleration;\n\n  /// The entities to separate from.\n  final Iterable<PositionComponent> entities;\n\n  @override\n  void update(double dt) {\n    steer(\n      Separation(\n        entities,\n        maxDistance: maxDistance,\n        maxAcceleration: maxAcceleration,\n      ),\n      dt,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/lib/src/behaviors/steering_behavior.dart",
    "content": "import 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\n\n/// {@template steering_behavior}\n/// Abstract base class for steering behaviors.\n/// {@endtemplate}\nabstract class SteeringBehavior<Parent extends Steerable>\n    extends Behavior<Parent>\n    with Steering {}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/lib/src/behaviors/wander_behavior.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\n\n/// {@template wander_behavior}\n/// Wander steering behavior.\n/// {@endtemplate}\nclass WanderBehavior<Parent extends Steerable>\n    extends SteeringBehavior<Parent> {\n  /// {@macro wander_behavior}\n  WanderBehavior({\n    required this.circleDistance,\n    required this.maximumAngle,\n    required double startingAngle,\n    Random? random,\n  }) : _angle = startingAngle,\n       random = random ?? Random();\n\n  /// The distance to the circle center of the next target.\n  final double circleDistance;\n\n  /// The rate at which the wander angle can change in radians.\n  final double maximumAngle;\n\n  /// The current wander angle in radians.\n  double get angle => _angle;\n  double _angle;\n\n  /// The random number generator used to calculate the next wander [angle].\n  final Random random;\n\n  @override\n  void update(double dt) {\n    steer(\n      Wander(\n        circleDistance: circleDistance,\n        maximumAngle: maximumAngle,\n        angle: _angle,\n        onNewAngle: (angle) => _angle = angle,\n        random: random,\n      ),\n      dt,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/lib/src/mixins/mixins.dart",
    "content": "export 'steerable.dart';\nexport 'steering.dart';\n"
  },
  {
    "path": "packages/flame_steering_behaviors/lib/src/mixins/steerable.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flutter/material.dart';\n\n/// {@template steerable}\n/// Mixin that makes an [Entity] steerable.\n/// {@endtemplate}\nmixin Steerable on EntityMixin, PositionComponent {\n  /// The max velocity of the entity.\n  ///\n  /// Used for clamping the [velocity] distance.\n  double get maxVelocity;\n\n  /// The current velocity of the entity.\n  final velocity = Vector2.zero();\n\n  @override\n  @mustCallSuper\n  void update(double dt) {\n    final velocityDelta = velocity * dt;\n    position.add(velocityDelta);\n    velocity.sub(velocityDelta);\n\n    super.update(dt);\n  }\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/lib/src/mixins/steering.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\nimport 'package:flutter/material.dart';\n\n/// {@template steering}\n/// Mixin that turns a [Behavior] into a steering behavior for a [Steerable].\n/// {@endtemplate}\nmixin Steering<T extends Steerable> on Behavior<T> {\n  /// The current velocity of the parent entity.\n  Vector2 get velocity => parent.velocity;\n\n  /// Updates the velocity by the given linear acceleration from [steering].\n  void steer(SteeringCore steering, double dt) {\n    final linearAcceleration = steering.getSteering(parent)..scale(dt);\n    velocity.add(linearAcceleration);\n\n    if (velocity.length > parent.maxVelocity) {\n      velocity.setFrom(velocity.normalized()..scale(parent.maxVelocity));\n    }\n  }\n\n  @override\n  @mustCallSuper\n  void renderDebugMode(Canvas canvas) {\n    canvas.drawLine(\n      (parent.size / 2).toOffset(),\n      velocity.toOffset(),\n      debugPaint,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/lib/src/steering/flee.dart",
    "content": "import 'package:flame/effects.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\n\n/// {@template flee}\n/// Flee steering algorithm.\n/// {@endtemplate}\nclass Flee extends SteeringCore {\n  /// {@macro flee}\n  const Flee(\n    this.target, {\n    required this.maxAcceleration,\n  });\n\n  /// The target to flee from.\n  final ReadOnlyPositionProvider target;\n\n  /// The maximum acceleration of the entity.\n  final double maxAcceleration;\n\n  @override\n  Vector2 getSteering(Steerable parent) {\n    final desiredVelocity = (parent.position - target.position)\n      ..normalize()\n      ..scale(parent.maxVelocity);\n\n    final steering = desiredVelocity - parent.velocity;\n    if (steering.length > maxAcceleration) {\n      steering.setFrom(steering.normalized() * maxAcceleration);\n    }\n\n    return steering;\n  }\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/lib/src/steering/pursue.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\n\n/// {@template pursue}\n/// Pursue steering algorithm.\n/// {@endtemplate}\nclass Pursue extends SteeringCore {\n  /// {@macro pursue}\n  const Pursue(\n    this.target, {\n    required this.maxPrediction,\n  });\n\n  /// The target to pursue.\n  final ReadOnlyPositionProvider target;\n\n  /// The maximum prediction time.\n  final double maxPrediction;\n\n  @override\n  Vector2 getSteering(Steerable parent) {\n    final displacement = target.position - parent.position;\n    final distance = displacement.length;\n\n    final speed = parent.velocity.length;\n    final double prediction;\n    if (speed <= distance / maxPrediction) {\n      prediction = maxPrediction;\n    } else {\n      prediction = distance / speed;\n    }\n\n    final explicitTarget = target.position.clone()\n      ..add(parent.velocity)\n      ..multiply(Vector2.all(prediction));\n\n    return seek(parent, explicitTarget);\n  }\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/lib/src/steering/separation.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\n\n/// {@template separation}\n/// Separation steering algorithm.\n/// {@endtemplate}\nclass Separation extends SteeringCore {\n  /// {@macro separation}\n  const Separation(\n    this.entities, {\n    required this.maxDistance,\n    required this.maxAcceleration,\n  });\n\n  /// The maximum distance at which the entity will separate.\n  final double maxDistance;\n\n  /// The maximum acceleration the entity can apply to enable separation.\n  final double maxAcceleration;\n\n  /// The entities to separate from.\n  final Iterable<PositionComponent> entities;\n\n  @override\n  Vector2 getSteering(Steerable parent) {\n    final acceleration = Vector2.zero();\n    for (final entity in entities) {\n      if (entity == parent) {\n        continue;\n      }\n      final direction = entity.position - parent.position;\n      final dist = direction.length;\n      if (dist < maxDistance) {\n        final strength =\n            maxAcceleration *\n            (maxDistance - dist) /\n            (maxDistance - entity.size.x - parent.size.x);\n\n        direction.normalize();\n        acceleration.add(direction * strength);\n      }\n    }\n    return acceleration;\n  }\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/lib/src/steering/steering.dart",
    "content": "export 'flee.dart';\nexport 'pursue.dart';\nexport 'separation.dart';\nexport 'steering_core.dart';\nexport 'wander.dart';\n"
  },
  {
    "path": "packages/flame_steering_behaviors/lib/src/steering/steering_core.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\n\n/// {@template steering_core}\n/// Base class for all steering behaviors.\n/// {@endtemplate}\nabstract class SteeringCore {\n  /// {@macro steering_core}\n  const SteeringCore();\n\n  /// Calculates the next target position to steer towards.\n  Vector2 getSteering(Steerable parent);\n\n  /// Seek steering behavior.\n  Vector2 seek(Steerable parent, Vector2 target) {\n    final acceleration = (target - parent.position)..normalize();\n    return acceleration * parent.maxVelocity;\n  }\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/lib/src/steering/wander.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\nimport 'package:flutter/material.dart';\n\n/// {@template wander}\n/// Wander steering algorithm.\n///\n/// The [onNewAngle] should be used to get the [angle] value for the next\n/// [Wander] creation.\n/// {@endtemplate}\nclass Wander extends SteeringCore {\n  /// {@macro wander}\n  Wander({\n    required this.circleDistance,\n    required this.maximumAngle,\n    required this.angle,\n    required this.onNewAngle,\n    required this.random,\n  });\n\n  /// The distance to the circle center of the next target.\n  final double circleDistance;\n\n  /// The maximum angle used to calculate the next wander [angle].\n  ///\n  /// Value is represented in radians.\n  final double maximumAngle;\n\n  /// The current wander angle in radians.\n  final double angle;\n\n  /// Called when the next [angle] value is calculated.\n  ///\n  /// The next call to [Wander] expects the angle to be this value.\n  final ValueChanged<double> onNewAngle;\n\n  /// The random number generator used to calculate the next wander [angle].\n  final Random random;\n\n  @override\n  Vector2 getSteering(Steerable parent) {\n    // Calculate the circle center for the next target that is right in front\n    // of the parent.\n    final circleCenter = parent.velocity.normalized()..scale(circleDistance);\n\n    // Calculate the displacement needed for displacing the circle center.\n    final displacement = Vector2(0, -1)\n      ..scale(circleDistance)\n      ..setAngle(angle);\n\n    // Randomly pick a new angle based on the maximum angle and expose it.\n    onNewAngle(angle + random.nextDouble() * maximumAngle - maximumAngle * 0.5);\n\n    // Calculate the next target position by displacing the circle center.\n    return circleCenter.clone()..add(displacement);\n  }\n}\n\nextension on Vector2 {\n  void setAngle(double radians) {\n    final len = length;\n    x = cos(radians) * len;\n    y = sin(radians) * len;\n  }\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/pubspec.yaml",
    "content": "name: flame_steering_behaviors\nresolution: workspace\ndescription: Flame Steering Behaviors brings steering algorithms to the behavior pattern, originally built by Very Good Ventures.\nversion: 0.2.1+4\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_steering_behaviors\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_behaviors: ^1.3.4\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flame_test: ^2.2.3\n  flutter_test:\n    sdk: flutter\n  mocktail: ^1.0.4\n\n"
  },
  {
    "path": "packages/flame_steering_behaviors/test/helpers/helpers.dart",
    "content": "export 'steerable_entity.dart';\nexport 'test_game.dart';\n"
  },
  {
    "path": "packages/flame_steering_behaviors/test/helpers/steerable_entity.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\n\nclass SteerableEntity extends PositionedEntity with Steerable {\n  SteerableEntity({\n    super.position,\n    super.behaviors,\n    super.size,\n  });\n\n  @override\n  double get maxVelocity => 100;\n\n  @override\n  void renderDebugMode(Canvas canvas) {\n    // Custom debug render mode so that the PositionComponent text doesn't get\n    // rendered.\n    canvas.drawRect(Vector2.zero() & size, debugPaint);\n  }\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/test/helpers/test_game.dart",
    "content": "import 'package:flame/game.dart';\n\nclass TestGame extends FlameGame with HasCollisionDetection {}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/test/src/behaviors/flee_behavior_test.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../helpers/helpers.dart';\n\nvoid main() {\n  final flameTester = FlameTester(TestGame.new);\n\n  group('FleeBehavior', () {\n    flameTester.testGameWidget(\n      'only flee if target is within panic distance',\n      setUp: (game, tester) async {\n        final target = SteerableEntity();\n        final parent = SteerableEntity(\n          behaviors: [\n            FleeBehavior(\n              target,\n              maxAcceleration: 100,\n              panicDistance: 20,\n            ),\n          ],\n          position: Vector2.zero(),\n          size: Vector2.all(32),\n        );\n        await game.ensureAddAll([parent, target]);\n      },\n      verify: (game, tester) async {\n        final parent = game.children.whereType<SteerableEntity>().first;\n        final target = game.children.whereType<SteerableEntity>().last;\n\n        // Move target outside panic distance.\n        target.position.setValues(20, 20);\n        game.update(1);\n\n        expect(parent.velocity, closeToVector(Vector2(0, 0), 0.01));\n\n        // Move target inside panic distance.\n        target.position.setValues(5, 5);\n        game.update(1);\n\n        expect(parent.velocity, closeToVector(Vector2(-70.71, -70.71), 0.01));\n      },\n    );\n\n    flameTester.testGameWidget(\n      'render panic distance as a circle in debug mode',\n      setUp: (game, tester) async {\n        final entity = SteerableEntity(\n          behaviors: [\n            FleeBehavior(\n              SteerableEntity(),\n              maxAcceleration: 100,\n              panicDistance: 64,\n            ),\n          ],\n          position: game.size / 2,\n          size: Vector2.all(32),\n        )..debugMode = true;\n\n        await game.ensureAdd(entity);\n      },\n      verify: (game, tester) async {\n        await tester.pump();\n\n        await expectLater(\n          find.byGame<TestGame>(),\n          matchesGoldenFile('golden/flee_behavior/render_debug_mode.png'),\n        );\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/test/src/behaviors/pursue_behavior_test.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../helpers/helpers.dart';\n\nvoid main() {\n  final flameTester = FlameTester(TestGame.new);\n\n  group('PursueBehavior', () {\n    flameTester.testGameWidget(\n      'only pursue if target is within pursue distance',\n      setUp: (game, tester) async {\n        final target = SteerableEntity();\n        final parent = SteerableEntity(\n          behaviors: [PursueBehavior(target, pursueRange: 20)],\n          position: Vector2.zero(),\n          size: Vector2.all(32),\n        );\n        await game.ensureAddAll([parent, target]);\n      },\n      verify: (game, tester) async {\n        final parent = game.children.whereType<SteerableEntity>().first;\n        final target = game.children.whereType<SteerableEntity>().last;\n\n        // Move target outside pursue distance.\n        target.position.setValues(20, 20);\n        game.update(1);\n\n        expect(parent.velocity, closeToVector(Vector2(0, 0), 0.01));\n\n        // Move target inside pursue distance.\n        target.position.setValues(5, 5);\n        game.update(1);\n\n        expect(parent.velocity, closeToVector(Vector2(70.71, 70.71), 0.01));\n      },\n    );\n\n    flameTester.testGameWidget(\n      'render pursue distance as a circle in debug mode',\n      setUp: (game, tester) async {\n        final entity = SteerableEntity(\n          behaviors: [PursueBehavior(SteerableEntity(), pursueRange: 64)],\n          position: game.size / 2,\n          size: Vector2.all(32),\n        )..debugMode = true;\n\n        await game.ensureAdd(entity);\n      },\n      verify: (game, tester) async {\n        await tester.pump();\n\n        await expectLater(\n          find.byGame<TestGame>(),\n          matchesGoldenFile('golden/pursue_behavior/render_debug_mode.png'),\n        );\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/test/src/behaviors/separation_behavior_test.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../helpers/helpers.dart';\n\nvoid main() {\n  final flameTester = FlameTester(TestGame.new);\n\n  group('SeparationBehavior', () {\n    flameTester.testGameWidget(\n      'calculates the acceleration needed to separate',\n      setUp: (game, tester) async {\n        final parent = SteerableEntity(\n          behaviors: [\n            SeparationBehavior(\n              [\n                SteerableEntity(\n                  position: Vector2.all(10),\n                  size: Vector2.all(32),\n                ),\n                SteerableEntity(\n                  position: Vector2.all(20),\n                  size: Vector2.all(32),\n                ),\n              ],\n              maxDistance: 50,\n              maxAcceleration: 10,\n            ),\n          ],\n          position: Vector2.zero(),\n          size: Vector2.all(32),\n        );\n        await game.ensureAdd(parent);\n      },\n      verify: (game, tester) async {\n        final parent = game.firstChild<SteerableEntity>()!;\n        game.update(1);\n\n        expect(parent.velocity, closeToVector(Vector2(-29.08, -29.08), 0.01));\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/test/src/behaviors/wander_behavior_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nimport '../../helpers/helpers.dart';\n\nclass _MockRandom extends Mock implements Random {}\n\nconst _startAngle = 90 * degrees2Radians;\nconst _randomValue = 0.25;\nconst _maximumAngle = 90 * degrees2Radians;\n\nvoid main() {\n  final flameTester = FlameTester(TestGame.new);\n\n  group('WanderBehavior', () {\n    late Random random;\n\n    setUp(() {\n      random = _MockRandom();\n      when(random.nextDouble).thenReturn(_randomValue);\n    });\n\n    test('fall back to normal Random if none is given', () {\n      final behavior = WanderBehavior(\n        circleDistance: 1,\n        maximumAngle: 2,\n        startingAngle: 3,\n      );\n\n      expect(behavior.random, isA<Random>());\n    });\n\n    flameTester.testGameWidget(\n      'calculates the new angle and wandering force',\n      setUp: (game, tester) async {\n        final behavior = WanderBehavior(\n          circleDistance: 40,\n          maximumAngle: _maximumAngle,\n          startingAngle: _startAngle,\n          random: random,\n        );\n\n        final parent = SteerableEntity(behaviors: [behavior]);\n        await game.ensureAdd(parent);\n      },\n      verify: (game, tester) async {\n        final parent = game.firstChild<SteerableEntity>()!;\n        final behavior = parent.findBehavior<WanderBehavior>();\n\n        // The verify does a pump before it calls this so we have a new initial\n        // angle.\n        const initialAngle =\n            _startAngle + _randomValue * _maximumAngle - _maximumAngle * 0.5;\n        expect(behavior.angle, initialAngle);\n\n        game.update(1);\n\n        expect(\n          behavior.angle,\n          initialAngle + _randomValue * _maximumAngle - _maximumAngle * 0.5,\n        );\n        expect(parent.velocity, closeToVector(Vector2(15.3, 36.95), 0.01));\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/test/src/mixins/steerable_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../helpers/helpers.dart';\n\nvoid main() {\n  final flameTester = FlameTester(TestGame.new);\n\n  group('Steerable', () {\n    flameTester.testGameWidget(\n      'add velocity delta to position and subtract the delta from velocity',\n      setUp: (game, tester) async {\n        final entity = SteerableEntity();\n        entity.velocity.setValues(100, 100);\n        await game.ensureAdd(entity);\n      },\n      verify: (game, tester) async {\n        final entity = game.firstChild<SteerableEntity>()!;\n        game.update(0.25);\n\n        expect(entity.velocity, closeToVector(Vector2(75, 75)));\n        expect(entity.position, closeToVector(Vector2(25, 25)));\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/test/src/mixins/steering_test.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_behaviors/flame_behaviors.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nimport '../../helpers/helpers.dart';\n\nclass _TestBehavior extends Behavior<SteerableEntity> with Steering {}\n\nclass _MockSteeringCore extends Mock implements SteeringCore {}\n\nclass _FakeSteerable extends Fake implements Steerable {}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  final flameTester = FlameTester(TestGame.new);\n\n  group('Steering', () {\n    late SteeringCore steeringCore;\n\n    setUp(() {\n      registerFallbackValue(_FakeSteerable());\n\n      steeringCore = _MockSteeringCore();\n      when(() => steeringCore.getSteering(any())).thenReturn(\n        Vector2.all(100),\n      );\n    });\n\n    flameTester.testGameWidget(\n      'update velocity by the linear acceleration from steering',\n      setUp: (game, tester) async {\n        final behavior = _TestBehavior();\n        final entity = SteerableEntity(\n          behaviors: [behavior],\n          size: Vector2.all(32),\n        );\n        await game.ensureAdd(entity);\n      },\n      verify: (game, tester) async {\n        final entity = game.firstChild<SteerableEntity>()!;\n        final behavior = entity.findBehavior<_TestBehavior>();\n\n        behavior.steer(steeringCore, 0.25);\n\n        expect(entity.velocity, closeToVector(Vector2(25, 25)));\n      },\n    );\n\n    flameTester.testGameWidget(\n      'clamp velocity to max velocity when the linear acceleration is too high',\n      setUp: (game, tester) async {\n        final behavior = _TestBehavior();\n        final entity = SteerableEntity(\n          behaviors: [behavior],\n          size: Vector2.all(32),\n        );\n        await game.ensureAdd(entity);\n      },\n      verify: (game, tester) async {\n        final entity = game.firstChild<SteerableEntity>()!;\n        final behavior = entity.findBehavior<_TestBehavior>();\n\n        behavior.steer(steeringCore, 2);\n\n        expect(entity.velocity, closeToVector(Vector2(70.71, 70.71), 0.01));\n      },\n    );\n\n    flameTester.testGameWidget(\n      'render current velocity as a line in debug mode',\n      setUp: (game, tester) async {\n        final behavior = _TestBehavior();\n        final entity = SteerableEntity(\n          position: game.size / 2,\n          behaviors: [behavior],\n          size: Vector2.all(32),\n        )..debugMode = true;\n\n        await game.ensureAdd(entity);\n\n        behavior.steer(steeringCore, 0.25);\n      },\n      verify: (game, tester) async {\n        await tester.pump();\n\n        await expectLater(\n          find.byGame<TestGame>(),\n          matchesGoldenFile('golden/steering/render_debug_mode.png'),\n        );\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/test/src/steering/flee_test.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../helpers/helpers.dart';\n\nvoid main() {\n  group('Flee', () {\n    test('calculates the fleeing acceleration', () {\n      final parent = SteerableEntity(position: Vector2.all(50));\n      final target = SteerableEntity(position: Vector2.zero());\n\n      final flee = Flee(\n        target,\n        maxAcceleration: 100,\n      );\n\n      final steering = flee.getSteering(parent);\n\n      expect(steering, closeToVector(Vector2(70.71, 70.71), 0.01));\n    });\n\n    test(\n      'the fleeing acceleration length is clamped to max acceleration',\n      () {\n        final parent = SteerableEntity(position: Vector2.all(50));\n        final target = SteerableEntity(position: Vector2.zero());\n\n        final flee = Flee(\n          target,\n          maxAcceleration: 50,\n        );\n\n        final steering = flee.getSteering(parent);\n\n        expect(steering.length, closeTo(50, 0.01));\n        expect(steering, closeToVector(Vector2(35.35, 35.35), 0.01));\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/test/src/steering/pursue_test.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../helpers/helpers.dart';\n\nvoid main() {\n  group('Pursue', () {\n    test('calculates the pursue-ing acceleration', () {\n      final parent = SteerableEntity(position: Vector2.all(50));\n      final target = SteerableEntity(position: Vector2.zero());\n\n      final pursue = Pursue(\n        target,\n        maxPrediction: 0.1,\n      );\n\n      final steering = pursue.getSteering(parent);\n\n      expect(steering, closeToVector(Vector2(-70.71, -70.71), 0.01));\n    });\n\n    test('clamps prediction by max prediction based on speed and distance', () {\n      final parent = SteerableEntity(position: Vector2.all(19))\n        ..velocity.setValues(10, 10);\n      final target = SteerableEntity(position: Vector2.zero());\n\n      final pursue = Pursue(\n        target,\n        maxPrediction: 2,\n      );\n\n      final steering = pursue.getSteering(parent);\n\n      expect(steering, closeToVector(Vector2(0, 0)));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/test/src/steering/separation_test.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../helpers/helpers.dart';\n\nvoid main() {\n  group('Separation', () {\n    test('calculates the acceleration needed to separate', () {\n      final parent = SteerableEntity(\n        position: Vector2.zero(),\n        size: Vector2.all(32),\n      );\n\n      final steering = Separation(\n        [\n          SteerableEntity(\n            position: Vector2.all(10),\n            size: Vector2.all(32),\n          ),\n          SteerableEntity(\n            position: Vector2.all(20),\n            size: Vector2.all(32),\n          ),\n        ],\n        maxDistance: 50,\n        maxAcceleration: 10,\n      );\n\n      final linearAcceleration = steering.getSteering(parent);\n\n      expect(linearAcceleration, closeToVector(Vector2(-29.08, -29.08), 0.01));\n    });\n\n    test('skips self', () {\n      final parent = SteerableEntity(\n        position: Vector2.zero(),\n        size: Vector2.all(32),\n      );\n\n      final steering = Separation(\n        [parent],\n        maxDistance: 50,\n        maxAcceleration: 10,\n      );\n\n      final linearAcceleration = steering.getSteering(parent);\n\n      expect(linearAcceleration, closeToVector(Vector2(0, 0)));\n    });\n\n    test('does not separate if other entities are too far away', () {\n      final parent = SteerableEntity(position: Vector2.zero());\n\n      final steering = Separation(\n        [\n          SteerableEntity(\n            position: Vector2.all(36),\n            size: Vector2.all(32),\n          ),\n          SteerableEntity(\n            position: Vector2.all(-36),\n            size: Vector2.all(32),\n          ),\n        ],\n        maxDistance: 50,\n        maxAcceleration: 10,\n      );\n\n      final linearAcceleration = steering.getSteering(parent);\n\n      expect(linearAcceleration, closeToVector(Vector2(0, 0)));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/test/src/steering/steering_core_test.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../helpers/helpers.dart';\n\nclass _TestSteeringCore extends SteeringCore {\n  @override\n  Vector2 getSteering(Steerable parent) {\n    throw UnimplementedError();\n  }\n}\n\nvoid main() {\n  group('SteeringCore', () {\n    test('calculates the acceleration value towards the target', () {\n      final parent = SteerableEntity(position: Vector2(10, 0));\n      final target = Vector2(0, 10);\n\n      final steeringCore = _TestSteeringCore();\n      final steering = steeringCore.seek(parent, target);\n\n      expect(steering, closeToVector(Vector2(-70.71, 70.71), 0.01));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_steering_behaviors/test/src/steering/wander_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame_steering_behaviors/flame_steering_behaviors.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nimport '../../helpers/helpers.dart';\n\nclass _MockRandom extends Mock implements Random {}\n\nvoid main() {\n  group('Wander', () {\n    late Random random;\n\n    setUp(() {\n      random = _MockRandom();\n      when(random.nextDouble).thenReturn(0);\n    });\n\n    test('calculates new orientation', () {\n      const startAngle = 90 * degrees2Radians;\n      const randomValue = 0.25;\n      const maximumAngle = 90 * degrees2Radians;\n\n      const expectedAngle =\n          startAngle + (randomValue * maximumAngle) - (maximumAngle * 0.5);\n\n      final parent = SteerableEntity(position: Vector2.zero());\n      final steering = Wander(\n        circleDistance: 40,\n        maximumAngle: maximumAngle,\n        angle: startAngle,\n        onNewAngle: (angle) {\n          expect(angle, equals(expectedAngle));\n        },\n        random: random,\n      );\n\n      when(random.nextDouble).thenReturn(0.25);\n\n      steering.getSteering(parent);\n    });\n\n    group('calculates the wandering force', () {\n      test('when the start angle is 0 it should go to the right', () {\n        final parent = SteerableEntity(position: Vector2.zero());\n\n        final steering = Wander(\n          circleDistance: 40,\n          angle: 0,\n          maximumAngle: 0,\n          onNewAngle: (angle) {},\n          random: random,\n        );\n\n        final linearAcceleration = steering.getSteering(parent);\n        expect(linearAcceleration, closeToVector(Vector2(40, 0), 0.01));\n      });\n\n      test('when the start angle is 180 it should go to the left', () {\n        final parent = SteerableEntity(position: Vector2.zero());\n\n        final steering = Wander(\n          circleDistance: 40,\n          angle: 180 * degrees2Radians,\n          maximumAngle: 0,\n          onNewAngle: (angle) {},\n          random: random,\n        );\n\n        when(random.nextDouble).thenReturn(0.25);\n\n        final linearAcceleration = steering.getSteering(parent);\n        expect(linearAcceleration, closeToVector(Vector2(-40, 0), 0.01));\n      });\n\n      test('when the start angle is 90 it should go to the bottom', () {\n        final parent = SteerableEntity(position: Vector2.zero());\n\n        final steering = Wander(\n          circleDistance: 40,\n          maximumAngle: 0,\n          angle: 90 * degrees2Radians,\n          onNewAngle: (angle) {},\n          random: random,\n        );\n\n        when(random.nextDouble).thenReturn(0.25);\n\n        final linearAcceleration = steering.getSteering(parent);\n        expect(linearAcceleration, closeToVector(Vector2(0, 40), 0.01));\n      });\n\n      test('when the start angle is 270 it should go to the top', () {\n        final parent = SteerableEntity(position: Vector2.zero());\n\n        final steering = Wander(\n          circleDistance: 40,\n          angle: 270 * degrees2Radians,\n          maximumAngle: 0,\n          onNewAngle: (angle) {},\n          random: random,\n        );\n\n        final linearAcceleration = steering.getSteering(parent);\n        expect(linearAcceleration, closeToVector(Vector2(0, -40), 0.01));\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_studio/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/flame_studio/README.md",
    "content": "# Flame Studio\n"
  },
  {
    "path": "packages/flame_studio/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_studio/example/main.dart",
    "content": "import 'dart:math';\nimport 'package:flame/game.dart';\nimport 'package:flame/geometry.dart';\nimport 'package:flame_studio/flame_studio.dart';\nimport 'package:flutter/widgets.dart';\n\nvoid main() {\n  runFlameStudio(\n    Container(\n      color: const Color(0xff6385b9),\n      child: GameWidget(game: MyGame()),\n    ),\n  );\n}\n\nclass MyGame extends FlameGame {\n  @override\n  Color backgroundColor() => const Color(0x00000000);\n\n  @override\n  void onLoad() {\n    final size = findGame()!.canvasSize;\n    final random = Random();\n    for (var i = 0; i < 10; i++) {\n      final speed = random.nextDouble() * 500;\n      final angle = random.nextDouble() * 12;\n      add(\n        Circle(\n          radius: random.nextDouble() * 10 + 10,\n          position: Vector2(\n            (0.8 * random.nextDouble() + 0.1) * size.x,\n            (0.8 * random.nextDouble() + 0.1) * size.y,\n          ),\n          velocity: Vector2(speed * sin(angle), speed * cos(angle)),\n        ),\n      );\n    }\n  }\n}\n\nclass Circle extends CircleComponent {\n  Circle({\n    required this.velocity,\n    required super.position,\n    super.radius = 15.0,\n  });\n\n  final Vector2 velocity;\n  final Vector2 gameSize = Vector2.zero();\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    gameSize.setFrom(size);\n  }\n\n  @override\n  void update(double dt) {\n    var newX = x + velocity.x * dt;\n    var newY = y + velocity.y * dt;\n    if (newX < 0 || newX > gameSize.x - 2 * radius) {\n      velocity.x = -velocity.x;\n      newX = newX < 0 ? 0 : gameSize.x - 2 * radius;\n    }\n    if (newY < 0 || newY > gameSize.y - 2 * radius) {\n      velocity.y = -velocity.y;\n      newY = newY < 0 ? 0 : gameSize.y - 2 * radius;\n    }\n    position.setValues(newX, newY);\n  }\n}\n"
  },
  {
    "path": "packages/flame_studio/lib/flame_studio.dart",
    "content": "export 'src/run_flame_studio.dart' show runFlameStudio;\n"
  },
  {
    "path": "packages/flame_studio/lib/src/core/component_tree.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_studio/src/core/game_controller.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_riverpod/legacy.dart';\n\nfinal componentTreeProvider =\n    StateNotifierProvider<ComponentTreeObserver, ComponentTreeState>((ref) {\n      final gameState = ref.watch(gameControllerProvider);\n      return ComponentTreeObserver(gameState.game as FlameGame?);\n    });\n\n@immutable\nclass ComponentTreeNode {\n  const ComponentTreeNode._(this.component, this.children);\n\n  factory ComponentTreeNode.fromComponent(Component component) {\n    return ComponentTreeNode._(\n      component,\n      component.hasChildren\n          ? component.children.map(ComponentTreeNode.fromComponent).toList()\n          : null,\n    );\n  }\n\n  final Component component;\n  final List<ComponentTreeNode>? children;\n  String get name => component.runtimeType.toString();\n  bool get hasChildren => children?.isNotEmpty ?? false;\n\n  @override\n  bool operator ==(Object other) =>\n      other is ComponentTreeNode &&\n      component == other.component &&\n      listEquals(children, other.children);\n\n  @override\n  int get hashCode => Object.hash(component, children);\n}\n\n@immutable\nclass ComponentTreeState {\n  ComponentTreeState(Component? rootComponent)\n    : root = rootComponent == null\n          ? null\n          : ComponentTreeNode.fromComponent(rootComponent);\n\n  final ComponentTreeNode? root;\n\n  @override\n  bool operator ==(Object other) =>\n      other is ComponentTreeState && root == other.root;\n\n  @override\n  int get hashCode => root?.hashCode ?? 0;\n}\n\nclass ComponentTreeObserver extends StateNotifier<ComponentTreeState> {\n  ComponentTreeObserver(Component? rootComponent)\n    : super(ComponentTreeState(rootComponent)) {\n    if (rootComponent != null) {\n      _refresh();\n    }\n  }\n\n  static const Duration refreshFrequency = Duration(milliseconds: 300);\n\n  void _refresh() {\n    if (!mounted) {\n      // The Future.delayed may complete after the observer was disposed.\n      return;\n    }\n    final newState = ComponentTreeState(state.root!.component);\n    if (newState != state) {\n      state = newState;\n    }\n    Future.delayed(refreshFrequency, _refresh);\n  }\n}\n"
  },
  {
    "path": "packages/flame_studio/lib/src/core/game_controller.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_riverpod/legacy.dart';\n\nfinal gameControllerProvider =\n    StateNotifierProvider<_GameController, _GameState>(\n      (ref) => _GameController(),\n    );\n\n@immutable\nclass _GameState {\n  const _GameState({this.game, this.paused = false});\n\n  final Game? game;\n  final bool paused;\n\n  _GameState copyWith({\n    Game? game,\n    bool? paused,\n  }) {\n    return _GameState(\n      game: game ?? this.game,\n      paused: paused ?? this.paused,\n    );\n  }\n\n  @override\n  bool operator ==(Object other) =>\n      other is _GameState && game == other.game && paused == other.paused;\n\n  @override\n  int get hashCode => Object.hash(game, paused);\n}\n\nclass _GameController extends StateNotifier<_GameState> {\n  _GameController() : super(const _GameState()) {\n    WidgetsFlutterBinding.ensureInitialized();\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      final game = _findGame();\n      useGame(game);\n    });\n  }\n\n  bool get isPaused => state.paused;\n\n  void useGame(Game? game) {\n    state = state.copyWith(game: game);\n  }\n\n  void pauseGame() {\n    state.game!.pauseEngine();\n    state = state.copyWith(paused: true);\n  }\n\n  void resumeGame() {\n    state.game!.resumeEngine();\n    state = state.copyWith(paused: false);\n  }\n\n  void stepGame() {\n    state.game!.stepEngine();\n  }\n\n  static Game? _findGame() {\n    Game? game;\n    void visitor(Element element) {\n      if (element.widget is GameWidget) {\n        final dynamic state = (element as StatefulElement).state;\n        // ignore: avoid_dynamic_calls\n        game = state.currentGame as Game;\n      } else {\n        element.visitChildElements(visitor);\n      }\n    }\n\n    WidgetsBinding.instance.rootElement?.visitChildElements(visitor);\n    return game;\n  }\n}\n"
  },
  {
    "path": "packages/flame_studio/lib/src/core/theme.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nfinal textDirectionProvider = Provider((ref) => TextDirection.ltr);\n\nfinal themeProvider = Provider((ref) => Theme());\n\nclass Theme {\n  final Color backdropColor = const Color(0xFF484848);\n  final Color toolbarColor = const Color(0xFF303030);\n  final Color panelColor = const Color(0xFF383838);\n\n  final double buttonRadius = 5.0;\n  final Color buttonColor = const Color(0xFF404040);\n  final Color buttonHoverColor = const Color(0xFF606060);\n  final Color buttonActiveColor = const Color(0xFFA0A0A0);\n  final Color buttonDisabledColor = const Color(0x44404040);\n  final Color buttonTextColor = const Color(0xFFffd78d);\n  final Color buttonHoverTextColor = const Color(0xffffe95d);\n  final Color buttonActiveTextColor = const Color(0xffffffff);\n  final Color buttonDisabledTextColor = const Color(0x16ffffff);\n}\n"
  },
  {
    "path": "packages/flame_studio/lib/src/run_flame_studio.dart",
    "content": "import 'package:flame_studio/src/widgets/flame_studio.dart';\nimport 'package:flutter/widgets.dart';\n\nvoid runFlameStudio(Widget app, {bool enabled = true}) {\n  var allowRun = true;\n  assert(() {\n    if (enabled) {\n      runApp(FlameStudio(app));\n      allowRun = false;\n    }\n    return true;\n  }());\n  if (allowRun) {\n    runApp(app);\n  }\n}\n"
  },
  {
    "path": "packages/flame_studio/lib/src/widgets/flame_studio.dart",
    "content": "import 'package:flame_studio/src/widgets/ui_scaffold.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nclass FlameStudio extends StatelessWidget {\n  const FlameStudio(this.child, {super.key});\n\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return ProviderScope(\n      child: UiScaffold(gameApp: child),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_studio/lib/src/widgets/left_panel.dart",
    "content": "import 'package:flame_studio/src/core/theme.dart';\nimport 'package:flame_studio/src/widgets/panels/hierarchy_view.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:flutter_riverpod/legacy.dart';\n\nclass LeftPanel extends ConsumerWidget {\n  const LeftPanel({super.key});\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final width = ref.watch(leftPanelWidthProvider);\n\n    return Directionality(\n      textDirection: ref.watch(textDirectionProvider),\n      child: MediaQuery.fromView(\n        view: View.of(context),\n        child: Container(\n          constraints: BoxConstraints.tightFor(width: width),\n          decoration: BoxDecoration(\n            boxShadow: const [\n              BoxShadow(\n                blurRadius: 4.0,\n                color: Color(0xA1000000),\n                offset: Offset(2, 0),\n              ),\n            ],\n            color: ref.watch(themeProvider).panelColor,\n          ),\n          child: const HierarchyView(),\n        ),\n      ),\n    );\n  }\n}\n\nclass _WidthNotifier extends StateNotifier<double> {\n  _WidthNotifier() : super(250.0);\n\n  static const minWidth = 200.0;\n  static const maxWidth = 500.0;\n\n  void setValue(double value) {\n    state = value.clamp(minWidth, maxWidth);\n  }\n}\n\nfinal leftPanelWidthProvider = StateNotifierProvider<_WidthNotifier, double>(\n  (ref) => _WidthNotifier(),\n);\n"
  },
  {
    "path": "packages/flame_studio/lib/src/widgets/left_panel_grip.dart",
    "content": "import 'package:flame_studio/src/core/theme.dart';\nimport 'package:flame_studio/src/widgets/left_panel.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nclass LeftPanelGrip extends ConsumerWidget {\n  const LeftPanelGrip({super.key});\n\n  /// Tracks the initial coordinates of a drag event. This can be static\n  /// because we support only one drag at a time.\n  static double _delta = 0;\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final panelWidth = ref.watch(leftPanelWidthProvider);\n\n    return Row(\n      textDirection: ref.watch(textDirectionProvider),\n      children: [\n        Container(\n          constraints: BoxConstraints.tightFor(width: panelWidth - 5.0),\n        ),\n        GestureDetector(\n          onHorizontalDragStart: (DragStartDetails details) {\n            _delta = panelWidth - details.globalPosition.dx;\n          },\n          onHorizontalDragUpdate: (DragUpdateDetails details) {\n            final mutableWidth = ref.read(leftPanelWidthProvider.notifier);\n            mutableWidth.setValue(_delta + details.globalPosition.dx);\n          },\n          child: MouseRegion(\n            cursor: SystemMouseCursors.resizeLeftRight,\n            child: Container(\n              constraints: const BoxConstraints.tightFor(width: 10.0),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_studio/lib/src/widgets/panels/hierarchy_view.dart",
    "content": "import 'package:flame/components.dart' hide Matrix4;\nimport 'package:flame_studio/src/core/component_tree.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nclass HierarchyView extends ConsumerStatefulWidget {\n  const HierarchyView({super.key});\n\n  @override\n  HierarchyViewState createState() => HierarchyViewState();\n}\n\nclass HierarchyViewState extends ConsumerState<ConsumerStatefulWidget> {\n  Component? selectedComponent;\n  Set<Component> expandedComponents = {};\n\n  @override\n  Widget build(BuildContext context) {\n    final componentTree = ref.watch(componentTreeProvider).root;\n    if (componentTree == null) {\n      return Container();\n    }\n\n    final listItems = <Widget>[];\n    _buildList(componentTree, 0, listItems);\n\n    return ListView(\n      padding: const EdgeInsets.all(8),\n      children: listItems,\n    );\n  }\n\n  void _buildList(ComponentTreeNode node, int depth, List<Widget> out) {\n    out.add(_ListItem(this, node, depth, isFirst: out.isEmpty));\n    final isExpanded = expandedComponents.contains(node.component);\n    if (isExpanded && node.hasChildren) {\n      for (final childNode in node.children!) {\n        _buildList(childNode, depth + 1, out);\n      }\n      out.add(_ClosingBrace(depth));\n    }\n  }\n\n  /// Used by the expander icons in the gutter: the component expands/collapses\n  /// without affecting the selection state.\n  void toggle(Component component) {\n    setState(() {\n      if (expandedComponents.contains(component)) {\n        expandedComponents.remove(component);\n      } else {\n        expandedComponents.add(component);\n      }\n    });\n  }\n\n  void toggleOrSelect(Component component) {\n    final isExpanded = expandedComponents.contains(component);\n    final isSelected = selectedComponent == component;\n    setState(() {\n      if (isExpanded && isSelected) {\n        expandedComponents.remove(component);\n      } else {\n        expandedComponents.add(component);\n      }\n      selectedComponent = component;\n    });\n  }\n}\n\nclass _ListItem extends StatelessWidget {\n  _ListItem(this.state, this.node, this.indent, {required this.isFirst})\n    : super(key: ObjectKey(node.component));\n\n  final HierarchyViewState state;\n  final ComponentTreeNode node;\n  final int indent;\n  final bool isFirst;\n\n  @override\n  Widget build(BuildContext context) {\n    final isSelected = state.selectedComponent == node.component;\n    final isExpanded = state.expandedComponents.contains(node.component);\n    final hasChildren = node.hasChildren;\n\n    Widget expanderIcon = _ExpanderIcon(\n      hasChildren: hasChildren,\n      isExpanded: isExpanded,\n      isFirst: isFirst,\n      isLast: indent == 0 && !(isExpanded && hasChildren),\n    );\n    if (hasChildren) {\n      expanderIcon = MouseRegion(\n        cursor: SystemMouseCursors.click,\n        child: GestureDetector(\n          onTap: () => state.toggle(node.component),\n          child: expanderIcon,\n        ),\n      );\n    }\n    final componentName = MouseRegion(\n      cursor: SystemMouseCursors.click,\n      child: GestureDetector(\n        onTap: () => state.toggleOrSelect(node.component),\n        child: Container(\n          padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),\n          decoration: BoxDecoration(\n            borderRadius: BorderRadius.circular(4.0),\n            color: isSelected ? const Color(0xffc78938) : null,\n          ),\n          child: Text(\n            node.name,\n            style: const TextStyle(\n              color: Color(0xffffffff),\n            ),\n          ),\n        ),\n      ),\n    );\n    final punctuation = Transform(\n      transform: Matrix4.translationValues(-3.0, 0, 0),\n      child: Text(\n        hasChildren ? (isExpanded ? ' {' : ' {...}') : ',',\n        style: const TextStyle(color: Color(0x66f5d49a)),\n      ),\n    );\n\n    return Row(\n      children: [\n        expanderIcon,\n        SizedBox(width: 15.0 * indent),\n        componentName,\n        punctuation,\n      ],\n    );\n  }\n}\n\nclass _ClosingBrace extends StatelessWidget {\n  const _ClosingBrace(this.indent);\n  final int indent;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        _ExpanderIcon(hasChildren: false, isLast: indent == 0),\n        SizedBox(width: 15.0 * indent + 5.0),\n        const Text('}', style: TextStyle(color: Color(0x66f5d49a))),\n      ],\n    );\n  }\n}\n\nclass _ExpanderIcon extends StatelessWidget {\n  const _ExpanderIcon({\n    required this.hasChildren,\n    this.isExpanded = false,\n    this.isFirst = false,\n    this.isLast = false,\n  });\n\n  final bool hasChildren;\n  final bool isExpanded;\n  final bool isFirst;\n  final bool isLast;\n\n  @override\n  Widget build(BuildContext context) {\n    return CustomPaint(\n      size: const Size(15, 20),\n      painter: _ExpanderIconPainter(this),\n    );\n  }\n}\n\nclass _ExpanderIconPainter extends CustomPainter {\n  _ExpanderIconPainter(this.icon);\n\n  final _ExpanderIcon icon;\n  static final _paint = Paint()\n    ..color = const Color(0xFFFFFFFF)\n    ..style = PaintingStyle.stroke;\n\n  @override\n  bool shouldRepaint(_ExpanderIconPainter old) =>\n      (icon.hasChildren != old.icon.hasChildren) ||\n      (icon.isExpanded != old.icon.isExpanded);\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    const iconSize = 10.0;\n    const halfSize = iconSize / 2;\n    final xExtent = size.width / 2;\n    final yExtent = size.height / 2;\n    canvas.save();\n    canvas.translate(xExtent, yExtent);\n    if (icon.hasChildren) {\n      if (!icon.isFirst) {\n        canvas.drawLine(\n          Offset(0, -yExtent),\n          const Offset(0, -halfSize),\n          _paint,\n        );\n      }\n      canvas.drawRect(\n        const Rect.fromLTWH(-halfSize, -halfSize, iconSize, iconSize),\n        _paint,\n      );\n      canvas.drawLine(\n        const Offset(-iconSize / 4, 0),\n        const Offset(iconSize / 4, 0),\n        _paint,\n      );\n      if (!icon.isExpanded) {\n        canvas.drawLine(\n          const Offset(0, -iconSize / 4),\n          const Offset(0, iconSize / 4),\n          _paint,\n        );\n      }\n      if (!icon.isLast) {\n        canvas.drawLine(\n          const Offset(0, halfSize),\n          Offset(0, yExtent),\n          _paint,\n        );\n      }\n    } else {\n      canvas.drawLine(\n        Offset(0, icon.isFirst ? 0 : -yExtent),\n        Offset(0, icon.isLast ? -2.0 : yExtent),\n        _paint,\n      );\n      if (icon.isLast) {\n        canvas.drawCircle(Offset.zero, 2.0, _paint);\n      }\n    }\n    canvas.restore();\n  }\n}\n"
  },
  {
    "path": "packages/flame_studio/lib/src/widgets/toolbar/flame_studio_toolbar.dart",
    "content": "import 'package:flame_studio/src/core/theme.dart';\nimport 'package:flame_studio/src/widgets/toolbar/next_frame_button.dart';\nimport 'package:flame_studio/src/widgets/toolbar/pause_button.dart';\nimport 'package:flame_studio/src/widgets/toolbar/start_button.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nfinal toolbarHeightProvider = Provider((ref) => 28.0);\n\nclass FlameStudioToolbar extends ConsumerWidget {\n  const FlameStudioToolbar({super.key});\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    const middleButtons = <Widget>[\n      StartButton(),\n      PauseButton(),\n      NextFrameButton(),\n    ];\n\n    final height = ref.watch(toolbarHeightProvider);\n    final gap = height * 0.1;\n\n    return Container(\n      constraints: BoxConstraints.tightFor(height: height),\n      decoration: BoxDecoration(\n        boxShadow: const [\n          BoxShadow(\n            color: Color(0x66000000),\n            offset: Offset(0, 3),\n            blurRadius: 7.0,\n          ),\n          BoxShadow(\n            color: Color(0xbf000000),\n            offset: Offset(0, 1),\n            blurRadius: 2.0,\n          ),\n        ],\n        color: ref.watch(themeProvider).toolbarColor,\n      ),\n      child: Row(\n        textDirection: TextDirection.ltr,\n        children: [\n          Padding(\n            padding: EdgeInsets.fromLTRB(gap * 2, gap, gap * 10, gap * 2),\n            child: Image.asset('logo_flame.png', fit: BoxFit.scaleDown),\n          ),\n          Expanded(child: Container()),\n          for (final button in middleButtons)\n            Padding(padding: EdgeInsets.all(gap), child: button),\n          Expanded(child: Container()),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_studio/lib/src/widgets/toolbar/next_frame_button.dart",
    "content": "import 'package:flame_studio/src/core/game_controller.dart';\nimport 'package:flame_studio/src/widgets/toolbar/toolbar_button.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nclass NextFrameButton extends ConsumerWidget {\n  const NextFrameButton({super.key});\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final gameState = ref.watch(gameControllerProvider);\n\n    return ToolbarButton(\n      icon: Path()\n        ..moveTo(-4, 5)\n        ..lineTo(-4, -5)\n        ..lineTo(1, 0)\n        ..close()\n        ..addRect(const Rect.fromLTWH(1, -4.5, 3, 9)),\n      disabled: !gameState.paused,\n      onClick: () {\n        ref.read(gameControllerProvider.notifier).stepGame();\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_studio/lib/src/widgets/toolbar/pause_button.dart",
    "content": "import 'package:flame_studio/src/core/game_controller.dart';\nimport 'package:flame_studio/src/widgets/toolbar/toolbar_button.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nclass PauseButton extends ConsumerWidget {\n  const PauseButton({super.key});\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final gameState = ref.watch(gameControllerProvider);\n\n    return ToolbarButton(\n      icon: Path()\n        ..addRect(const Rect.fromLTRB(-4, -4.5, -1, 4.5))\n        ..addRect(const Rect.fromLTRB(1, -4.5, 4, 4.5)),\n      disabled: gameState.paused,\n      onClick: () {\n        ref.read(gameControllerProvider.notifier).pauseGame();\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_studio/lib/src/widgets/toolbar/start_button.dart",
    "content": "import 'package:flame_studio/src/core/game_controller.dart';\nimport 'package:flame_studio/src/widgets/toolbar/toolbar_button.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nclass StartButton extends ConsumerWidget {\n  const StartButton({super.key});\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final gameState = ref.watch(gameControllerProvider);\n\n    return ToolbarButton(\n      icon: Path()\n        ..moveTo(-2.5, 5)\n        ..lineTo(-2.5, -5)\n        ..lineTo(6, 0)\n        ..close(),\n      disabled: !gameState.paused,\n      onClick: () {\n        ref.read(gameControllerProvider.notifier).resumeGame();\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_studio/lib/src/widgets/toolbar/toolbar_button.dart",
    "content": "import 'package:flame_studio/src/core/theme.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nclass ToolbarButton extends ConsumerStatefulWidget {\n  const ToolbarButton({\n    required this.icon,\n    this.onClick,\n    this.disabled = false,\n    super.key,\n  });\n\n  final void Function()? onClick;\n  final Path icon;\n  final bool disabled;\n\n  @override\n  ToolbarButtonState createState() => ToolbarButtonState();\n}\n\nclass ToolbarButtonState extends ConsumerState<ToolbarButton> {\n  bool _isHovered = false;\n  bool _isActive = false;\n\n  @override\n  Widget build(BuildContext context) {\n    final painter = CustomPaint(\n      painter: _ToolbarButtonPainter(\n        widget.icon,\n        ref.watch(themeProvider),\n        isDisabled: widget.disabled,\n        isHovered: _isHovered,\n        isActive: _isActive,\n      ),\n    );\n\n    return AspectRatio(\n      aspectRatio: 1.25,\n      child: widget.disabled\n          ? painter\n          : MouseRegion(\n              onEnter: (_) => setState(() => _isHovered = true),\n              onExit: (_) => setState(() => _isHovered = false),\n              cursor: SystemMouseCursors.click,\n              child: GestureDetector(\n                onTapDown: (_) => setState(() => _isActive = true),\n                onTapCancel: () => setState(() => _isActive = false),\n                onTapUp: (_) {\n                  if (_isActive) {\n                    widget.onClick?.call();\n                  }\n                  setState(() => _isActive = false);\n                },\n                child: painter,\n              ),\n            ),\n    );\n  }\n\n  @override\n  void didUpdateWidget(ToolbarButton oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    if (widget.icon != oldWidget.icon ||\n        widget.disabled != oldWidget.disabled) {\n      _isActive = false;\n      _isHovered = false;\n    }\n  }\n}\n\nenum _ToolbarButtonRenderState {\n  disabled,\n  active,\n  hovered,\n  normal,\n}\n\nclass _ToolbarButtonPainter extends CustomPainter {\n  _ToolbarButtonPainter(\n    this.icon,\n    this.theme, {\n    required this.isDisabled,\n    required this.isHovered,\n    required this.isActive,\n  });\n\n  final bool isDisabled;\n  final bool isHovered;\n  final bool isActive;\n  final Path icon;\n  final Theme theme;\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    final scale = size.height / 20.0;\n    canvas.save();\n    canvas.scale(size.height / 20.0);\n\n    final renderState = _renderState;\n\n    final radius = Radius.circular(theme.buttonRadius);\n    final color = switch (renderState) {\n      _ToolbarButtonRenderState.disabled => theme.buttonDisabledColor,\n      _ToolbarButtonRenderState.active => theme.buttonActiveColor,\n      _ToolbarButtonRenderState.hovered => theme.buttonHoverColor,\n      _ToolbarButtonRenderState.normal => theme.buttonColor,\n    };\n    canvas.drawRRect(\n      RRect.fromLTRBR(0, 0, size.width / scale, 20.0, radius),\n      Paint()..color = color,\n    );\n\n    final textColor = switch (renderState) {\n      _ToolbarButtonRenderState.disabled => theme.buttonDisabledTextColor,\n      _ToolbarButtonRenderState.active => theme.buttonActiveTextColor,\n      _ToolbarButtonRenderState.hovered => theme.buttonHoverTextColor,\n      _ToolbarButtonRenderState.normal => theme.buttonTextColor,\n    };\n    canvas.translate(size.width / scale / 2, 10);\n    canvas.drawPath(icon, Paint()..color = textColor);\n    canvas.restore();\n  }\n\n  @override\n  bool shouldRepaint(_ToolbarButtonPainter old) {\n    return isHovered != old.isHovered ||\n        isActive != old.isActive ||\n        isDisabled != old.isDisabled ||\n        icon != old.icon;\n  }\n\n  _ToolbarButtonRenderState get _renderState {\n    if (isDisabled) {\n      return _ToolbarButtonRenderState.disabled;\n    }\n    if (isActive) {\n      return _ToolbarButtonRenderState.active;\n    }\n    if (isHovered) {\n      return _ToolbarButtonRenderState.hovered;\n    }\n    return _ToolbarButtonRenderState.normal;\n  }\n}\n"
  },
  {
    "path": "packages/flame_studio/lib/src/widgets/ui_scaffold.dart",
    "content": "import 'package:flame_studio/src/core/theme.dart';\nimport 'package:flame_studio/src/widgets/left_panel.dart';\nimport 'package:flame_studio/src/widgets/left_panel_grip.dart';\nimport 'package:flame_studio/src/widgets/toolbar/flame_studio_toolbar.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nclass UiScaffold extends ConsumerWidget {\n  const UiScaffold({required this.gameApp, super.key});\n\n  final Widget gameApp;\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final toolbarHeight = ref.watch(toolbarHeightProvider);\n    return Container(\n      color: ref.watch(themeProvider).backdropColor,\n      child: Stack(\n        textDirection: ref.watch(textDirectionProvider),\n        children: [\n          Container(\n            padding: EdgeInsets.fromLTRB(\n              ref.watch(leftPanelWidthProvider) + 20,\n              toolbarHeight + 20,\n              20,\n              20,\n            ),\n            child: ClipRect(child: gameApp),\n          ),\n          Container(\n            padding: EdgeInsets.fromLTRB(0, toolbarHeight, 0, 0),\n            child: const LeftPanel(),\n          ),\n          const LeftPanelGrip(),\n          const FlameStudioToolbar(),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_studio/pubspec.yaml",
    "content": "name: flame_studio\nresolution: workspace\ndescription: An IDE-like environment for developing and debugging Flame games.\nversion: 0.0.0\npublish_to: none\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_studio\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  collection: ^1.17.1\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  flutter_riverpod: ^3.0.3\n  meta: ^1.12.0\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n  test: any\n\nassets:\n  assets/logo_flame.png\n"
  },
  {
    "path": "packages/flame_svg/CHANGELOG.md",
    "content": "## 1.12.0\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n - **FEAT**: Support package argument in asset loading methods and widgets ([#3835](https://github.com/flame-engine/flame/issues/3835)). ([3f6f95b0](https://github.com/flame-engine/flame/commit/3f6f95b0225df9e4035c74de6cfad501e8c45167))\n\n## 1.11.20\n\n - Update a dependency to the latest release.\n\n## 1.11.19\n\n - Update a dependency to the latest release.\n\n## 1.11.18\n\n - Update a dependency to the latest release.\n\n## 1.11.17\n\n - Update a dependency to the latest release.\n\n## 1.11.16\n\n - Update a dependency to the latest release.\n\n## 1.11.15\n\n - Update a dependency to the latest release.\n\n## 1.11.14\n\n - Update a dependency to the latest release.\n\n## 1.11.13\n\n - **FIX**: Calculate zoom ratio before rendering svg ([#3616](https://github.com/flame-engine/flame/issues/3616)). ([f8b7ef82](https://github.com/flame-engine/flame/commit/f8b7ef82b7fc54af7171c94ae2112b18cebb236a))\n\n## 1.11.12\n\n - Update a dependency to the latest release.\n\n## 1.11.11\n\n - Update a dependency to the latest release.\n\n## 1.11.10\n\n - **FIX**: SvgComponent should use `drawImage` on first render too ([#3549](https://github.com/flame-engine/flame/issues/3549)). ([91721a6b](https://github.com/flame-engine/flame/commit/91721a6b7b2ecda338d64d3c982e448e9cd71122))\n\n## 1.11.9\n\n - Update a dependency to the latest release.\n\n## 1.11.8\n\n - Update a dependency to the latest release.\n\n## 1.11.7\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 1.11.6\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 1.11.5\n\n - Update a dependency to the latest release.\n\n## 1.11.4\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n\n## 1.11.3\n\n - Update a dependency to the latest release.\n\n## 1.11.2\n\n - Update a dependency to the latest release.\n\n## 1.11.1\n\n - Update a dependency to the latest release.\n\n## 1.11.0\n\n - **FEAT**: Fixing tests on flutter 3.24.0 ([#3259](https://github.com/flame-engine/flame/issues/3259)). ([bf9a2481](https://github.com/flame-engine/flame/commit/bf9a2481fbeb77413a26ae96b57843ca51411f9f))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 1.10.2\n\n - Update a dependency to the latest release.\n\n## 1.10.1\n\n - Update a dependency to the latest release.\n\n## 1.10.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 1.9.0\n\n - **FEAT**: Add `loadFromString` to Svg class ([#3030](https://github.com/flame-engine/flame/issues/3030)). ([b0cafb2a](https://github.com/flame-engine/flame/commit/b0cafb2a5561de136af93eb7a09df37b93d38ce0))\n\n## 1.8.10\n\n - Update a dependency to the latest release.\n\n## 1.8.9\n\n - Update a dependency to the latest release.\n\n## 1.8.8\n\n - Update a dependency to the latest release.\n\n## 1.8.7\n\n - Update a dependency to the latest release.\n\n## 1.8.6\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n\n## 1.8.5\n\n - Update a dependency to the latest release.\n\n## 1.8.4\n\n - **FIX**: Do not render SVGs bigger than requested when pixelRatio > 1 and no match in _imageCache ([#2795](https://github.com/flame-engine/flame/issues/2795)). ([5fa4e09f](https://github.com/flame-engine/flame/commit/5fa4e09f7c464ce2f676df81049a95dad46bf2bd))\n\n## 1.8.3\n\n - Update a dependency to the latest release.\n\n## 1.8.2\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **FIX**: Change to `FilterQuality.medium` instead of `high` ([#2733](https://github.com/flame-engine/flame/issues/2733)). ([fc19890c](https://github.com/flame-engine/flame/commit/fc19890c87a78599ea49ee0dfb52a04ea6b09a99))\n\n## 1.8.1\n\n - Update a dependency to the latest release.\n\n## 1.8.0\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FEAT**: ComponentKey API ([#2566](https://github.com/flame-engine/flame/issues/2566)). ([b3efb612](https://github.com/flame-engine/flame/commit/b3efb612cb3cb77f69bc030e9ba71516348035d2))\n\n## 1.7.4\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n## 1.7.3\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n - **FIX**: Remove memory leak when creating the image from PictureRecorder ([#2493](https://github.com/flame-engine/flame/issues/2493)). ([a66f2bc0](https://github.com/flame-engine/flame/commit/a66f2bc0a97415f4f57b6c55174a2930cdf9e61b))\n\n## 1.7.2\n\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n\n## 1.7.1\n\n - Update a dependency to the latest release.\n\n## 1.7.0\n\n - **FIX**: Exception when having multiple calls to dispose() function of a Svg instance ([#2085](https://github.com/flame-engine/flame/issues/2085)). ([a287904e](https://github.com/flame-engine/flame/commit/a287904eb5dbbe70128207a6f6a56ff98dfbf579))\n - **FIX**: SvgComponent getting blurred and pixelized ([#2084](https://github.com/flame-engine/flame/issues/2084)). ([0911d10b](https://github.com/flame-engine/flame/commit/0911d10b9177c0dbcc6f9ba927f99cb7e04182a5))\n - **FEAT**: Expose paint from svgComponent to set opacity and have opacity effects ([#2092](https://github.com/flame-engine/flame/issues/2092)). ([bedacd0c](https://github.com/flame-engine/flame/commit/bedacd0c8c79f4f060b002eeddaa9d2ef68d316c))\n\n## 1.6.0\n\n - **FEAT**: Add avoid_final_parameters, depend_on_referenced_packages, unnecessary_to_list_in_spreads ([#1927](https://github.com/flame-engine/flame/issues/1927)). ([deccb434](https://github.com/flame-engine/flame/commit/deccb4349d38b6a91ccf5bdf229980b2a3296ce5))\n\n## 1.5.0\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n## 1.4.0\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n## 1.3.0\n\n - **REFACTOR**: Update and guarantee consistency on mocktail dev dependency version across repo ([#1701](https://github.com/flame-engine/flame/issues/1701)). ([f4a98878](https://github.com/flame-engine/flame/commit/f4a98878062dbd4fe8238a8b014e6be3e528c5d8))\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Optional key for Images.load ([#1624](https://github.com/flame-engine/flame/issues/1624)). ([067c34b5](https://github.com/flame-engine/flame/commit/067c34b5f29e1a9bd51861d872092ae5ee0a551f))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n\n## 1.2.0\n\n - **FEAT**: Added children parameter to Component constructor (#1525). ([f0b31fcf](https://github.com/flame-engine/flame/commit/f0b31fcfc0fc6b0f8f96895ef6a68fd5a30a3159))\n\n## 1.1.0\n\n - Graduate package to a stable release. See pre-releases prior to this version for changelog entries.\n\n## 1.1.0-releasecandidate.5\n\n - Update a dependency to the latest release.\n\n## 1.1.0-releasecandidate.4\n\n - Update a dependency to the latest release.\n\n## 1.1.0-releasecandidate.3\n\n - Update a dependency to the latest release.\n\n## 1.1.0-releasecandidate.2\n\n - Update a dependency to the latest release.\n\n## 1.1.0-releasecandidate.1\n\n> Note: This release has breaking changes.\n\n - **FIX**: flame svg perfomance (#1373). ([bce24173](https://github.com/flame-engine/flame/commit/bce2417330b5165842f15d0409a213a1c5ad1cd3))\n - **FIX**: preventing svg rendering from affecting other renderings (#1339). ([6e66baa1](https://github.com/flame-engine/flame/commit/6e66baa12fdad7b27f35ca274433acd55165f106))\n - **FEAT**: adding default constructor on SvgComponent (#1334). ([00619f80](https://github.com/flame-engine/flame/commit/00619f80475b1e66802a6d2020ea6d521c84059d))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n - **BREAKING** **FEAT**: Use a broadphase to make collision detection more efficient (#1252). ([29dd09ca](https://github.com/flame-engine/flame/commit/29dd09ca925e934f3ca4e266a8a0cdb8ad62ef3b))\n\n# CHANGELOG\n\n## [1.0.0]\n - Update to Flame 1.0.0\n\n## [1.0.0-releasecandidate.15]\n - Takes priority as an argument on SvgComponent\n - Simplify implementation of `renderPosition`\n\n## [1.0.0-rc4]\n - Update to flutter_svg 0.22.0\n\n## [1.0.0-rc3]\n - Updated to use flame v1.0.0-releasecandidate.11\n\n## [1.0.0-rc2]\n - Updated to use flame v1.0.0-rc8\n - Migrate to null-safety\n\n## [1.0.0-rc1]\n - Updated to use flame v1.0.0\n\n## [0.0.1]\n - Initial release\n"
  },
  {
    "path": "packages/flame_svg/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_svg/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nAdds support for rendering SVG (using the <a href=\"https://github.com/dnfield/flutter_svg\">flutter_svg</a> library) to your <a href=\"https://github.com/flame-engine/flame\">Flame</a> games.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_svg\" ><img src=\"https://img.shields.io/pub/v/flame_svg.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n\n# flame_svg\n\nPackage to add SVG rendering support for the Flame Engine.\n\nMore [here](https://docs.flame-engine.org/main/images.html#svg).\n"
  },
  {
    "path": "packages/flame_svg/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    - public_member_api_docs\n"
  },
  {
    "path": "packages/flame_svg/example/.gitignore",
    "content": "linux/\nmacos/\nweb/\nwindows/\n\n# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\nmigrate_working_dir/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\n/build/\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Android Studio will place build artifacts here\n/android/app/debug\n/android/app/profile\n/android/app/release\n"
  },
  {
    "path": "packages/flame_svg/example/README.md",
    "content": "# Example of rendering an SVG image\n\nThis is just a very simple example of how to render SVG content.\n"
  },
  {
    "path": "packages/flame_svg/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    public_member_api_docs: false\n"
  },
  {
    "path": "packages/flame_svg/example/lib/main.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame_svg/flame_svg.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  runApp(GameWidget(game: MyGame()));\n}\n\nclass MyGame extends FlameGame {\n  late Svg svgInstance;\n\n  @override\n  void render(Canvas canvas) {\n    super.render(canvas);\n    svgInstance.renderPosition(canvas, Vector2(100, 200), Vector2.all(300));\n  }\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    svgInstance = await loadSvg('android.svg');\n    final android = SvgComponent(\n      svg: svgInstance,\n      position: Vector2.all(100),\n      size: Vector2.all(100),\n    );\n    add(android);\n  }\n}\n"
  },
  {
    "path": "packages/flame_svg/example/pubspec.yaml",
    "content": "name: flame_svg_example\nresolution: workspace\ndescription: Example app of how to use the flame_svg package\n\npublish_to: 'none'\n\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_svg: ^1.12.0\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n\nflutter:\n  uses-material-design: true\n  assets:\n    - assets/android.svg\n"
  },
  {
    "path": "packages/flame_svg/lib/flame_svg.dart",
    "content": "library flame_svg;\n\nexport './svg.dart';\nexport './svg_component.dart';\n"
  },
  {
    "path": "packages/flame_svg/lib/svg.dart",
    "content": "import 'dart:math' as math;\nimport 'dart:ui';\n\nimport 'package:flame/cache.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart' show WidgetsBinding;\nimport 'package:flutter_svg/flutter_svg.dart';\n\n/// A [Svg] to be rendered on a Flame [Game].\nclass Svg {\n  /// Creates an [Svg] with the received [pictureInfo].\n  /// Default [pixelRatio] is the device pixel ratio.\n  Svg(this.pictureInfo, {double? pixelRatio})\n    : pixelRatio =\n          pixelRatio ??\n          WidgetsBinding\n              .instance\n              .platformDispatcher\n              .views\n              .first\n              .devicePixelRatio;\n\n  /// The [PictureInfo] that this [Svg] represents.\n  final PictureInfo pictureInfo;\n\n  /// The pixel ratio that this [Svg] is rendered based on.\n  final double pixelRatio;\n\n  final MemoryCache<Size, Image> _imageCache = MemoryCache();\n\n  final _paint = Paint()..filterQuality = FilterQuality.medium;\n\n  /// Loads an [Svg] with the received [cache]. When no [cache] is provided,\n  /// the global [Flame.assets] is used.\n  static Future<Svg> load(\n    String fileName, {\n    AssetsCache? cache,\n    double? pixelRatio,\n    String? package,\n  }) async {\n    cache ??= Flame.assets;\n    final svgString = await cache.readFile(fileName, package: package);\n    return Svg.loadFromString(svgString, pixelRatio: pixelRatio);\n  }\n\n  /// Loads an [Svg] from a string.\n  static Future<Svg> loadFromString(\n    String svgString, {\n    double? pixelRatio,\n  }) async {\n    final pictureInfo = await vg.loadPicture(SvgStringLoader(svgString), null);\n    return Svg(\n      pictureInfo,\n      pixelRatio: pixelRatio,\n    );\n  }\n\n  /// Renders the svg on the [canvas] using the dimensions provided by [size].\n  void render(\n    Canvas canvas,\n    Vector2 size, {\n    Paint? overridePaint,\n  }) {\n    // Scale the canvas to the size of the destination clip bounds\n    // This is necessary to avoid blurriness when having a\n    // camera.viewfinder.zoom larger than 1.0\n    final destinationClipBounds = canvas.getDestinationClipBounds();\n    final localClipBounds = canvas.getLocalClipBounds();\n    final widthRatio =\n        destinationClipBounds.size.width / localClipBounds.size.width;\n    final heightRatio =\n        destinationClipBounds.size.height / localClipBounds.size.height;\n\n    final localSize = Size(size.x, size.y);\n    final image = _getImage(localSize, widthRatio, heightRatio);\n\n    canvas.save();\n    canvas.scale(\n      1 / (pixelRatio * widthRatio),\n      1 / (pixelRatio * heightRatio),\n    );\n    canvas.drawImage(\n      image,\n      Offset.zero,\n      overridePaint ?? _paint,\n    );\n    canvas.restore();\n  }\n\n  /// Renders the svg on the [canvas] on the given [position] using the\n  /// dimensions provided by [size].\n  void renderPosition(\n    Canvas canvas,\n    Vector2 position,\n    Vector2 size,\n  ) {\n    canvas.renderAt(position, (c) => render(c, size));\n  }\n\n  Image _getImage(Size size, double widthRatio, double heightRatio) {\n    final cacheKey = Size(size.width * widthRatio, size.height * heightRatio);\n    final image = _imageCache.getValue(cacheKey);\n\n    if (image == null) {\n      final recorder = PictureRecorder();\n      final canvas = Canvas(recorder);\n      canvas.scale(pixelRatio * widthRatio, pixelRatio * heightRatio);\n      _render(canvas, size);\n      final picture = recorder.endRecording();\n      final image = picture.toImageSync(\n        (size.width * pixelRatio * widthRatio).ceil(),\n        (size.height * pixelRatio * heightRatio).ceil(),\n      );\n\n      picture.dispose();\n      _imageCache.setValue(cacheKey, image);\n      return image;\n    }\n\n    return image;\n  }\n\n  void _render(Canvas canvas, Size size) {\n    final scale = math.min(\n      size.width / pictureInfo.size.width,\n      size.height / pictureInfo.size.height,\n    );\n    canvas.translate(\n      (size.width - pictureInfo.size.width * scale) / 2,\n      (size.height - pictureInfo.size.height * scale) / 2,\n    );\n    canvas.scale(scale);\n    canvas.drawPicture(pictureInfo.picture);\n  }\n\n  /// Clear all the stored cache from this SVG, be sure to call\n  /// this method once the instance is no longer needed to avoid\n  /// memory leaks\n  void dispose() {\n    _imageCache.keys.forEach((key) {\n      _imageCache.getValue(key)?.dispose();\n    });\n    _imageCache.clearCache();\n  }\n}\n\n/// Provides a loading method for [Svg] on the [Game] class.\nextension SvgLoader on Game {\n  /// Loads an [Svg] using the [Game]'s own asset loader.\n  Future<Svg> loadSvg(String fileName, {String? package}) =>\n      Svg.load(fileName, cache: assets, package: package);\n}\n"
  },
  {
    "path": "packages/flame_svg/lib/svg_component.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame_svg/svg.dart';\n\n/// Wraps [Svg] in a Flame component.\nclass SvgComponent extends PositionComponent with HasPaint {\n  /// Creates an [SvgComponent]\n  SvgComponent({\n    Svg? svg,\n    super.position,\n    super.size,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n    Paint? paint,\n    super.key,\n  }) : _svg = svg {\n    this.paint = paint ?? (this.paint..filterQuality = FilterQuality.medium);\n  }\n\n  /// The wrapped instance of [Svg].\n  Svg? _svg;\n\n  set svg(Svg? svg) {\n    _svg?.dispose();\n    _svg = svg;\n  }\n\n  /// Returns the current [svg] instance\n  Svg? get svg => _svg;\n\n  @override\n  void render(Canvas canvas) {\n    _svg?.render(canvas, size, overridePaint: paint);\n  }\n\n  @override\n  void onRemove() {\n    super.onRemove();\n\n    _svg?.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/flame_svg/pubspec.yaml",
    "content": "name: flame_svg\nresolution: workspace\ndescription: Package to add SVG rendering support for the Flame game engine\nversion: 1.12.0\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_svg\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - svg\n  - vector-graphics\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  flutter_svg: ^2.0.5\n\ndev_dependencies:\n  dartdoc: ^9.0.0\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n  mocktail: ^1.0.4\n  test: any\n"
  },
  {
    "path": "packages/flame_svg/test/svg_component_test.dart",
    "content": "import 'package:flame_svg/flame_svg.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nclass _MockSvg extends Mock implements Svg {}\n\nvoid main() {\n  group('SvgComponent', () {\n    late Svg svg;\n\n    setUp(() {\n      svg = _MockSvg();\n      when(svg.dispose).thenAnswer((_) {});\n    });\n\n    test('disposes the svg instance when it is removed', () {\n      final component = SvgComponent(svg: svg);\n      component.onRemove();\n\n      verify(svg.dispose).called(1);\n    });\n\n    test('disposes the old svg instance when a new one is received', () {\n      final component = SvgComponent(svg: svg);\n\n      final newSvg = _MockSvg();\n      component.svg = newSvg;\n\n      verify(svg.dispose).called(1);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_svg/test/svg_test.dart",
    "content": "import 'dart:io';\nimport 'dart:ui';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame_svg/svg.dart' as flame_svg;\nimport 'package:flutter/material.dart';\nimport 'package:flutter_svg/flutter_svg.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass _SvgPainter extends CustomPainter {\n  final flame_svg.Svg svg;\n\n  _SvgPainter(this.svg);\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    svg.render(canvas, Vector2(size.width, size.height));\n  }\n\n  @override\n  bool shouldRepaint(covariant CustomPainter oldDelegate) {\n    return false;\n  }\n}\n\nFuture<flame_svg.Svg> _parseSvgFromTestFile(String path) async {\n  final svgString = File(path).readAsStringSync();\n  final pictureInfo = await vg.loadPicture(SvgStringLoader(svgString), null);\n  return flame_svg.Svg(pictureInfo);\n}\n\nvoid main() {\n  group('Svg', () {\n    late flame_svg.Svg svgInstance;\n\n    setUp(() async {\n      svgInstance = await _parseSvgFromTestFile('test/_resources/android.svg');\n    });\n\n    test('multiple calls to dispose should not throw error', () async {\n      svgInstance.render(Canvas(PictureRecorder()), Vector2.all(100));\n      await Future<void>.delayed(const Duration(milliseconds: 200));\n      svgInstance.dispose();\n      svgInstance.dispose();\n    });\n\n    test('load from svg string', () async {\n      final svg = await flame_svg.Svg.loadFromString(\n        File('test/_resources/android.svg').readAsStringSync(),\n      );\n      expect(svg, isNotNull);\n    });\n\n    testWidgets(\n      'render sharply',\n      (tester) async {\n        final flameSvg = await _parseSvgFromTestFile(\n          'test/_resources/hand.svg',\n        );\n        flameSvg.render(Canvas(PictureRecorder()), Vector2.all(300));\n        await tester.binding.setSurfaceSize(const Size(800, 600));\n        tester.view.devicePixelRatio = 3;\n        await tester.pumpWidget(\n          MaterialApp(\n            debugShowCheckedModeBanner: false,\n            home: Scaffold(\n              body: Column(\n                children: [\n                  Expanded(\n                    child: Center(\n                      child: SvgPicture.string(\n                        File('test/_resources/hand.svg').readAsStringSync(),\n                        width: 300,\n                        height: 300,\n                      ),\n                    ),\n                  ),\n                  Expanded(\n                    child: CustomPaint(\n                      size: const Size(300, 300),\n                      painter: _SvgPainter(flameSvg),\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await expectLater(\n          find.byType(MaterialApp),\n          matchesGoldenFile('./_goldens/render_sharply.png'),\n        );\n      },\n    );\n\n    testWidgets(\n      'render sharply with viewfinder zoom',\n      (tester) async {\n        addTearDown(() async {\n          await tester.binding.setSurfaceSize(null);\n        });\n\n        final flameSvg = await _parseSvgFromTestFile(\n          'test/_resources/hand.svg',\n        );\n\n        tester.view.devicePixelRatio = 1;\n        await tester.binding.setSurfaceSize(const Size(100, 100));\n\n        await tester.pumpWidget(\n          MaterialApp(\n            debugShowCheckedModeBanner: false,\n            home: Transform.scale(\n              scale: 2,\n              child: CustomPaint(\n                painter: _SvgPainter(flameSvg),\n              ),\n            ),\n          ),\n        );\n\n        await tester.pumpAndSettle();\n        await expectLater(\n          find.byType(MaterialApp),\n          matchesGoldenFile(\n            './_goldens/render_sharply_with_viewfinder_zoom.png',\n          ),\n        );\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_test/.min_coverage",
    "content": "54.0\n"
  },
  {
    "path": "packages/flame_test/CHANGELOG.md",
    "content": "## 2.2.3\n\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n\n## 2.2.2\n\n - Update a dependency to the latest release.\n\n## 2.2.1\n\n - Update a dependency to the latest release.\n\n## 2.2.0\n\n - **FEAT**: Add scaling gesture for components ([#3770](https://github.com/flame-engine/flame/issues/3770)). ([f413eddb](https://github.com/flame-engine/flame/commit/f413eddbf1581f30087ba53f9516e22e035bda7a))\n\n## 2.1.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FEAT**: Support secondary taps (right click) on new callbacks system ([#3741](https://github.com/flame-engine/flame/issues/3741)). ([46bd3856](https://github.com/flame-engine/flame/commit/46bd385675ae781c4614d997e4792f53fc43271d))\n\n## 2.0.3\n\n - Update a dependency to the latest release.\n\n## 2.0.2\n\n - Update a dependency to the latest release.\n\n## 2.0.1\n\n - Update a dependency to the latest release.\n\n## 2.0.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FEAT**: Pass `WidgetTester` for `testGolden` prepare function ([#3624](https://github.com/flame-engine/flame/issues/3624)). ([10509326](https://github.com/flame-engine/flame/commit/105093266431408db0f9e74042e03e2234d9b22e))\n\n## 1.19.2\n\n - Update a dependency to the latest release.\n\n## 1.19.1\n\n - Update a dependency to the latest release.\n\n## 1.19.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Use 32bit Vector2 in Flame to be compatible with shaders and forge2d ([#3515](https://github.com/flame-engine/flame/issues/3515)). ([19dcecf5](https://github.com/flame-engine/flame/commit/19dcecf5df85345fd4fac168e1f360cee4665658))\n\n## 1.18.3\n\n - Update a dependency to the latest release.\n\n## 1.18.2\n\n - Update a dependency to the latest release.\n\n## 1.18.1\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 1.18.0\n\n - **FEAT**: Implement closeToVector4 and closeToQuaternion by extracing a generic CloseToVector base ([#3480](https://github.com/flame-engine/flame/issues/3480)). ([57e58514](https://github.com/flame-engine/flame/commit/57e58514091248884505d3936e3e0aa076efb30a))\n - **FEAT**: Add closeToMatrix4 test matcher ([#3478](https://github.com/flame-engine/flame/issues/3478)). ([8db2476e](https://github.com/flame-engine/flame/commit/8db2476e8c39723670641f1c2646ecf0d7bb09fb))\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 1.17.5\n\n - **FIX**: Don't use a future when assets for SpriteButton is already loaded (#3456).\n\n## 1.17.4\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n\n## 1.17.3\n\n - Update a dependency to the latest release.\n\n## 1.17.2\n\n - **FIX**: Widgets flickering ([#3343](https://github.com/flame-engine/flame/issues/3343)). ([ff170dc5](https://github.com/flame-engine/flame/commit/ff170dc5c2acc41190249b48e61767ea459fabb4))\n - **FIX**: Fix SpriteBatch to comply with new drawAtlas requirement ([#3338](https://github.com/flame-engine/flame/issues/3338)). ([a17fe4cd](https://github.com/flame-engine/flame/commit/a17fe4cdfaafa071cfd2ab8ef8279b26b79f00a7))\n\n## 1.17.1\n\n - Update a dependency to the latest release.\n\n## 1.17.0\n\n - **FEAT**: Add a closeToVector3 matcher to flame_test ([#3242](https://github.com/flame-engine/flame/issues/3242)). ([965b684a](https://github.com/flame-engine/flame/commit/965b684a286ae2e2a89ba303839004d0b12cb3ef))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 1.16.2\n\n - Update a dependency to the latest release.\n\n## 1.16.1\n\n - Update a dependency to the latest release.\n\n## 1.16.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 1.15.4\n\n - **FIX**: Lifecycle completers to be called for FlameGame ([#3007](https://github.com/flame-engine/flame/issues/3007)). ([3804f524](https://github.com/flame-engine/flame/commit/3804f52434cf1bcaf28b501bf96858ecd3636164))\n\n## 1.15.3\n\n - Update a dependency to the latest release.\n\n## 1.15.2\n\n - Update a dependency to the latest release.\n\n## 1.15.1\n\n - Update a dependency to the latest release.\n\n## 1.15.0\n\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n - **FEAT**: Expose addition test arguments on flame test ([#2832](https://github.com/flame-engine/flame/issues/2832)). ([08b4bd6d](https://github.com/flame-engine/flame/commit/08b4bd6d3f308013d18fec4eb126927239cd6481))\n\n## 1.14.0\n\n - **FEAT**: Expose addition test arguments on flame test ([#2832](https://github.com/flame-engine/flame/issues/2832)). ([08b4bd6d](https://github.com/flame-engine/flame/commit/08b4bd6d3f308013d18fec4eb126927239cd6481))\n\n## 1.13.2\n\n - **REFACTOR**: Remove unnecessary 'async' keyword across the codebase [DCM] ([#2803](https://github.com/flame-engine/flame/issues/2803)). ([2dfe0e5a](https://github.com/flame-engine/flame/commit/2dfe0e5a431213c7148ab6389e3e8c8dc49fbf3d))\n\n## 1.13.1\n\n - Update a dependency to the latest release.\n\n## 1.13.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **FIX**: HasGameReference should default to FlameGame ([#2710](https://github.com/flame-engine/flame/issues/2710)). ([93dcb3a1](https://github.com/flame-engine/flame/commit/93dcb3a117c365767e3f20569b2d82abc8a7b152))\n - **FEAT**: Add HoverCallbacks ([#2706](https://github.com/flame-engine/flame/issues/2706)). ([d460b846](https://github.com/flame-engine/flame/commit/d460b846c23fb1f67041469c99c81e4c78b89c2e))\n - **BREAKING** **REFACTOR**: Rename (Text) Elements, Nodes and Styles for clarity, add docs ([#2700](https://github.com/flame-engine/flame/issues/2700)). ([4b420b79](https://github.com/flame-engine/flame/commit/4b420b7952ab8d675140b9d8d132015ff2780f92))\n - **BREAKING** **REFACTOR**: Kill TextRenderer, Long Live TextRenderer ([#2683](https://github.com/flame-engine/flame/issues/2683)). ([a1cb9a06](https://github.com/flame-engine/flame/commit/a1cb9a06ada6f87bf22bc20e3c190ccd53517389))\n - **BREAKING** **REFACTOR**: Make TextElement more usable on its own ([#2679](https://github.com/flame-engine/flame/issues/2679)). ([1a64443c](https://github.com/flame-engine/flame/commit/1a64443ccaae32e71fe7d016ad1e8f18a75c93da))\n - **BREAKING** **FEAT**: Add CameraComponent to FlameGame ([#2740](https://github.com/flame-engine/flame/issues/2740)). ([7c2f4000](https://github.com/flame-engine/flame/commit/7c2f4000761580dbabb5d73b27f64d5819b34e8d))\n\n## 1.12.1\n\n - Update a dependency to the latest release.\n\n## 1.12.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Set constraint for test dependency in flame_test ([#2558](https://github.com/flame-engine/flame/issues/2558)). ([aeef9464](https://github.com/flame-engine/flame/commit/aeef9464f6ca448e3aa2b578af8b3443cbbf6f71))\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **BREAKING** **FIX**: Convert PositionEvent.canvasPosition to local coordinates ([#2598](https://github.com/flame-engine/flame/issues/2598)). ([87139c85](https://github.com/flame-engine/flame/commit/87139c854534782638fe1b0c24d2dc92f98a3e59))\n\n## 1.11.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n - **BREAKING** **REFACTOR**: Move `CameraComponent` and events out of experimental ([#2505](https://github.com/flame-engine/flame/issues/2505)). ([87b8a067](https://github.com/flame-engine/flame/commit/87b8a067f3e0096cebff3db4f5767e68616928fd))\n\n## 1.10.1\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n## 1.10.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Override `remove()` method to fix the functionality issue in the `FlameMultiBlocProvider` ([#2280](https://github.com/flame-engine/flame/issues/2280)). ([6a818464](https://github.com/flame-engine/flame/commit/6a818464f5f942ce25c3c3c59839b6bddaada386))\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n - **BREAKING** **FEAT**: The `HasTappableComponents` mixin is no longer needed ([#2450](https://github.com/flame-engine/flame/issues/2450)). ([b5bdf4ec](https://github.com/flame-engine/flame/commit/b5bdf4ec173e87907a59a9f62fcdf35cc968af2a))\n\n## 1.9.2\n\n - **FIX**: Only use initialized game for tests and remove setMount from onGameResize ([#2246](https://github.com/flame-engine/flame/issues/2246)). ([2a0f1d4b](https://github.com/flame-engine/flame/commit/2a0f1d4bdc2688e596481aad39762f94bf1cc8f1))\n - **FIX**: Depend on test: any for flame_test ([#2207](https://github.com/flame-engine/flame/issues/2207)). ([acfd418d](https://github.com/flame-engine/flame/commit/acfd418d882ee6872f3aa9961c39680ec123c2e6))\n - **FIX**: Depend on test: any for flame_test. ([fcf5521c](https://github.com/flame-engine/flame/commit/fcf5521ce4e975830f728481591a1731ce5edb77))\n\n## 1.9.1\n\n - **FIX**: Depend on test: any for flame_test. ([fcf5521c](https://github.com/flame-engine/flame/commit/fcf5521ce4e975830f728481591a1731ce5edb77))\n\n## 1.9.0\n\n - **FEAT**: Add support for styles propagating through the text node tree ([#1915](https://github.com/flame-engine/flame/issues/1915)). ([b5780d42](https://github.com/flame-engine/flame/commit/b5780d421234636144794e663559cec8987656a4))\n\n## 1.8.0\n\n> Note: This release has breaking changes.\n\n - **FEAT**: Add avoid_final_parameters, depend_on_referenced_packages, unnecessary_to_list_in_spreads ([#1927](https://github.com/flame-engine/flame/issues/1927)). ([deccb434](https://github.com/flame-engine/flame/commit/deccb4349d38b6a91ccf5bdf229980b2a3296ce5))\n - **FEAT**: Added DebugTextFormatter ([#1921](https://github.com/flame-engine/flame/issues/1921)). ([426827d1](https://github.com/flame-engine/flame/commit/426827d19e803158dab271dce1fbf93bd09f07de))\n - **BREAKING** **CHORE**: Remove functions/classes that were scheduled for removal in v1.3.0 ([#1867](https://github.com/flame-engine/flame/issues/1867)). ([00ab347c](https://github.com/flame-engine/flame/commit/00ab347c57b151c9232c85150e36a8a7781511a3))\n\n## 1.7.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Added size parameter for testGolden() ([#1780](https://github.com/flame-engine/flame/issues/1780)). ([8e41d83e](https://github.com/flame-engine/flame/commit/8e41d83ea4e057e1a428f0456450d697351683bf))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **BREAKING** **REFACTOR**: Matcher closeToVector() now accepts Vector2 as an argument ([#1761](https://github.com/flame-engine/flame/issues/1761)). ([c5083501](https://github.com/flame-engine/flame/commit/c5083501d54023f04d3f09e3358bce039ade9a20))\n\n## 1.6.0\n\n> Note: This release has breaking changes.\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FEAT**: Added size parameter for testGolden() ([#1780](https://github.com/flame-engine/flame/issues/1780)). ([8e41d83e](https://github.com/flame-engine/flame/commit/8e41d83ea4e057e1a428f0456450d697351683bf))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n - **BREAKING** **REFACTOR**: Matcher closeToVector() now accepts Vector2 as an argument ([#1761](https://github.com/flame-engine/flame/issues/1761)). ([c5083501](https://github.com/flame-engine/flame/commit/c5083501d54023f04d3f09e3358bce039ade9a20))\n\n## 1.5.0\n\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Callbacks in `HudButtonComponent` constructor and `ViewportMargin` mixin to avoid code duplication ([#1685](https://github.com/flame-engine/flame/issues/1685)). ([f55b2e0d](https://github.com/flame-engine/flame/commit/f55b2e0dc01c98718e4871430c6745472c221821))\n - **FEAT**: Aligned text in the TextBoxComponent ([#1620](https://github.com/flame-engine/flame/issues/1620)). ([c64aedae](https://github.com/flame-engine/flame/commit/c64aedaeb3fed908722b8872b71e288ff87bc761))\n - **FEAT**: add options to flutter test ([#1690](https://github.com/flame-engine/flame/issues/1690)). ([5dcf2664](https://github.com/flame-engine/flame/commit/5dcf26642363dd245f541c76d6190f8d523c1acb))\n - **FEAT**: Add helper function for creating golden tests ([#1623](https://github.com/flame-engine/flame/issues/1623)). ([d0faaada](https://github.com/flame-engine/flame/commit/d0faaada2bb971c2dde5a37dfa20d316c532ea28))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n\n## 1.4.0\n\n - **FEAT**: Added closeToAabb() (#1531). ([f7b6cc69](https://github.com/flame-engine/flame/commit/f7b6cc69abd6af89cafd892c7f2518b9b7bf3fc6))\n - **FEAT**: flame tests can now generate golden tests (#1501). ([316a0b3b](https://github.com/flame-engine/flame/commit/316a0b3bb0996ed20a3b93175102524b38bfa3e2))\n\n## 1.3.0\n\n - **FIX**: Fix calculation of AABB for `ShapeHitbox`es (#1481). ([a559d9a1](https://github.com/flame-engine/flame/commit/a559d9a12bfb42e161469745795fb91cdf161f8b))\n - **FEAT**: flame tests can now generate golden tests (#1501). ([316a0b3b](https://github.com/flame-engine/flame/commit/316a0b3bb0996ed20a3b93175102524b38bfa3e2))\n\n## 1.2.0\n\n - Graduate package to a stable release. See pre-releases prior to this version for changelog entries.\n\n## 1.2.0-releasecandidate.6\n\n - Update a dependency to the latest release.\n\n## 1.2.0-releasecandidate.5\n\n - Update a dependency to the latest release.\n\n## 1.2.0-releasecandidate.4\n\n - Update a dependency to the latest release.\n\n## 1.2.0-releasecandidate.3\n\n - Update a dependency to the latest release.\n\n## 1.2.0-releasecandidate.2\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Add a few more rules to flame_lint, including use_key_in_widget_constructors (#1248). ([bac6c8a4](https://github.com/flame-engine/flame/commit/bac6c8a4469f2c5c2926335f2f589eec9b1a5b5b))\n - **FIX**: remove vector_math dependency (#1361). ([56b33da2](https://github.com/flame-engine/flame/commit/56b33da29cfe547db75c89d97a09550a324fb0f5))\n - **FEAT**: Components are now always added in the correct order (#1337). ([c753fc46](https://github.com/flame-engine/flame/commit/c753fc4636d337d850a5a5cc684be8155f08b214))\n - **FEAT**: Added parameter repeatCount into function testRandom (#1265). ([49a2d0b9](https://github.com/flame-engine/flame/commit/49a2d0b9ec00fa9067756dd975e8b3ffd19de0bc))\n - **FEAT**: Added closeToVector in flame_test (#1245). ([af45ea6c](https://github.com/flame-engine/flame/commit/af45ea6cc4b5de80ecb27f07b827f55567cf602b))\n - **DOCS**: Upgrade documentation site (#1365). ([12cf8f70](https://github.com/flame-engine/flame/commit/12cf8f70963dc25b4e12182d0c7d80fe7d5a00e0))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n - **BREAKING** **FEAT**: Added SequenceEffect (#1218). ([7c6ae6de](https://github.com/flame-engine/flame/commit/7c6ae6def36ae5feb813fb2ba15d6fb3b9aaf341))\n\n## 1.2.0-releasecandidate.1\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Add a few more rules to flame_lint, including use_key_in_widget_constructors (#1248). ([bac6c8a4](https://github.com/flame-engine/flame/commit/bac6c8a4469f2c5c2926335f2f589eec9b1a5b5b))\n - **FIX**: remove vector_math dependency (#1361). ([56b33da2](https://github.com/flame-engine/flame/commit/56b33da29cfe547db75c89d97a09550a324fb0f5))\n - **FEAT**: Components are now always added in the correct order (#1337). ([c753fc46](https://github.com/flame-engine/flame/commit/c753fc4636d337d850a5a5cc684be8155f08b214))\n - **FEAT**: Added parameter repeatCount into function testRandom (#1265). ([49a2d0b9](https://github.com/flame-engine/flame/commit/49a2d0b9ec00fa9067756dd975e8b3ffd19de0bc))\n - **FEAT**: Added closeToVector in flame_test (#1245). ([af45ea6c](https://github.com/flame-engine/flame/commit/af45ea6cc4b5de80ecb27f07b827f55567cf602b))\n - **DOCS**: Upgrade documentation site (#1365). ([12cf8f70](https://github.com/flame-engine/flame/commit/12cf8f70963dc25b4e12182d0c7d80fe7d5a00e0))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n - **BREAKING** **FEAT**: Added SequenceEffect (#1218). ([7c6ae6de](https://github.com/flame-engine/flame/commit/7c6ae6def36ae5feb813fb2ba15d6fb3b9aaf341))\n\n## 1.1.0\n\n - Add `closeToVector` matcher.\n\n## 1.0.1\n\n - Bump versions not to use paths\n\n## 1.0.0\n\n - Fix tests with multiples futures finishing abruptly\n\n## 1.0.0-releasecandidate.15\n\n - Add `pumpWidget` to flame widget test\n\n## 0.1.1-releasecandidate.14\n\n - Add `flameTest` and `flameWidgetTest`\n\n## 0.1.1-releasecandidate.13\n\n - Move dartdoc and dart_code_metrics to dev_dependencies\n\n## 0.1.0-releasecandidate.13\n\n - Initial release containing classes to help testing classes using `Flame`\n"
  },
  {
    "path": "packages/flame_test/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_test/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nA set of useful helpers and utilities for testing your <a href=\"https://github.com/flame-engine/flame\">Flame</a> games.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_test\" ><img src=\"https://img.shields.io/pub/v/flame_test.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n\n# flame_test\n\nThis package contains classes that helps with testing applications using the Flame Engine.  \nIt is also used for testing parts of Flame itself.\n"
  },
  {
    "path": "packages/flame_test/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_test/example/README.md",
    "content": "# Example of how to use flame_test\n\nThis is just a very simple example of how to use `flame_test`, most relevant\ncode is in the test directory.\n\nYou can also find many examples in the test directory of Flame for how to use\nit.\n"
  },
  {
    "path": "packages/flame_test/example/lib/game.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/material.dart';\n\nclass MyGameWidget extends StatelessWidget {\n  const MyGameWidget({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return GameWidget(game: MyGame());\n  }\n}\n\nclass Background extends SpriteComponent with HasGameReference<MyGame> {\n  @override\n  Future<void> onLoad() async {\n    sprite = await game.loadSprite('city.png');\n    size = Vector2.all(200);\n    position = Vector2.all(100);\n  }\n}\n\nclass MyGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    await world.add(Background());\n  }\n}\n"
  },
  {
    "path": "packages/flame_test/example/lib/main.dart",
    "content": "import 'package:flame/extensions.dart';\n\nvoid main() {}\n\nclass MyVectorChanger {\n  Vector2 addOne(Vector2 vector) {\n    return vector + Vector2.all(1.0);\n  }\n}\n\nclass MyDoubleChanger {\n  double addOne(double number) {\n    return number + 1.0;\n  }\n}\n"
  },
  {
    "path": "packages/flame_test/example/pubspec.yaml",
    "content": "name: flame_test_example\nresolution: workspace\ndescription: Example of how to test with the flame_test package\npublish_to: 'none'\n\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flame_test: ^2.2.3\n  flutter_test:\n    sdk: flutter\n  test: any\n\nflutter:\n  assets:\n    - assets/images/\n"
  },
  {
    "path": "packages/flame_test/example/test/flame_test_test.dart",
    "content": "import 'package:flame_test/flame_test.dart';\nimport 'package:flame_test_example/game.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nfinal myGame = FlameTester(MyGame.new);\nvoid main() {\n  group('flameTest', () {\n    TestWidgetsFlutterBinding.ensureInitialized();\n\n    testWithGame<MyGame>(\n      'can load the game',\n      MyGame.new,\n      (game) async {\n        expect(game.world.children.length, 1);\n      },\n    );\n\n    myGame.testGameWidget(\n      'render the game widget',\n      verify: (game, tester) async {\n        expect(\n          find.byGame<MyGame>(),\n          findsOneWidget,\n        );\n      },\n    );\n\n    myGame.testGameWidget(\n      'render the background correctly',\n      setUp: (game, _) async {\n        await game.ready();\n      },\n      verify: (game, tester) async {\n        await expectLater(\n          find.byGame<MyGame>(),\n          matchesGoldenFile('goldens/game.png'),\n        );\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_test/example/test/main_test.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flame_test_example/main.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('flame_test', () {\n    test('can test vector2', () {\n      final changer = MyVectorChanger();\n      final vector = Vector2.all(1.0);\n      final changedVector = changer.addOne(vector);\n      expect(vector + Vector2.all(1.0), closeToVector(changedVector));\n      expect(vector + Vector2.all(1.1), closeToVector(changedVector, 0.2));\n    });\n\n    test('can test double', () {\n      final changer = MyDoubleChanger();\n      const one = 1.0;\n      final two = changer.addOne(one);\n      expectDouble(one + 1.0, two);\n      expectDouble(one + 1.1, two, epsilon: 0.11);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_test/lib/flame_test.dart",
    "content": "export 'src/close_to_aabb.dart';\nexport 'src/close_to_matrix4.dart';\nexport 'src/close_to_quaternion.dart';\nexport 'src/close_to_vector.dart';\nexport 'src/close_to_vector3.dart';\nexport 'src/close_to_vector4.dart';\nexport 'src/debug_text_renderer.dart';\nexport 'src/epsilon.dart';\nexport 'src/expect_color.dart';\nexport 'src/expect_double.dart';\nexport 'src/fails_assert.dart';\nexport 'src/flame_test.dart';\nexport 'src/mock_gesture_events.dart';\nexport 'src/mock_image.dart';\nexport 'src/mock_pointer_move_event.dart';\nexport 'src/mock_tap_drag_events.dart';\nexport 'src/random_test.dart';\nexport 'src/test_flame_game.dart';\nexport 'src/test_golden.dart' show testGolden;\n"
  },
  {
    "path": "packages/flame_test/lib/src/close_to_aabb.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// Returns a matcher which checks if the argument is an axis-aligned bounding\n/// box sufficiently close (within distance [epsilon]) to [expected]. Example\n/// of usage:\n/// ```dart\n/// expect(\n///   component.aabb,\n///   closeToAabb(Aabb2.minMax(Vector2.zero(), Vector2.all(1)), 1e-5),\n/// );\n/// ```\nMatcher closeToAabb(Aabb2 expected, [double epsilon = 1e-15]) {\n  return _IsCloseToAabb(expected, epsilon);\n}\n\nclass _IsCloseToAabb extends Matcher {\n  const _IsCloseToAabb(this._expected, this._epsilon);\n\n  final Aabb2 _expected;\n  final double _epsilon;\n\n  @override\n  bool matches(dynamic item, Map matchState) {\n    return (item is Aabb2) &&\n        (item.min - _expected.min).length <= _epsilon &&\n        (item.max - _expected.max).length <= _epsilon;\n  }\n\n  @override\n  Description describe(Description description) {\n    return description.add(\n      'an Aabb2 object within $_epsilon of Aabb2([${_expected.min.x}, '\n      '${_expected.min.y}]..[${_expected.max.x}, ${_expected.max.y}])',\n    );\n  }\n\n  @override\n  Description describeMismatch(\n    dynamic item,\n    Description mismatchDescription,\n    Map matchState,\n    bool verbose,\n  ) {\n    if (item is! Aabb2) {\n      return mismatchDescription.add('is not an instance of Aabb2');\n    }\n    final minDiff = (item.min - _expected.min).length;\n    final maxDiff = (item.max - _expected.max).length;\n    if (minDiff > _epsilon) {\n      mismatchDescription.add('min corner is at distance $minDiff\\n');\n    }\n    if (maxDiff > _epsilon) {\n      mismatchDescription.add('max corner is at distance $maxDiff\\n');\n    }\n    return mismatchDescription.add(\n      'Aabb2([${item.min.x}, ${item.min.y}]..[${item.max.x}, ${item.max.y}])',\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_test/lib/src/close_to_matrix4.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// Returns a matcher which checks if each entry in a 4x4 matrix is within\n/// a distance [epsilon] of the corresponding entry in [matrix].\n///\n/// For example:\n///\n/// ```dart\n/// expect(\n///   transform,\n///   closeToMatrix4(\n///     Matrix4.columns(\n///       Vector4(0, -1, 0, 0),\n///       Vector4(1, 0, 0, 0),\n///       Vector4(0, 0, 1, 0),\n///       Vector4(0, 0, 0, 1),\n///     ),\n///   ),\n/// );\n/// ```\nMatcher closeToMatrix4(Matrix4 matrix, [double epsilon = 1e-15]) {\n  return _IsCloseTo(matrix, epsilon);\n}\n\nclass _IsCloseTo extends Matcher {\n  const _IsCloseTo(this._value, this._epsilon);\n\n  final Matrix4 _value;\n  final double _epsilon;\n\n  @override\n  bool matches(dynamic item, Map matchState) {\n    if (item is! Matrix4) {\n      return false;\n    }\n    for (var i = 0; i < 15; i++) {\n      if ((_value[i] - item[i]).abs() > _epsilon) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  @override\n  Description describe(Description description) {\n    final entries = _value.storage.join(', ');\n    return description.add('a Matrix4 object within $_epsilon of ($entries)');\n  }\n\n  @override\n  Description describeMismatch(\n    dynamic item,\n    Description mismatchDescription,\n    Map matchState,\n    bool verbose,\n  ) {\n    if (item is! Matrix4) {\n      return mismatchDescription.add('is not an instance of Matrix4');\n    }\n    final entries = _value.storage.join(', ');\n    return mismatchDescription.add('is not within $_epsilon of ($entries)');\n  }\n}\n"
  },
  {
    "path": "packages/flame_test/lib/src/close_to_quaternion.dart",
    "content": "import 'package:flame_test/src/is_close_to_vector.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// Returns a matcher which checks if the argument is a Quaternion within\n/// distance [epsilon] of [quaternion]. For example:\n///\n/// ```dart\n/// expect(\n///   rotation,\n///   closeToQuaternion(Quaternion.axisAngle(Vector3(1, 0, 0), pi / 2)),\n/// );\n/// ```\nMatcher closeToQuaternion(Quaternion quaternion, [double epsilon = 1e-15]) {\n  return _IsCloseToQuaternion(quaternion, epsilon);\n}\n\nclass _IsCloseToQuaternion extends IsCloseToVector<Quaternion> {\n  const _IsCloseToQuaternion(super.value, super.epsilon);\n\n  @override\n  double dist(Quaternion a, Quaternion b) => (a - b).length;\n\n  @override\n  List<double> storage(Quaternion value) => value.storage;\n}\n"
  },
  {
    "path": "packages/flame_test/lib/src/close_to_vector.dart",
    "content": "import 'package:flame_test/src/is_close_to_vector.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// Returns a matcher which checks if the argument is a vector within distance\n/// [epsilon] of [vector]. For example:\n///\n/// ```dart\n/// expect(scale, closeToVector(Vector2(2, -2)));\n/// expect(position, closeToVector(expectedPosition, 1e-10));\n/// ```\nMatcher closeToVector(Vector2 vector, [double epsilon = 1e-15]) {\n  return _IsCloseToVector2(vector, epsilon);\n}\n\nclass _IsCloseToVector2 extends IsCloseToVector<Vector2> {\n  const _IsCloseToVector2(super.value, super.epsilon);\n\n  @override\n  double dist(Vector2 a, Vector2 b) => (a - b).length;\n\n  @override\n  List<double> storage(Vector2 value) => value.storage;\n}\n"
  },
  {
    "path": "packages/flame_test/lib/src/close_to_vector3.dart",
    "content": "import 'package:flame_test/src/is_close_to_vector.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// Returns a matcher which checks if the argument is a 3d vector within\n/// distance [epsilon] of [vector]. For example:\n///\n/// ```dart\n/// expect(scale, closeToVector3(Vector3(2, -2, 0)));\n/// expect(position, closeToVector3(expectedPosition, 1e-10));\n/// ```\nMatcher closeToVector3(Vector3 vector, [double epsilon = 1e-15]) {\n  return _IsCloseToVector3(vector, epsilon);\n}\n\nclass _IsCloseToVector3 extends IsCloseToVector<Vector3> {\n  const _IsCloseToVector3(super.value, super.epsilon);\n\n  @override\n  double dist(Vector3 a, Vector3 b) => (a - b).length;\n\n  @override\n  List<double> storage(Vector3 value) => value.storage;\n}\n"
  },
  {
    "path": "packages/flame_test/lib/src/close_to_vector4.dart",
    "content": "import 'package:flame_test/src/is_close_to_vector.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:vector_math/vector_math.dart';\n\n/// Returns a matcher which checks if the argument is a 4d vector within\n/// distance [epsilon] of [vector]. For example:\n///\n/// ```dart\n/// expect(matrix4.row1, closeToVector4(Vector4(1, 0, 0, 1)));\n/// expect(matrix4.row2, closeToVector4(Vector4(0, 1, 0, -1), 1e-10));\n/// ```\nMatcher closeToVector4(Vector4 vector, [double epsilon = 1e-15]) {\n  return _IsCloseToVector4(vector, epsilon);\n}\n\nclass _IsCloseToVector4 extends IsCloseToVector<Vector4> {\n  const _IsCloseToVector4(super.value, super.epsilon);\n\n  @override\n  double dist(Vector4 a, Vector4 b) => (a - b).length;\n\n  @override\n  List<double> storage(Vector4 value) => value.storage;\n}\n"
  },
  {
    "path": "packages/flame_test/lib/src/debug_text_renderer.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/text.dart';\n\n/// Text formatter suitable for use in golden tests. This formatter renders\n/// words as rectangles.\n///\n/// Rendering regular text in golden tests is unreliable due to differences in\n/// font definitions across platforms and different algorithms used for anti-\n/// aliasing.\nclass DebugTextRenderer extends TextRenderer {\n  DebugTextRenderer({\n    this.color = const Color(0xFFFFFFFF),\n    this.fontSize = 16.0,\n    this.lineHeight = 1.2,\n    this.fontWeight = FontWeight.normal,\n    this.fontStyle = FontStyle.normal,\n  });\n\n  final Color color;\n  final double fontSize;\n  final double lineHeight;\n  final FontWeight fontWeight;\n  final FontStyle fontStyle;\n\n  @override\n  InlineTextElement format(String text) => _DebugTextElement(this, text);\n}\n\nclass _DebugTextElement extends InlineTextElement {\n  _DebugTextElement(this.style, this.text) {\n    final charWidth = style.fontSize * 1.0;\n    final charHeight = style.fontSize;\n    paint\n      ..color = style.color\n      ..style = PaintingStyle.stroke\n      ..strokeWidth = 2.0 * (style.fontWeight.value / 100) / 4;\n    metrics = LineMetrics(\n      width: text.length * charWidth,\n      ascent: charHeight * (style.lineHeight + 1) / 2,\n      descent: charHeight * (style.lineHeight - 1) / 2,\n      baseline: charHeight * (style.lineHeight + 1) / 2,\n    );\n    assert(metrics.left == 0 && metrics.top == 0);\n    _initRects(charWidth, charHeight);\n  }\n\n  final DebugTextRenderer style;\n  final String text;\n  final List<Rect> rects = [];\n  final Paint paint = Paint();\n  @override\n  late final LineMetrics metrics;\n\n  @override\n  void draw(Canvas canvas) {\n    canvas.save();\n    if (style.fontStyle == FontStyle.italic) {\n      canvas.skew(-0.25, 0);\n      canvas.translate(metrics.bottom * 0.25, 0);\n    }\n    for (final rect in rects) {\n      canvas.drawRect(rect, paint);\n    }\n    canvas.restore();\n  }\n\n  @override\n  void translate(double dx, double dy) {\n    metrics.translate(dx, dy);\n    for (var i = 0; i < rects.length; i++) {\n      rects[i] = rects[i].translate(dx, dy);\n    }\n  }\n\n  void _initRects(double charWidth, double charHeight) {\n    var i0 = 0;\n    while (true) {\n      while (i0 < text.length && text[i0] == ' ') {\n        i0 += 1;\n      }\n      if (i0 == text.length) {\n        break;\n      }\n      var i1 = i0;\n      while (i1 < text.length && text[i1] != ' ') {\n        i1 += 1;\n      }\n      rects.add(\n        Rect.fromLTRB(\n          i0 * charWidth,\n          0,\n          i1 * charWidth,\n          charHeight,\n        ).deflate(paint.strokeWidth / 2),\n      );\n      i0 = i1;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_test/lib/src/epsilon.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:vector_math/vector_math.dart' show Vector2;\n\n/// Returns the next larger representable float32 value from [value].\n///\n/// This is useful for creating test values that are just above a target value.\n/// For example, if you want a value that's just above 1.0, you can use:\n/// `final justAbove1 = nextFloat32(1.0);`\ndouble nextFloat32(double value) {\n  // Handle special cases\n  if (value.isNaN) {\n    return double.nan;\n  }\n  if (value == double.infinity) {\n    return double.infinity;\n  }\n  if (value == double.negativeInfinity) {\n    // The largest negative finite float32\n    return -3.4028234663852886e+38;\n  }\n\n  // For negative zero, return smallest positive number\n  if (value == 0 && _isNegativeZero(value)) {\n    return 1.401298464324817e-45; // Smallest positive float32\n  }\n\n  // Convert to bits\n  final bits = _doubleToInt32Bits(value);\n\n  // For positive numbers, increase by 1\n  // For negative numbers, decrease by 1\n  final nextBits = value >= 0 ? bits + 1 : bits - 1;\n\n  // Convert back to double\n  return _int32BitsToDouble(nextBits);\n}\n\n/// Returns the next smaller representable float32 value from [value].\n///\n/// This is useful for creating test values that are just below a target value.\n/// For example, if you want a value that's just below 1.0, you can use:\n/// `final justBelow1 = prevFloat32(1.0);`\ndouble prevFloat32(double value) {\n  // Handle special cases\n  if (value.isNaN) {\n    return double.nan;\n  }\n  if (value == double.negativeInfinity) {\n    return double.negativeInfinity;\n  }\n  if (value == double.infinity) {\n    // The largest finite float32\n    return 3.4028234663852886e+38;\n  }\n\n  // For positive zero, return smallest negative number\n  if (value == 0 && !_isNegativeZero(value)) {\n    return -1.401298464324817e-45; // Smallest negative float32\n  }\n\n  // Convert to bits\n  final bits = _doubleToInt32Bits(value);\n\n  // For positive numbers, decrease by 1\n  // For negative numbers, increase by 1\n  final prevBits = value > 0 ? bits - 1 : bits + 1;\n\n  // Convert back to double\n  return _int32BitsToDouble(prevBits);\n}\n\n/// Calculate the tolerance for tests of float32 [value] that use double\n/// precision. This is a wrapper for calling [nextFloat32] - [prevFloat32]\n/// for [value].\ndouble toleranceFloat32(double value) {\n  // Handle special cases\n  if (value.isNaN) {\n    return double.nan;\n  }\n  if (!value.isFinite) {\n    return 0.0;\n  }\n\n  // Return the difference between the next and previous float32 values.\n  return nextFloat32(value) - prevFloat32(value);\n}\n\n/// Calculate the float32 tolerance for the Vector2 [value] that uses double\n/// precision. This is a wrapper for calling sum([toleranceFloat32]) for each\n/// element.\ndouble toleranceVector2Float32(Vector2 value) {\n  // Return the maximum difference between the next and previous float32 values.\n  return toleranceFloat32(value.x) + toleranceFloat32(value.y);\n}\n\n/// Converts a double to its raw 32-bit float representation\nint _doubleToInt32Bits(double value) {\n  // Create a ByteData to store the floating point value\n  final data = ByteData(4);\n  // Write the value as a float32\n  data.setFloat32(0, value);\n  // Read it back as int32\n  return data.getInt32(0);\n}\n\n/// Converts a raw 32-bit representation back to a double\ndouble _int32BitsToDouble(int bits) {\n  // Create a ByteData to store the bits\n  final data = ByteData(4);\n  // Write the bits as int32\n  data.setInt32(0, bits);\n  // Read it back as float32\n  return data.getFloat32(0);\n}\n\n/// Detects if a zero is negative zero\nbool _isNegativeZero(double value) {\n  if (value != 0) {\n    return false;\n  }\n\n  // Use bit-level inspection\n  final data = ByteData(8);\n  data.setFloat64(0, value);\n  // The sign bit is the most significant bit\n  return (data.getUint8(0) & 0x80) != 0;\n}\n"
  },
  {
    "path": "packages/flame_test/lib/src/expect_color.dart",
    "content": "import 'dart:ui' show Color;\nimport 'package:test/test.dart';\n\n/// A test helper function that compares two [Color] objects for equality with\n/// floating-point tolerance.\n///\n/// Each color component (red, green, blue, alpha) is compared using [closeTo]\n/// matcher with a configurable epsilon value (default 0.000001) to account for\n/// floating-point precision differences that can occur due to binary\n/// representation and endianness when colors are stored/retrieved.\n///\n/// Example usage:\n/// ```dart\n/// test('color matching', () {\n///   final actual = Paint()..color = Color.fromRGBO(255, 0, 0, 0.5);\n///   final expected = Color.fromRGBO(255, 0, 0, 0.5);\n///   expectColor(actual.color, expected, reason: 'paint color should be red');\n/// });\n/// ```\n///\n/// Parameters:\n/// - [actual]: The Color being tested\n/// - [expected]: The Color to test against\n/// - [reason]: Optional description that will be included in the failure\n///   message to provide context about what was being tested. If omitted, uses\n///   'color' as the default context.\n/// - [delta]: Optional tolerance value for floating-point comparison. Defaults\n///   to 0.000001.\nvoid expectColor(\n  Color actual,\n  Color expected, {\n  String? reason,\n  double delta = 0.0000001,\n}) {\n  expect(\n    actual.r,\n    closeTo(expected.r, delta),\n    reason: '${reason ?? 'color'} red value is wrong',\n  );\n  expect(\n    actual.g,\n    closeTo(expected.g, delta),\n    reason: '${reason ?? 'color'} green value is wrong',\n  );\n  expect(\n    actual.b,\n    closeTo(expected.b, delta),\n    reason: '${reason ?? 'color'} blue value is wrong',\n  );\n  expect(\n    actual.a,\n    closeTo(expected.a, delta),\n    reason: '${reason ?? 'color'} alpha value is wrong',\n  );\n}\n\n/// A test helper function that compares only the alpha component of two [Color]\n/// objects with floating-point tolerance.\n///\n/// The alpha component is compared using [closeTo] matcher with a configurable\n/// epsilon value (default 0.000001) to account for floating-point precision\n/// differences that can occur due to binary representation and endianness when\n/// colors are stored/retrieved.\n///\n/// Example usage:\n/// ```dart\n/// test('alpha matching', () {\n///   final actual = Paint()..color = Color.fromRGBO(255, 0, 0, 0.5);\n///   final expected = Color.fromRGBO(0, 255, 0, 0.5);\n///   expectColorAlpha(\n///     actual.color,\n///     expected,\n///     reason: 'paint alpha should match',\n///   );\n/// });\n/// ```\n///\n/// Parameters:\n/// - [actual]: The Color being tested\n/// - [expected]: The Color to test against\n/// - [reason]: Optional description that will be included in the failure\n///   message to provide context about what was being tested. If omitted, uses\n///   'color' as the default context.\n/// - [delta]: Optional tolerance value for floating-point comparison.\n///   Defaults to 0.000001.\nvoid expectColorAlpha(\n  Color actual,\n  Color expected, {\n  String? reason,\n  double delta = 0.0000001,\n}) {\n  expect(\n    actual.a,\n    closeTo(expected.a, delta),\n    reason: '${reason ?? 'color'} alpha value is wrong',\n  );\n}\n"
  },
  {
    "path": "packages/flame_test/lib/src/expect_double.dart",
    "content": "import 'package:test/test.dart';\n\nvoid expectDouble(\n  double d1,\n  double d2, {\n  double epsilon = 0.01,\n  String? reason,\n}) {\n  expect(d1, closeTo(d2, epsilon), reason: reason);\n}\n"
  },
  {
    "path": "packages/flame_test/lib/src/fails_assert.dart",
    "content": "import 'package:test/test.dart';\n\n/// Matcher that can be used in a test that expects an assertion error.\n///\n/// This is similar to standard `throwsAssertionError` matcher, but also\n/// allows an optional message string to verify that the assertion has the\n/// expected message.\n///\n/// For example:\n/// ```dart\n/// expect(\n///   () => PositionComponent(size: Vector2.all(-1)),\n///   failsAssert('size of a PositionComponent cannot be negative'),\n/// )\n/// ```\nMatcher failsAssert([String? message]) {\n  var typeMatcher = isA<AssertionError>();\n  if (message != null) {\n    typeMatcher = typeMatcher.having((e) => e.message, 'message', message);\n  }\n  return throwsA(typeMatcher);\n}\n"
  },
  {
    "path": "packages/flame_test/lib/src/flame_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter_test/flutter_test.dart' hide test;\nimport 'package:meta/meta.dart';\n\nextension FlameFinds on CommonFinders {\n  Finder byGame<T extends Game>() {\n    return find.byWidgetPredicate(\n      (widget) => widget is GameWidget<T>,\n    );\n  }\n}\n\n@visibleForTesting\nextension FlameGameExtension on Component {\n  /// Makes sure that the [component] is added to the tree if you wait for the\n  /// returned future to resolve.\n  Future<void> ensureAdd(Component component) async {\n    await add(component);\n    await component.findGame()!.ready();\n  }\n\n  /// Makes sure that the [components] are added to the tree if you wait for the\n  /// returned future to resolve.\n  Future<void> ensureAddAll(Iterable<Component> components) async {\n    await addAll(components);\n    await components.first.findGame()!.ready();\n  }\n\n  /// Makes sure that the [component] is removed from the tree if you wait for\n  /// the returned future to resolve.\n  Future<void> ensureRemove(Component component) async {\n    remove(component);\n    await component.findGame()!.ready();\n  }\n\n  /// Makes sure that the [components] are removed from the tree if you wait for\n  /// the returned future to resolve.\n  Future<void> ensureRemoveAll(Iterable<Component> components) async {\n    removeAll(components);\n    await components.first.findGame()!.ready();\n  }\n}\n\ntypedef GameCreateFunction<T extends Game> = T Function();\ntypedef VerifyFunction<T extends Game> = dynamic Function(T);\n\ntypedef GameWidgetCreateFunction<T extends Game> =\n    GameWidget<T> Function(\n      T game,\n    );\ntypedef WidgetVerifyFunction<T extends Game> =\n    Future<void> Function(\n      T,\n      WidgetTester,\n    );\ntypedef WidgetSetupFunction<T extends Game> =\n    Future<void> Function(\n      T,\n      WidgetTester,\n    );\ntypedef PumpWidgetFunction<T extends Game> =\n    Future<void> Function(\n      GameWidget<T>,\n      WidgetTester tester,\n    );\n\n/// Customize this class with your specific Game type [T] and a custom\n/// provider `() -> T`, plus some additional configurations including a game\n/// widget builder [createGameWidget], a custom [pumpWidget] function and a\n/// custom [gameSize].\nclass GameTester<T extends Game> {\n  /// Use [createGame] to create your game instance.\n  final GameCreateFunction<T> createGame;\n\n  /// Use [createGameWidget] to create the [GameWidget]. If omitted,\n  /// the game instance returned by [createGame] will be wrapped into\n  /// an empty [GameWidget] instance.\n  final GameWidgetCreateFunction<T>? createGameWidget;\n\n  /// Use [pumpWidget] to define your own function to pump widgets into\n  /// the Flutter test environment. When omitted, [testGameWidget] simply\n  /// will pass the created game widget instance to the test.\n  final PumpWidgetFunction<T>? pumpWidget;\n\n  /// Override the game size to be provided during `onGameResize`.\n  /// By default it will be a 500x500 square.\n  final Vector2? gameSize;\n\n  /// If true, the game will be brought into the \"fully ready\" state (meaning\n  /// all its pending lifecycle events will be resolved) before the start of\n  /// the test.\n  bool makeReady = true;\n\n  GameTester(\n    this.createGame, {\n    this.gameSize,\n    this.createGameWidget,\n    this.pumpWidget,\n  });\n\n  /// Creates a [Game] specific test case with given [description]\n  /// which runs inside the Flutter test environment.\n  ///\n  /// Use [setUp] closure to prepare your game instance (e.g. add components to\n  /// it)\n  /// Use [verify] closure to make verifications/assertions.\n  @isTest\n  void testGameWidget(\n    String description, {\n    WidgetSetupFunction<T>? setUp,\n    WidgetVerifyFunction<T>? verify,\n    bool? skip,\n    Timeout? timeout,\n    bool? semanticsEnabled,\n    dynamic tags,\n  }) {\n    testWidgets(\n      description,\n      (tester) async {\n        final game = createGame();\n\n        await tester.runAsync(() async {\n          final gameWidget =\n              createGameWidget?.call(game) ?? GameWidget(game: game);\n\n          final pump =\n              pumpWidget ??\n              (GameWidget<T> pumpWidget, WidgetTester tester) =>\n                  tester.pumpWidget(pumpWidget);\n\n          await pump(gameWidget, tester);\n          await tester.pump();\n\n          if (setUp != null) {\n            await setUp.call(game, tester);\n            await tester.pump();\n          }\n        });\n\n        if (verify != null) {\n          await verify(game, tester);\n        }\n      },\n      skip: skip,\n      timeout: timeout,\n      semanticsEnabled: semanticsEnabled ?? true,\n      tags: tags,\n    );\n  }\n\n  GameTester<T> configure({\n    GameCreateFunction<T>? createGame,\n    Vector2? gameSize,\n    GameWidgetCreateFunction<T>? createGameWidget,\n    PumpWidgetFunction<T>? pumpWidget,\n  }) {\n    return GameTester<T>(\n      createGame ?? this.createGame,\n      gameSize: gameSize ?? this.gameSize,\n      createGameWidget: createGameWidget ?? this.createGameWidget,\n      pumpWidget: pumpWidget ?? this.pumpWidget,\n    );\n  }\n}\n\n/// Customize this class with your specific FlameGame type [T] and a custom\n/// provider `() -> T`, plus some additional configurations including a game\n/// widget builder [createGameWidget], a custom [pumpWidget] function and a\n/// custom [gameSize].\nclass FlameTester<T extends FlameGame> extends GameTester<T> {\n  FlameTester(\n    super.createGame, {\n    super.gameSize,\n    super.createGameWidget,\n    super.pumpWidget,\n  });\n}\n\n/// Default instance of Flame Tester to be used when you don't care about\n/// changing any configuration.\nfinal flameGame = FlameTester<FlameGame>(FlameGame.new);\n"
  },
  {
    "path": "packages/flame_test/lib/src/is_close_to_vector.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\n\nabstract class IsCloseToVector<V> extends Matcher {\n  const IsCloseToVector(this._value, this._epsilon);\n\n  final V _value;\n  final double _epsilon;\n\n  double dist(V a, V b);\n  List<double> storage(V value);\n\n  @override\n  bool matches(dynamic item, Map matchState) {\n    return (item is V) && dist(item, _value) <= _epsilon;\n  }\n\n  @override\n  Description describe(Description description) {\n    final coords = storage(_value).join(', ');\n    return description.add('a $V object within $_epsilon of ($coords)');\n  }\n\n  @override\n  Description describeMismatch(\n    dynamic item,\n    Description mismatchDescription,\n    Map matchState,\n    bool verbose,\n  ) {\n    if (item is! V) {\n      return mismatchDescription.add('is not an instance of $V');\n    }\n    final distance = dist(item, _value);\n    return mismatchDescription.add('is at distance $distance');\n  }\n}\n"
  },
  {
    "path": "packages/flame_test/lib/src/mock_gesture_events.dart",
    "content": "import 'package:flutter/gestures.dart';\n\nTapUpDetails createTapUpDetails({\n  Offset? globalPosition,\n  Offset? localPosition,\n  PointerDeviceKind kind = PointerDeviceKind.mouse,\n}) {\n  return TapUpDetails(\n    localPosition: localPosition,\n    globalPosition: globalPosition ?? Offset.zero,\n    kind: PointerDeviceKind.mouse,\n  );\n}\n"
  },
  {
    "path": "packages/flame_test/lib/src/mock_image.dart",
    "content": "import 'dart:typed_data';\nimport 'dart:ui';\n\nFuture<Image> generateImage([int width = 1, int height = 1]) {\n  final recorder = PictureRecorder();\n  final canvas = Canvas(recorder);\n  canvas.drawRect(\n    Rect.fromLTWH(\n      0,\n      0,\n      height.toDouble(),\n      width.toDouble(),\n    ),\n    Paint()..color = const Color(0xFFFFFFFF),\n  );\n\n  final picture = recorder.endRecording();\n  final image = picture.toImage(\n    width,\n    height,\n  );\n  return image;\n}\n\nByteData generatePNGByteData() => ByteData.sublistView(\n  Uint8List.fromList([\n    0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG file signature\n    0x00, 0x00, 0x00, 0x0D, // IHDR chunk length\n    0x49, 0x48, 0x44, 0x52, // IHDR chunk type\n    0x00, 0x00, 0x00, 0x01, // Width: 1\n    0x00, 0x00, 0x00, 0x01, // Height: 1\n    0x08, // Bit depth: 8\n    0x02, // Color type: Truecolor\n    0x00, // Compression method: Deflate\n    0x00, // Filter method: Adaptive\n    0x00, // Interlace method: No interlace\n    0x90, 0x77, 0x53, 0xDE, // CRC\n    0x00, 0x00, 0x00, 0x0A, // IDAT chunk length\n    0x49, 0x44, 0x41, 0x54, // IDAT chunk type\n    0x08, 0xD7, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xE2, 0x21,\n    0xBC, 0x33, // IDAT data and CRC\n    0x00, 0x00, 0x00, 0x00, // IEND chunk length\n    0x49, 0x45, 0x4E, 0x44, // IEND chunk type\n    0xAE, 0x42, 0x60, 0x82, // CRC\n  ]),\n);\n"
  },
  {
    "path": "packages/flame_test/lib/src/mock_pointer_move_event.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/gestures.dart' as flutter;\n\nPointerMoveEvent createMouseMoveEvent({\n  required Game game,\n  int? pointerId,\n  Vector2? position,\n  Vector2? delta,\n  Duration? timestamp,\n}) {\n  return PointerMoveEvent(\n    pointerId ?? 1,\n    game,\n    flutter.PointerHoverEvent(\n      timeStamp: timestamp ?? Duration.zero,\n      position: position?.toOffset() ?? Offset.zero,\n      delta: delta?.toOffset() ?? Offset.zero,\n    ),\n  );\n}\n"
  },
  {
    "path": "packages/flame_test/lib/src/mock_tap_drag_events.dart",
    "content": "import 'package:flame/events.dart';\nimport 'package:flame/game.dart';\nimport 'package:flutter/gestures.dart';\n\nTapDownEvent createTapDownEvents({\n  required Game game,\n  int? pointerId,\n  PointerDeviceKind? kind,\n  Offset? globalPosition,\n  Offset? localPosition,\n}) {\n  return TapDownEvent(\n    pointerId ?? 1,\n    game,\n    TapDownDetails(\n      localPosition: localPosition ?? Offset.zero,\n      globalPosition: globalPosition ?? Offset.zero,\n      kind: kind ?? PointerDeviceKind.touch,\n    ),\n  );\n}\n\nTapUpEvent createTapUpEvents({\n  required Game game,\n  int? pointerId,\n  PointerDeviceKind? kind,\n  Offset? globalPosition,\n  Offset? localPosition,\n}) {\n  return TapUpEvent(\n    pointerId ?? 1,\n    game,\n    TapUpDetails(\n      localPosition: localPosition ?? Offset.zero,\n      globalPosition: globalPosition ?? Offset.zero,\n      kind: kind ?? PointerDeviceKind.touch,\n    ),\n  );\n}\n\nSecondaryTapDownEvent createSecondaryTapDownEvents({\n  required Game game,\n  PointerDeviceKind? kind,\n  Offset? globalPosition,\n  Offset? localPosition,\n}) {\n  return SecondaryTapDownEvent(\n    game,\n    TapDownDetails(\n      localPosition: localPosition ?? Offset.zero,\n      globalPosition: globalPosition ?? Offset.zero,\n      kind: kind ?? PointerDeviceKind.touch,\n    ),\n  );\n}\n\nSecondaryTapUpEvent createSecondaryTapUpEvents({\n  required Game game,\n  PointerDeviceKind? kind,\n  Offset? globalPosition,\n  Offset? localPosition,\n}) {\n  return SecondaryTapUpEvent(\n    game,\n    TapUpDetails(\n      localPosition: localPosition ?? Offset.zero,\n      globalPosition: globalPosition ?? Offset.zero,\n      kind: kind ?? PointerDeviceKind.touch,\n    ),\n  );\n}\n\nDragStartEvent createDragStartEvents({\n  required Game game,\n  int? pointerId,\n  PointerDeviceKind? kind,\n  Offset? globalPosition,\n  Offset? localPosition,\n}) {\n  return DragStartEvent(\n    pointerId ?? 1,\n    game,\n    DragStartDetails(\n      localPosition: localPosition ?? Offset.zero,\n      globalPosition: globalPosition ?? Offset.zero,\n    ),\n  );\n}\n\nDragUpdateEvent createDragUpdateEvents({\n  required Game game,\n  int? pointerId,\n  PointerDeviceKind? kind,\n  Offset? globalPosition,\n  Offset? localPosition,\n}) {\n  return DragUpdateEvent(\n    pointerId ?? 1,\n    game,\n    DragUpdateDetails(\n      localPosition: localPosition ?? Offset.zero,\n      globalPosition: globalPosition ?? Offset.zero,\n    ),\n  );\n}\n\nScaleStartEvent createScaleStartEvents({\n  required Game game,\n  int? pointerId,\n  PointerDeviceKind? kind,\n  Offset? focalPoint,\n  Offset? localFocalPoint,\n  int? pointerCount,\n}) {\n  return ScaleStartEvent(\n    pointerId ?? 1,\n    game,\n    ScaleStartDetails(\n      focalPoint: focalPoint ?? Offset.zero,\n      localFocalPoint: localFocalPoint ?? Offset.zero,\n      pointerCount: pointerCount ?? 1,\n    ),\n  );\n}\n\nScaleUpdateEvent createScaleUpdateEvents({\n  required Game game,\n  int? pointerId,\n  PointerDeviceKind? kind,\n  Offset? localFocalPoint,\n  Offset? focalPoint,\n  double? scale,\n  double? horizontalScale,\n  double? verticalScale,\n  double? rotation,\n  int? pointerCount,\n  Offset? focalPointDelta,\n}) {\n  return ScaleUpdateEvent(\n    pointerId ?? 1,\n    game,\n    ScaleUpdateDetails(\n      localFocalPoint: localFocalPoint ?? Offset.zero,\n      focalPoint: focalPoint ?? Offset.zero,\n      scale: scale ?? 1,\n      horizontalScale: horizontalScale ?? 1,\n      verticalScale: verticalScale ?? 1,\n      rotation: rotation ?? 0,\n      pointerCount: pointerCount ?? 1,\n      focalPointDelta: focalPointDelta ?? Offset.zero,\n    ),\n  );\n}\n"
  },
  {
    "path": "packages/flame_test/lib/src/random_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:meta/meta.dart';\n\nfinal _seedGenerator = Random();\n// Maximum value allowed for `Random.nextInt()`\nconst _maxSeed = 1 << 32;\n\n/// Get the random seed for a test. If the [seed] parameter is passed in,\n/// it takes precedence. Otherwise, if the environment variable\n/// `RANDOM_SEED` is set, it is used. If neither is set, returns null.\n/// Note: When using this, the `random_test_test` will fail because it will\n/// use the same seed for all tests. This is expected.\nint? seedFromEnvironment(int? seed) {\n  if (seed != null) {\n    return seed;\n  }\n\n  // ignore: do_not_use_environment\n  const seedString = String.fromEnvironment(\n    'RANDOM_SEED',\n  );\n  if (seedString.isEmpty) {\n    return null;\n  }\n  return int.tryParse(seedString);\n}\n\n/// This function is equivalent to `test(name, body)`, except that it is\n/// better suited for randomized testing: it will create a Random\n/// generator and pass it to the test body, but also record the seed\n/// that was used for creating the random generator. Thus, if a test\n/// fails for a specific rare seed, it would be easy to reproduce this\n/// failure.\n///\n/// In order for this to work properly, all random numbers used within\n/// `testRandom()` must be obtained through the provided random generator.\n///\n/// Example of use:\n/// ```dart\n/// testRandom('description', (Random random) {\n///   expect(random.nextDouble() == random.nextDouble(), false);\n/// });\n/// ```\n/// Then if the test output shows that the test failed with seed `s`,\n/// simply adding parameter `seed=s` into the function will force it\n/// to use that specific seed.\n///\n/// Optional parameter `repeatCount` allows the test to be repeated multiple\n/// times, each time with a different seed.\n@isTest\nvoid testRandom(\n  String name,\n  void Function(Random random) body, {\n  int? seed,\n  String? testOn,\n  Timeout? timeout,\n  dynamic skip,\n  dynamic tags,\n  Map<String, dynamic>? onPlatform,\n  int? retry,\n  int repeatCount = 1,\n}) {\n  assert(repeatCount > 0, 'repeatCount needs to be a positive number');\n  final resolvedSeed = seedFromEnvironment(seed);\n  for (var i = 0; i < repeatCount; i++) {\n    final seed0 = resolvedSeed ?? _seedGenerator.nextInt(_maxSeed);\n    test(\n      '$name [seed=$seed0]',\n      () => body(Random(seed0)),\n      testOn: testOn,\n      timeout: timeout,\n      skip: skip,\n      tags: tags,\n      onPlatform: onPlatform,\n      retry: retry,\n    );\n  }\n}\n\ntypedef TestWidgetsCallback =\n    Future<void> Function(\n      Random random,\n      WidgetTester widgetTester,\n    );\n\n/// This function is equivalent to `testWidgets(name, body)`, except that\n/// it is better suited for randomized testing: it will create a Random\n/// generator and pass it to the test body, but also record the seed\n/// that was used for creating the random generator. Thus, if a test\n/// fails for a specific rare seed, it would be easy to reproduce this\n/// failure.\n///\n/// In order for this to work properly, all random number generation\n/// within `testRandomWidgets()` must be performed through the provided\n/// random generator.\n///\n/// Example of use:\n/// ```dart\n/// testRandomWidgets(\n///   'description',\n///   (Random random, WidgetTester tester) async {\n///     ...\n///   },\n/// );\n/// ```\n/// Then if the test output shows that the test failed with seed `s`,\n/// simply adding parameter `seed=s` into the function will force it\n/// to run for that specific seed.\n@isTest\nvoid testWidgetsRandom(\n  String description,\n  TestWidgetsCallback callback, {\n  int? seed,\n  bool? skip,\n  Timeout? timeout,\n  bool semanticsEnabled = true,\n  dynamic tags,\n}) {\n  seed ??= _seedGenerator.nextInt(_maxSeed);\n  testWidgets(\n    '$description [seed=$seed]',\n    (WidgetTester widgetTester) => callback(Random(seed), widgetTester),\n    skip: skip,\n    timeout: timeout,\n    semanticsEnabled: semanticsEnabled,\n    tags: tags,\n  );\n}\n"
  },
  {
    "path": "packages/flame_test/lib/src/test_flame_game.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:meta/meta.dart';\n\n/// Utility function for writing tests that require a [FlameGame] instance.\n///\n/// This function creates a [FlameGame] object, properly initializes it, then\n/// passes on to the user-provided [testBody], and in the end disposes of the\n/// game object.\n///\n/// Example of usage:\n/// ```dart\n/// testWithFlameGame(\n///   'MyComponent can be added to a game',\n///   (game) async {\n///     final component = MyComponent()..addToParent(game);\n///     await game.ready();\n///     expect(component.isMounted, true);\n///   },\n/// );\n/// ```\n///\n/// The `game` instance supplied by this function to your [testBody] is a\n/// standard [FlameGame]. If you want to have any other game instance, use the\n/// [testWithGame] function.\n@isTest\nvoid testWithFlameGame(\n  String testName,\n  AsyncGameFunction<FlameGame> testBody, {\n  Timeout? timeout,\n  dynamic tags,\n  dynamic skip,\n  Map<String, dynamic>? onPlatform,\n  int? retry,\n}) {\n  return testWithGame<FlameGame>(\n    testName,\n    FlameGame.new,\n    testBody,\n    timeout: timeout,\n    tags: tags,\n    skip: skip,\n    onPlatform: onPlatform,\n    retry: retry,\n  );\n}\n\n/// Utility function for writing tests that require a custom game instance.\n///\n/// This function [create]s the game instance, initializes it, then passes it\n/// to the user-provided [testBody], and in the end disposes of the game object.\n///\n/// Example of usage:\n/// ```dart\n/// testWithGame<MyGame>(\n///   'MyComponent can be added to MyGame',\n///   () => MyGame(mySecret: 3781),\n///   (MyGame game) async {\n///     final component = MyComponent()..addToParent(game);\n///     await game.ready();\n///     expect(component.isMounted, true);\n///   },\n/// );\n/// ```\n@isTest\nvoid testWithGame<T extends FlameGame>(\n  String testName,\n  CreateFunction<T> create,\n  AsyncGameFunction<T> testBody, {\n  Timeout? timeout,\n  dynamic tags,\n  dynamic skip,\n  Map<String, dynamic>? onPlatform,\n  int? retry,\n}) {\n  test(\n    testName,\n    () async {\n      final game = await initializeGame<T>(create);\n\n      try {\n        await testBody(game);\n      } finally {\n        game.onRemove();\n      }\n    },\n    timeout: timeout,\n    tags: tags,\n    skip: skip,\n    onPlatform: onPlatform,\n    retry: retry,\n  );\n}\n\nFuture<T> initializeGame<T extends FlameGame>(CreateFunction<T> create) async {\n  final game = create();\n  game.onGameResize(Vector2(800, 600));\n  // ignore: invalid_use_of_internal_member\n  await game.load();\n  // ignore: invalid_use_of_internal_member\n  game.mount();\n  game.update(0);\n  return game;\n}\n\nFuture<FlameGame> initializeFlameGame() => initializeGame(FlameGame.new);\n\ntypedef CreateFunction<T> = T Function();\ntypedef AsyncVoidFunction = Future<void> Function();\ntypedef AsyncGameFunction<T extends Game> = Future<void> Function(T game);\n"
  },
  {
    "path": "packages/flame_test/lib/src/test_golden.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:meta/meta.dart';\n\n/// Test that a game renders correctly.\n///\n/// The way golden tests work is as follows: you set up a scene in [testBody],\n/// then the test framework renders your game widget into an image, and compares\n/// that image against stored [goldenFile]. The test passes if two images are\n/// identical, or fails if the images differ even in a single pixel.\n///\n/// The term _golden file_ refers to the true rendering of a given game scene,\n/// captured at the creation of the test. In order to create a golden file, you\n/// first specify its desired name in the [goldenFile] parameter, and then run\n/// the tests using the command\n/// ```\n/// flutter test --update-goldens\n/// ```\n///\n/// The [testBody] is given a `game` parameter (which is by default a new\n/// [FlameGame] instance, but you can also supply your own [game] object), and\n/// is expected to set up a scene for rendering. Usually this involves adding\n/// necessary game components, and possibly advancing the game clock. As a\n/// convenience, we will run `await game.ready()` before rendering, to ensure\n/// that all components that might be pending are properly mounted.\n///\n/// The [size] parameter controls the size of the \"device\" on which the game\n/// widget is rendered, if omitted it defaults to 2400x1800. This size will be\n/// equal to the canvas size of the game.\n@isTest\nvoid testGolden(\n  String testName,\n  PrepareFunction testBody, {\n  required String goldenFile,\n  Vector2? size,\n  Color? backgroundColor,\n  FlameGame? game,\n  bool skip = false,\n}) {\n  testWidgets(\n    testName,\n    (tester) async {\n      final gameInstance =\n          game ??\n          (backgroundColor != null\n              ? GameWithBackgroundColor(backgroundColor)\n              : FlameGame());\n      const myKey = ValueKey('game-instance');\n\n      await tester.runAsync(() async {\n        Widget widget = GameWidget(key: myKey, game: gameInstance);\n        if (size != null) {\n          widget = Center(\n            child: SizedBox(\n              width: size.x,\n              height: size.y,\n              child: RepaintBoundary(\n                child: widget,\n              ),\n            ),\n          );\n        }\n        await tester.pumpWidget(widget);\n        await tester.pump();\n        await testBody(gameInstance, tester);\n        await gameInstance.ready();\n        await tester.pump();\n      });\n\n      await expectLater(\n        find.byKey(myKey),\n        matchesGoldenFile(goldenFile),\n      );\n    },\n    skip: skip,\n  );\n}\n\ntypedef PrepareFunction =\n    Future<void> Function(\n      FlameGame game,\n      WidgetTester tester,\n    );\n\nclass GameWithBackgroundColor extends FlameGame {\n  final Color _backgroundColor;\n\n  GameWithBackgroundColor(this._backgroundColor);\n\n  @override\n  Color backgroundColor() => _backgroundColor;\n}\n"
  },
  {
    "path": "packages/flame_test/pubspec.yaml",
    "content": "name: flame_test\nresolution: workspace\ndescription: A package with classes to help testing applications using Flame\nversion: 2.2.3\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_test\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - test\n  - testing\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  flutter_test:\n    sdk: flutter\n  meta: ^1.12.0\n  test: ^1.24.1\n  typed_data: ^1.4.0\n  vector_math: ^2.1.4\n\ndev_dependencies:\n  dartdoc: ^9.0.0\n  flame_lint: ^1.4.3"
  },
  {
    "path": "packages/flame_test/test/close_to_aabb_test.dart",
    "content": "import 'package:flame_test/src/close_to_aabb.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:vector_math/vector_math.dart';\n\nvoid main() {\n  group('closeToAabb', () {\n    test('zero aabb', () {\n      expect(Aabb2(), closeToAabb(Aabb2()));\n      expect(\n        Aabb2(),\n        closeToAabb(Aabb2.minMax(Vector2.all(1e-5), Vector2.all(1e-4)), 1e-3),\n      );\n    });\n\n    test('matches normally', () {\n      final aabb = Aabb2.minMax(Vector2.zero(), Vector2.all(1));\n      expect(aabb, closeToAabb(aabb));\n      expect(aabb, closeToAabb(Aabb2(), 2));\n      expect(\n        aabb,\n        closeToAabb(Aabb2.minMax(Vector2.all(1e-16), Vector2.all(1))),\n      );\n    });\n\n    test('fails on type mismatch', () {\n      try {\n        expect(3.14, closeToAabb(Aabb2()));\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains(\n            'Expected: an Aabb2 object within 1e-15 of '\n            'Aabb2([0.0, 0.0]..[0.0, 0.0])',\n          ),\n        );\n        expect(e.message, contains('Actual: <3.14>'));\n        expect(e.message, contains('Which: is not an instance of Aabb2'));\n      }\n    });\n\n    test('fails on value mismatch', () {\n      try {\n        expect(\n          Aabb2.minMax(Vector2.zero(), Vector2.all(1)),\n          closeToAabb(Aabb2(), 0.01),\n        );\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains(\n            'Expected: an Aabb2 object within 0.01 of '\n            'Aabb2([0.0, 0.0]..[0.0, 0.0])',\n          ),\n        );\n        expect(e.message, contains(\"Actual: <Instance of 'Aabb2'>\"));\n        expect(e.message, contains('Which: max corner is at distance 1.4142'));\n        expect(e.message, contains('Aabb2([0.0, 0.0]..[1.0, 1.0])'));\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_test/test/close_to_matrix4_test.dart",
    "content": "import 'package:flame_test/src/close_to_matrix4.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:vector_math/vector_math.dart';\n\nvoid main() {\n  group('closeToMatrix4', () {\n    test('matches normally', () {\n      expect(\n        Matrix4.zero(),\n        closeToMatrix4(\n          Matrix4.columns(\n            Vector4.zero(),\n            Vector4.zero(),\n            Vector4.zero(),\n            Vector4.zero(),\n          ),\n        ),\n      );\n      expect(\n        Matrix4(\n          -14,\n          99,\n          -99,\n          0, //\n          0,\n          0,\n          0,\n          0, //\n          1e-20,\n          -1e-16,\n          0,\n          0, //\n          0,\n          0,\n          0,\n          0, //\n        ),\n        closeToMatrix4(\n          Matrix4.columns(\n            Vector4(-14, 99, -99, 0),\n            Vector4(0, 0, 0, 0),\n            Vector4(0, 0, 0, 0),\n            Vector4(0, 0, 0, 0),\n          ),\n        ),\n      );\n\n      expect(\n        Matrix4.columns(\n          Vector4(1.0001, 2.0, -1.0001, 1.99999),\n          Vector4(0, 0, 0, 0),\n          Vector4(0, 0, 0, 0),\n          Vector4(0, 0, 0, 0),\n        ),\n        closeToMatrix4(\n          Matrix4.columns(\n            Vector4(1, 2, -1, 2),\n            Vector4(0, 0, 0, 0),\n            Vector4(0, 0, 0, 0),\n            Vector4(0, 0, 0, 0),\n          ),\n          0.01,\n        ),\n      );\n    });\n\n    test('fails on type mismatch', () {\n      try {\n        expect(3.14, closeToMatrix4(Matrix4.zero()));\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains(\n            'Expected: a Matrix4 object within 1e-15 of '\n            '(0.0, 0.0, 0.0, 0.0, '\n            '0.0, 0.0, 0.0, 0.0, '\n            '0.0, 0.0, 0.0, 0.0, '\n            '0.0, 0.0, 0.0, 0.0)',\n          ),\n        );\n        expect(e.message, contains('Actual: <3.14>'));\n        expect(e.message, contains('Which: is not an instance of Matrix4'));\n      }\n    });\n\n    test('fails on value mismatch', () {\n      try {\n        expect(\n          Matrix4.columns(\n            Vector4(101, 217, 100, 0),\n            Vector4(0, 0, 0, 0),\n            Vector4(0, 0, 0, 0),\n            Vector4(0, 0, 0, 0),\n          ),\n          closeToMatrix4(\n            Matrix4.columns(\n              Vector4(100, 220, 101, 0),\n              Vector4(0, 0, 0, 0),\n              Vector4(0, 0, 0, 0),\n              Vector4(0, 0, 0, 0),\n            ),\n          ),\n        );\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains(\n            'Expected: a Matrix4 object within 1e-15 of '\n            '(100.0, 220.0, 101.0, 0.0, '\n            '0.0, 0.0, 0.0, 0.0, '\n            '0.0, 0.0, 0.0, 0.0, '\n            '0.0, 0.0, 0.0, 0.0)',\n          ),\n        );\n        expect(\n          e.message,\n          contains('Actual: Matrix4:<[0] [101.0,0.0,0.0,0.0]\\n'),\n        );\n        expect(e.message, contains('[1] [217.0,0.0,0.0,0.0]\\n'));\n        expect(e.message, contains('[2] [100.0,0.0,0.0,0.0]\\n'));\n        expect(e.message, contains('[3] [0.0,0.0,0.0,0.0]\\n'));\n        expect(\n          e.message,\n          contains(\n            '>\\n',\n          ),\n        );\n        expect(\n          e.message,\n          contains(\n            'Which: is not within 1e-15 of (100.0, 220.0, 101.0, 0.0, '\n            '0.0, 0.0, 0.0, 0.0, '\n            '0.0, 0.0, 0.0, 0.0, '\n            '0.0, 0.0, 0.0, 0.0)',\n          ),\n        );\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_test/test/close_to_quaternion_test.dart",
    "content": "import 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:vector_math/vector_math.dart';\n\nvoid main() {\n  group('closeToQuaternion', () {\n    test('matches normally', () {\n      expect(\n        Quaternion.fromRotation(Matrix3.identity()),\n        closeToQuaternion(Quaternion(0, 0, 0, 1)),\n      );\n      expect(\n        Quaternion(-14, 99, -99, 14),\n        closeToQuaternion(Quaternion(-14, 99, -99, 14)),\n      );\n      expect(\n        Quaternion(1e-20, -1e-16, 0, -0),\n        closeToQuaternion(Quaternion(0, 0, 0, 0)),\n      );\n\n      expect(\n        Quaternion(1.0001, 2.0, -1.0001, -0),\n        closeToQuaternion(Quaternion(1, 2, -1, 0), 0.01),\n      );\n      expect(\n        Quaternion(5, 9, 11, 15),\n        closeToQuaternion(Quaternion(10, 10, 10, 10), 10),\n      );\n    });\n\n    test('fails on type mismatch', () {\n      try {\n        expect(4.14, closeToQuaternion(Quaternion(0, 0, 0, 0)));\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains(\n            'Expected: a Quaternion object within 1e-15 of '\n            '(0.0, 0.0, 0.0, 0.0)',\n          ),\n        );\n        expect(e.message, contains('Actual: <4.14>'));\n        expect(e.message, contains('Which: is not an instance of Quaternion'));\n      }\n    });\n\n    test('fails on value mismatch', () {\n      try {\n        expect(\n          Quaternion(101, 217, 100, 0),\n          closeToQuaternion(Quaternion(100, 220, 101, 0)),\n        );\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains(\n            'Expected: a Quaternion object within 1e-15 of '\n            '(100.0, 220.0, 101.0, 0.0)',\n          ),\n        );\n        expect(\n          e.message,\n          contains('Actual: Quaternion:<101.0, 217.0, 100.0 @ 0.0>'),\n        );\n        expect(e.message, contains('Which: is at distance 3.3166247903554'));\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_test/test/close_to_vector3_test.dart",
    "content": "import 'package:flame_test/src/close_to_vector3.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:vector_math/vector_math.dart';\n\nvoid main() {\n  group('closeToVector3', () {\n    test('matches normally', () {\n      expect(Vector3.zero(), closeToVector3(Vector3(0, 0, 0)));\n      expect(Vector3(-14, 99, -99), closeToVector3(Vector3(-14, 99, -99)));\n      expect(Vector3(1e-20, -1e-16, 0), closeToVector3(Vector3(0, 0, 0)));\n\n      expect(\n        Vector3(1.0001, 2.0, -1.0001),\n        closeToVector3(Vector3(1, 2, -1), 0.01),\n      );\n      expect(Vector3(9, 10, 11), closeToVector3(Vector3.all(10), 10));\n    });\n\n    test('fails on type mismatch - double', () {\n      try {\n        expect(3.14, closeToVector3(Vector3.zero()));\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains(\n            'Expected: a Vector3 object within 1e-15 of (0.0, 0.0, 0.0)',\n          ),\n        );\n        expect(e.message, contains('Actual: <3.14>'));\n        expect(e.message, contains('Which: is not an instance of Vector3'));\n      }\n    });\n\n    test('fails on type mismatch - vector2', () {\n      try {\n        expect(Vector2(1, 2), closeToVector3(Vector3.zero()));\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains(\n            'Expected: a Vector3 object within 1e-15 of (0.0, 0.0, 0.0)',\n          ),\n        );\n        expect(e.message, contains('Actual: Vector2:<[1.0,2.0]>'));\n        expect(e.message, contains('Which: is not an instance of Vector3'));\n      }\n    });\n\n    test('fails on type mismatch - vector4', () {\n      try {\n        expect(Vector4(1, 2, 3, 4), closeToVector3(Vector3.zero()));\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains(\n            'Expected: a Vector3 object within 1e-15 of (0.0, 0.0, 0.0)',\n          ),\n        );\n        expect(e.message, contains('Actual: Vector4:<[1.0,2.0,3.0,4.0]>'));\n        expect(e.message, contains('Which: is not an instance of Vector3'));\n      }\n    });\n\n    test('fails on value mismatch', () {\n      try {\n        expect(Vector3(101, 217, 100), closeToVector3(Vector3(100, 220, 101)));\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains(\n            'Expected: a Vector3 object within 1e-15 of (100.0, 220.0, 101.0)',\n          ),\n        );\n        expect(e.message, contains('Actual: Vector3:<[101.0,217.0,100.0]>'));\n        expect(e.message, contains('Which: is at distance 3.3166247903554'));\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_test/test/close_to_vector4_test.dart",
    "content": "import 'package:flame_test/src/close_to_vector4.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:vector_math/vector_math.dart';\n\nvoid main() {\n  group('closeToVector4', () {\n    test('matches normally', () {\n      expect(Vector4.zero(), closeToVector4(Vector4(0, 0, 0, 0)));\n      expect(\n        Vector4(-14, 99, -99, 14),\n        closeToVector4(Vector4(-14, 99, -99, 14)),\n      );\n      expect(\n        Vector4(1e-20, -1e-16, 0, -0),\n        closeToVector4(Vector4(0, 0, 0, 0)),\n      );\n\n      expect(\n        Vector4(1.0001, 2.0, -1.0001, -0),\n        closeToVector4(Vector4(1, 2, -1, 0), 0.01),\n      );\n      expect(Vector4(5, 9, 11, 15), closeToVector4(Vector4.all(10), 10));\n    });\n\n    test('fails on type mismatch - double', () {\n      try {\n        expect(4.14, closeToVector4(Vector4.zero()));\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains(\n            'Expected: a Vector4 object within 1e-15 of (0.0, 0.0, 0.0, 0.0)',\n          ),\n        );\n        expect(e.message, contains('Actual: <4.14>'));\n        expect(e.message, contains('Which: is not an instance of Vector4'));\n      }\n    });\n\n    test('fails on type mismatch - vector2', () {\n      try {\n        expect(Vector2(1, 2), closeToVector4(Vector4.zero()));\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains(\n            'Expected: a Vector4 object within 1e-15 of (0.0, 0.0, 0.0, 0.0)',\n          ),\n        );\n        expect(e.message, contains('Actual: Vector2:<[1.0,2.0]>'));\n        expect(e.message, contains('Which: is not an instance of Vector4'));\n      }\n    });\n\n    test('fails on type mismatch - vector3', () {\n      try {\n        expect(Vector3(1, 2, 3), closeToVector4(Vector4.zero()));\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains(\n            'Expected: a Vector4 object within 1e-15 of (0.0, 0.0, 0.0, 0.0)',\n          ),\n        );\n        expect(e.message, contains('Actual: Vector3:<[1.0,2.0,3.0]>'));\n        expect(e.message, contains('Which: is not an instance of Vector4'));\n      }\n    });\n\n    test('fails on value mismatch', () {\n      try {\n        expect(\n          Vector4(101, 217, 100, 0),\n          closeToVector4(Vector4(100, 220, 101, 0)),\n        );\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains(\n            'Expected: a Vector4 object within 1e-15 of '\n            '(100.0, 220.0, 101.0, 0.0)',\n          ),\n        );\n        expect(\n          e.message,\n          contains('Actual: Vector4:<[101.0,217.0,100.0,0.0]>'),\n        );\n        expect(\n          e.message,\n          contains('Which: is at distance 3.3166247903554'),\n        );\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_test/test/close_to_vector_test.dart",
    "content": "import 'package:flame_test/src/close_to_vector.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:vector_math/vector_math.dart';\n\nvoid main() {\n  group('closeToVector', () {\n    test('matches normally', () {\n      expect(Vector2.zero(), closeToVector(Vector2(0, 0)));\n      expect(Vector2(-14, 99), closeToVector(Vector2(-14, 99)));\n      expect(Vector2(1e-20, -1e-16), closeToVector(Vector2(0, 0)));\n\n      expect(Vector2(1.0001, 2.0), closeToVector(Vector2(1, 2), 0.01));\n      expect(Vector2(13, 14), closeToVector(Vector2(10, 10), 5));\n    });\n\n    test('fails on type mismatch - double', () {\n      try {\n        expect(3.14, closeToVector(Vector2.zero()));\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains('Expected: a Vector2 object within 1e-15 of (0.0, 0.0)'),\n        );\n        expect(e.message, contains('Actual: <3.14>'));\n        expect(e.message, contains('Which: is not an instance of Vector2'));\n      }\n    });\n\n    test('fails on type mismatch - vector3', () {\n      try {\n        expect(Vector3(1, 2, 3), closeToVector(Vector2.zero()));\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains('Expected: a Vector2 object within 1e-15 of (0.0, 0.0)'),\n        );\n        expect(e.message, contains('Actual: Vector3:<[1.0,2.0,3.0]>'));\n        expect(e.message, contains('Which: is not an instance of Vector2'));\n      }\n    });\n\n    test('fails on type mismatch - vector4', () {\n      try {\n        expect(Vector4(1, 2, 3, 4), closeToVector(Vector2.zero()));\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains('Expected: a Vector2 object within 1e-15 of (0.0, 0.0)'),\n        );\n        expect(e.message, contains('Actual: Vector4:<[1.0,2.0,3.0,4.0]>'));\n        expect(e.message, contains('Which: is not an instance of Vector2'));\n      }\n    });\n\n    test('fails on value mismatch', () {\n      try {\n        expect(Vector2(101, 217), closeToVector(Vector2(100, 220)));\n      } on TestFailure catch (e) {\n        expect(\n          e.message,\n          contains('Expected: a Vector2 object within 1e-15 of (100.0, 220.0)'),\n        );\n        expect(e.message, contains('Actual: Vector2:<[101.0,217.0]>'));\n        expect(e.message, contains('Which: is at distance 3.16227766'));\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_test/test/debug_text_formatter_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/text.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('DebugTextFormatter', () {\n    testGolden(\n      'Render debug text',\n      (game, tester) async {\n        game.add(\n          _TextElementsComponent([\n            DebugTextRenderer().format('one two  three')..translate(5, 5),\n            DebugTextRenderer().format(' x ')..translate(5, 25),\n            DebugTextRenderer().format('  ')..translate(5, 45),\n            DebugTextRenderer().format('')..translate(25, 45),\n            DebugTextRenderer(\n              color: const Color(0xFFFF88AA),\n            ).format('Flame Engine')..translate(5, 65),\n            DebugTextRenderer(fontWeight: FontWeight.bold).format('Blue Fire')\n              ..translate(5, 85),\n            DebugTextRenderer(fontWeight: FontWeight.w900).format('Blue Fire')\n              ..translate(5, 105),\n            DebugTextRenderer(fontStyle: FontStyle.italic).format('Blue Fire')\n              ..translate(5, 125),\n            DebugTextRenderer(\n              fontWeight: FontWeight.bold,\n              fontStyle: FontStyle.italic,\n              color: const Color(0xFF0088FF),\n            ).format('a b c d e f g h i')..translate(5, 145),\n            DebugTextRenderer(fontSize: 10).format('www.flame-engine.org')\n              ..translate(5, 165),\n          ]),\n        );\n      },\n      goldenFile: 'golden_debug_text.png',\n      size: Vector2(300, 200),\n    );\n  });\n}\n\nclass _TextElementsComponent extends PositionComponent {\n  _TextElementsComponent(this.elements);\n\n  final List<InlineTextElement> elements;\n\n  @override\n  void render(Canvas canvas) {\n    for (final element in elements) {\n      element.draw(canvas);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_test/test/epsilon_test.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:flame_test/src/close_to_vector.dart';\nimport 'package:flame_test/src/epsilon.dart';\nimport 'package:test/test.dart';\nimport 'package:vector_math/vector_math.dart';\n\nvoid main() {\n  group('Float32 epsilon tests', () {\n    test('getNextFloat32 should return a slightly larger value', () {\n      const original = 1.0;\n      final next = nextFloat32(original);\n\n      expect(next, greaterThan(original));\n      expect(\n        next - original,\n        closeTo(1.1920928955078125e-7, 1e-7),\n      ); // Expected epsilon for 1.0\n    });\n\n    test('getPrevFloat32 should return a slightly smaller value', () {\n      const original = 1.0;\n      final prev = prevFloat32(original);\n\n      expect(prev, lessThan(original));\n      expect(\n        original - prev,\n        closeTo(1.1920928955078125e-7, 1e-7),\n      ); // Expected epsilon for 1.0\n    });\n\n    test('next > original > prev for positive values', () {\n      final testValues = [0.5, 1.0, 2.0, 10.0, 100.0, 1000.0];\n\n      for (final value in testValues) {\n        final next = nextFloat32(value);\n        final prev = prevFloat32(value);\n\n        expect(\n          next,\n          greaterThan(value),\n          reason: 'Next value should be greater than $value',\n        );\n        expect(\n          prev,\n          lessThan(value),\n          reason: 'Previous value should be less than $value',\n        );\n        expect(\n          next,\n          greaterThan(prev),\n          reason: 'Next should be greater than previous for $value',\n        );\n      }\n    });\n\n    test('next > original > prev for negative values', () {\n      final testValues = [-0.5, -1.0, -2.0, -10.0, -100.0, -1000.0];\n\n      for (final value in testValues) {\n        final next = nextFloat32(value);\n        final prev = prevFloat32(value);\n\n        expect(\n          next,\n          greaterThan(value),\n          reason: 'Next value should be greater than $value',\n        );\n        expect(\n          prev,\n          lessThan(value),\n          reason: 'Previous value should be less than $value',\n        );\n      }\n    });\n\n    test('epsilon scales with value magnitude', () {\n      // Test with powers of 10.\n      for (var i = 0; i <= 5; i++) {\n        final value = math.pow(10, i).toDouble();\n        final next = nextFloat32(value);\n        final epsilon = next - value;\n\n        // The ratio of epsilon to value should be roughly constant.\n        final relativeEpsilon = epsilon / value;\n        expect(\n          relativeEpsilon,\n          closeTo(1.1920928955078125e-7, 1e-7),\n          reason: 'Relative epsilon should be consistent for value $value',\n        );\n      }\n    });\n\n    test('when value is zero', () {\n      const zero = 0.0;\n      final nextAfterZero = nextFloat32(zero);\n      final prevBeforeZero = prevFloat32(zero);\n\n      expect(nextAfterZero, greaterThan(zero));\n      expect(prevBeforeZero, lessThan(zero));\n\n      // The smallest positive float32 is approximately 1.4e-45\n      expect(nextAfterZero, closeTo(1.401298464324817e-45, 1e-45));\n      expect(prevBeforeZero, closeTo(-1.401298464324817e-45, 1e-45));\n    });\n\n    test('when value is near infinity', () {\n      // Float32 max is approximately 3.4028235e+38\n      const largeValue = 3.4028234e+38;\n      final next = nextFloat32(largeValue);\n      final prev = prevFloat32(largeValue);\n\n      expect(next, greaterThan(largeValue));\n      expect(prev, lessThan(largeValue));\n      expect(\n        next.isFinite,\n        isFalse,\n        reason: 'Next value should be infinity for largeValue',\n      );\n    });\n\n    test('next / prev should be ~ reversible', () {\n      final testValues = [1.0, -1.0, 123.0, -42.0];\n\n      for (final value in testValues) {\n        final next = nextFloat32(value);\n        final backToOriginal = prevFloat32(next);\n\n        // Small precision tolerance.\n        expect(\n          backToOriginal,\n          closeTo(value, 1e-6),\n          reason: 'getPrev(getNext(value)) should be close to value',\n        );\n\n        final prev = prevFloat32(value);\n        final alsoBackToOriginal = nextFloat32(prev);\n\n        expect(\n          alsoBackToOriginal,\n          closeTo(value, 1e-6),\n          reason: 'getNext(getPrev(value)) should be close to value',\n        );\n      }\n    });\n\n    test('NaN stays NaN', () {\n      const nan = double.nan;\n      expect(nextFloat32(nan).isNaN, isTrue);\n      expect(prevFloat32(nan).isNaN, isTrue);\n    });\n\n    test('infinite stays inf, next(-inf) and prev(inf) are finite', () {\n      // Positive infinity should remain infinity when getting next\n      expect(nextFloat32(double.infinity), equals(double.infinity));\n      // Previous value before positive infinity should be finite\n      expect(prevFloat32(double.infinity).isFinite, isTrue);\n\n      // Next value after negative infinity should be finite\n      expect(nextFloat32(double.negativeInfinity).isFinite, isTrue);\n      // Negative infinity should remain infinity when getting previous\n      expect(\n        prevFloat32(double.negativeInfinity),\n        equals(double.negativeInfinity),\n      );\n    });\n    group('Vector2Float32Precision', () {\n      test('test with synthetic game environment', () {\n        const dt = 0.013;\n        const speed = double.infinity;\n        final actualPursuer = Vector2.zero();\n\n        final delta = Vector2(\n          0.0,\n          0.0,\n        );\n\n        for (var i = 0; i < 10000; i++) {\n          final targetPosition = Vector2.random()..scale(1e10);\n          delta.setValues(\n            targetPosition.x - actualPursuer.x,\n            targetPosition.y - actualPursuer.y,\n          );\n          final distance = delta.length;\n          const deltaOffset = speed * dt;\n          if (distance > deltaOffset) {\n            delta.scale(deltaOffset / distance);\n          }\n          final tolerancePursuer = toleranceVector2Float32(actualPursuer);\n          actualPursuer.setFrom(delta..add(actualPursuer));\n\n          // Calculate tolerance\n          final tolerance =\n              toleranceVector2Float32(targetPosition) + tolerancePursuer;\n\n          expect(\n            actualPursuer,\n            closeToVector(targetPosition, tolerance),\n            reason:\n                'Pursuer position should be close to target position'\n                ' at iteration $i',\n          );\n        }\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_test/test/expect_double_test.dart",
    "content": "import 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('expectDouble', () {\n    test('can test double', () {\n      const one = 1.0;\n      expectDouble(one + 1.0, 2.0);\n      expectDouble(one + 1.1, 2.0, epsilon: 0.11);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_test/test/fails_assert_test.dart",
    "content": "import 'package:flame_test/flame_test.dart';\nimport 'package:test/test.dart';\n\nvoid main() {\n  group('failsAssert', () {\n    test('without message', () {\n      expect(\n        () {\n          // ignore: prefer_asserts_with_message\n          assert(2 + 2 == 5);\n        },\n        failsAssert(),\n      );\n    });\n\n    test('with message', () {\n      expect(\n        () {\n          assert(2 + 2 == 5, 'Basic arithmetic error');\n        },\n        failsAssert('Basic arithmetic error'),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_test/test/flame_async_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  var instructions = 0;\n\n  tearDown(() {\n    assert(instructions == 9, 'There should be exactly 9 instructions');\n  });\n  testWithFlameGame(\n    'runs all the async tests',\n    (game) async {\n      final world = game.world;\n      await world.ensureAdd(Component());\n      instructions++;\n      await world.ensureAdd(Component());\n      instructions++;\n      await world.ensureAdd(Component());\n      instructions++;\n      await world.ensureAdd(Component());\n      instructions++;\n      await world.ensureAdd(Component());\n      instructions++;\n      await world.ensureAdd(Component());\n      instructions++;\n      await world.ensureAdd(Component());\n      instructions++;\n      await world.ensureAdd(Component());\n      instructions++;\n\n      expect(world.children.length, equals(8));\n      instructions++;\n    },\n  );\n}\n"
  },
  {
    "path": "packages/flame_test/test/golden_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Golden tests', () {\n    final tester = FlameTester(FlameGame.new);\n\n    tester.testGameWidget(\n      'renders correctly',\n      setUp: (game, _) async {\n        await game.ensureAdd(\n          CircleComponent(\n            radius: 10,\n            position: Vector2.all(100),\n            paint: Paint()..color = Colors.white,\n          ),\n        );\n      },\n      verify: (game, tester) async {\n        await expectLater(\n          find.byGame<FlameGame>(),\n          matchesGoldenFile('golden_test.png'),\n        );\n      },\n    );\n\n    testGolden(\n      'Same test but with testGolden',\n      (game, tester) async {\n        final paint = Paint()..color = Colors.white;\n        game.add(\n          CircleComponent(radius: 10, position: Vector2.all(100), paint: paint),\n        );\n      },\n      goldenFile: 'golden_test.png',\n    );\n\n    testGolden(\n      'Same test, but with smaller size',\n      (game, tester) async {\n        final paint = Paint()..color = Colors.white;\n        game.add(\n          CircleComponent(radius: 10, position: Vector2.all(100), paint: paint),\n        );\n      },\n      size: Vector2(200, 200),\n      goldenFile: 'golden_test_small.png',\n    );\n\n    testGolden(\n      'skipped test',\n      (game, tester) async {},\n      goldenFile: 'golden_test.png',\n      skip: true,\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_test/test/random_test_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('testRandom', () {\n    // The primary difficulty with testing `testRandom` is that it is a *test*\n    // and tests cannot be invoked within other tests. So, instead we will\n    // invoke `testRandom` in the main body, and then hope that all these tests\n    // will be invoked in order.\n\n    group('Uses different seed each time', () {\n      final seeds = <int>[];\n      for (var i = 0; i < 50; i++) {\n        testRandom('a', (Random rnd) => seeds.add(rnd.nextInt(1000000)));\n      }\n      test('verify', () {\n        final nTotal = seeds.length;\n        // Allow some seeds to coincide by pure luck\n        expect(seeds.toSet().length, greaterThanOrEqualTo(nTotal - 2));\n      });\n    });\n\n    group('Uses specific seed', () {\n      final seeds = <int>[];\n      testRandom(\n        'b',\n        (Random rnd) => seeds.add(rnd.nextInt(1000000)),\n        seed: 123456,\n      );\n      test('verify', () {\n        expect(seeds[0], 778213);\n      });\n    });\n\n    group('Repeat count works', () {\n      final seeds = <int>[];\n      testRandom(\n        'c',\n        (Random rnd) => seeds.add(rnd.nextInt(1000000)),\n        repeatCount: 20,\n      );\n      test('verify', () {\n        expect(seeds.length, 20);\n        expect(seeds.toSet().length, greaterThanOrEqualTo(18));\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_test/test/test_with_game_test.dart",
    "content": "import 'package:flame/game.dart';\nimport 'package:flame_test/flame_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('testWithFlameGame', () {\n    testWithFlameGame(\n      'game is properly initialized',\n      (game) async {\n        expect(game.isLoaded, true);\n        expect(game.isMounted, true);\n      },\n    );\n  });\n\n  group('testWithGame', () {\n    List<String>? storedEvents;\n\n    testWithGame<_RecordedGame>(\n      'correct event sequence',\n      _RecordedGame.new,\n      (game) async {\n        var events = <String>[];\n        events = game.events;\n        expect(\n          events,\n          ['onGameResize [800.0,600.0]', 'onLoad', 'onMount'],\n        );\n        // Save for the next test\n        storedEvents = events;\n      },\n    );\n\n    // This test may only be called after the previous test has run\n    test(\n      'Game.onRemove is called',\n      () {\n        expect(\n          storedEvents,\n          ['onGameResize [800.0,600.0]', 'onLoad', 'onMount', 'onRemove'],\n        );\n      },\n    );\n  });\n}\n\nclass _RecordedGame extends FlameGame {\n  final List<String> events = [];\n\n  @override\n  void onMount() {\n    super.onMount();\n    events.add('onMount');\n  }\n\n  @override\n  Future<void> onLoad() async {\n    await super.onLoad();\n    events.add('onLoad');\n  }\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    events.add('onGameResize $size');\n  }\n\n  @override\n  void onRemove() {\n    events.add('onRemove');\n    super.onRemove();\n  }\n}\n"
  },
  {
    "path": "packages/flame_texturepacker/CHANGELOG.md",
    "content": "## 5.1.0\n\n - **REFACTOR**: Asset path resolution/loading in TexturePackerAtlas to correctly handle prefixes and different asset types. ([17fac08d](https://github.com/flame-engine/flame/commit/17fac08d8964991a421cde8f5dd1ace4dc4e9063))\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n - **FEAT**: Support package argument in asset loading methods and widgets ([#3835](https://github.com/flame-engine/flame/issues/3835)). ([3f6f95b0](https://github.com/flame-engine/flame/commit/3f6f95b0225df9e4035c74de6cfad501e8c45167))\n\n## 5.0.5\n\n - Update a dependency to the latest release.\n\n## 5.0.4\n\n - Update a dependency to the latest release.\n\n## 5.0.3\n\n - Update a dependency to the latest release.\n\n## 5.0.2\n\n - Update a dependency to the latest release.\n\n## 5.0.1\n\n - Update a dependency to the latest release.\n\n## 5.0.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **PERF**: TexturePacker optimizations ([#3647](https://github.com/flame-engine/flame/issues/3647)). ([5cc2eedb](https://github.com/flame-engine/flame/commit/5cc2eedb1cb17f249c97889ba924e763f83d774e))\n\n## 4.4.1\n\n - Update a dependency to the latest release.\n\n## 4.4.0\n\n - **FEAT**: Implement measure to fix ghost lines and graphical artifacts in Sprites ([#3590](https://github.com/flame-engine/flame/issues/3590)). ([6fd36bc1](https://github.com/flame-engine/flame/commit/6fd36bc1d883d61621806fba54a792dc6924c4e8))\n\n## 4.3.1\n\n - Update a dependency to the latest release.\n\n## 4.3.0\n\n - **FEAT**: Use `XFile` to support web platform ([#3569](https://github.com/flame-engine/flame/issues/3569)). ([20731167](https://github.com/flame-engine/flame/commit/20731167e8574e98d784b9734dbcee9ba6e6aa88))\n\n## 4.2.0\n\n - **FEAT**: Whitelist textures ([#3505](https://github.com/flame-engine/flame/issues/3505)). ([9ca9e858](https://github.com/flame-engine/flame/commit/9ca9e858442031cf91798e0fe09cbadc232b3900))\n\n## 4.1.9\n\n - **FIX**: Remove outdated deprecations ([#3541](https://github.com/flame-engine/flame/issues/3541)). ([b918e40d](https://github.com/flame-engine/flame/commit/b918e40d0ce14b89ba9b5c82aed8ff51d6f549c3))\n\n## 4.1.8\n\n - Update a dependency to the latest release.\n\n## 4.1.7\n\n - **FIX**: Use game's asset cache for texture packer ([#3523](https://github.com/flame-engine/flame/issues/3523)). ([835c40fc](https://github.com/flame-engine/flame/commit/835c40fc6bbc81218fe5c7d321a4a81e1853cf85))\n - **FIX**: Unhandled Exception: Unable to load asset. Introduced on Texturepacker 4.16 ([#3506](https://github.com/flame-engine/flame/issues/3506)). ([3af91b0e](https://github.com/flame-engine/flame/commit/3af91b0e6cbb28c0862317d572dde5f659592c2b))\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 4.1.6\n\n - **FIX**: Remove forced location of the atlas file. ([#3481](https://github.com/flame-engine/flame/issues/3481)). ([bac68dcb](https://github.com/flame-engine/flame/commit/bac68dcbb95ec420c1401e32e60adf42dc338695))\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 4.1.5\n\n - Update a dependency to the latest release.\n\n## 4.1.4\n\n - Update a dependency to the latest release.\n\n## 4.1.3\n\n - Update a dependency to the latest release.\n\n## 4.1.2\n\n - Update a dependency to the latest release.\n\n## 4.1.1\n\n - Update a dependency to the latest release.\n\n## 4.1.0\n\n - **PERF**: Optimize `TexturePackerSprite` when sprites do not need to be rotated ([#3236](https://github.com/flame-engine/flame/issues/3236)). ([e9512e9b](https://github.com/flame-engine/flame/commit/e9512e9b28188476d5956e875430f1ef195f5882))\n - **FEAT**: Enhance TexturePackerSprite ([#3224](https://github.com/flame-engine/flame/issues/3224)). ([0b0a6c1b](https://github.com/flame-engine/flame/commit/0b0a6c1bacfca8772d1b9518e9433d994e68bae1))\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 4.0.1\n\n - **FIX**: TexturePacker fixes the wrong path for the atlas file. ([#3124](https://github.com/flame-engine/flame/issues/3124)). ([69f5c388](https://github.com/flame-engine/flame/commit/69f5c388ce4e0a64ba5f7331a596777a9eab1e40))\n\n## 4.0.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FEAT**: Use `Flame.images` in flame_texturepacker ([#3103](https://github.com/flame-engine/flame/issues/3103)). ([418cc578](https://github.com/flame-engine/flame/commit/418cc578053d969a4a5c3789b1713b9e1a4b3bdd))\n\n## 3.2.0\n\n - **REFACTOR**: Deprecate `fromAtlas` in favour of `atlasFromAssets` and `atlasFromStorage` ([#3098](https://github.com/flame-engine/flame/issues/3098)). ([6c8190b7](https://github.com/flame-engine/flame/commit/6c8190b7215671e7d6e1e271b6aac2a9723ec20d))\n - **FEAT**: Support for new atlas format and rotated sprites ([#3097](https://github.com/flame-engine/flame/issues/3097)). ([ed690b30](https://github.com/flame-engine/flame/commit/ed690b3048924749f829c7c44156e258bf4ab3e7))\n - **FEAT**(flame_texturepacker): Expose TexturePackerAtlas ([#3047](https://github.com/flame-engine/flame/issues/3047)). ([892052b9](https://github.com/flame-engine/flame/commit/892052b99a21a8e371c4163e1e1918fd187c6e11))\n\n## 3.1.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 3.0.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Update `flame_texturepacker`'s file structure ([#3014](https://github.com/flame-engine/flame/issues/3014)). ([982f2263](https://github.com/flame-engine/flame/commit/982f2263daae882fb456e750298c874b77c5471b))\n - **FEAT**: TexturePacker atlas can be generated from device's file ([#3006](https://github.com/flame-engine/flame/issues/3006)). ([4e6968a0](https://github.com/flame-engine/flame/commit/4e6968a05c659aae09e9f613870c6e5b326f4b44))\n - **BREAKING** **FEAT**: Transfer flame_texturepacker to monorepo ([#2987](https://github.com/flame-engine/flame/issues/2987)). ([45c87ddf](https://github.com/flame-engine/flame/commit/45c87ddfb668b035f095cdc17f1a4b7662a3ae11))\n\n## 2.1.0\n\n - Load spritesheets as Map\n\n## 1.0.0\n\n - Load spritesheets from TexturePacker\n"
  },
  {
    "path": "packages/flame_texturepacker/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_texturepacker/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nA flame plugin to import sprite sheets generated by <a href=\"https://github.com/crashinvaders/gdx-texture-packer-gui\">Gdx Texture Packer</a> and\n<a href=\"https://www.codeandweb.com/texturepacker\">Code & Web Texture Packer</a>\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_texturepacker\" ><img src=\"https://img.shields.io/pub/v/flame_texturepacker.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n\n# flame_texturepacker\n\nTexturePacker is a tool to create efficient sprite sheets. This plugin allows you to import sprite\nsheets generated by [Gdx Texture Packer][2] and [Code and Web Texture Packer][1] into your Flame game.\n\n\n## Install from Pub\n\n```console\nflutter pub add flame_texturepacker\n```\n\n\n## Usage\n\n\n### Asset Storage\n\nDrop generated atlas file and sprite sheet images into the `assets/images/` and link the files in your\n`pubspec.yaml` file:\n\n```yaml\n assets:\n   - assets/images/atlas_map.atlas\n   - assets/images/sprite_sheet1.png\n```\n\nImport the plugin like this:\n\n`import 'package:flame_texturepacker/flame_texturepacker.dart';`\n\nLoad the TextureAtlas passing the path of the sprite sheet atlas file:\n\n```Dart\nfinal atlas = await TexturePackerAtlas.load('atlas_map.atlas');\n```\n\n\n### Extension on Game\n\n\nFor convenience, there is also an extension on `Game` (and `FlameGame`)\nthat allows you to load an atlas directly:\n\n\n```dart\nclass MyGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    final atlas = await atlasFromAssets('atlas_map.atlas');\n    // ...\n  }\n}\n```\n\n\n### Loading from a Package\n\n\nTo load an atlas from another Flutter package, use the `package` parameter:\n\n```Dart\nfinal atlas = await TexturePackerAtlas.load(\n  'atlas_map.atlas',\n  package: 'my_assets_package',\n);\n```\n\n\n### Paths and Prefixes\n\n\nBy default, `TexturePackerAtlas.load` looks for files in `assets/images/`. This is controlled by the\n`assetsPrefix` parameter, which defaults to `'images'`.\n\n\n#### 1. Default usage (relative to `assets/images/`)\n\n\n```dart\n// Path: assets/images/hero.atlas\nfinal atlas = await TexturePackerAtlas.load('hero.atlas');\n```\n\n\n#### 2. Custom prefix (relative to `assets/`)\n\n\n```dart\n// Path: assets/atlases/hero.atlas\nfinal atlas = await TexturePackerAtlas.load(\n  'hero.atlas',\n  assetsPrefix: 'atlases',\n);\n```\n\n\n#### 3. Full path (stripping standard prefix)\n\n\nIf you provide a path that already includes the standard `assets/`\nor `images/` prefix, the library will automatically strip them to avoid duplication.\nThis is particularly useful when working with full asset paths.\n\n\n```dart\n// Path: assets/images/mega_explosions.atlas\nfinal atlas = await TexturePackerAtlas.load(\n  'assets/images/mega_explosions.atlas',\n  assetsPrefix: '',\n);\n```\n\n\n#### 4. Automatic Package Detection\n\n\nIf you provide a path that starts with `packages/package_name/...`,\nthe library will automatically detect the package name\nand adjust the internal loading logic.\n\n```dart\n// Path: packages/my_assets_package/assets/images/heroes.atlas\nfinal atlas = await TexturePackerAtlas.load('packages/my_assets_package/assets/images/heroes.atlas');\n```\n\n\n### Whitelist Images\n\nThis is optional, but recommended to avoid loading every sprite from your texture pack into memory.\nUse a list of relative paths to load only the Sprites you need into memory.\n\n```Dart\nfinal regions = await TexturePackerAtlas.loadAtlas('atlas_map.atlas');\nfinal atlas = TexturePackerAtlas.fromAtlas(regions, whiteList: [\n  'weapons/',\n  'ships/',\n  'explosions/'\n]);\n```\n\n\n### File Storage\n\nIf you are using file storage, grab your atlas file like this:\n\n```Dart\nfinal documentsPath = (await getApplicationDocumentsDirectory()).path;\nfinal atlas = await TexturePackerAtlas.load(\n  '$documentsPath/atlas_map.atlas',\n  fromStorage: true,\n);\n```\n\nGet a list of sprites ordered by their index, you can use the list to generate an animation:\n\n```Dart\nfinal spriteList = atlas.findSpritesByName('robot_walk');\n\nfinal animation = SpriteAnimation.spriteList(\n spriteList,\n stepTime: 0.1,\n loop: true,\n);\n```\n\nOr use the convenience method `getAnimation`:\n\n```Dart\nfinal animation = atlas.getAnimation('robot_walk', stepTime: 0.1, loop: true);\n```\n\nIf your atlas contains multiple animations, load it once and reuse it:\n\n```Dart\nfinal atlas = await TexturePackerAtlas.load('atlas_map.atlas');\n\nfinal walkAnim = atlas.getAnimation('robot_walk');\nfinal runAnim  = atlas.getAnimation('robot_run');\nfinal jumpAnim = atlas.getAnimation('robot_jump', loop: false);\n```\n\nGet individual sprites by name:\n\n```Dart\nfinal jumpSprite = atlas.findSpriteByName('robot_jump')!;\nfinal fallSprite = atlas.findSpriteByName('robot_fall')!;\nfinal idleSprite = atlas.findSpriteByName('robot_idle')!;\n```\n\n\n## Supported Features\n\n| Feature            | Supported |\n|--------------------|-----------|\n| Allow Rotation     | YES       |\n| Multiple Pages     | YES       |\n| Use indices        | YES       |\n| Strip whitespace X | YES       |\n| Strip whitespace Y | YES       |\n\n\n## Example\n\nFull working example can be found in [example folder][3].\n\nNote: Sprites used in this example can be found OpenGameArt [here][4].\n\n\n## Credits\n\nThanks to [Jonas Fröber][5] for the original implementation.\nThanks to [Gnarhard][6] for the feature to load an atlas file from a device's storage,\nsprite whitelisting, and memory optimizations.\n\n[1]: https://www.codeandweb.com/texturepacker 'Code & Web Texture Packer'\n[2]: https://github.com/crashinvaders/gdx-texture-packer-gui 'Gdx Texture Packer'\n[3]: example/lib/main.dart 'Full working example'\n[4]: https://opengameart.org/content/toon-characters-1 'Robot sprite'\n[5]: https://github.com/Brixto\n[6]: https://github.com/gnarhard\n"
  },
  {
    "path": "packages/flame_texturepacker/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_texturepacker/example/README.md",
    "content": "# flame_texturepacker example\n\nSimple project to showcase the usage of flame_packer by rendering a few sprites and an animation\nloaded from a texture packer atlas.\n"
  },
  {
    "path": "packages/flame_texturepacker/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_texturepacker/example/assets/images/atlas_map.atlas",
    "content": "\nsprite_sheet1.png\nsize: 512, 512\nformat: RGBA8888\nfilter: Nearest, Nearest\nrepeat: none\nrobot_jump\n  rotate: true\n  xy: 196, 260\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: -1\nrobot_walk\n  rotate: false\n  xy: 2, 196\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 0\nrobot_walk\n  rotate: true\n  xy: 2, 2\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 5\nrobot_walk\n  rotate: false\n  xy: 260, 2\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 2\n\nsprite_sheet2.png\nsize: 512, 512\nformat: RGBA8888\nfilter: Nearest, Nearest\nrepeat: none\nrobot_duck\n  rotate: true\n  xy: 2, 2\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: -1\nrobot_fall\n  rotate: false\n  xy: 260, 2\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: -1\nrobot_walk\n  rotate: false\n  xy: 2, 196\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 7\nrobot_walk\n  rotate: true\n  xy: 196, 260\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 4\n\nsprite_sheet3.png\nsize: 512, 512\nformat: RGBA8888\nfilter: Nearest, Nearest\nrepeat: none\nrobot_idle\n  rotate: false\n  xy: 2, 196\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: -1\nrobot_walk\n  rotate: true\n  xy: 2, 2\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 1\nrobot_walk\n  rotate: true\n  xy: 196, 260\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 6\nrobot_walk\n  rotate: false\n  xy: 260, 2\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 3\n"
  },
  {
    "path": "packages/flame_texturepacker/example/lib/main.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_texturepacker/flame_texturepacker.dart';\nimport 'package:flutter/material.dart';\n\nvoid main() {\n  final myGame = MyGame();\n  runApp(\n    GameWidget(\n      game: myGame,\n    ),\n  );\n}\n\nclass MyGame extends FlameGame {\n  @override\n  Future<void> onLoad() async {\n    super.onLoad();\n\n    // Load the atlasMap.\n    final atlas = await atlasFromAssets('atlas_map.atlas');\n\n    // Get a list of sprites ordered by their index\n    final walkingSprites = atlas.findSpritesByName('robot_walk');\n\n    // Create animation with the list of sprites\n    final walkingAnimation = SpriteAnimation.spriteList(\n      walkingSprites,\n      stepTime: 0.1,\n    );\n\n    // Get individual sprites by name\n    final jumpSprite = atlas.findSpriteByName('robot_jump')!;\n    final fallSprite = atlas.findSpriteByName('robot_fall')!;\n    final idleSprite = atlas.findSpriteByName('robot_idle')!;\n\n    // Get the list of all sprites in the sprite sheet\n    final allSprites = atlas.sprites; // ignore: unused_local_variable\n\n    add(\n      SpriteComponent(\n        sprite: jumpSprite,\n        position: Vector2(200, 100),\n        size: Vector2(72, 96),\n      ),\n    );\n\n    add(\n      SpriteComponent(\n        sprite: fallSprite,\n        position: Vector2(300, 100),\n        size: Vector2(72, 96),\n      ),\n    );\n\n    add(\n      SpriteComponent(\n        sprite: idleSprite,\n        position: Vector2(400, 100),\n        size: Vector2(72, 96),\n      ),\n    );\n\n    add(\n      SpriteAnimationComponent(\n        animation: walkingAnimation,\n        position: Vector2(300, 200),\n        size: Vector2(72, 96),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_texturepacker/example/pubspec.yaml",
    "content": "name: flame_texturepacker_example\nresolution: workspace\ndescription: An example of how to use the flame_texturepacker package.\nversion: 1.0.0\npublish_to: none\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_texturepacker: ^5.1.0\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n\nflutter:\n  assets:\n    - assets/images/"
  },
  {
    "path": "packages/flame_texturepacker/lib/flame_texturepacker.dart",
    "content": "library flame_texturepacker;\n\nexport 'package:flame_texturepacker/src/extension_on_game.dart';\nexport 'package:flame_texturepacker/src/texture_packer_atlas.dart';\nexport 'package:flame_texturepacker/src/texture_packer_parser.dart';\nexport 'package:flame_texturepacker/src/texture_packer_sprite.dart';\n"
  },
  {
    "path": "packages/flame_texturepacker/lib/src/extension_on_game.dart",
    "content": "import 'package:flame/cache.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_texturepacker/flame_texturepacker.dart';\n\nextension TexturepackerLoader on Game {\n  /// Loads the specified pack file from assets\n  /// Uses the parent directory of the pack file to find the page images.\n  Future<TexturePackerAtlas> atlasFromAssets(\n    String assetsPath, {\n    Images? images,\n    AssetsCache? assets,\n    bool useOriginalSize = true,\n    List<String> whiteList = const [],\n    String assetsPrefix = 'images',\n    String? package,\n  }) => TexturePackerAtlas.load(\n    assetsPath,\n    images: images ?? this.images,\n    assets: assets ?? this.assets,\n    useOriginalSize: useOriginalSize,\n    whiteList: whiteList,\n    assetsPrefix: assetsPrefix,\n    package: package,\n  );\n\n  /// Loads the specified pack file from storage\n  /// Uses the parent directory of the pack file to find the page images.\n  Future<TexturePackerAtlas> atlasFromStorage(\n    String storagePath, {\n    Images? images,\n    bool useOriginalSize = true,\n    List<String> whiteList = const [],\n  }) => TexturePackerAtlas.load(\n    storagePath,\n    fromStorage: true,\n    images: images ?? this.images,\n    useOriginalSize: useOriginalSize,\n    whiteList: whiteList,\n  );\n}\n"
  },
  {
    "path": "packages/flame_texturepacker/lib/src/model/page.dart",
    "content": "import 'dart:ui';\n\nclass Page {\n  late String textureFile;\n  Image? texture;\n  late int width;\n  late int height;\n  late String format;\n  late String minFilter;\n  late String magFilter;\n  String? repeat;\n\n  /// Returns true if the texture has been loaded.\n  bool get isLoaded => texture != null;\n}\n"
  },
  {
    "path": "packages/flame_texturepacker/lib/src/model/region.dart",
    "content": "import 'package:flame_texturepacker/src/model/page.dart';\nimport 'package:flutter/foundation.dart';\n\n/// Represents a region within the texture packer atlas.\n@immutable\nfinal class Region {\n  /// The page in the texture pack this region belongs to.\n  final Page page;\n\n  /// The name of the original image file, without the file's extension.\n  /// If the name ends with an underscore followed by only numbers, that part is\n  /// excluded: underscores denote special instructions to the texture packer.\n  final String name;\n\n  /// The left position of the region in the texture atlas.\n  final double left;\n\n  /// The top position of the region in the texture atlas.\n  final double top;\n\n  /// The width of the image, after whitespace was removed for packing.\n  final double width;\n\n  /// The height of the image, after whitespace was removed for packing.\n  final double height;\n\n  /// The offset from the left of the original image to the left of the packed\n  /// image, after whitespace was removed for packing.\n  final double offsetX;\n\n  /// The offset from the bottom of the original image to the bottom of the\n  /// packed image, after whitespace was removed for packing.\n  final double offsetY;\n\n  /// The width of the image, before whitespace was removed and rotation was\n  /// applied for packing.\n  final double originalWidth;\n\n  /// The height of the image, before whitespace was removed for packing.\n  final double originalHeight;\n\n  /// The degrees the region has been rotated, counter clockwise between 0 and\n  /// 359. Most atlas region handling deals only with 0 or 90 degree rotation\n  /// (enough to handle rectangles).\n  /// More advanced texture packing may support other rotations (eg, for tightly\n  /// packing polygons).\n  final int degrees;\n\n  /// If true, the region has been rotated 90 degrees counter clockwise.\n  final bool rotate;\n\n  /// The number at the end of the original image file name, or -1 if none.\n  ///\n  /// When sprites are packed, if the original file name ends with a number, it\n  /// is stored as the index and is not considered as part of the sprite's name.\n  /// This is useful for keeping animation frames in order.\n  final int index;\n\n  /// Creates a new [Region] with the given properties.\n  const Region({\n    required this.page,\n    required this.name,\n    this.left = 0,\n    this.top = 0,\n    this.width = 0,\n    this.height = 0,\n    this.offsetX = 0,\n    this.offsetY = 0,\n    double? originalWidth,\n    double? originalHeight,\n    this.degrees = 0,\n    this.rotate = false,\n    this.index = -1,\n  }) : originalWidth = originalWidth ?? width,\n       originalHeight = originalHeight ?? height;\n}\n"
  },
  {
    "path": "packages/flame_texturepacker/lib/src/texture_packer_atlas.dart",
    "content": "library flame_texturepacker;\n\nimport 'package:collection/collection.dart';\nimport 'package:flame/cache.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/sprite.dart';\nimport 'package:flame_texturepacker/src/texture_packer_parser.dart';\nimport 'package:flame_texturepacker/src/texture_packer_sprite.dart';\n\n/// A texture atlas that contains a collection of [TexturePackerSprite]s.\n///\n/// This class provides methods to load and query sprites from a texture atlas\n/// created by TexturePacker or similar tools.\nclass TexturePackerAtlas {\n  /// List of all sprites contained in this atlas.\n  final List<TexturePackerSprite> sprites;\n\n  /// Creates a new [TexturePackerAtlas] with the given [sprites].\n  TexturePackerAtlas(this.sprites);\n\n  /// Creates a [TexturePackerAtlas] from parsed atlas data.\n  ///\n  /// [atlasData] - The parsed atlas data containing pages and regions\n  /// [whiteList] - Optional list of sprite names to include.\n  ///               If empty, all sprites are included\n  /// [useOriginalSize] - Use original sprite dimensions before packing or not.\n  factory TexturePackerAtlas.fromAtlas(\n    TextureAtlasData atlasData, {\n    List<String> whiteList = const [],\n    bool useOriginalSize = true,\n  }) {\n    return TexturePackerAtlas(\n      atlasData.regions\n          .where(\n            (e) {\n              return whiteList.isEmpty ||\n                  whiteList.any((key) => e.name.contains(key));\n            },\n          )\n          .map(\n            (e) => TexturePackerSprite(\n              e,\n              useOriginalSize: useOriginalSize,\n            ),\n          )\n          .toList(),\n    );\n  }\n\n  /// Loads a texture atlas from a file path.\n  ///\n  /// [path] - The path to the atlas file\n  /// [fromStorage] - Load from device storage (true) or assets (false)\n  /// [useOriginalSize] - Use original sprite dimensions before packing or not.\n  /// [images] - Optional Images cache to use for loading textures\n  /// [assetsPrefix] - Prefix for asset paths (default: 'images')\n  /// [assets] - Optional AssetsCache to use for loading assets\n  /// [whiteList] - Optional list of sprite names to include.\n  ///               If empty, all sprites are included\n  ///\n  /// Returns a [Future] that completes with the loaded [TexturePackerAtlas].\n  static Future<TexturePackerAtlas> load(\n    String path, {\n    bool fromStorage = false,\n    bool useOriginalSize = true,\n    Images? images,\n    String assetsPrefix = 'images',\n    AssetsCache? assets,\n    List<String> whiteList = const [],\n    String? package,\n  }) async {\n    final atlasData = await loadAtlas(\n      path,\n      fromStorage: fromStorage,\n      images: images,\n      assets: assets,\n      assetsPrefix: assetsPrefix,\n      package: package,\n    );\n\n    return TexturePackerAtlas.fromAtlas(\n      atlasData,\n      whiteList: whiteList,\n      useOriginalSize: useOriginalSize,\n    );\n  }\n\n  /// Loads atlas data without creating a [TexturePackerAtlas] instance.\n  ///\n  /// [path] - The path to the atlas file\n  /// [fromStorage] - Load from device storage (true) or assets (false)\n  /// [images] - Optional Images cache to use for loading textures\n  /// [assetsPrefix] - Prefix for asset paths (default: 'images')\n  /// [assets] - Optional AssetsCache to use for loading assets\n  /// [loadImages] - Whether to load images (default: true)\n  ///\n  /// Returns a [Future] that completes with the raw [TextureAtlasData].\n  static Future<TextureAtlasData> loadAtlas(\n    String path, {\n    bool fromStorage = false,\n    Images? images,\n    AssetsCache? assets,\n    String assetsPrefix = 'images',\n    String? package,\n    bool loadImages = true,\n  }) async {\n    try {\n      final atlasData = await TexturePackerParser.parseAtlasMetadata(\n        path,\n        fromStorage: fromStorage,\n        assets: assets,\n        assetsPrefix: assetsPrefix,\n        package: package,\n      );\n\n      if (loadImages) {\n        await TexturePackerParser.loadAtlasDataImages(\n          atlasData,\n          path,\n          fromStorage: fromStorage,\n          images: images,\n          package: package,\n          assetsPrefix: assetsPrefix,\n          assets: assets,\n        );\n      }\n      return atlasData;\n    } on Exception catch (e, stack) {\n      final source = fromStorage ? 'storage' : 'assets';\n      Error.throwWithStackTrace(\n        Exception('Error loading $path from $source: $e'),\n        stack,\n      );\n    }\n  }\n\n  /// Finds a sprite by its name.\n  ///\n  /// [name] - The name of the sprite to find\n  ///\n  /// Returns the first [TexturePackerSprite] with the given name\n  /// or null if not found.\n  TexturePackerSprite? findSpriteByName(String name) {\n    return sprites.firstWhereOrNull(\n      (e) => e.region.name == name,\n    );\n  }\n\n  /// Finds a sprite by its name and index.\n  ///\n  /// [name] - The name of the sprite to find\n  /// [index] - The index of the sprite to find\n  ///\n  /// Returns the [TexturePackerSprite] with the given name and index\n  /// or null if not found.\n  TexturePackerSprite? findSpriteByNameIndex(String name, int index) {\n    return sprites.firstWhereOrNull(\n      (sprite) => sprite.region.name == name && sprite.region.index == index,\n    );\n  }\n\n  /// Finds all sprites with the given name.\n  ///\n  /// [name] - The name of the sprites to find\n  ///\n  /// Returns a list of all [TexturePackerSprite]s with the given name.\n  List<TexturePackerSprite> findSpritesByName(String name) {\n    return sprites\n        .where(\n          (sprite) => sprite.region.name == name,\n        )\n        .toList();\n  }\n\n  /// Creates a [SpriteAnimation] from sprites with the given name.\n  ///\n  /// [name] - The name of the sprites to use for the animation\n  /// [stepTime] - The duration to display each frame, in seconds\n  /// [loop] - Whether the animation should loop\n  SpriteAnimation getAnimation(\n    String name, {\n    double stepTime = 0.1,\n    bool loop = true,\n  }) {\n    final animationSprites = findSpritesByName(name);\n    if (animationSprites.isEmpty) {\n      throw Exception('No sprites found with name \"$name\" in atlas');\n    }\n    return SpriteAnimation.spriteList(\n      animationSprites,\n      stepTime: stepTime,\n      loop: loop,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/flame_texturepacker/lib/src/texture_packer_parser.dart",
    "content": "import 'dart:collection';\nimport 'dart:convert';\n\nimport 'package:cross_file/cross_file.dart';\nimport 'package:flame/cache.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame_texturepacker/src/model/page.dart';\nimport 'package:flame_texturepacker/src/model/region.dart';\nimport 'package:flutter/painting.dart';\n\n/// Type definition for texture atlas data containing pages and regions.\ntypedef TextureAtlasData = ({List<Page> pages, List<Region> regions});\n\n/// Internal parser for TexturePacker atlas files.\nabstract class TexturePackerParser {\n  /// Parses structural data of a texture atlas file.\n  static Future<TextureAtlasData> parseAtlasMetadata(\n    String path, {\n    required bool fromStorage,\n    AssetsCache? assets,\n    String? assetsPrefix,\n    String? package,\n  }) async {\n    final pages = <Page>[];\n    final regions = <Region>[];\n\n    final String fileContent;\n    if (fromStorage) {\n      fileContent = await XFile(path).readAsString();\n    } else {\n      final assetsCache = assets ?? Flame.assets;\n      final prefix = (assetsPrefix ?? '').trim();\n      final cleanPath = path.trim().replaceFirst(RegExp('^/'), '');\n\n      var fullPath = cleanPath;\n      if (prefix.isNotEmpty &&\n          !cleanPath.contains('packages/') &&\n          !cleanPath.startsWith('assets/')) {\n        final effectivePrefix = prefix.endsWith('/') ? prefix : '$prefix/';\n        if (!cleanPath.startsWith(effectivePrefix)) {\n          fullPath = '$effectivePrefix$cleanPath';\n        }\n      }\n\n      final resolved = resolvePath(fullPath, package);\n      final finalPath = resolved.path.startsWith(assetsCache.prefix)\n          ? resolved.path.substring(assetsCache.prefix.length)\n          : resolved.path;\n\n      fileContent = await assetsCache.readFile(\n        finalPath,\n        package: resolved.package,\n      );\n    }\n\n    final lines = LineSplitter.split(\n      fileContent,\n    ).where((line) => line.trim().isNotEmpty);\n\n    final lineQueue = ListQueue<String>.from(lines);\n\n    while (lineQueue.isNotEmpty) {\n      final page = _parsePageMetadata(lineQueue);\n      pages.add(page);\n\n      while (lineQueue.isNotEmpty) {\n        final line = lineQueue.first.trim();\n\n        if (isTextureFile(line)) {\n          if (lineQueue.length > 1) {\n            final nextLine = lineQueue.elementAt(1).trim();\n            final isRegionProperty =\n                nextLine.startsWith('bounds:') ||\n                nextLine.startsWith('rotate:') ||\n                nextLine.startsWith('xy:') ||\n                nextLine.startsWith('offsets:') ||\n                nextLine.startsWith('orig:') ||\n                nextLine.startsWith('offset:') ||\n                nextLine.startsWith('index:');\n\n            if (isRegionProperty) {\n              final region = _parseRegion(lineQueue, page);\n              regions.add(region);\n              continue;\n            }\n          }\n          break;\n        }\n\n        final region = _parseRegion(lineQueue, page);\n        regions.add(region);\n      }\n    }\n\n    final hasIndexes = regions.any((r) => r.index != -1);\n\n    if (hasIndexes) {\n      regions.sort((a, b) {\n        final i1 = a.index == -1 ? 0x7FFFFFFF : a.index;\n        final i2 = b.index == -1 ? 0x7FFFFFFF : b.index;\n        return i1 - i2;\n      });\n    }\n\n    return (pages: pages, regions: regions);\n  }\n\n  /// Loads images for all pages in the given atlas data.\n  static Future<void> loadAtlasDataImages(\n    TextureAtlasData atlasData,\n    String path, {\n    required bool fromStorage,\n    Images? images,\n    String? package,\n    String? assetsPrefix,\n    AssetsCache? assets,\n  }) async {\n    final img = images ?? Flame.images;\n    for (final page in atlasData.pages) {\n      final parentPath = (path.split('/')..removeLast()).join('/');\n      var texturePath = parentPath.isEmpty\n          ? page.textureFile\n          : '$parentPath/${page.textureFile}';\n\n      if (fromStorage) {\n        final bytes = await XFile(texturePath).readAsBytes();\n        final image = await decodeImageFromList(bytes);\n        img.add(texturePath, image);\n        page.texture = img.fromCache(texturePath);\n      } else {\n        final prefix = (assetsPrefix ?? '').trim();\n        if (prefix.isNotEmpty &&\n            !texturePath.contains('packages/') &&\n            !texturePath.startsWith('assets/')) {\n          final effectivePrefix = prefix.endsWith('/') ? prefix : '$prefix/';\n          if (!texturePath.startsWith(effectivePrefix)) {\n            texturePath = '$effectivePrefix$texturePath';\n          }\n        }\n\n        final resolved = resolvePath(texturePath, package);\n        final assetsCachePrefix = (assets ?? Flame.assets).prefix;\n\n        String toRelative(String p) => p.startsWith(assetsCachePrefix)\n            ? p.substring(assetsCachePrefix.length)\n            : p;\n\n        final relativePath = toRelative(resolved.path);\n        final relativePrefix = toRelative(img.prefix);\n\n        final finalTexturePath =\n            (relativePrefix.isNotEmpty &&\n                relativePath.startsWith(relativePrefix))\n            ? relativePath.substring(relativePrefix.length)\n            : relativePath;\n\n        page.texture = await img.load(\n          finalTexturePath,\n          package: resolved.package,\n        );\n      }\n    }\n  }\n\n  static Page _parsePageMetadata(ListQueue<String> lineQueue) {\n    final page = Page();\n    page.textureFile = lineQueue.removeFirst();\n\n    while (lineQueue.isNotEmpty && lineQueue.first.contains(':')) {\n      final line = lineQueue.removeFirst();\n      final parts = line.split(':');\n      final key = parts[0].trim();\n      final value = parts[1].trim();\n\n      if (key == 'size') {\n        final sizeParts = value.split(',');\n        page.width = int.parse(sizeParts[0]);\n        page.height = int.parse(sizeParts[1]);\n      } else if (key == 'format') {\n        page.format = value;\n      } else if (key == 'filter') {\n        final filterParts = value.split(',');\n        page.minFilter = filterParts[0];\n        page.magFilter = filterParts[1];\n      } else if (key == 'repeat') {\n        page.repeat = value;\n      }\n    }\n\n    return page;\n  }\n\n  static Region _parseRegion(ListQueue<String> lineQueue, Page page) {\n    final originalName = lineQueue.removeFirst().trim();\n    var name = originalName;\n    var extractedIndex = -1;\n\n    final extensionMatch = RegExp(\n      r'\\.(png|jpg|jpeg|bmp|tga|webp)$',\n      caseSensitive: false,\n    ).firstMatch(name);\n    if (extensionMatch != null) {\n      name = name.substring(0, extensionMatch.start);\n    }\n\n    final nameBeforeIndex = name;\n\n    final indexMatch = RegExp(r'(_?)(\\d+)$').firstMatch(name);\n    if (indexMatch != null) {\n      try {\n        extractedIndex = int.parse(indexMatch.group(2)!);\n        name = name.substring(0, indexMatch.start);\n      } on FormatException catch (e, stack) {\n        Error.throwWithStackTrace(\n          FormatException(\n            'Failed to parse index from sprite name \"$name\". '\n            'Ensure the numeric suffix fits within an integer range.',\n          ),\n          stack,\n        );\n      }\n    }\n\n    final values = <String, List<String>>{};\n\n    while (lineQueue.isNotEmpty) {\n      final line = lineQueue.first.trim();\n\n      if (isTextureFile(line)) {\n        break;\n      }\n\n      final (:count, :entry) = _readEntry(line);\n\n      if (count == 0) {\n        break;\n      }\n\n      values[entry[0]] = entry.sublist(1);\n      lineQueue.removeFirst();\n    }\n\n    final indexValue = values['index'];\n    if (indexValue != null) {\n      extractedIndex = int.parse(indexValue[0]);\n    }\n\n    final xy = values['xy'];\n    final size = values['size'];\n    final bounds = values['bounds'];\n    final offset = values['offset'];\n    final orig = values['orig'];\n    final offsets = values['offsets'];\n    final rotate = values['rotate'];\n    final index = values['index'];\n\n    final offsetOrNull = offsets ?? offset;\n\n    final offsetX = offsetOrNull != null ? double.parse(offsetOrNull[0]) : 0.0;\n    final offsetY = offsetOrNull != null ? double.parse(offsetOrNull[1]) : 0.0;\n\n    final originalWidth = offsets != null\n        ? double.parse(offsets[2])\n        : (orig != null ? double.parse(orig[0]) : 0.0);\n\n    final finalOriginalWidth = originalWidth == 0.0 ? null : originalWidth;\n\n    final originalHeight = offsets != null\n        ? double.parse(offsets[3])\n        : (orig != null ? double.parse(orig[1]) : 0.0);\n\n    final finalOriginalHeight = originalHeight == 0.0 ? null : originalHeight;\n\n    final finalIndex = index != null ? int.parse(index[0]) : extractedIndex;\n    final finalName = finalIndex == -1 ? nameBeforeIndex : name;\n\n    return Region(\n      page: page,\n      name: finalName,\n      left: bounds != null\n          ? double.parse(bounds[0])\n          : (xy != null ? double.parse(xy[0]) : 0.0),\n      top: bounds != null\n          ? double.parse(bounds[1])\n          : (xy != null ? double.parse(xy[1]) : 0.0),\n      width: bounds != null\n          ? double.parse(bounds[2])\n          : (size != null ? double.parse(size[0]) : 0.0),\n      height: bounds != null\n          ? double.parse(bounds[3])\n          : (size != null ? double.parse(size[1]) : 0.0),\n      offsetX: offsetX,\n      offsetY: offsetY,\n      originalWidth: finalOriginalWidth,\n      originalHeight: finalOriginalHeight,\n      degrees: _parseDegrees(rotate?.first),\n      rotate: _parseDegrees(rotate?.first) == 90,\n      index: finalIndex,\n    );\n  }\n\n  static bool isTextureFile(String line) {\n    const imageExtensions = ['.png', '.jpg', '.jpeg', '.bmp', '.tga', '.webp'];\n    final trimmed = line.trim().toLowerCase();\n    return imageExtensions.any(trimmed.endsWith);\n  }\n\n  static ({String path, String? package}) resolvePath(\n    String path,\n    String? package,\n  ) {\n    const pkg = 'packages/';\n    final index = path.indexOf(pkg);\n    if (index != -1) {\n      final subPath = path.substring(index + pkg.length);\n      final segments = subPath.split('/');\n      if (segments.length > 1) {\n        return (\n          path: segments.sublist(1).join('/'),\n          package: package ?? segments[0],\n        );\n      }\n    }\n    return (path: path, package: package);\n  }\n\n  static int _parseDegrees(String? value) {\n    if (value == null) {\n      return 0;\n    }\n    if (value == 'true') {\n      return 90;\n    }\n    if (value == 'false') {\n      return 0;\n    }\n    return int.parse(value);\n  }\n\n  static ({int count, List<String> entry}) _readEntry(String line) {\n    final trimmedLine = line.trim();\n    if (trimmedLine.isEmpty) {\n      return (count: 0, entry: []);\n    }\n    final colonIndex = trimmedLine.indexOf(':');\n    if (colonIndex == -1) {\n      return (count: 0, entry: []);\n    }\n    final entry = <String>[];\n    entry.add(trimmedLine.substring(0, colonIndex).trim());\n    for (var i = 1, lastMatch = colonIndex + 1; ; i++) {\n      final commaIndex = trimmedLine.indexOf(',', lastMatch);\n      if (commaIndex == -1) {\n        entry.add(trimmedLine.substring(lastMatch).trim());\n        return (count: i, entry: entry);\n      }\n      entry.add(trimmedLine.substring(lastMatch, commaIndex).trim());\n      lastMatch = commaIndex + 1;\n      if (i == 4) {\n        return (count: 4, entry: entry);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_texturepacker/lib/src/texture_packer_sprite.dart",
    "content": "import 'dart:math' as math;\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame/rendering.dart';\nimport 'package:flame_texturepacker/src/model/region.dart';\n\n/// {@template _texture_packer_sprite}\n/// A [Sprite] extracted from a texture packer file.\n/// {@endtemplate}\nclass TexturePackerSprite extends Sprite {\n  /// {@macro _texture_packer_sprite}\n  TexturePackerSprite(this.region, {this.useOriginalSize = true})\n    : super(\n        region.page.texture ?? _emptyImage,\n        srcPosition: Vector2(region.left, region.top),\n        srcSize: Vector2(\n          useOriginalSize ? region.originalWidth : region.width,\n          useOriginalSize ? region.originalHeight : region.height,\n        ),\n      ) {\n    if (region.rotate) {\n      final transform = Transform2D()..angle = math.pi / 2;\n      _decorator = Transform2DDecorator(transform);\n    } else {\n      _decorator = null;\n    }\n  }\n\n  /// Region object for [clone] function, don't modify this object properties.\n  final Region region;\n\n  /// If true, use [Region.originalWidth] and [Region.originalHeight] as size;\n  /// otherwise use [Region.width] and [Region.height] as size.\n  final bool useOriginalSize;\n\n  /// The [Region.degrees] field (angle) represented as radians.\n  double get angle => radians(region.degrees.toDouble());\n\n  /// Clone current object with new value for argument [useOriginalSize].\n  TexturePackerSprite clone({bool useOriginalSize = true}) =>\n      TexturePackerSprite(region, useOriginalSize: useOriginalSize);\n\n  Vector2 get _srcSize => Vector2(src.width, src.height);\n\n  Vector2 get _srcSizeRotated => Vector2(src.height, src.width);\n\n  Vector2 get _srcSizeRender => region.rotate ? _srcSizeRotated : _srcSize;\n\n  Vector2 get _offset => Vector2(region.offsetX, region.offsetY);\n\n  Vector2 get _packedSize => Vector2(region.width, region.height);\n\n  Vector2 get _originalSize => Vector2(\n    region.originalWidth,\n    region.originalHeight,\n  );\n\n  Vector2 get offset => useOriginalSize ? _offset : Vector2.zero();\n\n  @override\n  Vector2 get originalSize => useOriginalSize ? _originalSize : _packedSize;\n\n  @override\n  Vector2 get srcSize => _srcSizeRender\n    ..divide(_packedSize)\n    ..multiply(originalSize);\n\n  @override\n  set srcSize(Vector2? size) {\n    final actualSize = Vector2.copy(size ?? originalSize)\n      ..divide(originalSize)\n      ..multiply(_packedSize);\n    if (region.rotate) {\n      actualSize.setValues(actualSize.y, actualSize.x);\n    }\n    src = srcPosition.toPositionedRect(actualSize);\n  }\n\n  @override\n  set srcPosition(Vector2? position) {\n    src = (position ?? Vector2.zero()).toPositionedRect(_srcSize);\n  }\n\n  late final Decorator? _decorator;\n\n  // Used to avoid the creation of new Vector2 objects in render.\n  static final _tmpRenderPosition = Vector2.zero();\n  static final _tmpRenderSize = Vector2.zero();\n  static final _tmpRenderScale = Vector2.zero();\n  static final _tmpRenderImageSize = Vector2.zero();\n  static final _tmpRenderOffset = Vector2.zero();\n\n  @override\n  void render(\n    Canvas canvas, {\n    Vector2? position,\n    Vector2? size,\n    Anchor anchor = Anchor.topLeft,\n    Paint? overridePaint,\n    double? bleed,\n  }) {\n    if (!region.page.isLoaded) {\n      throw StateError(\n        'Texture for page \"${region.page.textureFile}\" has not been loaded. '\n        'Call loadAtlasDataImages() before rendering.',\n      );\n    }\n    if (position != null) {\n      _tmpRenderPosition.setFrom(position);\n    } else {\n      _tmpRenderPosition.setZero();\n    }\n\n    // Get sprite size\n    _tmpRenderSize.setFrom(size ?? srcSize);\n\n    // Calculate topLeft position\n    _tmpRenderPosition.setValues(\n      _tmpRenderPosition.x - (anchor.x * _tmpRenderSize.x),\n      _tmpRenderPosition.y - (anchor.y * _tmpRenderSize.y),\n    );\n\n    // Calculate multiplier value\n    _tmpRenderScale\n      ..setFrom(_tmpRenderSize)\n      ..divide(originalSize);\n\n    // Calculate image size rendered based on packedSize\n    _tmpRenderImageSize\n      ..setFrom(_packedSize)\n      ..multiply(_tmpRenderScale);\n\n    // Calculate image rendered offset from topLeft position\n    _tmpRenderOffset\n      ..setFrom(offset)\n      ..multiply(_tmpRenderScale)\n      ..add(_tmpRenderPosition);\n\n    if (!region.rotate) {\n      // Calculate and render for non-rotated image, must call function render\n      // from super class with anchor = Anchor.topLeft, because we already\n      // calculated size and position based on Anchor.topLeft to rendered by\n      // super class function.\n      _tmpRenderSize.setFrom(_tmpRenderImageSize);\n      _tmpRenderPosition.setFrom(_tmpRenderOffset);\n      return super.render(\n        canvas,\n        position: _tmpRenderPosition,\n        size: _tmpRenderSize,\n        overridePaint: overridePaint,\n      );\n    }\n\n    // Calculate and render for rotated image, must call function render\n    // from super class with anchor = Anchor.topLeft, because we already\n    // calculated size and position based on Anchor.topLeft to rendered by super\n    // class function.\n    _tmpRenderSize.setValues(_tmpRenderImageSize.y, _tmpRenderImageSize.x);\n    _tmpRenderPosition.setValues(\n      _tmpRenderOffset.y - 0,\n      -_tmpRenderOffset.x - _tmpRenderImageSize.x,\n    );\n\n    _decorator?.applyChain(\n      (applyCanvas) => super.render(\n        applyCanvas,\n        position: _tmpRenderPosition,\n        size: _tmpRenderSize,\n        overridePaint: overridePaint,\n        bleed: bleed,\n      ),\n      canvas,\n    );\n  }\n\n  /// Placeholder image used when texture is not yet loaded.\n  static final Image _emptyImage = _createEmptyImage();\n\n  static Image _createEmptyImage() {\n    final recorder = PictureRecorder();\n    Canvas(recorder);\n    final picture = recorder.endRecording();\n    return picture.toImageSync(1, 1);\n  }\n}\n"
  },
  {
    "path": "packages/flame_texturepacker/pubspec.yaml",
    "content": "name: flame_texturepacker\nresolution: workspace\ndescription: A simple plugin for the Flame Engine to import spritesheets generated by the TexturePacker tool.\nversion: 5.1.0\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_texturepacker\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - spritesheet\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  collection: ^1.17.1\n  cross_file: ^0.3.4+2\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n  mocktail: ^1.0.4\n"
  },
  {
    "path": "packages/flame_texturepacker/test/assets/atlasMap.atlas",
    "content": "atlasMap.png\n\tsize: 778, 776\n\trepeat: none\nrobot_duck\n\tbounds: 390, 518, 192, 256\nrobot_fall\n\tbounds: 390, 260, 192, 256\nrobot_idle\n\tbounds: 584, 518, 192, 256\nrobot_jump\n\tbounds: 196, 518, 192, 256\nrobot_walk\n\tindex: 0\n\tbounds: 2, 518, 192, 256\nrobot_walk\n\tindex: 5\n\tbounds: 2, 260, 192, 256\nrobot_walk\n\tindex: 2\n\tbounds: 2, 2, 192, 256\nrobot_walk\n\tindex: 7\n\tbounds: 196, 260, 192, 256\nrobot_walk\n\tindex: 4\n\tbounds: 196, 2, 192, 256\nrobot_walk\n\tindex: 1\n\tbounds: 390, 2, 192, 256\nrobot_walk\n\tindex: 6\n\tbounds: 584, 260, 192, 256\nrobot_walk\n\tindex: 3\n\tbounds: 584, 2, 192, 256\n"
  },
  {
    "path": "packages/flame_texturepacker/test/assets/legacy/multiplePages/MultiplePageAtlasMap.atlas",
    "content": "\nMultiplePageAtlasMap.png\nsize: 454, 454\nformat: RGBA8888\nfilter: Nearest, Nearest\nrepeat: none\nrobot_jump\n  rotate: true\n  xy: 196, 260\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: -1\nrobot_walk\n  rotate: false\n  xy: 2, 196\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 0\nrobot_walk\n  rotate: true\n  xy: 2, 2\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 5\nrobot_walk\n  rotate: false\n  xy: 260, 2\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 2\n\nMultiplePageAtlasMap2.png\nsize: 454, 454\nformat: RGBA8888\nfilter: Nearest, Nearest\nrepeat: none\nrobot_duck\n  rotate: true\n  xy: 2, 2\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: -1\nrobot_fall\n  rotate: false\n  xy: 260, 2\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: -1\nrobot_walk\n  rotate: false\n  xy: 2, 196\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 7\nrobot_walk\n  rotate: true\n  xy: 196, 260\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 4\n\nMultiplePageAtlasMap3.png\nsize: 454, 454\nformat: RGBA8888\nfilter: Nearest, Nearest\nrepeat: none\nrobot_idle\n  rotate: false\n  xy: 2, 196\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: -1\nrobot_walk\n  rotate: true\n  xy: 2, 2\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 1\nrobot_walk\n  rotate: true\n  xy: 196, 260\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 6\nrobot_walk\n  rotate: false\n  xy: 260, 2\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 3\n"
  },
  {
    "path": "packages/flame_texturepacker/test/assets/legacy/multiplePages/MultipleTrimmedPageAtlasMap.atlas",
    "content": "\nMultipleTrimmedPageAtlasMap.png\nsize: 189, 502\nformat: RGBA8888\nfilter: Linear, Linear\nrepeat: none\nrobot_fall\n  rotate: true\n  xy: 0, 317\n  size: 185, 189\n  orig: 192, 256\n  offset: 6, 2\n  index: -1\nrobot_jump\n  rotate: true\n  xy: 0, 152\n  size: 163, 189\n  orig: 192, 256\n  offset: 17, 1\n  index: -1\nrobot_walk\n  rotate: true\n  xy: 0, 0\n  size: 150, 183\n  orig: 192, 256\n  offset: 14, 2\n  index: 0\n\nMultipleTrimmedPageAtlasMap2.png\nsize: 249, 469\nformat: RGBA8888\nfilter: Linear, Linear\nrepeat: none\nrobot_walk\n  rotate: true\n  xy: 0, 318\n  size: 151, 181\n  orig: 192, 256\n  offset: 13, 0\n  index: 4\nrobot_walk\n  rotate: true\n  xy: 0, 187\n  size: 129, 185\n  orig: 192, 256\n  offset: 23, 0\n  index: 7\nrobot_walk\n  rotate: false\n  xy: 0, 0\n  size: 129, 185\n  orig: 192, 256\n  offset: 23, 0\n  index: 3\nrobot_walk\n  rotate: false\n  xy: 131, 4\n  size: 118, 181\n  orig: 192, 256\n  offset: 34, 0\n  index: 2\n\nMultipleTrimmedPageAtlasMap3.png\nsize: 250, 492\nformat: RGBA8888\nfilter: Linear, Linear\nrepeat: none\nrobot_duck\n  rotate: false\n  xy: 132, 159\n  size: 116, 150\n  orig: 192, 256\n  offset: 34, 0\n  index: -1\nrobot_idle\n  rotate: false\n  xy: 0, 310\n  size: 130, 182\n  orig: 192, 256\n  offset: 31, 0\n  index: -1\nrobot_walk\n  rotate: false\n  xy: 0, 131\n  size: 130, 177\n  orig: 192, 256\n  offset: 23, 0\n  index: 5\nrobot_walk\n  rotate: false\n  xy: 132, 311\n  size: 118, 181\n  orig: 192, 256\n  offset: 34, 0\n  index: 6\nrobot_walk\n  rotate: true\n  xy: 0, 0\n  size: 129, 177\n  orig: 192, 256\n  offset: 24, 0\n  index: 1\n"
  },
  {
    "path": "packages/flame_texturepacker/test/assets/legacy/singlePage/SinglePageAtlasMap.atlas",
    "content": "\nSinglePageAtlasMap.png\nsize: 778, 776\nformat: RGBA8888\nfilter: Nearest, Nearest\nrepeat: none\nrobot_duck\n  rotate: false\n  xy: 390, 518\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: -1\nrobot_fall\n  rotate: false\n  xy: 390, 260\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: -1\nrobot_idle\n  rotate: false\n  xy: 584, 518\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: -1\nrobot_jump\n  rotate: false\n  xy: 196, 518\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: -1\nrobot_walk\n  rotate: false\n  xy: 2, 518\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 0\nrobot_walk\n  rotate: false\n  xy: 2, 260\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 5\nrobot_walk\n  rotate: false\n  xy: 2, 2\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 2\nrobot_walk\n  rotate: false\n  xy: 196, 260\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 7\nrobot_walk\n  rotate: false\n  xy: 196, 2\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 4\nrobot_walk\n  rotate: false\n  xy: 390, 2\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 1\nrobot_walk\n  rotate: false\n  xy: 584, 260\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 6\nrobot_walk\n  rotate: false\n  xy: 584, 2\n  size: 192, 256\n  orig: 192, 256\n  offset: 0, 0\n  index: 3\n"
  },
  {
    "path": "packages/flame_texturepacker/test/assets/legacy/singlePage/SingleTrimmedPageAtlasMap.atlas",
    "content": "\nSingleTrimmedPageAtlasMap.png\nsize: 500, 682\nformat: RGBA8888\nfilter: Linear, Linear\nrepeat: none\nrobot_duck\n  rotate: false\n  xy: 0, 8\n  size: 116, 150\n  orig: 192, 256\n  offset: 34, 0\n  index: -1\nrobot_fall\n  rotate: true\n  xy: 0, 497\n  size: 185, 189\n  orig: 192, 256\n  offset: 6, 2\n  index: -1\nrobot_idle\n  rotate: false\n  xy: 0, 160\n  size: 130, 182\n  orig: 192, 256\n  offset: 31, 0\n  index: -1\nrobot_jump\n  rotate: true\n  xy: 191, 519\n  size: 163, 189\n  orig: 192, 256\n  offset: 17, 1\n  index: -1\nrobot_walk\n  rotate: false\n  xy: 382, 501\n  size: 118, 181\n  orig: 192, 256\n  offset: 34, 0\n  index: 2\nrobot_walk\n  rotate: true\n  xy: 191, 367\n  size: 150, 183\n  orig: 192, 256\n  offset: 14, 2\n  index: 0\nrobot_walk\n  rotate: false\n  xy: 376, 318\n  size: 118, 181\n  orig: 192, 256\n  offset: 34, 0\n  index: 6\nrobot_walk\n  rotate: true\n  xy: 0, 344\n  size: 151, 181\n  orig: 192, 256\n  offset: 13, 0\n  index: 4\nrobot_walk\n  rotate: true\n  xy: 183, 236\n  size: 129, 185\n  orig: 192, 256\n  offset: 23, 0\n  index: 7\nrobot_walk\n  rotate: false\n  xy: 370, 131\n  size: 129, 185\n  orig: 192, 256\n  offset: 23, 0\n  index: 3\nrobot_walk\n  rotate: true\n  xy: 132, 104\n  size: 130, 177\n  orig: 192, 256\n  offset: 23, 0\n  index: 5\nrobot_walk\n  rotate: true\n  xy: 311, 0\n  size: 129, 177\n  orig: 192, 256\n  offset: 24, 0\n  index: 1\n"
  },
  {
    "path": "packages/flame_texturepacker/test/assets/newFormat/multiplePages/MultiplePageAtlasMap.atlas",
    "content": "MultiplePageAtlasMap.png\nsize:454,454\nrepeat:none\nrobot_jump\nbounds:196,260,192,256\nrotate:true\nrobot_walk\nindex:0\nbounds:2,196,192,256\nrobot_walk\nindex:5\nbounds:2,2,192,256\nrotate:true\nrobot_walk\nindex:2\nbounds:260,2,192,256\n\nMultiplePageAtlasMap2.png\nsize:454,454\nrepeat:none\nrobot_duck\nbounds:2,2,192,256\nrotate:true\nrobot_fall\nbounds:260,2,192,256\nrobot_walk\nindex:7\nbounds:2,196,192,256\nrobot_walk\nindex:4\nbounds:196,260,192,256\nrotate:true\n\nMultiplePageAtlasMap3.png\nsize:454,454\nrepeat:none\nrobot_idle\nbounds:2,196,192,256\nrobot_walk\nindex:1\nbounds:2,2,192,256\nrotate:true\nrobot_walk\nindex:6\nbounds:196,260,192,256\nrotate:true\nrobot_walk\nindex:3\nbounds:260,2,192,256\n"
  },
  {
    "path": "packages/flame_texturepacker/test/assets/newFormat/multiplePages/MultipleTrimmedPageAtlasMap.atlas",
    "content": "\nMultipleTrimmedPageAtlasMap.png\nsize: 189, 502\nformat: RGBA8888\nfilter: Linear, Linear\nrepeat: none\nrobot_fall\n  rotate: true\n  xy: 0, 317\n  size: 185, 189\n  orig: 192, 256\n  offset: 6, 2\n  index: -1\nrobot_jump\n  rotate: true\n  xy: 0, 152\n  size: 163, 189\n  orig: 192, 256\n  offset: 17, 1\n  index: -1\nrobot_walk\n  rotate: true\n  xy: 0, 0\n  size: 150, 183\n  orig: 192, 256\n  offset: 14, 2\n  index: 0\n\nMultipleTrimmedPageAtlasMap2.png\nsize: 249, 469\nformat: RGBA8888\nfilter: Linear, Linear\nrepeat: none\nrobot_walk\n  rotate: true\n  xy: 0, 318\n  size: 151, 181\n  orig: 192, 256\n  offset: 13, 0\n  index: 4\nrobot_walk\n  rotate: true\n  xy: 0, 187\n  size: 129, 185\n  orig: 192, 256\n  offset: 23, 0\n  index: 7\nrobot_walk\n  rotate: false\n  xy: 0, 0\n  size: 129, 185\n  orig: 192, 256\n  offset: 23, 0\n  index: 3\nrobot_walk\n  rotate: false\n  xy: 131, 4\n  size: 118, 181\n  orig: 192, 256\n  offset: 34, 0\n  index: 2\n\nMultipleTrimmedPageAtlasMap3.png\nsize: 250, 492\nformat: RGBA8888\nfilter: Linear, Linear\nrepeat: none\nrobot_duck\n  rotate: false\n  xy: 132, 159\n  size: 116, 150\n  orig: 192, 256\n  offset: 34, 0\n  index: -1\nrobot_idle\n  rotate: false\n  xy: 0, 310\n  size: 130, 182\n  orig: 192, 256\n  offset: 31, 0\n  index: -1\nrobot_walk\n  rotate: false\n  xy: 0, 131\n  size: 130, 177\n  orig: 192, 256\n  offset: 23, 0\n  index: 5\nrobot_walk\n  rotate: false\n  xy: 132, 311\n  size: 118, 181\n  orig: 192, 256\n  offset: 34, 0\n  index: 6\nrobot_walk\n  rotate: true\n  xy: 0, 0\n  size: 129, 177\n  orig: 192, 256\n  offset: 24, 0\n  index: 1\n"
  },
  {
    "path": "packages/flame_texturepacker/test/assets/newFormat/singlePage/SinglePageAtlasMap.atlas",
    "content": "SinglePageAtlasMap.png\n\tsize: 778, 776\n\trepeat: none\nrobot_duck\n\tbounds: 390, 518, 192, 256\nrobot_fall\n\tbounds: 390, 260, 192, 256\nrobot_idle\n\tbounds: 584, 518, 192, 256\nrobot_jump\n\tbounds: 196, 518, 192, 256\nrobot_walk\n\tindex: 0\n\tbounds: 2, 518, 192, 256\nrobot_walk\n\tindex: 5\n\tbounds: 2, 260, 192, 256\nrobot_walk\n\tindex: 2\n\tbounds: 2, 2, 192, 256\nrobot_walk\n\tindex: 7\n\tbounds: 196, 260, 192, 256\nrobot_walk\n\tindex: 4\n\tbounds: 196, 2, 192, 256\nrobot_walk\n\tindex: 1\n\tbounds: 390, 2, 192, 256\nrobot_walk\n\tindex: 6\n\tbounds: 584, 260, 192, 256\nrobot_walk\n\tindex: 3\n\tbounds: 584, 2, 192, 256\n"
  },
  {
    "path": "packages/flame_texturepacker/test/assets/newFormat/singlePage/SingleTrimmedPageAtlasMap.atlas",
    "content": "SingleTrimmedPageAtlasMap.png\nsize:500,682\nfilter:Linear,Linear\nrepeat:none\nrobot_duck\nbounds:0,8,116,150\noffsets:34,0,192,256\nrobot_fall\nbounds:0,497,185,189\noffsets:6,2,192,256\nrotate:true\nrobot_idle\nbounds:0,160,130,182\noffsets:31,0,192,256\nrobot_jump\nbounds:191,519,163,189\noffsets:17,1,192,256\nrotate:true\nrobot_walk\nindex:2\nbounds:382,501,118,181\noffsets:34,0,192,256\nrobot_walk\nindex:0\nbounds:191,367,150,183\noffsets:14,2,192,256\nrotate:true\nrobot_walk\nindex:6\nbounds:376,318,118,181\noffsets:34,0,192,256\nrobot_walk\nindex:4\nbounds:0,344,151,181\noffsets:13,0,192,256\nrotate:true\nrobot_walk\nindex:7\nbounds:183,236,129,185\noffsets:23,0,192,256\nrotate:true\nrobot_walk\nindex:3\nbounds:370,131,129,185\noffsets:23,0,192,256\nrobot_walk\nindex:5\nbounds:132,104,130,177\noffsets:23,0,192,256\nrotate:true\nrobot_walk\nindex:1\nbounds:311,0,129,177\noffsets:24,0,192,256\nrotate:true\n"
  },
  {
    "path": "packages/flame_texturepacker/test/assets/whitelist/whitelist_test.atlas",
    "content": "whitelist_test.png\nsize: 80, 20\nformat: RGBA8888\nfilter: Linear,Linear\nrepeat: none\njunk-1_layer0\n  rotate: false\n  xy: 1, 1\n  size: 18, 18\n  orig: 18, 18\n  offset: 0, 0\n  index: -1\njunk-1_layer1\n  rotate: false\n  xy: 21, 1\n  size: 18, 18\n  orig: 18, 18\n  offset: 0, 0\n  index: -1\njunk-2_layer0\n  rotate: false\n  xy: 41, 1\n  size: 18, 18\n  orig: 18, 18\n  offset: 0, 0\n  index: -1\njunk-2_layer1\n  rotate: false\n  xy: 61, 1\n  size: 18, 18\n  orig: 18, 18\n  offset: 0, 0\n  index: -1\n"
  },
  {
    "path": "packages/flame_texturepacker/test/assets/whitelist/whitelist_test.tps",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<data version=\"1.0\">\n    <struct type=\"Settings\">\n        <key>fileFormatVersion</key>\n        <int>6</int>\n        <key>texturePackerVersion</key>\n        <string>7.4.0</string>\n        <key>autoSDSettings</key>\n        <array>\n            <struct type=\"AutoSDSettings\">\n                <key>scale</key>\n                <double>1</double>\n                <key>extension</key>\n                <string></string>\n                <key>spriteFilter</key>\n                <string></string>\n                <key>acceptFractionalValues</key>\n                <false/>\n                <key>maxTextureSize</key>\n                <QSize>\n                    <key>width</key>\n                    <int>-1</int>\n                    <key>height</key>\n                    <int>-1</int>\n                </QSize>\n            </struct>\n        </array>\n        <key>allowRotation</key>\n        <true/>\n        <key>shapeDebug</key>\n        <false/>\n        <key>dpi</key>\n        <uint>72</uint>\n        <key>dataFormat</key>\n        <string>libgdx</string>\n        <key>textureFileName</key>\n        <filename></filename>\n        <key>flipPVR</key>\n        <false/>\n        <key>pvrQualityLevel</key>\n        <uint>3</uint>\n        <key>astcQualityLevel</key>\n        <uint>2</uint>\n        <key>basisUniversalQualityLevel</key>\n        <uint>2</uint>\n        <key>etc1QualityLevel</key>\n        <uint>70</uint>\n        <key>etc2QualityLevel</key>\n        <uint>70</uint>\n        <key>dxtCompressionMode</key>\n        <enum type=\"SettingsBase::DxtCompressionMode\">DXT_PERCEPTUAL</enum>\n        <key>ditherType</key>\n        <enum type=\"SettingsBase::DitherType\">PngQuantLow</enum>\n        <key>backgroundColor</key>\n        <uint>0</uint>\n        <key>libGdx</key>\n        <struct type=\"LibGDX\">\n            <key>filtering</key>\n            <struct type=\"LibGDXFiltering\">\n                <key>x</key>\n                <enum type=\"LibGDXFiltering::Filtering\">Linear</enum>\n                <key>y</key>\n                <enum type=\"LibGDXFiltering::Filtering\">Linear</enum>\n            </struct>\n        </struct>\n        <key>shapePadding</key>\n        <uint>0</uint>\n        <key>jpgQuality</key>\n        <uint>80</uint>\n        <key>pngOptimizationLevel</key>\n        <uint>1</uint>\n        <key>webpQualityLevel</key>\n        <uint>101</uint>\n        <key>textureSubPath</key>\n        <string></string>\n        <key>textureFormat</key>\n        <enum type=\"SettingsBase::TextureFormat\">png8</enum>\n        <key>borderPadding</key>\n        <uint>0</uint>\n        <key>maxTextureSize</key>\n        <QSize>\n            <key>width</key>\n            <int>2048</int>\n            <key>height</key>\n            <int>2048</int>\n        </QSize>\n        <key>fixedTextureSize</key>\n        <QSize>\n            <key>width</key>\n            <int>-1</int>\n            <key>height</key>\n            <int>-1</int>\n        </QSize>\n        <key>algorithmSettings</key>\n        <struct type=\"AlgorithmSettings\">\n            <key>algorithm</key>\n            <enum type=\"AlgorithmSettings::AlgorithmId\">MaxRects</enum>\n            <key>freeSizeMode</key>\n            <enum type=\"AlgorithmSettings::AlgorithmFreeSizeMode\">Best</enum>\n            <key>sizeConstraints</key>\n            <enum type=\"AlgorithmSettings::SizeConstraints\">AnySize</enum>\n            <key>forceSquared</key>\n            <false/>\n            <key>maxRects</key>\n            <struct type=\"AlgorithmMaxRectsSettings\">\n                <key>heuristic</key>\n                <enum type=\"AlgorithmMaxRectsSettings::Heuristic\">Best</enum>\n            </struct>\n            <key>basic</key>\n            <struct type=\"AlgorithmBasicSettings\">\n                <key>sortBy</key>\n                <enum type=\"AlgorithmBasicSettings::SortBy\">Best</enum>\n                <key>order</key>\n                <enum type=\"AlgorithmBasicSettings::Order\">Ascending</enum>\n            </struct>\n            <key>polygon</key>\n            <struct type=\"AlgorithmPolygonSettings\">\n                <key>alignToGrid</key>\n                <uint>1</uint>\n            </struct>\n        </struct>\n        <key>dataFileNames</key>\n        <map type=\"GFileNameMap\">\n            <key>data</key>\n            <struct type=\"DataFile\">\n                <key>name</key>\n                <filename>whitelist_test.atlas</filename>\n            </struct>\n        </map>\n        <key>multiPackMode</key>\n        <enum type=\"SettingsBase::MultiPackMode\">MultiPackOff</enum>\n        <key>forceIdenticalLayout</key>\n        <false/>\n        <key>outputFormat</key>\n        <enum type=\"SettingsBase::OutputFormat\">RGBA8888</enum>\n        <key>alphaHandling</key>\n        <enum type=\"SettingsBase::AlphaHandling\">ClearTransparentPixels</enum>\n        <key>contentProtection</key>\n        <struct type=\"ContentProtection\">\n            <key>key</key>\n            <string></string>\n        </struct>\n        <key>autoAliasEnabled</key>\n        <true/>\n        <key>trimSpriteNames</key>\n        <false/>\n        <key>prependSmartFolderName</key>\n        <false/>\n        <key>autodetectAnimations</key>\n        <true/>\n        <key>globalSpriteSettings</key>\n        <struct type=\"SpriteSettings\">\n            <key>scale</key>\n            <double>1</double>\n            <key>scaleMode</key>\n            <enum type=\"ScaleMode\">Smooth</enum>\n            <key>extrude</key>\n            <uint>1</uint>\n            <key>trimThreshold</key>\n            <uint>1</uint>\n            <key>trimMargin</key>\n            <uint>1</uint>\n            <key>trimMode</key>\n            <enum type=\"SpriteSettings::TrimMode\">Trim</enum>\n            <key>tracerTolerance</key>\n            <int>200</int>\n            <key>heuristicMask</key>\n            <false/>\n            <key>defaultPivotPoint</key>\n            <point_f>0.5,0.5</point_f>\n            <key>writePivotPoints</key>\n            <false/>\n        </struct>\n        <key>individualSpriteSettings</key>\n        <map type=\"IndividualSpriteSettingsMap\">\n            <key type=\"filename\">../../../../../../_assets/gigabull/texture packs/texture_packed_images/production/enemies/junk/junk-1/junk-1_layer0.png</key>\n            <key type=\"filename\">../../../../../../_assets/gigabull/texture packs/texture_packed_images/production/enemies/junk/junk-1/junk-1_layer1.png</key>\n            <key type=\"filename\">../../../../../../_assets/gigabull/texture packs/texture_packed_images/production/enemies/junk/junk-2/junk-2_layer0.png</key>\n            <key type=\"filename\">../../../../../../_assets/gigabull/texture packs/texture_packed_images/production/enemies/junk/junk-2/junk-2_layer1.png</key>\n            <struct type=\"IndividualSpriteSettings\">\n                <key>pivotPoint</key>\n                <point_f>0.5,0.5</point_f>\n                <key>spriteScale</key>\n                <double>1</double>\n                <key>scale9Enabled</key>\n                <false/>\n                <key>scale9Borders</key>\n                <rect>5,5,9,9</rect>\n                <key>scale9Paddings</key>\n                <rect>5,5,9,9</rect>\n                <key>scale9FromFile</key>\n                <false/>\n            </struct>\n        </map>\n        <key>fileLists</key>\n        <map type=\"SpriteSheetMap\">\n            <key>default</key>\n            <struct type=\"SpriteSheet\">\n                <key>files</key>\n                <array>\n                    <filename>../../../../../../_assets/gigabull/texture packs/texture_packed_images/production/enemies/junk/junk-1</filename>\n                    <filename>../../../../../../_assets/gigabull/texture packs/texture_packed_images/production/enemies/junk/junk-2</filename>\n                </array>\n            </struct>\n        </map>\n        <key>ignoreFileList</key>\n        <array/>\n        <key>replaceList</key>\n        <array/>\n        <key>ignoredWarnings</key>\n        <array/>\n        <key>commonDivisorX</key>\n        <uint>1</uint>\n        <key>commonDivisorY</key>\n        <uint>1</uint>\n        <key>packNormalMaps</key>\n        <false/>\n        <key>autodetectNormalMaps</key>\n        <true/>\n        <key>normalMapFilter</key>\n        <string></string>\n        <key>normalMapSuffix</key>\n        <string></string>\n        <key>normalMapSheetFileName</key>\n        <filename></filename>\n        <key>exporterProperties</key>\n        <map type=\"ExporterProperties\"/>\n    </struct>\n</data>\n"
  },
  {
    "path": "packages/flame_texturepacker/test/atlas_path_resolution_test.dart",
    "content": "import 'dart:ui' as ui;\nimport 'package:flame/cache.dart';\nimport 'package:flame_texturepacker/flame_texturepacker.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass _MockAssetBundle extends Mock implements AssetBundle {}\n\nclass _MockImages extends Mock implements Images {}\n\nclass FakeImage extends Mock implements ui.Image {}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('TexturePackerAtlas Path Resolution', () {\n    late _MockAssetBundle bundle;\n    late _MockImages images;\n    late String atlasContent;\n\n    setUpAll(() {\n      registerFallbackValue(const Symbol('package'));\n    });\n\n    setUp(() {\n      bundle = _MockAssetBundle();\n      images = _MockImages();\n      atlasContent = '''\ntest.png\nsize: 64, 64\nfilter: Nearest, Nearest\nrepeat: none\nsprite1\n  bounds: 0, 0, 32, 32\n''';\n\n      // Mock loading the atlas file\n      when(\n        () => bundle.loadString(any(), cache: any(named: 'cache')),\n      ).thenAnswer((_) async => atlasContent);\n\n      // Mock loading an image\n      when(\n        () => images.load(any(), package: any(named: 'package')),\n      ).thenAnswer((_) async => FakeImage());\n\n      when(() => images.prefix).thenReturn('assets/images/');\n    });\n\n    test('should resolve paths correctly with leading slashes', () async {\n      final assets = AssetsCache(bundle: bundle);\n\n      await TexturePackerAtlas.load(\n        '/path/to/atlas_name.atlas',\n        assets: assets,\n        images: images,\n      );\n\n      // Verify it tried to load 'images/path/to/atlas.atlas'\n      // The leading slash in /path/to/atlas.atlas should be removed.\n      verify(\n        () => bundle.loadString(\n          'assets/images/path/to/atlas_name.atlas',\n          cache: any(named: 'cache'),\n        ),\n      ).called(1);\n    });\n\n    test('should handle assetsPrefix WITH trailing slash', () async {\n      final assets = AssetsCache(bundle: bundle);\n\n      await TexturePackerAtlas.load(\n        'atlas_name.atlas',\n        assetsPrefix: 'custom/',\n        assets: assets,\n        images: images,\n      );\n\n      verify(\n        () => bundle.loadString(\n          'assets/custom/atlas_name.atlas',\n          cache: any(named: 'cache'),\n        ),\n      ).called(1);\n    });\n\n    test('should handle assetsPrefix WITHOUT trailing slash', () async {\n      final assets = AssetsCache(bundle: bundle);\n\n      await TexturePackerAtlas.load(\n        'atlas_name.atlas',\n        assetsPrefix: 'custom',\n        assets: assets,\n        images: images,\n      );\n\n      verify(\n        () => bundle.loadString(\n          'assets/custom/atlas_name.atlas',\n          cache: any(named: 'cache'),\n        ),\n      ).called(1);\n    });\n\n    test('should pass package parameter to AssetsCache and Images', () async {\n      final assets = AssetsCache(bundle: bundle);\n\n      await TexturePackerAtlas.load(\n        'atlas_name.atlas',\n        assets: assets,\n        images: images,\n        package: 'my_package',\n      );\n\n      // Verify bundle call includes the package-prefixed path\n      verify(\n        () => bundle.loadString(\n          'packages/my_package/assets/images/atlas_name.atlas',\n          cache: any(named: 'cache'),\n        ),\n      ).called(1);\n\n      // Verify images.load call also includes the package\n      verify(\n        () => images.load('test.png', package: 'my_package'),\n      ).called(1);\n    });\n\n    test(\n      'should auto-detect package from path if package parameter is null',\n      () async {\n        final assets = AssetsCache(bundle: bundle);\n\n        await TexturePackerAtlas.load(\n          'packages/custom_package/assets/images/atlas_name.atlas',\n          assets: assets,\n          images: images,\n        );\n\n        // Verify bundle call extracted 'custom_package' and cleaned the path\n        verify(\n          () => bundle.loadString(\n            'packages/custom_package/assets/images/atlas_name.atlas',\n            cache: any(named: 'cache'),\n          ),\n        ).called(1);\n\n        // Verify images.load also uses the extracted package\n        verify(\n          () => images.load('test.png', package: 'custom_package'),\n        ).called(1);\n      },\n    );\n\n    test(\n      'should load correctly when full assets/ path is provided with empty prefix',\n      () async {\n        final assets = AssetsCache(bundle: bundle);\n\n        await TexturePackerAtlas.load(\n          'assets/images/atlas_name.atlas',\n          assetsPrefix: '',\n          assets: assets,\n          images: images,\n        );\n\n        verify(\n          () => bundle.loadString(\n            'assets/images/atlas_name.atlas',\n            cache: any(named: 'cache'),\n          ),\n        ).called(1);\n      },\n    );\n\n    test(\n      'should handle redundant images/ prefix in atlas file for page images',\n      () async {\n        final assets = AssetsCache(bundle: bundle);\n        const redundantAtlasContent = '''\nimages/test.png\nsize: 64, 64\nfilter: Nearest, Nearest\nrepeat: none\nsprite1\n  bounds: 0, 0, 32, 32\n''';\n\n        when(\n          () => bundle.loadString(any(), cache: any(named: 'cache')),\n        ).thenAnswer((_) async => redundantAtlasContent);\n\n        await TexturePackerAtlas.load(\n          'atlas_name.atlas',\n          assets: assets,\n          images: images,\n        );\n\n        // Verify images.load call strips the redundant 'images/' from inside the atlas\n        verify(\n          () => images.load('test.png', package: any(named: 'package')),\n        ).called(1);\n      },\n    );\n\n    test(\n      'should correctly parse region names with .png and extracted indexes',\n      () async {\n        final assets = AssetsCache(bundle: bundle);\n        const complexAtlasContent = '''\nknight.png\nsize: 64, 64\nfilter: Nearest, Nearest\nrepeat: none\nknight_walk_01.png\n  bounds: 0, 0, 32, 32\nknight_walk_02.png\n  bounds: 32, 0, 32, 32\n''';\n\n        when(\n          () => bundle.loadString(any(), cache: any(named: 'cache')),\n        ).thenAnswer((_) async => complexAtlasContent);\n\n        final atlas = await TexturePackerAtlas.load(\n          'knight.atlas',\n          assets: assets,\n          images: images,\n        );\n\n        expect(atlas.sprites.length, 2);\n        expect(atlas.sprites[0].region.name, 'knight_walk');\n        expect(atlas.sprites[0].region.index, 1);\n        expect(atlas.sprites[1].region.name, 'knight_walk');\n        expect(atlas.sprites[1].region.index, 2);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_texturepacker/test/flame_texturepacker_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:flame/cache.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_texturepacker/flame_texturepacker.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass _MockAssetBundle extends Mock implements AssetBundle {}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('TexturepackerLoader', () {\n    const atlasPath =\n        'test/assets/newFormat/multiplePages/MultiplePageAtlasMap.atlas';\n    const atlasWhitelistPath = 'test/assets/whitelist/whitelist_test.atlas';\n    const atlasImage1 =\n        'test/assets/newFormat/multiplePages/MultiplePageAtlasMap.png';\n\n    test('load atlas from storage', () async {\n      final flameGame = FlameGame();\n      final atlas = await flameGame.atlasFromStorage(atlasPath);\n\n      expect(atlas, isNotNull);\n      expect(atlas.sprites.length, equals(12));\n\n      final firstSprite = atlas.findSpriteByName('robot_walk');\n      expect(firstSprite, isNotNull);\n      expect(firstSprite!.srcSize, isNotNull);\n      expect(firstSprite.srcPosition, isNotNull);\n    });\n\n    test('load atlas from assets', () async {\n      final bundle = _MockAssetBundle();\n      when(\n        () => bundle.loadString(any()),\n      ).thenAnswer((_) async => File(atlasPath).readAsString());\n      when(() => bundle.load(any())).thenAnswer(\n        (_) async => ByteData.sublistView(\n          File(atlasImage1).readAsBytesSync(),\n        ),\n      );\n\n      final flameGame = FlameGame()\n        ..assets = AssetsCache(bundle: bundle, prefix: '')\n        ..images = Images(bundle: bundle, prefix: '');\n\n      final atlas = await flameGame.atlasFromAssets(atlasPath);\n\n      expect(atlas, isNotNull);\n      expect(atlas.sprites.length, equals(12));\n\n      final firstSprite = atlas.findSpriteByName('robot_walk');\n      expect(firstSprite, isNotNull);\n      expect(firstSprite!.srcSize.x, greaterThan(0));\n      expect(firstSprite.srcSize.y, greaterThan(0));\n      expect(firstSprite.srcPosition, isNotNull);\n    });\n\n    test('throws exception for invalid atlas path', () async {\n      final flameGame = FlameGame();\n      expect(\n        () => flameGame.atlasFromStorage('invalid_path.atlas'),\n        throwsException,\n      );\n    });\n\n    test('only loads whitelisted path matches', () async {\n      final flameGame = FlameGame();\n      final atlas = await flameGame.atlasFromStorage(\n        atlasWhitelistPath,\n        whiteList: ['junk-1'],\n      );\n\n      expect(atlas, isNotNull);\n      expect(atlas.sprites.length, equals(2));\n\n      final firstSprite = atlas.findSpriteByName('junk-1_layer0');\n      expect(firstSprite, isNotNull);\n      expect(firstSprite!.srcSize, isNotNull);\n      expect(firstSprite.srcPosition, isNotNull);\n\n      final secondSprite = atlas.findSpriteByName('junk-2_layer0');\n      expect(secondSprite, isNull);\n    });\n\n    test('generates region data', () async {\n      final regions = await TexturePackerAtlas.loadAtlas(\n        atlasWhitelistPath,\n        fromStorage: true,\n      );\n\n      expect(regions.regions, isNotEmpty);\n      expect(regions.regions.length, equals(4));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_texturepacker/test/legacy_format_test.dart",
    "content": "import 'dart:math' as math;\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame_texturepacker/flame_texturepacker.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass _MockCanvas extends Mock implements Canvas {}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Multiple pages atlas', () {\n    const atlasPath =\n        'test/assets/legacy/multiplePages/MultiplePageAtlasMap.atlas';\n    const atlasTrimmedPath =\n        'test/assets/legacy/multiplePages/MultipleTrimmedPageAtlasMap.atlas';\n\n    test('Texture atlas contains 12 Sprites', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n      expect(atlas.sprites.length, 12);\n    });\n\n    test('findSpritesByName will return 8 sprites', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n      final walkingSprites = atlas.findSpritesByName('robot_walk');\n      expect(walkingSprites.length, 8);\n    });\n\n    test('findSpriteByName will return 1 sprite', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n      final jumpSprite = atlas.findSpriteByName('robot_jump');\n      expect(jumpSprite, isNotNull);\n    });\n\n    test('Sprite data is loaded correctly', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n\n      final jumpSprite = atlas.findSpriteByName('robot_jump')!;\n      expect(jumpSprite.region.rotate, true);\n      expect(jumpSprite.region.name, 'robot_jump');\n      expect(jumpSprite.region.height, 256);\n      expect(jumpSprite.region.width, 192);\n      expect(jumpSprite.region.index, -1);\n      expect(jumpSprite.region.offsetY, 0);\n      expect(jumpSprite.region.offsetX, 0);\n      expect(jumpSprite.region.originalWidth, 192);\n      expect(jumpSprite.region.originalHeight, 256);\n      expect(jumpSprite.srcPosition, Vector2(196, 260));\n      expect(jumpSprite.src, const Rect.fromLTWH(196, 260, 256, 192));\n      expect(jumpSprite.srcSize, Vector2(192, 256));\n      expect(jumpSprite.originalSize, Vector2(192, 256));\n      expect(jumpSprite.offset, Vector2(0, 0));\n      expect(jumpSprite.angle, math.pi / 2);\n\n      final walkSprite = atlas.findSpriteByName('robot_walk')!;\n      expect(walkSprite.region.rotate, false);\n      expect(walkSprite.region.name, 'robot_walk');\n      expect(walkSprite.region.height, 256);\n      expect(walkSprite.region.width, 192);\n      expect(walkSprite.region.index, 0);\n      expect(walkSprite.region.offsetY, 0);\n      expect(walkSprite.region.offsetX, 0);\n      expect(walkSprite.region.originalWidth, 192);\n      expect(walkSprite.region.originalHeight, 256);\n      expect(walkSprite.srcPosition, Vector2(2, 196));\n      expect(walkSprite.src, const Rect.fromLTWH(2, 196, 192, 256));\n      expect(walkSprite.srcSize, Vector2(192, 256));\n      expect(walkSprite.originalSize, Vector2(192, 256));\n      expect(walkSprite.offset, Vector2(0, 0));\n      expect(walkSprite.angle, 0);\n    });\n\n    test('Trimmed sprite data is loaded correctly', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasTrimmedPath,\n        fromStorage: true,\n      );\n\n      final idleSprite = atlas.findSpriteByName('robot_idle')!;\n      expect(idleSprite.region.rotate, false);\n      expect(idleSprite.region.name, 'robot_idle');\n      expect(idleSprite.region.height, 182);\n      expect(idleSprite.region.width, 130);\n      expect(idleSprite.region.index, -1);\n      expect(idleSprite.region.offsetY, 0);\n      expect(idleSprite.region.offsetX, 31);\n      expect(idleSprite.region.originalWidth, 192);\n      expect(idleSprite.region.originalHeight, 256);\n      expect(idleSprite.srcPosition, Vector2(0, 310));\n      expect(idleSprite.src, const Rect.fromLTWH(0, 310, 130, 182));\n      expect(idleSprite.srcSize, Vector2(192, 256));\n      expect(idleSprite.originalSize, Vector2(192, 256));\n      expect(idleSprite.offset, Vector2(31, 0));\n      expect(idleSprite.angle, 0);\n\n      final idleSpritePackedSize = idleSprite.clone(useOriginalSize: false);\n      expect(idleSpritePackedSize.src, const Rect.fromLTWH(0, 310, 130, 182));\n      expect(idleSpritePackedSize.srcSize, Vector2(130, 182));\n      expect(idleSpritePackedSize.originalSize, Vector2(130, 182));\n      expect(idleSpritePackedSize.offset, Vector2(0, 0));\n      expect(idleSpritePackedSize.angle, 0);\n\n      final walkSprite = atlas.findSpriteByName('robot_walk')!;\n      expect(walkSprite.region.rotate, true);\n      expect(walkSprite.region.name, 'robot_walk');\n      expect(walkSprite.region.height, 183);\n      expect(walkSprite.region.width, 150);\n      expect(walkSprite.region.index, 0);\n      expect(walkSprite.region.offsetY, 2);\n      expect(walkSprite.region.offsetX, 14);\n      expect(walkSprite.region.originalWidth, 192);\n      expect(walkSprite.region.originalHeight, 256);\n      expect(walkSprite.srcPosition, Vector2(0, 0));\n      expect(walkSprite.src, const Rect.fromLTWH(0, 0, 183, 150));\n      expect(walkSprite.srcSize, Vector2(192, 256));\n      expect(walkSprite.originalSize, Vector2(192, 256));\n      expect(walkSprite.offset, Vector2(14, 2));\n      expect(walkSprite.angle, math.pi / 2);\n\n      final walkSpritePackedSize = walkSprite.clone(useOriginalSize: false);\n      expect(walkSpritePackedSize.src, const Rect.fromLTWH(0, 0, 183, 150));\n      expect(walkSpritePackedSize.srcSize, Vector2(150, 183));\n      expect(walkSpritePackedSize.originalSize, Vector2(150, 183));\n      expect(walkSpritePackedSize.offset, Vector2(0, 0));\n      expect(walkSpritePackedSize.angle, math.pi / 2);\n    });\n  });\n  group('Single page atlas', () {\n    const atlasPath = 'test/assets/legacy/singlePage/SinglePageAtlasMap.atlas';\n    const atlasTrimmedPath =\n        'test/assets/legacy/singlePage/SingleTrimmedPageAtlasMap.atlas';\n\n    test('Texture atlas contains 12 Sprites', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n      expect(atlas.sprites.length, 12);\n    });\n\n    test('findSpritesByName will return 8 sprites', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n      final walkingSprites = atlas.findSpritesByName('robot_walk');\n      expect(walkingSprites.length, 8);\n    });\n\n    test('findSpriteByName will return 1 sprite', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n      final jumpSprite = atlas.findSpriteByName('robot_jump');\n      expect(jumpSprite, isNotNull);\n    });\n\n    test('findSpriteByNameIndex will return 1 sprite', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n      final jumpSprite = atlas.findSpriteByNameIndex('robot_jump', -1);\n      expect(jumpSprite, isNotNull);\n    });\n\n    test('Sprite data is loaded correctly', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n\n      final jumpSprite = atlas.findSpriteByName('robot_jump')!;\n      expect(jumpSprite.region.rotate, false);\n      expect(jumpSprite.region.name, 'robot_jump');\n      expect(jumpSprite.region.height, 256);\n      expect(jumpSprite.region.width, 192);\n      expect(jumpSprite.region.index, -1);\n      expect(jumpSprite.region.offsetY, 0);\n      expect(jumpSprite.region.offsetX, 0);\n      expect(jumpSprite.region.originalWidth, 192);\n      expect(jumpSprite.region.originalHeight, 256);\n      expect(jumpSprite.srcPosition, Vector2(196, 518));\n      expect(jumpSprite.src, const Rect.fromLTWH(196, 518, 192, 256));\n      expect(jumpSprite.srcSize, Vector2(192, 256));\n      expect(jumpSprite.originalSize, Vector2(192, 256));\n      expect(jumpSprite.offset, Vector2(0, 0));\n      expect(jumpSprite.angle, 0);\n\n      final walkSprite = atlas.findSpriteByName('robot_walk')!;\n      expect(walkSprite.region.rotate, false);\n      expect(walkSprite.region.name, 'robot_walk');\n      expect(walkSprite.region.height, 256);\n      expect(walkSprite.region.width, 192);\n      expect(walkSprite.region.index, 0);\n      expect(walkSprite.region.offsetY, 0);\n      expect(walkSprite.region.offsetX, 0);\n      expect(walkSprite.region.originalWidth, 192);\n      expect(walkSprite.region.originalHeight, 256);\n      expect(walkSprite.srcPosition, Vector2(2, 518));\n      expect(walkSprite.src, const Rect.fromLTWH(2, 518, 192, 256));\n      expect(walkSprite.srcSize, Vector2(192, 256));\n      expect(walkSprite.originalSize, Vector2(192, 256));\n      expect(walkSprite.offset, Vector2(0, 0));\n      expect(walkSprite.angle, 0);\n    });\n\n    test('Trimmed sprite data is loaded correctly', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasTrimmedPath,\n        fromStorage: true,\n      );\n\n      final idleSprite = atlas.findSpriteByName('robot_idle')!;\n      expect(idleSprite.region.rotate, false);\n      expect(idleSprite.region.name, 'robot_idle');\n      expect(idleSprite.region.height, 182);\n      expect(idleSprite.region.width, 130);\n      expect(idleSprite.region.index, -1);\n      expect(idleSprite.region.offsetY, 0);\n      expect(idleSprite.region.offsetX, 31);\n      expect(idleSprite.region.originalWidth, 192);\n      expect(idleSprite.region.originalHeight, 256);\n      expect(idleSprite.srcPosition, Vector2(0, 160));\n      expect(idleSprite.src, const Rect.fromLTWH(0, 160, 130, 182));\n      expect(idleSprite.srcSize, Vector2(192, 256));\n      expect(idleSprite.originalSize, Vector2(192, 256));\n      expect(idleSprite.offset, Vector2(31, 0));\n      expect(idleSprite.angle, 0);\n\n      final idleSpritePackedSize = idleSprite.clone(useOriginalSize: false);\n      expect(idleSpritePackedSize.src, const Rect.fromLTWH(0, 160, 130, 182));\n      expect(idleSpritePackedSize.srcSize, Vector2(130, 182));\n      expect(idleSpritePackedSize.originalSize, Vector2(130, 182));\n      expect(idleSpritePackedSize.offset, Vector2(0, 0));\n      expect(idleSpritePackedSize.angle, 0);\n\n      final walkSprite = atlas.findSpriteByName('robot_walk')!;\n      expect(walkSprite.region.rotate, true);\n      expect(walkSprite.region.name, 'robot_walk');\n      expect(walkSprite.region.height, 183);\n      expect(walkSprite.region.width, 150);\n      expect(walkSprite.region.index, 0);\n      expect(walkSprite.region.offsetY, 2);\n      expect(walkSprite.region.offsetX, 14);\n      expect(walkSprite.region.originalWidth, 192);\n      expect(walkSprite.region.originalHeight, 256);\n      expect(walkSprite.srcPosition, Vector2(191, 367));\n      expect(walkSprite.src, const Rect.fromLTWH(191, 367, 183, 150));\n      expect(walkSprite.srcSize, Vector2(192, 256));\n      expect(walkSprite.originalSize, Vector2(192, 256));\n      expect(walkSprite.offset, Vector2(14, 2));\n      expect(walkSprite.angle, math.pi / 2);\n\n      final walkSpritePackedSize = walkSprite.clone(useOriginalSize: false);\n      expect(walkSpritePackedSize.src, const Rect.fromLTWH(191, 367, 183, 150));\n      expect(walkSpritePackedSize.srcSize, Vector2(150, 183));\n      expect(walkSpritePackedSize.originalSize, Vector2(150, 183));\n      expect(walkSpritePackedSize.offset, Vector2(0, 0));\n      expect(walkSpritePackedSize.angle, math.pi / 2);\n    });\n\n    test('Sprite renders correctly when not rotated', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n\n      final sprite = atlas.findSpriteByName('robot_jump')!;\n      final canvas = _MockCanvas();\n\n      sprite.render(\n        canvas,\n        position: Vector2(100, 100),\n        size: Vector2(384, 512),\n      );\n\n      expect(sprite.region.rotate, false);\n      expect(sprite.offset, Vector2.zero());\n      expect(sprite.originalSize, Vector2(192, 256));\n      expect(sprite.srcSize, Vector2(192, 256));\n    });\n\n    test('Sprite renders correctly when rotated', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasTrimmedPath,\n        fromStorage: true,\n      );\n\n      final sprite = atlas.findSpriteByName('robot_walk')!;\n      final canvas = _MockCanvas();\n\n      sprite.render(\n        canvas,\n        position: Vector2(100, 100),\n        size: Vector2(384, 512),\n      );\n\n      expect(sprite.region.rotate, true);\n      expect(sprite.offset, Vector2(14, 2));\n      expect(sprite.originalSize, Vector2(192, 256));\n      expect(sprite.srcSize, Vector2(192, 256));\n      expect(sprite.angle, math.pi / 2);\n    });\n\n    test('Sprite renders with correct anchor point', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n\n      final sprite = atlas.findSpriteByName('robot_jump')!;\n      final canvas = _MockCanvas();\n\n      sprite.render(\n        canvas,\n        position: Vector2(100, 100),\n        size: Vector2(192, 256),\n        anchor: Anchor.center,\n      );\n\n      expect(sprite.originalSize, Vector2(192, 256));\n    });\n\n    test('Sprite renders correctly with default position', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n\n      final sprite = atlas.findSpriteByName('robot_jump')!;\n      final canvas = _MockCanvas();\n\n      sprite.render(\n        canvas,\n        size: Vector2(192, 256),\n      );\n\n      expect(sprite.originalSize, Vector2(192, 256));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_texturepacker/test/naming_index_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:flame_texturepacker/flame_texturepacker.dart';\nimport 'package:flame_texturepacker/src/model/region.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Index Pattern Parsing', () {\n    late Directory tempDir;\n\n    setUp(() async {\n      tempDir = await Directory.systemTemp.createTemp('atlas_test');\n    });\n\n    tearDown(() async {\n      await tempDir.delete(recursive: true);\n    });\n\n    Future<Iterable<Region>> createAndLoadRegions(String content) async {\n      final atlasFile = File('${tempDir.path}/test.atlas');\n      await atlasFile.writeAsString(content);\n\n      final imageFile = File('${tempDir.path}/test.png');\n      // Copy a real valid PNG from assets to avoid \"Invalid image data\" errors\n      final realPngBytes = await File(\n        'test/assets/whitelist/whitelist_test.png',\n      ).readAsBytes();\n      await imageFile.writeAsBytes(realPngBytes);\n\n      final atlasData = await TexturePackerAtlas.loadAtlas(\n        atlasFile.path,\n        fromStorage: true,\n      );\n      return atlasData.regions;\n    }\n\n    test('Pattern image1', () async {\n      final regions = await createAndLoadRegions('''\ntest.png\nsize: 64, 64\nformat: RGBA8888\nfilter: Linear,Linear\nrepeat: none\nimage1\n  rotate: false\n  xy: 0, 0\n  size: 32, 32\n  orig: 32, 32\n  offset: 0, 0\n''');\n      expect(regions.first.name, 'image');\n      expect(regions.first.index, 1);\n    });\n\n    test('Pattern image01', () async {\n      final regions = await createAndLoadRegions('''\ntest.png\nsize: 64, 64\nformat: RGBA8888\nfilter: Linear,Linear\nrepeat: none\nimage01\n  rotate: false\n  xy: 0, 0\n  size: 32, 32\n  orig: 32, 32\n  offset: 0, 0\n''');\n      expect(regions.first.name, 'image');\n      expect(regions.first.index, 1);\n    });\n\n    test('Pattern image_1', () async {\n      final regions = await createAndLoadRegions('''\ntest.png\nsize: 64, 64\nformat: RGBA8888\nfilter: Linear,Linear\nrepeat: none\nimage_1\n  rotate: false\n  xy: 0, 0\n  size: 32, 32\n  orig: 32, 32\n  offset: 0, 0\n''');\n      expect(regions.first.name, 'image');\n      expect(regions.first.index, 1);\n    });\n\n    test('Pattern image_01', () async {\n      final regions = await createAndLoadRegions('''\ntest.png\nsize: 64, 64\nformat: RGBA8888\nfilter: Linear,Linear\nrepeat: none\nimage_01\n  rotate: false\n  xy: 0, 0\n  size: 32, 32\n  orig: 32, 32\n  offset: 0, 0\n''');\n      expect(regions.first.name, 'image');\n      expect(regions.first.index, 1);\n    });\n\n    test('Pattern image001', () async {\n      final regions = await createAndLoadRegions('''\ntest.png\nsize: 64, 64\nformat: RGBA8888\nfilter: Linear,Linear\nrepeat: none\nimage001\n  rotate: false\n  xy: 0, 0\n  size: 32, 32\n  orig: 32, 32\n  offset: 0, 0\n''');\n      expect(regions.first.name, 'image');\n      expect(regions.first.index, 1);\n    });\n\n    test('Index field overrides name-based index if positive', () async {\n      final regions = await createAndLoadRegions('''\ntest.png\nsize: 64, 64\nformat: RGBA8888\nfilter: Linear,Linear\nrepeat: none\nimage_01\n  rotate: false\n  xy: 0, 0\n  size: 32, 32\n  orig: 32, 32\n  offset: 0, 0\n  index: 5\n''');\n      expect(regions.first.name, 'image');\n      expect(regions.first.index, 5);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_texturepacker/test/new_format_test.dart",
    "content": "import 'dart:math' as math;\nimport 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame_texturepacker/flame_texturepacker.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass _MockCanvas extends Mock implements Canvas {}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n  group('Multiple pages atlas', () {\n    const atlasPath =\n        'test/assets/newFormat/multiplePages/MultiplePageAtlasMap.atlas';\n    const atlasTrimmedPath =\n        'test/assets/newFormat/multiplePages/MultipleTrimmedPageAtlasMap.atlas';\n\n    test('Texture atlas contains 12 Sprites', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n      expect(atlas.sprites.length, 12);\n    });\n\n    test('findSpritesByName will return 8 sprites', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n      final walkingSprites = atlas.findSpritesByName('robot_walk');\n      expect(walkingSprites.length, 8);\n    });\n\n    test('findSpriteByName will return 1 sprite', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n      final jumpSprite = atlas.findSpriteByName('robot_jump');\n      expect(jumpSprite, isNotNull);\n    });\n\n    test('findSpriteByNameIndex will return 1 sprite', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n      final jumpSprite = atlas.findSpriteByNameIndex('robot_jump', -1);\n      expect(jumpSprite, isNotNull);\n    });\n\n    test('Sprite data is loaded correctly', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n\n      final jumpSprite = atlas.findSpriteByName('robot_jump')!;\n      expect(jumpSprite.region.rotate, true);\n      expect(jumpSprite.region.name, 'robot_jump');\n      expect(jumpSprite.srcPosition, Vector2(196, 260));\n      expect(jumpSprite.region.height, 256);\n      expect(jumpSprite.region.width, 192);\n      expect(jumpSprite.region.index, -1);\n      expect(jumpSprite.region.offsetY, 0);\n      expect(jumpSprite.region.offsetX, 0);\n      expect(jumpSprite.region.originalWidth, 192);\n      expect(jumpSprite.region.originalHeight, 256);\n      expect(jumpSprite.src, const Rect.fromLTWH(196, 260, 256, 192));\n      expect(jumpSprite.srcSize, Vector2(192, 256));\n      expect(jumpSprite.originalSize, Vector2(192, 256));\n      expect(jumpSprite.offset, Vector2(0, 0));\n      expect(jumpSprite.angle, math.pi / 2);\n\n      final walkSprite = atlas.findSpriteByName('robot_walk')!;\n      expect(walkSprite.region.rotate, false);\n      expect(walkSprite.region.name, 'robot_walk');\n      expect(walkSprite.srcPosition, Vector2(2, 196));\n      expect(walkSprite.region.height, 256);\n      expect(walkSprite.region.width, 192);\n      expect(walkSprite.region.index, 0);\n      expect(walkSprite.region.offsetY, 0);\n      expect(walkSprite.region.offsetX, 0);\n      expect(walkSprite.region.originalWidth, 192);\n      expect(walkSprite.region.originalHeight, 256);\n      expect(walkSprite.src, const Rect.fromLTWH(2, 196, 192, 256));\n      expect(walkSprite.srcSize, Vector2(192, 256));\n      expect(walkSprite.originalSize, Vector2(192, 256));\n      expect(walkSprite.offset, Vector2(0, 0));\n      expect(walkSprite.angle, 0);\n    });\n    test('Trimmed sprite data is loaded correctly', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasTrimmedPath,\n        fromStorage: true,\n      );\n\n      final idleSprite = atlas.findSpriteByName('robot_idle')!;\n      expect(idleSprite.region.rotate, false);\n      expect(idleSprite.region.name, 'robot_idle');\n      expect(idleSprite.region.height, 182);\n      expect(idleSprite.region.width, 130);\n      expect(idleSprite.region.index, -1);\n      expect(idleSprite.region.offsetY, 0);\n      expect(idleSprite.region.offsetX, 31);\n      expect(idleSprite.region.originalWidth, 192);\n      expect(idleSprite.region.originalHeight, 256);\n      expect(idleSprite.srcPosition, Vector2(0, 310));\n      expect(idleSprite.src, const Rect.fromLTWH(0, 310, 130, 182));\n      expect(idleSprite.srcSize, Vector2(192, 256));\n      expect(idleSprite.originalSize, Vector2(192, 256));\n      expect(idleSprite.offset, Vector2(31, 0));\n      expect(idleSprite.angle, 0);\n\n      final idleSpritePackedSize = idleSprite.clone(useOriginalSize: false);\n      expect(idleSpritePackedSize.src, const Rect.fromLTWH(0, 310, 130, 182));\n      expect(idleSpritePackedSize.srcSize, Vector2(130, 182));\n      expect(idleSpritePackedSize.originalSize, Vector2(130, 182));\n      expect(idleSpritePackedSize.offset, Vector2(0, 0));\n      expect(idleSpritePackedSize.angle, 0);\n\n      final walkSprite = atlas.findSpriteByName('robot_walk')!;\n      expect(walkSprite.region.rotate, true);\n      expect(walkSprite.region.name, 'robot_walk');\n      expect(walkSprite.region.height, 183);\n      expect(walkSprite.region.width, 150);\n      expect(walkSprite.region.index, 0);\n      expect(walkSprite.region.offsetY, 2);\n      expect(walkSprite.region.offsetX, 14);\n      expect(walkSprite.region.originalWidth, 192);\n      expect(walkSprite.region.originalHeight, 256);\n      expect(walkSprite.srcPosition, Vector2(0, 0));\n      expect(walkSprite.src, const Rect.fromLTWH(0, 0, 183, 150));\n      expect(walkSprite.srcSize, Vector2(192, 256));\n      expect(walkSprite.originalSize, Vector2(192, 256));\n      expect(walkSprite.offset, Vector2(14, 2));\n      expect(walkSprite.angle, math.pi / 2);\n\n      final walkSpritePackedSize = walkSprite.clone(useOriginalSize: false);\n      expect(walkSpritePackedSize.src, const Rect.fromLTWH(0, 0, 183, 150));\n      expect(walkSpritePackedSize.srcSize, Vector2(150, 183));\n      expect(walkSpritePackedSize.originalSize, Vector2(150, 183));\n      expect(walkSpritePackedSize.offset, Vector2(0, 0));\n      expect(walkSpritePackedSize.angle, math.pi / 2);\n    });\n\n    test('Sprite renders correctly', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n\n      final sprite = atlas.findSpriteByName('robot_jump')!;\n      final canvas = _MockCanvas();\n\n      sprite.render(\n        canvas,\n        position: Vector2(100, 100),\n        size: Vector2(384, 512),\n      );\n\n      expect(sprite.region.rotate, true);\n      expect(sprite.offset, Vector2.zero());\n      expect(sprite.originalSize, Vector2(192, 256));\n      expect(sprite.srcSize, Vector2(192, 256));\n      expect(sprite.angle, math.pi / 2);\n    });\n\n    test('Sprite renders with correct anchor point', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n\n      final sprite = atlas.findSpriteByName('robot_jump')!;\n      final canvas = _MockCanvas();\n\n      sprite.render(\n        canvas,\n        position: Vector2(100, 100),\n        size: Vector2(192, 256),\n        anchor: Anchor.center,\n      );\n\n      expect(sprite.originalSize, Vector2(192, 256));\n    });\n\n    test('Sprite renders correctly with default position', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n\n      final sprite = atlas.findSpriteByName('robot_jump')!;\n      final canvas = _MockCanvas();\n\n      sprite.render(\n        canvas,\n        size: Vector2(192, 256),\n      );\n\n      expect(sprite.originalSize, Vector2(192, 256));\n    });\n  });\n\n  group('Single page atlas', () {\n    const atlasPath =\n        'test/assets/newFormat/singlePage/SinglePageAtlasMap.atlas';\n    const atlasTrimmedPath =\n        'test/assets/newFormat/singlePage/SingleTrimmedPageAtlasMap.atlas';\n\n    test('Texture atlas contains 12 Sprites', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n      expect(atlas.sprites.length, 12);\n    });\n\n    test('findSpritesByName will return 8 sprites', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n      final walkingSprites = atlas.findSpritesByName('robot_walk');\n      expect(walkingSprites.length, 8);\n    });\n\n    test('findSpriteByName will return 1 sprite', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n      final jumpSprite = atlas.findSpriteByName('robot_jump');\n      expect(jumpSprite, isNotNull);\n    });\n\n    test('Sprite data is loaded correctly', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n\n      final jumpSprite = atlas.findSpriteByName('robot_jump')!;\n      expect(jumpSprite.region.rotate, false);\n      expect(jumpSprite.region.name, 'robot_jump');\n      expect(jumpSprite.region.height, 256);\n      expect(jumpSprite.region.width, 192);\n      expect(jumpSprite.region.index, -1);\n      expect(jumpSprite.region.offsetY, 0);\n      expect(jumpSprite.region.offsetX, 0);\n      expect(jumpSprite.region.originalWidth, 192);\n      expect(jumpSprite.region.originalHeight, 256);\n      expect(jumpSprite.srcPosition, Vector2(196, 518));\n      expect(jumpSprite.src, const Rect.fromLTWH(196, 518, 192, 256));\n      expect(jumpSprite.srcSize, Vector2(192, 256));\n      expect(jumpSprite.originalSize, Vector2(192, 256));\n      expect(jumpSprite.offset, Vector2(0, 0));\n      expect(jumpSprite.angle, 0);\n\n      final walkSprite = atlas.findSpriteByName('robot_walk')!;\n      expect(walkSprite.region.rotate, false);\n      expect(walkSprite.region.name, 'robot_walk');\n      expect(walkSprite.region.height, 256);\n      expect(walkSprite.region.width, 192);\n      expect(walkSprite.region.index, 0);\n      expect(walkSprite.region.offsetY, 0);\n      expect(walkSprite.region.offsetX, 0);\n      expect(walkSprite.region.originalWidth, 192);\n      expect(walkSprite.region.originalHeight, 256);\n      expect(walkSprite.srcPosition, Vector2(2, 518));\n      expect(walkSprite.src, const Rect.fromLTWH(2, 518, 192, 256));\n      expect(walkSprite.srcSize, Vector2(192, 256));\n      expect(walkSprite.originalSize, Vector2(192, 256));\n      expect(walkSprite.offset, Vector2(0, 0));\n      expect(walkSprite.angle, 0);\n    });\n    test('Trimmed sprite data is loaded correctly', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasTrimmedPath,\n        fromStorage: true,\n      );\n\n      final idleSprite = atlas.findSpriteByName('robot_idle')!;\n      expect(idleSprite.region.rotate, false);\n      expect(idleSprite.region.name, 'robot_idle');\n      expect(idleSprite.region.height, 182);\n      expect(idleSprite.region.width, 130);\n      expect(idleSprite.region.index, -1);\n      expect(idleSprite.region.offsetY, 0);\n      expect(idleSprite.region.offsetX, 31);\n      expect(idleSprite.region.originalWidth, 192);\n      expect(idleSprite.region.originalHeight, 256);\n      expect(idleSprite.srcPosition, Vector2(0, 160));\n      expect(idleSprite.src, const Rect.fromLTWH(0, 160, 130, 182));\n      expect(idleSprite.srcSize, Vector2(192, 256));\n      expect(idleSprite.originalSize, Vector2(192, 256));\n      expect(idleSprite.offset, Vector2(31, 0));\n      expect(idleSprite.angle, 0);\n\n      final idleSpritePackedSize = idleSprite.clone(useOriginalSize: false);\n      expect(idleSpritePackedSize.src, const Rect.fromLTWH(0, 160, 130, 182));\n      expect(idleSpritePackedSize.srcSize, Vector2(130, 182));\n      expect(idleSpritePackedSize.originalSize, Vector2(130, 182));\n      expect(idleSpritePackedSize.offset, Vector2(0, 0));\n      expect(idleSpritePackedSize.angle, 0);\n\n      final walkSprite = atlas.findSpriteByName('robot_walk')!;\n      expect(walkSprite.region.rotate, true);\n      expect(walkSprite.region.name, 'robot_walk');\n      expect(walkSprite.region.height, 183);\n      expect(walkSprite.region.width, 150);\n      expect(walkSprite.region.index, 0);\n      expect(walkSprite.region.offsetY, 2);\n      expect(walkSprite.region.offsetX, 14);\n      expect(walkSprite.region.originalWidth, 192);\n      expect(walkSprite.region.originalHeight, 256);\n      expect(walkSprite.srcPosition, Vector2(191, 367));\n      expect(walkSprite.src, const Rect.fromLTWH(191, 367, 183, 150));\n      expect(walkSprite.srcSize, Vector2(192, 256));\n      expect(walkSprite.originalSize, Vector2(192, 256));\n      expect(walkSprite.offset, Vector2(14, 2));\n      expect(walkSprite.angle, math.pi / 2);\n\n      final walkSpritePackedSize = walkSprite.clone(useOriginalSize: false);\n      expect(walkSpritePackedSize.src, const Rect.fromLTWH(191, 367, 183, 150));\n      expect(walkSpritePackedSize.srcSize, Vector2(150, 183));\n      expect(walkSpritePackedSize.originalSize, Vector2(150, 183));\n      expect(walkSpritePackedSize.offset, Vector2(0, 0));\n      expect(walkSpritePackedSize.angle, math.pi / 2);\n    });\n\n    test('Sprite renders correctly', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasTrimmedPath,\n        fromStorage: true,\n      );\n\n      final sprite = atlas.findSpriteByName('robot_walk')!;\n      final canvas = _MockCanvas();\n\n      sprite.render(\n        canvas,\n        position: Vector2(100, 100),\n        size: Vector2(384, 512),\n      );\n\n      expect(sprite.region.rotate, true);\n      expect(sprite.offset, Vector2(14, 2));\n      expect(sprite.originalSize, Vector2(192, 256));\n      expect(sprite.srcSize, Vector2(192, 256));\n      expect(sprite.angle, math.pi / 2);\n    });\n\n    test('Sprite renders with correct anchor point', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n\n      final sprite = atlas.findSpriteByName('robot_jump')!;\n      final canvas = _MockCanvas();\n\n      sprite.render(\n        canvas,\n        position: Vector2(100, 100),\n        size: Vector2(192, 256),\n        anchor: Anchor.center,\n      );\n\n      expect(sprite.originalSize, Vector2(192, 256));\n    });\n\n    test('Sprite renders correctly with default position', () async {\n      final atlas = await TexturePackerAtlas.load(\n        atlasPath,\n        fromStorage: true,\n      );\n\n      final sprite = atlas.findSpriteByName('robot_jump')!;\n      final canvas = _MockCanvas();\n\n      sprite.render(\n        canvas,\n        size: Vector2(192, 256),\n      );\n\n      expect(sprite.originalSize, Vector2(192, 256));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_texturepacker/test/region_test.dart",
    "content": "import 'package:flame_texturepacker/src/model/page.dart';\nimport 'package:flame_texturepacker/src/model/region.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Region', () {\n    late Page dummyPage;\n\n    setUp(() {\n      dummyPage = Page();\n    });\n\n    test('encodes and decodes all fields correctly', () {\n      final region = Region(\n        page: dummyPage,\n        name: 'test_region',\n        left: 123,\n        top: 456,\n        width: 789,\n        height: 321,\n        offsetX: 50,\n        offsetY: 75,\n        originalWidth: 800,\n        originalHeight: 600,\n        degrees: 90,\n        rotate: true,\n        index: 3,\n      );\n\n      expect(region.name, 'test_region');\n      expect(region.page, dummyPage);\n\n      expect(region.left, 123);\n      expect(region.top, 456);\n      expect(region.width, 789);\n      expect(region.height, 321);\n      expect(region.offsetX, 50);\n      expect(region.offsetY, 75);\n      expect(region.originalWidth, 800);\n      expect(region.originalHeight, 600);\n\n      expect(region.degrees, 90);\n      expect(region.rotate, isTrue);\n      expect(region.index, 3);\n    });\n\n    test('default values decode correctly', () {\n      final region = Region(\n        page: dummyPage,\n        name: 'default_region',\n      );\n\n      expect(region.name, 'default_region');\n      expect(region.page, dummyPage);\n\n      expect(region.left, 0);\n      expect(region.top, 0);\n      expect(region.width, 0);\n      expect(region.height, 0);\n      expect(region.offsetX, 0);\n      expect(region.offsetY, 0);\n      expect(region.originalWidth, 0);\n      expect(region.originalHeight, 0);\n\n      expect(region.degrees, 0);\n      expect(region.rotate, isFalse);\n      expect(region.index, -1);\n    });\n\n    test('handles max values correctly', () {\n      const max = 4095;\n\n      final region = Region(\n        page: dummyPage,\n        name: 'max_region',\n        left: max.toDouble(),\n        top: max.toDouble(),\n        width: max.toDouble(),\n        height: max.toDouble(),\n        offsetX: max.toDouble(),\n        offsetY: max.toDouble(),\n        originalWidth: max.toDouble(),\n        originalHeight: max.toDouble(),\n        degrees: 359,\n        rotate: true,\n        index: 999999,\n      );\n\n      expect(region.left, max.toDouble());\n      expect(region.top, max.toDouble());\n      expect(region.width, max.toDouble());\n      expect(region.height, max.toDouble());\n      expect(region.offsetX, max.toDouble());\n      expect(region.offsetY, max.toDouble());\n      expect(region.originalWidth, max.toDouble());\n      expect(region.originalHeight, max.toDouble());\n\n      expect(region.degrees, 359);\n      expect(region.rotate, isTrue);\n      expect(region.index, 999999);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_texturepacker/test/separated_parsing_test.dart",
    "content": "import 'dart:io';\nimport 'package:flame_texturepacker/flame_texturepacker.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Separated Parsing and Loading', () {\n    late Directory tempDir;\n\n    setUp(() async {\n      tempDir = await Directory.systemTemp.createTemp('atlas_test');\n    });\n\n    tearDown(() async {\n      if (tempDir.existsSync()) {\n        await tempDir.delete(recursive: true);\n      }\n    });\n\n    const atlasContent = '''\ntest.png\nsize: 64, 64\nformat: RGBA8888\nfilter: Linear,Linear\nrepeat: none\nsprite1\n  rotate: false\n  xy: 0, 0\n  size: 32, 32\n  orig: 32, 32\n  offset: 0, 0\n''';\n\n    test('should parse atlas metadata without loading images', () async {\n      final atlasFile = File('${tempDir.path}/test.atlas');\n      await atlasFile.writeAsString(atlasContent);\n\n      // We don't even need the image file to exist if we don't load images\n      final atlasData = await TexturePackerAtlas.loadAtlas(\n        atlasFile.path,\n        fromStorage: true,\n        loadImages: false,\n      );\n\n      expect(atlasData.pages, hasLength(1));\n      expect(atlasData.pages.first.textureFile, 'test.png');\n      expect(atlasData.pages.first.texture, isNull);\n      expect(atlasData.regions, hasLength(1));\n      // 'sprite1' becomes 'sprite' with index 1\n      expect(atlasData.regions.first.name, 'sprite');\n      expect(atlasData.regions.first.index, 1);\n    });\n\n    test('should load images later for already parsed atlas data', () async {\n      final atlasFile = File('${tempDir.path}/test.atlas');\n      await atlasFile.writeAsString(atlasContent);\n\n      final imageFile = File('${tempDir.path}/test.png');\n      // Use a real PNG for decoding\n      final realPngBytes = await File(\n        'test/assets/whitelist/whitelist_test.png',\n      ).readAsBytes();\n      await imageFile.writeAsBytes(realPngBytes);\n\n      // 1. Initial parse without images\n      final atlasData = await TexturePackerAtlas.loadAtlas(\n        atlasFile.path,\n        fromStorage: true,\n        loadImages: false,\n      );\n      expect(atlasData.pages.first.texture, isNull);\n\n      // 2. Load images later\n      await TexturePackerParser.loadAtlasDataImages(\n        atlasData,\n        atlasFile.path,\n        fromStorage: true,\n      );\n\n      expect(atlasData.pages.first.texture, isNotNull);\n\n      // 3. Create atlas from data\n      final atlas = TexturePackerAtlas.fromAtlas(atlasData);\n      expect(atlas.sprites, hasLength(1));\n      expect(atlas.sprites.first.region.name, 'sprite');\n    });\n\n    test('getAnimation provides sequences easily', () async {\n      const animationAtlas = '''\nanim.png\nsize: 64, 64\nformat: RGBA8888\nfilter: Linear,Linear\nrepeat: none\nwalk_0\n  rotate: false\n  xy: 0, 0\n  size: 32, 32\n  orig: 32, 32\n  offset: 0, 0\nwalk_1\n  rotate: false\n  xy: 32, 0\n  size: 32, 32\n  orig: 32, 32\n  offset: 0, 0\n''';\n      final atlasFile = File('${tempDir.path}/anim.atlas');\n      await atlasFile.writeAsString(animationAtlas);\n\n      final imageFile = File('${tempDir.path}/anim.png');\n      final realPngBytes = await File(\n        'test/assets/whitelist/whitelist_test.png',\n      ).readAsBytes();\n      await imageFile.writeAsBytes(realPngBytes);\n\n      final atlasData = await TexturePackerAtlas.loadAtlas(\n        atlasFile.path,\n        fromStorage: true,\n      );\n      final atlas = TexturePackerAtlas.fromAtlas(atlasData);\n\n      final animation = atlas.getAnimation('walk');\n      expect(animation.frames.length, equals(2));\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_tiled/CHANGELOG.md",
    "content": "## 3.1.0\n\n - **FIX**: Use resolved path when loading single-image tilesets ([#3826](https://github.com/flame-engine/flame/issues/3826)). ([bbdff923](https://github.com/flame-engine/flame/commit/bbdff9230f96ab18ee83a1a0ffab5b4d7f4a43bf))\n - **FIX**: Bump Flutter min version to 3.41.0 ([#3807](https://github.com/flame-engine/flame/issues/3807)). ([0d505304](https://github.com/flame-engine/flame/commit/0d50530485e5be9ce1c9138a5b437607c7c5c628))\n - **FEAT**: Add dynamic layer opacity support to flame_tiled ([#3843](https://github.com/flame-engine/flame/issues/3843)). ([b1702997](https://github.com/flame-engine/flame/commit/b17029977b3c5890fe6794942a0553eb3f21ff8d))\n - **FEAT**: Support package argument in asset loading methods and widgets ([#3835](https://github.com/flame-engine/flame/issues/3835)). ([3f6f95b0](https://github.com/flame-engine/flame/commit/3f6f95b0225df9e4035c74de6cfad501e8c45167))\n\n## 3.0.11\n\n - Update a dependency to the latest release.\n\n## 3.0.10\n\n - **FIX**: Image layer paint area fix ([#3783](https://github.com/flame-engine/flame/issues/3783)). ([437d4bec](https://github.com/flame-engine/flame/commit/437d4becb5752e8429f67297b08ceeb2b971388c))\n\n## 3.0.9\n\n - Update a dependency to the latest release.\n\n## 3.0.8\n\n - **FIX**: Show assertion when tiled file is loaded in the wrong way ([#3747](https://github.com/flame-engine/flame/issues/3747)). ([3efedc46](https://github.com/flame-engine/flame/commit/3efedc4653fa33cb239a38cc6ac8c999bcd25a7f))\n\n## 3.0.7\n\n - **REFACTOR**: Move MutableRSTransform out of flame_tiled package and into flame package ([#3695](https://github.com/flame-engine/flame/issues/3695)). ([7d644dd8](https://github.com/flame-engine/flame/commit/7d644dd84ce27e292b53f7310967393cf4c60618))\n\n## 3.0.6\n\n - Update a dependency to the latest release.\n\n## 3.0.5\n\n - Update a dependency to the latest release.\n\n## 3.0.4\n\n - **FIX**: Add optional key parameter to TiledComponent.load method ([#3630](https://github.com/flame-engine/flame/issues/3630)). ([5a27746e](https://github.com/flame-engine/flame/commit/5a27746ee4dbab17565472133274dd1308525978))\n - **FIX**: PostProcessComponent should size dynamically ([#3611](https://github.com/flame-engine/flame/issues/3611)). ([baecb861](https://github.com/flame-engine/flame/commit/baecb86186a1bff7f21d804e7867f894d2f9d23c))\n\n## 3.0.3\n\n - Update a dependency to the latest release.\n\n## 3.0.2\n\n - Update a dependency to the latest release.\n\n## 3.0.1\n\n - Update a dependency to the latest release.\n\n## 3.0.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Add APIs to get TileData by layer index ([#3539](https://github.com/flame-engine/flame/issues/3539)). ([4676b1b7](https://github.com/flame-engine/flame/commit/4676b1b7a5aefe7a958de55b1d209e554c9b02a6))\n\n## 2.0.3\n\n - Update a dependency to the latest release.\n\n## 2.0.2\n\n - **DOCS**: Fix workflow status badge paths ([#3517](https://github.com/flame-engine/flame/issues/3517)). ([149f16fe](https://github.com/flame-engine/flame/commit/149f16fe29f1fb14b3612964b2226c9c5c7daf95))\n\n## 2.0.1\n\n - **DOCS**: Remove AI assist badges ([#3477](https://github.com/flame-engine/flame/issues/3477)). ([51d7fbc0](https://github.com/flame-engine/flame/commit/51d7fbc06d88adec2e0238c9c4738893b807ec80))\n\n## 2.0.0\n\n> Note: This release has breaking changes.\n\n - **BREAKING** **FIX**: Bump tiled to 0.11.0 and add ColorData extension (#3473).\n\n## 1.21.2\n\n - **REFACTOR**: Fix lint issues from latest flutter release ([#3390](https://github.com/flame-engine/flame/issues/3390)). ([978ad31b](https://github.com/flame-engine/flame/commit/978ad31b429d1801097b0db385a600c85a157867))\n\n## 1.21.1\n\n - Update a dependency to the latest release.\n\n## 1.21.0\n\n - **FEAT**: Add a getter for images cache keys ([#3324](https://github.com/flame-engine/flame/issues/3324)). ([7746f2f8](https://github.com/flame-engine/flame/commit/7746f2f867092c19222a40aec2b66dc80558dccb))\n\n## 1.20.4\n\n - Update a dependency to the latest release.\n\n## 1.20.3\n\n - **DOCS**: Add AI assist badge to readme(s) ([#3226](https://github.com/flame-engine/flame/issues/3226)). ([380d6aa9](https://github.com/flame-engine/flame/commit/380d6aa946d6b852c55f4ebbfce53d2087287fa2))\n\n## 1.20.2\n\n - **REFACTOR**: Modernize switch; use switch-expressions and no break; ([#3133](https://github.com/flame-engine/flame/issues/3133)). ([b283b82f](https://github.com/flame-engine/flame/commit/b283b82f6cfa7e7f2ce5ff7f657e6569667183d4))\n\n## 1.20.1\n\n - **FIX**: Respect tile offset when drawing tiles ([#3112](https://github.com/flame-engine/flame/issues/3112)). ([e3477474](https://github.com/flame-engine/flame/commit/e34774743038bc75fec14afc3c753fa997e71577))\n\n## 1.20.0\n\n - **FEAT**: Export `TileAtlas` from `flame_tiled` package ([#3049](https://github.com/flame-engine/flame/issues/3049)). ([41e9e4e3](https://github.com/flame-engine/flame/commit/41e9e4e38c643b07a3a7269b1cd8d3fa60cbeebb))\n\n## 1.19.0\n\n> Note: This release has breaking changes.\n\n - **FEAT**: Add TiledObjectHealpers extension on TiledObject ([#3032](https://github.com/flame-engine/flame/issues/3032)). ([78380b9d](https://github.com/flame-engine/flame/commit/78380b9d3bb895e20f382c4a1227bcc11e5038b9))\n - **BREAKING** **FIX**: Migrate from `RawKeyEvent` to `KeyEvent` ([#3002](https://github.com/flame-engine/flame/issues/3002)). ([330862c9](https://github.com/flame-engine/flame/commit/330862c98ecc7ed8d94e7cae0c34cd5781da0007))\n\n## 1.18.4\n\n - Update a dependency to the latest release.\n\n## 1.18.3\n\n - Update a dependency to the latest release.\n\n## 1.18.2\n\n - **FIX**: Image layers repeat indefinitely if repeated in Tiled ([#2921](https://github.com/flame-engine/flame/issues/2921)). ([6f79bc5e](https://github.com/flame-engine/flame/commit/6f79bc5ef920ace17d09c88156a73043357d514f))\n\n## 1.18.1\n\n - Update a dependency to the latest release.\n\n## 1.18.0\n\n - **FIX**: TiledComponent.atlases had duplicated values ([#2867](https://github.com/flame-engine/flame/issues/2867)). ([e56ad187](https://github.com/flame-engine/flame/commit/e56ad1878333ba19e0c8af3fb9c9758603662330))\n - **FIX**: Minor issues due Flutter 3.16 ([#2856](https://github.com/flame-engine/flame/issues/2856)). ([d51cd584](https://github.com/flame-engine/flame/commit/d51cd584c71a27c242c2f4600282cf8359daaa17))\n - **FEAT**: Adding configurable padding to Tiled atlas packing ([#2868](https://github.com/flame-engine/flame/issues/2868)). ([d0c10cbb](https://github.com/flame-engine/flame/commit/d0c10cbbea20415de471ad0269a22c168082b02d))\n - **FEAT**: Exposing atlases for reading in a TiledComponent ([#2865](https://github.com/flame-engine/flame/issues/2865)). ([e1b4d93a](https://github.com/flame-engine/flame/commit/e1b4d93ad43a4e1b1b55a3843e26612b73d45ed7))\n\n## 1.17.0\n\n - **FIX**: Configuration useAtlas was not been propagated correctly everywhere ([#2853](https://github.com/flame-engine/flame/issues/2853)). ([2f0dab9e](https://github.com/flame-engine/flame/commit/2f0dab9e59958176e6c46f6e417188e6c4fa3831))\n - **FEAT**: Adding way to configure a layer paint in flame tiled ([#2851](https://github.com/flame-engine/flame/issues/2851)). ([e893d115](https://github.com/flame-engine/flame/commit/e893d1152c2aeb1c976668c875a1c267bbf819c0))\n - **FEAT**: Expose useAtlas on Flame Tiled ([#2852](https://github.com/flame-engine/flame/issues/2852)). ([c4efb4f8](https://github.com/flame-engine/flame/commit/c4efb4f859fe08cc7fbd3e0ddb35c806d0060c78))\n\n## 1.16.0\n\n - **FEAT**: Allow flame tiled to skip tilesets when packing into a tile atlas ([#2847](https://github.com/flame-engine/flame/issues/2847)). ([b93bdd38](https://github.com/flame-engine/flame/commit/b93bdd38313fd273e3e4cf55f1b142969effbde4))\n\n## 1.15.1\n\n - Update a dependency to the latest release.\n\n## 1.15.0\n\n - **REFACTOR**: Remove unnecessary 'async' keyword across the codebase [DCM] ([#2803](https://github.com/flame-engine/flame/issues/2803)). ([2dfe0e5a](https://github.com/flame-engine/flame/commit/2dfe0e5a431213c7148ab6389e3e8c8dc49fbf3d))\n - **FIX**: Remove deprecations for 1.10.0 ([#2809](https://github.com/flame-engine/flame/issues/2809)). ([5b67b8f1](https://github.com/flame-engine/flame/commit/5b67b8f14ad4fdb38a249d0a41ecba49ba2fcc44))\n - **FIX**: Parallax offset calculations in flame_tiled don't scale properly ([#2766](https://github.com/flame-engine/flame/issues/2766)). ([89e8427a](https://github.com/flame-engine/flame/commit/89e8427a7a34258ec20276e4ec64d4a484277cdd))\n - **FEAT**(flame_tiled): Allowing tilesets with images in the same folder to load ([#2814](https://github.com/flame-engine/flame/issues/2814)). ([3b0d7e65](https://github.com/flame-engine/flame/commit/3b0d7e65c2bf158db378d66c4f7e687dd05b46e1))\n - **FEAT**: AssetsBundle can be customized in Images and AssetsCache. ([#2807](https://github.com/flame-engine/flame/issues/2807)). ([a23f80e9](https://github.com/flame-engine/flame/commit/a23f80e94a5d935fc8ba232956fe02e001d5a8f9))\n - **FEAT**: Add overriding of Images and Bundle in all classes ([#2806](https://github.com/flame-engine/flame/issues/2806)). ([2df90c9b](https://github.com/flame-engine/flame/commit/2df90c9ba8f2b1cc088c5270df571eee7e18bb57))\n\n## 1.14.1\n\n - Update a dependency to the latest release.\n\n## 1.14.0\n\n> Note: This release has breaking changes.\n\n - **REFACTOR**: Enable DCM linting ([#2667](https://github.com/flame-engine/flame/issues/2667)). ([27a8fd61](https://github.com/flame-engine/flame/commit/27a8fd61cb7f62513e07a93ff61cf03b426353f2))\n - **FEAT**: Expose atlas limits for `TiledComponent` ([#2701](https://github.com/flame-engine/flame/issues/2701)). ([99a1016f](https://github.com/flame-engine/flame/commit/99a1016f72d02f4a989986f224e0e77cddd0dfa8))\n - **FEAT**: Added prefix parameter to TiledComponent.load to specify assets folder for tiled maps ([#2651](https://github.com/flame-engine/flame/issues/2651)). ([d08284dd](https://github.com/flame-engine/flame/commit/d08284ddcaf5d2ad6e5312336a71a113702dc241))\n - **DOCS**: Enable CSpell on tests ([#2723](https://github.com/flame-engine/flame/issues/2723)). ([e051298c](https://github.com/flame-engine/flame/commit/e051298cba76550229780438b1a589557c7b488d))\n - **BREAKING** **FEAT**: Add CameraComponent to FlameGame ([#2740](https://github.com/flame-engine/flame/issues/2740)). ([7c2f4000](https://github.com/flame-engine/flame/commit/7c2f4000761580dbabb5d73b27f64d5819b34e8d))\n\n## 1.13.0\n\n - **FIX**: Compute scale in TileLayers based on native map tile size rather than image sizes to support oversized/undersized tiles. ([#2634](https://github.com/flame-engine/flame/issues/2634)). ([1c4d6cd0](https://github.com/flame-engine/flame/commit/1c4d6cd0654f133771a7af5795cc1de2343268c1))\n - **FEAT**: Possiblity to pass in FilterQuality to tiled layers ([#2627](https://github.com/flame-engine/flame/issues/2627)). ([f3de6650](https://github.com/flame-engine/flame/commit/f3de66507e623e2fe0100cfd4d002dea14f72470))\n\n## 1.12.0\n\n - **FIX**: Tiled component orthogonal test ([#2549](https://github.com/flame-engine/flame/issues/2549)). ([34e5f0e4](https://github.com/flame-engine/flame/commit/34e5f0e443e21923c311120ce8634a14339bc71d))\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FEAT**: TiledAtlas.clearCache function ([#2592](https://github.com/flame-engine/flame/issues/2592)). ([d40fefcf](https://github.com/flame-engine/flame/commit/d40fefcf08850a986304472d5369dcd74f2b9d4b))\n - **FEAT**: ComponentKey API ([#2566](https://github.com/flame-engine/flame/issues/2566)). ([b3efb612](https://github.com/flame-engine/flame/commit/b3efb612cb3cb77f69bc030e9ba71516348035d2))\n - **FEAT**: Add option for a custom image and asset loader ([#2569](https://github.com/flame-engine/flame/issues/2569)). ([dfe18251](https://github.com/flame-engine/flame/commit/dfe18251c1bac8aaca9bf146e03320efbbc3ce9c))\n\n## 1.11.0\n\n - **FIX**: Tiled component orthogonal test ([#2549](https://github.com/flame-engine/flame/issues/2549)). ([34e5f0e4](https://github.com/flame-engine/flame/commit/34e5f0e443e21923c311120ce8634a14339bc71d))\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FEAT**: Add option for a custom image and asset loader ([#2569](https://github.com/flame-engine/flame/issues/2569)). ([dfe18251](https://github.com/flame-engine/flame/commit/dfe18251c1bac8aaca9bf146e03320efbbc3ce9c))\n\n## 1.10.2\n\n - **FIX**: Update sdk constraints to >=3.0.0 ([#2554](https://github.com/flame-engine/flame/issues/2554)). ([2f71e06e](https://github.com/flame-engine/flame/commit/2f71e06eb86ffc65cd459c4d722eee2470be13e5))\n - **FIX**: Solve warnings from 3.10.0 analyzer ([#2532](https://github.com/flame-engine/flame/issues/2532)). ([b41622db](https://github.com/flame-engine/flame/commit/b41622db8faa7559328f83f8f1d93ec4c6386961))\n\n## 1.10.1\n\n - **REFACTOR**: Add new lint rules ([#2477](https://github.com/flame-engine/flame/issues/2477)). ([dbda37b8](https://github.com/flame-engine/flame/commit/dbda37b81a9a7411559a6ba919ffbda6018b85c2))\n\n## 1.10.0\n\n - **REFACTOR**: Divide TileLayer by its Layer type ([#2326](https://github.com/flame-engine/flame/issues/2326)). ([0c14d4cb](https://github.com/flame-engine/flame/commit/0c14d4cb87ba81957221695547bc06111a28617a))\n - **FIX**: TiledComponent now can be safely loaded regardless of the order ([#2391](https://github.com/flame-engine/flame/issues/2391)). ([4ddc4bba](https://github.com/flame-engine/flame/commit/4ddc4bba2b67ebd8c9c0e9e761eee34d2a74f62b))\n - **FEAT**: Use cached image when creating single source TiledAtlas if available ([#2348](https://github.com/flame-engine/flame/issues/2348)). ([73467c94](https://github.com/flame-engine/flame/commit/73467c941d89f68598c6dc297937af9d9896a949))\n - **FEAT**: Add ability to opt-out flip ([#2316](https://github.com/flame-engine/flame/issues/2316)). ([34c3b6bd](https://github.com/flame-engine/flame/commit/34c3b6bdc4c570f4e8641b11b94efe19bdd1ef32))\n - **DOCS**: Update funding links ([#2420](https://github.com/flame-engine/flame/issues/2420)). ([8294a2a1](https://github.com/flame-engine/flame/commit/8294a2a15638c504aa2b77f967f5963af1f23c2c))\n - **DOCS**: Fix broken image link on flame_tiled pub ([#2407](https://github.com/flame-engine/flame/issues/2407)). ([0d24a6c8](https://github.com/flame-engine/flame/commit/0d24a6c8ed4a5d4de2e653a6430a635ef881ee2e))\n - **DOCS**: Fix non-markdown section of README files ([#2406](https://github.com/flame-engine/flame/issues/2406)). ([426b3124](https://github.com/flame-engine/flame/commit/426b3124022e567633c76b80eb389ebce1772ca3))\n - **DOCS**: Update all README files for the bridge packages to be consistent and not broken ([#2402](https://github.com/flame-engine/flame/issues/2402)). ([5e8ecf54](https://github.com/flame-engine/flame/commit/5e8ecf5450688b1287368b3fbc7b0e718a29fce4))\n\n## 1.9.1\n\n - Update a dependency to the latest release.\n\n## 1.9.0\n\n - **FEAT**: Rename internal classes clashes with Tiled ([#2139](https://github.com/flame-engine/flame/issues/2139)). ([2224eaac](https://github.com/flame-engine/flame/commit/2224eaac701414deb76bac7f7c40a56387cdf817))\n\n## 1.8.0\n\n - **REFACTOR**: Split layers into files ([#1916](https://github.com/flame-engine/flame/issues/1916)). ([dac2ee13](https://github.com/flame-engine/flame/commit/dac2ee1375a0ed9535ccd5052e0960043ec8d3d2))\n - **FIX**: Take scale into account ([#1906](https://github.com/flame-engine/flame/issues/1906)). ([27ab12ff](https://github.com/flame-engine/flame/commit/27ab12ff6865e5d3d567c4714c4737cd7a0bc1fa))\n - **FEAT**: Animated tile support! ([#1930](https://github.com/flame-engine/flame/issues/1930)). ([6410dc75](https://github.com/flame-engine/flame/commit/6410dc753ce1d044e2d8ea8061186c88d80589e9))\n - **FEAT**: Add avoid_final_parameters, depend_on_referenced_packages, unnecessary_to_list_in_spreads ([#1927](https://github.com/flame-engine/flame/issues/1927)). ([deccb434](https://github.com/flame-engine/flame/commit/deccb4349d38b6a91ccf5bdf229980b2a3296ce5))\n - **FEAT**: Tiled component is positionable ([#1900](https://github.com/flame-engine/flame/issues/1900)). ([88cb2a05](https://github.com/flame-engine/flame/commit/88cb2a05c37535053ece3eb19311c4c78fac249c))\n - **FEAT**: Add support for isometric staggered maps ([#1895](https://github.com/flame-engine/flame/issues/1895)). ([96be8408](https://github.com/flame-engine/flame/commit/96be840899022a024cef1eb853818d8138592000))\n - **FEAT**: Adding support for Group layer nesting for RenderableTileMap ([#1886](https://github.com/flame-engine/flame/issues/1886)). ([5ed34547](https://github.com/flame-engine/flame/commit/5ed345471da0586a2a3071a523c4e5b6d7f184c0))\n - **FEAT**: Hexagonal maps ([#1892](https://github.com/flame-engine/flame/issues/1892)). ([29bda336](https://github.com/flame-engine/flame/commit/29bda336b8febe9cb08dc621da3dc0271c6d2802))\n - **FEAT**: Add isometric support for flame_tiled ([#1885](https://github.com/flame-engine/flame/issues/1885)). ([cf828823](https://github.com/flame-engine/flame/commit/cf82882390efc45a0b2323463b4b28e557f5df48))\n\n## 1.7.2\n\n - **FIX**: Remove unnecessary x offset ([#1838](https://github.com/flame-engine/flame/issues/1838)). ([4ea12b72](https://github.com/flame-engine/flame/commit/4ea12b724e04843b3b7dcd02dc2fb5060c9cf283))\n\n## 1.7.1\n\n - **FIX**: Remove unnecessary x offset ([#1838](https://github.com/flame-engine/flame/issues/1838)). ([4ea12b72](https://github.com/flame-engine/flame/commit/4ea12b724e04843b3b7dcd02dc2fb5060c9cf283))\n - **FEAT**: Adding support for additional layer rendering options ([#1794](https://github.com/flame-engine/flame/issues/1794)). ([112acf2a](https://github.com/flame-engine/flame/commit/112acf2aa70ded86e6c2b661f5d6a4855d043f99))\n\n## 1.7.0\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FIX**: tiled example size ([#1729](https://github.com/flame-engine/flame/issues/1729)). ([8306fc11](https://github.com/flame-engine/flame/commit/8306fc1104cb752ce71108abb3768f05ce1b1dac))\n - **FEAT**: Adding support for additional layer rendering options ([#1794](https://github.com/flame-engine/flame/issues/1794)). ([112acf2a](https://github.com/flame-engine/flame/commit/112acf2aa70ded86e6c2b661f5d6a4855d043f99))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n## 1.6.0\n\n - **FIX**: Correct flutter constraint ([#1731](https://github.com/flame-engine/flame/issues/1731)). ([c7383843](https://github.com/flame-engine/flame/commit/c738384314a1a5c3695d1c3adaebcb59604df83a))\n - **FIX**: tiled example size ([#1729](https://github.com/flame-engine/flame/issues/1729)). ([8306fc11](https://github.com/flame-engine/flame/commit/8306fc1104cb752ce71108abb3768f05ce1b1dac))\n - **FEAT**: Move to Flutter 3.0.0 and Dart 2.17.0 ([#1713](https://github.com/flame-engine/flame/issues/1713)). ([2a41d0d6](https://github.com/flame-engine/flame/commit/2a41d0d683391194b7209c47bde91199ab7a663e))\n\n## 1.5.0\n\n - **REFACTOR**: Move to package imports ([#1625](https://github.com/flame-engine/flame/issues/1625)). ([843ddc36](https://github.com/flame-engine/flame/commit/843ddc36249272fcb518b44672e1012307dfa1b5))\n - **FIX**: performance improvements on `SpriteBatch` APIs ([#1637](https://github.com/flame-engine/flame/issues/1637)). ([4b19a1b2](https://github.com/flame-engine/flame/commit/4b19a1b203c5cfca5bb412b91c795fe6a215506e))\n - **FIX**: Avoid leaks when using PictureRecorders ([#1643](https://github.com/flame-engine/flame/issues/1643)). ([d67065e5](https://github.com/flame-engine/flame/commit/d67065e52db453b0f4f190a7aec1bec6bc389e45))\n - **FIX**: Fix tile flips when using canvas.drawAtlas ([#1610](https://github.com/flame-engine/flame/issues/1610)). ([b4ad498f](https://github.com/flame-engine/flame/commit/b4ad498fe5488795deb2c2e098fcde357b448bf0))\n - **FIX**: Add centered anchor point for tile rotation ([#1570](https://github.com/flame-engine/flame/issues/1570)). ([f64d5264](https://github.com/flame-engine/flame/commit/f64d5264abb9e1548d26ae15269654e563cd0ee9))\n - **FEAT**: Add more lint rules ([#1703](https://github.com/flame-engine/flame/issues/1703)). ([49252f8e](https://github.com/flame-engine/flame/commit/49252f8ef29aa6b77144dcb97c24346f2f39380b))\n - **FEAT**: Bump to Flutter 2.10.0 ([#1617](https://github.com/flame-engine/flame/issues/1617)). ([beac9013](https://github.com/flame-engine/flame/commit/beac901313456cf0b39b6f4e6459f0feed183614))\n\n## 1.4.0\n\n - **FEAT**: Possibility to create RenderableTiledMap from TiledMap (#1534). ([5ed08333](https://github.com/flame-engine/flame/commit/5ed08333215658b9eaca049f6ba16b6509901bb9))\n - **FEAT**: Added children parameter to Component constructor (#1525). ([f0b31fcf](https://github.com/flame-engine/flame/commit/f0b31fcfc0fc6b0f8f96895ef6a68fd5a30a3159))\n\n## 1.3.0\n\n - Graduate package to a stable release. See pre-releases prior to this version for changelog entries.\n\n## 1.3.0-releasecandidate.6\n\n - Update a dependency to the latest release.\n\n## 1.3.0-releasecandidate.5\n\n - Update a dependency to the latest release.\n\n## 1.3.0-releasecandidate.4\n\n - Update a dependency to the latest release.\n\n## 1.3.0-releasecandidate.3\n\n - Update a dependency to the latest release.\n\n## 1.3.0-releasecandidate.2\n\n - Update a dependency to the latest release.\n\n## 1.3.0-releasecandidate.1\n\n> Note: This release has breaking changes.\n\n - **FEAT**: Added getImageLayer to flame_tiled (#1405). ([a037ada5](https://github.com/flame-engine/flame/commit/a037ada5ea18b0d1b0be68f9b3d3cceabc8c3b2b))\n - **FEAT**: modifiable Layer and TileData in RenderableTileMap (#1324). ([b56d5f3c](https://github.com/flame-engine/flame/commit/b56d5f3cd7d87a07ab0063defbf14a56c0cca1a5))\n - **FEAT**: Expose priority for TiledComponent (#1259). ([f6be66ab](https://github.com/flame-engine/flame/commit/f6be66abd5321a8c48f7200d62bd9e35a5aa71ff))\n - **DOCS**: Fix various dartdoc warnings (#1353). ([9f096053](https://github.com/flame-engine/flame/commit/9f096053fd3c8ebd52d301710625a187db09704f))\n - **DOCS**: Update flame_tiled readme (#1286). ([ee7298cb](https://github.com/flame-engine/flame/commit/ee7298cbe5cd02825cd7246f86ccb0c3655985a0))\n - **DOCS**: Update contributions to flame_tiled (#1197). ([93b763e1](https://github.com/flame-engine/flame/commit/93b763e1c000b1ebc538e393723aae2a85eb4838))\n - **BREAKING** **FIX**: fix multiple external tilesets (#1344). ([80a483f8](https://github.com/flame-engine/flame/commit/80a483f80df57ce6339f84d03d836efcbbf09579))\n - **BREAKING** **FIX**: Change Tiled batched rendering to batched rendering per layer (#1317). ([30fce398](https://github.com/flame-engine/flame/commit/30fce398dbe397d4368a0f721b20afcd663c489f))\n\n## 1.2.1\n\n> Note: This release has breaking changes that were supposed to be in 1.2.0\n\n - **BREAKING** **FIX**: fix multiple external tilesets (#1344). ([80a483f8](https://github.com/flame-engine/flame/commit/80a483f80df57ce6339f84d03d836efcbbf09579))\n\n## 1.2.0\n\n - **FEAT**: modifiable Layer and TileData in RenderableTileMap (#1324). ([b56d5f3c](https://github.com/flame-engine/flame/commit/b56d5f3cd7d87a07ab0063defbf14a56c0cca1a5))\n\n## 1.1.0\n\n - Change Tiled batched rendering to batched rendering per layer.\n\n## 1.0.0\n\n - Updated to Flame 1.0.0\n\n## 1.0.0-releasecandidate.15\n\n - Upgrade to work with latest Flame version\n - Move to the mono-repo\n\n## 0.1.0\n\n - Updated the Tiled class to use the SpriteBatch API for efficient drawing to canvas.\n\n## 0.0.1\n\n - Moving code from main repository to its own package\n"
  },
  {
    "path": "packages/flame_tiled/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Blue Fire\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/flame_tiled/README.md",
    "content": "<!-- markdownlint-disable MD013 -->\n<p align=\"center\">\n  <a href=\"https://flame-engine.org\">\n    <img alt=\"flame\" width=\"200px\" src=\"https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png\">\n  </a>\n</p>\n\n<p align=\"center\">\nAdds support for <a href=\"https://www.mapeditor.org/\">Tiled</a>'s tile maps to your <a href=\"https://github.com/flame-engine/flame\">Flame</a> games.\n</p>\n\n<p align=\"center\">\n  <a title=\"Pub\" href=\"https://pub.dev/packages/flame_tiled\" ><img src=\"https://img.shields.io/pub/v/flame_tiled.svg?style=popout\" /></a>\n  <a title=\"Test\" href=\"https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain\"><img src=\"https://github.com/flame-engine/flame/actions/workflows/cicd.yml/badge.svg?branch=main&event=push\"/></a>\n  <a title=\"Discord\" href=\"https://discord.gg/pxrBmy4\"><img src=\"https://img.shields.io/discord/509714518008528896.svg\"/></a>\n  <a title=\"Melos\" href=\"https://github.com/invertase/melos\"><img src=\"https://img.shields.io/badge/maintained%20with-melos-f700ff.svg\"/></a>\n</p>\n\n---\n<!-- markdownlint-enable MD013 -->\n\n<!-- markdownlint-disable-next-line MD002 -->\n\n# flame_tiled\n\n> :warning: Under the current sprite batch implementation, you might experience extra\n> lines while rendering due to a bug in Flutter,\n> see [this issue](https://github.com/flame-engine/flame/issues/1152).\n\nPackage to bridge the `tiled` library into easy-to-use Flame components.\n\n<p align=\"center\">\n    <img alt=\"flame_tiled example\" width=\"500px\" src=\"https://raw.githubusercontent.com/flame-engine/flame/master/packages/flame_tiled/example/screenshot.png\">\n</p>\n\nMore documentation can be found [here](https://docs.flame-engine.org/main/tiled.html).\n\n\n## Contributors (before the package moved into the monorepo)\n\n- [@feroult](https://github.com/feroult)\n- [@erickzanardo](https://github.com/erickzanardo)\n- [@Schnurber](https://github.com/schnurber)\n- [@Akida31](https://github.com/akida31)\n- [@pgainullin](https://github.com/pgainullin)\n- [@wolfenrain](https://github.com/wolfenrain)\n- [@rc-davis](https://github.com/rc-davis)\n- [@luanpotter](https://github.com/luanpotter)\n"
  },
  {
    "path": "packages/flame_tiled/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n"
  },
  {
    "path": "packages/flame_tiled/example/README.md",
    "content": "# flame_tiled example\n\nSimple project to showcase the usage of flame_tiled by rendering a tile map.\n\n<p align=\"center\">\n    <img alt=\"flame_tiled example\" width=\"200px\" src=\"/packages/flame_tiled/example/screenshot.png\">\n</p>\n\n\n## Credits\n\n- Level tilesets: [erayzesen.itch.io/pixel-platformer](https://erayzesen.itch.io/pixel-platformer)\n  created by [@erayzesen](https://twitter.com/erayzesen)\n"
  },
  {
    "path": "packages/flame_tiled/example/analysis_options.yaml",
    "content": "include: package:flame_lint/analysis_options_with_dcm.yaml\n\nlinter:\n  rules:\n    public_member_api_docs: false\n"
  },
  {
    "path": "packages/flame_tiled/example/assets/tiles/map.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.0\" orientation=\"orthogonal\" renderorder=\"right-down\" width=\"40\" height=\"40\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" backgroundcolor=\"#3a889e\" nextlayerid=\"10\" nextobjectid=\"15\">\n <tileset firstgid=\"1\" name=\"level_standard_tileset\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"640\" columns=\"32\">\n  <image source=\"../images/level_standard_tileset.png\" width=\"512\" height=\"320\"/>\n </tileset>\n <tileset firstgid=\"641\" name=\"level_ice_tileset\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"640\" columns=\"32\">\n  <image source=\"../images/level_ice_tileset.png\" width=\"512\" height=\"320\"/>\n </tileset>\n <imagelayer id=\"7\" name=\"Sky\" offsetx=\"0\" offsety=\"-130\" parallaxx=\"0.4\" parallaxy=\"0.4\" repeatx=\"1\">\n  <image source=\"../images/repeatable_background.png\" width=\"512\" height=\"303\"/>\n </imagelayer>\n <layer id=\"8\" name=\"Ground\" width=\"40\" height=\"40\">\n  <data encoding=\"base64\">\n   AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQAAAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGEAAABiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHAAAAiAAAAIkAAACKAAAAiwAAAIwAAACBAAAAggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApwAAAKgAAACpAAAAqgAAAKsAAACsAAAAoQAAAKIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMEAAADCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAChAAAAogAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwQAAAMIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGEAAABiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABHAQAASAEAAEkBAABKAQAASwEAAEwBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBAAAAggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZwEAAGgBAABpAQAAagEAAGsBAABsAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoQAAAKIAAAAAAAAASAEAAEgBAABIAQAASAEAAEgBAABIAQAASAEAAEgBAABIAQAASAEAAEgBAABIAQAASAEAAEgBAABIAQAATAEAAAAAAAAAAAAAAAAAAIcBAACIAQAAiQEAAIoBAACLAQAAjAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMEAAADCAAAAAAAAAGsBAABqAQAAawEAAGoBAABrAQAAagEAAGsBAABqAQAAawEAAGoBAABrAQAAagEAAGsBAABqAQAAawEAAGwBAAAAAAAAAAAAAAAAAACnAQAAqAEAAKkBAACqAQAAqwEAAKwBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABhAAAAYgAAAAAAAACLAQAAigEAAIsBAACKAQAAiwEAAIoBAACLAQAAigEAAIsBAACKAQAAiwEAAIoBAACLAQAAigEAAIsBAABsAQAAAAAAAAAAAAAAAAAAZwEAAGgBAABpAQAAagEAAGsBAABsAQAAAAAAAAAAAACHAAAAiAAAAIkAAACKAAAAiwAAAIwAAAAAAAAAAAAAAAAAAAAAAAAAgQAAAIIAAAAAAAAAawEAAGoBAABrAQAAagEAAGsBAABqAQAAawEAAGoBAABrAQAAagEAAGsBAABqAQAAawEAAGoBAABrAQAAbAEAAAAAAAAAAAAAAAAAAIcBAACIAQAAiQEAAIoBAACLAQAAjAEAAAAAAAAAAAAApwAAAKgAAACpAAAAqgAAAKsAAACsAAAAAAAAAAAAAAAAAAAAAAAAAKEAAACiAAAAAAAAAIsBAACKAQAAiwEAAIoBAACLAQAAigEAAIsBAACKAQAAiwEAAIoBAACLAQAAigEAAIsBAACKAQAAiwEAAGwBAAAAAAAAAAAAAAAAAACnAQAAqAEAAKkBAACqAQAAqwEAAKwBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBAAAAwgAAAAAAAACnAAAAqAAAAKkAAACoAAAAqQAAAKoAAACrAAAAqAAAAKkAAACqAAAAqwAAAKgAAACpAAAAqgAAAKsAAACsAAAAAAAAAAAAAAAAAAAAZwEAAGgBAABpAQAAagEAAGsBAABsAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYQAAAGIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIcBAACIAQAAiQEAAIoBAACLAQAAjAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIEAAACCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACnAQAAqAEAAKkBAACqAQAAqwEAAKwBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAChAAAAogAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZwEAAGgBAABpAQAAagEAAGsBAABsAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwQAAAMIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvAwAAMAMAAPUCAAD2AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIcBAACIAQAAiQEAAIoBAACLAQAAjAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOEAAADiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHAwAAyAMAAMkDAADKAwAAywMAAMwDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACnAQAAqAEAAKkBAACqAQAAqwEAAKwBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcDAAAIAwAACQMAAAoDAAALAwAADAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5wMAAOgDAADpAwAA6gMAAOsDAADsAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApwAAAKgAAACpAAAAqgAAAKsAAACsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnAwAAKAMAACkDAAAqAwAAKwMAACwDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcEAAAIBAAACQQAAAoEAAALBAAADAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnBAAAKAQAACkEAAAqBAAAKwQAACwEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANMCAADUAgAAAAAAAAAAAAAAAAAA0QIAANICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5wMAAOgDAADpAwAA6gMAAOsDAADsAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADzAgAA9AIAAAAAAAAAAAAAAAAAAPECAADyAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcEAAAIBAAACQQAAAoEAAALBAAADAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACDAwAAhAMAAIUDAACGAwAAyAMAAMkDAADKAwAAywMAAMgDAADJAwAAygMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnBAAAKAQAACkEAAAqBAAAKwQAACwEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAowMAAKQDAAClAwAApgMAAOgDAADpAwAA6gMAAOsDAADoAwAA6QMAAOoDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5wMAAOgDAADpAwAA6gMAAOsDAADsAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM8CAADQAgAAgwMAAIQDAACFAwAAhgMAAMMDAADoAwAA6QMAAOoDAADpAwAA6gMAAOkDAADqAwAA6QMAAOoDAAAKBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcEAAAIBAAACQQAAAoEAAALBAAADAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADvAgAA8AIAAKMDAACkAwAApQMAAKYDAADoAwAA6QMAAOoDAADrAwAACQQAAAoEAAAJBAAACgQAAAkEAAAKBAAA6QMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnBAAAKAQAACkEAAAqBAAAKwQAACwEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMcDAADIAwAAyQMAAMoDAADDAwAA6AMAAOkDAADqAwAA6QMAAOoDAADpAwAA6gMAACkEAAAqBAAA6QMAAOoDAADpAwAA6gMAAAkEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5wMAAOgDAADpAwAA6gMAAOsDAADsAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADnAwAA6AMAAOkDAADqAwAA4wMAAAgEAADpAwAA6gMAAAkEAAAKBAAACQQAAAoEAAAJBAAACgQAAAkEAAAKBAAACQQAAAoEAAApBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcEAAAIBAAACQQAAAoEAAALBAAADAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwQAAAgEAAAJBAAACgQAAOkDAADqAwAACQQAAAoEAAApBAAAKgQAACkEAAAqBAAAKQQAACoEAADpAwAA6gMAAOkDAADqAwAA6QMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnBAAAKAQAACkEAAAqBAAAKwQAACwEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACcEAAAoBAAAKQQAACoEAAAJBAAACgQAACkEAAAqBAAACAQAAAkEAADpAwAA6gMAAOkDAADqAwAACQQAAAoEAAAJBAAACgQAAOkDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5wMAAOgDAADpAwAA6gMAAOsDAADsAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADnAwAA6AMAAOkDAADqAwAAKQQAACoEAADpAwAA6gMAAOkDAADqAwAACQQAAAoEAAAJBAAACgQAAOkDAADqAwAAKQQAACoEAAAJBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcEAAAIBAAACQQAAAoEAAALBAAADAQAAEMDAABEAwAAQwMAAEQDAABDAwAARAMAAEMDAABEAwAABwQAAAgEAAAJBAAACgQAAOkDAADqAwAACQQAAAoEAAAJBAAACgQAACkEAAAqBAAAKQQAACoEAAAJBAAACgQAAOkDAADqAwAAKQQAAA==\n  </data>\n </layer>\n <layer id=\"9\" name=\"Ground Decoration\" width=\"40\" height=\"40\">\n  <data encoding=\"base64\">\n   AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG8AAABwAAAAdQAAAHYAAACzAAAAtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFEAAABSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxAAAAcgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK8AAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n  </data>\n </layer>\n <objectgroup id=\"3\" name=\"AnimatedCoins\">\n  <object id=\"5\" x=\"336\" y=\"190\">\n   <ellipse/>\n  </object>\n  <object id=\"6\" x=\"544\" y=\"91\">\n   <ellipse/>\n  </object>\n  <object id=\"7\" x=\"374\" y=\"190\">\n   <ellipse/>\n  </object>\n  <object id=\"9\" x=\"480\" y=\"248\">\n   <ellipse/>\n  </object>\n  <object id=\"10\" x=\"569\" y=\"375\">\n   <ellipse/>\n  </object>\n  <object id=\"12\" x=\"147\" y=\"381\">\n   <ellipse/>\n  </object>\n  <object id=\"13\" x=\"184\" y=\"381\">\n   <ellipse/>\n  </object>\n  <object id=\"14\" x=\"275\" y=\"506\">\n   <ellipse/>\n  </object>\n </objectgroup>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/example/lib/main.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_tiled/flame_tiled.dart';\nimport 'package:flutter/widgets.dart' hide Animation, Image;\n\nvoid main() {\n  runApp(GameWidget(game: TiledGame()));\n}\n\nclass TiledGame extends FlameGame {\n  late TiledComponent mapComponent;\n\n  TiledGame()\n    : super(\n        camera: CameraComponent.withFixedResolution(\n          width: 16 * 28,\n          height: 16 * 14,\n        ),\n      );\n\n  @override\n  Future<void> onLoad() async {\n    camera.viewfinder\n      ..zoom = 0.5\n      ..anchor = Anchor.topLeft\n      ..add(\n        MoveToEffect(\n          Vector2(1000, 0),\n          EffectController(\n            duration: 10,\n            alternate: true,\n            infinite: true,\n          ),\n        ),\n      );\n\n    mapComponent = await TiledComponent.load('map.tmx', Vector2.all(16));\n    world.add(mapComponent);\n\n    final objectGroup = mapComponent.tileMap.getLayer<ObjectGroup>(\n      'AnimatedCoins',\n    );\n    final coins = await Flame.images.load('coins.png');\n\n    // We are 100% sure that an object layer named `AnimatedCoins`\n    // exists in the example `map.tmx`.\n    for (final object in objectGroup!.objects) {\n      world.add(\n        SpriteAnimationComponent(\n          size: Vector2.all(20.0),\n          position: Vector2(object.x, object.y),\n          animation: SpriteAnimation.fromFrameData(\n            coins,\n            SpriteAnimationData.sequenced(\n              amount: 8,\n              stepTime: 0.15,\n              textureSize: Vector2.all(20),\n            ),\n          ),\n        ),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_tiled/example/pubspec.yaml",
    "content": "name: flame_tiled_example\nresolution: workspace\ndescription: Simple project to showcase the usage of flame_tiled\npublish_to: 'none'\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndependencies:\n  flame: ^1.36.0\n  flame_tiled: ^3.1.0\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flame_lint: ^1.4.3\nflutter:\n  uses-material-design: true\n  assets:\n    - assets/tiles/map.tmx\n    - assets/images/level_ice_tileset.png\n    - assets/images/level_standard_tileset.png\n    - assets/images/repeatable_background.png\n    - assets/images/coins.png\n"
  },
  {
    "path": "packages/flame_tiled/lib/flame_tiled.dart",
    "content": "library flame_tiled;\n\nexport 'package:tiled/tiled.dart';\n\nexport 'src/extensions.dart';\nexport 'src/flame_tsx_provider.dart';\nexport 'src/renderable_tile_map.dart';\nexport 'src/simple_flips.dart';\nexport 'src/tile_atlas.dart';\nexport 'src/tiled_component.dart';\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/extensions.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame_tiled/flame_tiled.dart';\n\n/// This extension adds some helpers for [TiledObject] that make it easier to\n/// use it with Flame.\nextension TiledObjectHelpers on TiledObject {\n  /// Returns the position of this tiled object as a Vector2.\n  Vector2 get position => Vector2(x, y);\n\n  /// Returns the size of this tiled object as a Vector2.\n  Vector2 get size => Vector2(width, height);\n}\n\nextension ColorDataExtension on ColorData {\n  /// Returns the color as a dart:ui Color.\n  Color toColor() => Color.fromARGB(alpha, red, green, blue);\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/flame_tsx_provider.dart",
    "content": "import 'package:flame/flame.dart';\nimport 'package:flutter/services.dart';\nimport 'package:tiled/tiled.dart';\nimport 'package:xml/xml.dart';\n\n/// A implementation of [TsxProvider] use by RenderableTileMap.\n///\n/// It uses [Flame.bundle] or a custom asset bundle\n/// and has a built-in cache for the file read.\nclass FlameTsxProvider implements TsxProvider {\n  /// Parsed data for this tsx file.\n  final String data;\n\n  /// Stored filename for corresponding tsx file.\n  final String _filename;\n\n  FlameTsxProvider._(this.data, this._filename);\n\n  @override\n  String get filename => _filename;\n\n  @override\n  Parser getSource(String key) {\n    final node = XmlDocument.parse(data).rootElement;\n    return XmlParser(node);\n  }\n\n  @override\n  Parser? getCachedSource() {\n    if (data.isEmpty) {\n      return null;\n    }\n    return getSource('');\n  }\n\n  /// Parses a file returning a [FlameTsxProvider].\n  ///\n  /// {@macro renderable_tile_prefix_path}\n  static Future<FlameTsxProvider> parse(\n    String key, [\n    AssetBundle? bundle,\n    String prefix = 'assets/tiles/',\n  ]) async {\n    final data = await (bundle ?? Flame.bundle).loadString('$prefix$key');\n    return FlameTsxProvider._(data, key);\n  }\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/mutable_rect.dart",
    "content": "import 'dart:ui' show Rect;\n\n/// A mutable version of [Rect] for tile map animations.\nclass MutableRect extends Rect {\n  /// Construct a rectangle from its left, top, right, and bottom edges.\n  MutableRect.fromLTRB(this.left, this.top, this.right, this.bottom)\n    : super.fromLTRB(left, top, right, bottom);\n\n  /// Create a new instance from [other].\n  factory MutableRect.fromRect(Rect other) =>\n      MutableRect.fromLTRB(other.left, other.top, other.right, other.bottom);\n\n  /// The offset of the left edge of this rectangle from the x axis.\n  @override\n  double left;\n\n  /// The offset of the top edge of this rectangle from the y axis.\n  @override\n  double top;\n\n  /// The offset of the right edge of this rectangle from the x axis.\n  @override\n  double right;\n\n  /// The offset of the bottom edge of this rectangle from the y axis.\n  @override\n  double bottom;\n\n  /// Update with [other]'s dimensions.\n  void copy(Rect other) {\n    left = other.left;\n    top = other.top;\n    right = other.right;\n    bottom = other.bottom;\n  }\n\n  /// Convert to immutable rectangle.\n  Rect toRect() => Rect.fromLTRB(left, top, right, bottom);\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/rectangle_bin_packer.dart",
    "content": "import 'dart:ui';\n\n/// A simple solution to packing a series of rectangles into a larger bin.\n///\n/// Packing different rectangles in the smallest area possible is an\n/// NP-Complete problem. We are not going for perfection here, just something\n/// that works.\n///\n/// Light reading:\n///   * https://en.wikipedia.org/wiki/Rectangle_packing#Packing_different_rectangles_in_a_minimum-area_rectangle\n///   * https://www.david-colson.com/2020/03/10/exploring-rect-packing.html\nclass RectangleBinPacker {\n  final double maxX;\n  final double maxY;\n\n  /// The bins of free space that we can search.\n  late final List<Rect> bins = [Rect.fromLTWH(0, 0, maxX, maxY)];\n\n  RectangleBinPacker(this.maxX, this.maxY);\n\n  /// Finds a free space for a rectangle of lengths [width] and [height] in\n  /// the atlas.\n  Rect pack(double width, double height) {\n    // Search the list of bins for the first one that can hold this rectangle.\n    Rect? search;\n    for (search in bins) {\n      if (search.width >= width && search.height >= height) {\n        // found one!\n        break;\n      }\n    }\n    // If we exhausted our search, return an empty rect.\n    if (search == null || search.width < width || search.height < height) {\n      assert(\n        false,\n        '''RectangleBinPacker failed to pack an image. Consider using a bigger atlas if you are sure that your target platform can handle it.''',\n      );\n      return Rect.zero;\n    }\n\n    // If we found one, record the found rectangle position and replace the\n    // bin with left over spaces (could be zero, 1, or two rectangles).\n    bins.remove(search);\n    final found = Rect.fromLTWH(search.left, search.top, width, height);\n    if (found == search) {\n      return found;\n    }\n\n    if (found.height != search.height) {\n      final newRect = Rect.fromLTRB(\n        search.left,\n        found.bottom,\n        search.right,\n        search.bottom,\n      );\n      bins.add(newRect);\n    }\n    if (found.width != search.width) {\n      final newRect = Rect.fromLTRB(\n        found.right,\n        search.top,\n        search.right,\n        found.bottom,\n      );\n      bins.add(newRect);\n    }\n    bins.sort(_compare);\n    return found;\n  }\n\n  /// Comparator function used to sort rectangles in a stable fashion.\n  int _compare(Rect a, Rect b) {\n    final height = a.height - b.height;\n    return (height != 0 ? height : a.width - b.width).toInt();\n  }\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/renderable_layers/group_layer.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame_tiled/src/renderable_layers/renderable_layer.dart';\nimport 'package:meta/meta.dart';\nimport 'package:tiled/tiled.dart';\n\n@internal\nclass GroupLayer extends RenderableLayer<Group> {\n  /// The child layers of this [Group] to be rendered recursively.\n  ///\n  /// NOTE: This is set externally instead of via constructor params because\n  ///       there are cyclic dependencies when loading the renderable layers.\n  late final List<RenderableLayer> children;\n\n  GroupLayer({\n    required super.layer,\n    required super.parent,\n    required super.map,\n    required super.destTileSize,\n    super.filterQuality,\n  });\n\n  @override\n  void refreshCache() {\n    for (final child in children) {\n      child.refreshCache();\n    }\n  }\n\n  @override\n  void handleResize(Vector2 canvasSize) {\n    for (final child in children) {\n      child.handleResize(canvasSize);\n    }\n  }\n\n  @override\n  void render(Canvas canvas, CameraComponent? camera) {\n    for (final child in children) {\n      child.render(canvas, camera);\n    }\n  }\n\n  @override\n  void update(double dt) {\n    for (final child in children) {\n      child.update(dt);\n    }\n  }\n\n  @override\n  void onOpacityChanged() {\n    for (final child in children) {\n      child.onOpacityChanged();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/renderable_layers/image_layer.dart",
    "content": "import 'package:flame/cache.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame_tiled/src/mutable_rect.dart';\nimport 'package:flame_tiled/src/renderable_layers/group_layer.dart';\nimport 'package:flame_tiled/src/renderable_layers/renderable_layer.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:meta/meta.dart';\nimport 'package:tiled/tiled.dart';\n\n@internal\nclass FlameImageLayer extends RenderableLayer<ImageLayer> {\n  final Image _image;\n  late final ImageRepeat _repeat;\n  final MutableRect _paintArea = MutableRect.fromLTRB(0, 0, 0, 0);\n  final Vector2 _maxTranslation = Vector2.zero();\n  late final Vector2 _mapSize;\n\n  FlameImageLayer({\n    required super.layer,\n    required super.parent,\n    required super.map,\n    required super.destTileSize,\n    required Image image,\n    super.filterQuality,\n  }) : _image = image {\n    _mapSize = Vector2(\n      map.width * destTileSize.x,\n      map.height * destTileSize.y,\n    );\n    _initImageRepeat();\n  }\n\n  @override\n  void handleResize(Vector2 canvasSize) {}\n\n  @override\n  void render(Canvas canvas, CameraComponent? camera) {\n    canvas.save();\n\n    canvas.translate(offsetX, offsetY);\n\n    if (camera != null) {\n      applyParallaxOffset(canvas, camera);\n    }\n\n    _resizePaintArea(camera);\n\n    paintImage(\n      canvas: canvas,\n      rect: _paintArea,\n      image: _image,\n      opacity: opacity,\n      alignment: Alignment.topLeft,\n      repeat: _repeat,\n      filterQuality: filterQuality,\n    );\n\n    canvas.restore();\n  }\n\n  void _resizePaintArea(CameraComponent? camera) {\n    // Track the maximum amount the canvas could have been translated\n    // for this layer so we can calculate how many extra images to draw\n    if (camera != null) {\n      _maxTranslation.x =\n          offsetX.abs() + camera.viewfinder.position.x.abs() * parallaxX;\n      _maxTranslation.y =\n          offsetY.abs() + camera.viewfinder.position.y.abs() * parallaxY;\n    } else {\n      _maxTranslation.x = offsetX.abs();\n      _maxTranslation.y = offsetY.abs();\n    }\n\n    // When the image is being repeated, make sure the _paintArea rect is\n    // big enough that it repeats off the edge of the canvas in both positive\n    // and negative directions on that axis (Tiled repeats forever on an axis).\n    // Also, make sure the rect's left and top are only moved by exactly the\n    // image's length along that axis (width or height) so that with repeats\n    // it still matches up with its initial layer offsets.\n\n    if (_repeat == ImageRepeat.repeatX || _repeat == ImageRepeat.repeat) {\n      // Calculate images needed for max translation and map size\n      final xImages = ((_maxTranslation.x + _mapSize.x) / _image.size.x).ceil();\n      _paintArea.left = -_image.size.x * xImages;\n      _paintArea.right = _image.size.x * xImages;\n    } else {\n      _paintArea.left = 0;\n      _paintArea.right = _mapSize.x;\n    }\n    if (_repeat == ImageRepeat.repeatY || _repeat == ImageRepeat.repeat) {\n      // Calculate images needed for max translation and map size\n      final yImages = ((_maxTranslation.y + _mapSize.y) / _image.size.y).ceil();\n      _paintArea.top = -_image.size.y * yImages;\n      _paintArea.bottom = _image.size.y * yImages;\n    } else {\n      _paintArea.top = 0;\n      _paintArea.bottom = _mapSize.y;\n    }\n  }\n\n  void _initImageRepeat() {\n    if (layer.repeatX && layer.repeatY) {\n      _repeat = ImageRepeat.repeat;\n    } else if (layer.repeatX) {\n      _repeat = ImageRepeat.repeatX;\n    } else if (layer.repeatY) {\n      _repeat = ImageRepeat.repeatY;\n    } else {\n      _repeat = ImageRepeat.noRepeat;\n    }\n  }\n\n  static Future<FlameImageLayer> load({\n    required ImageLayer layer,\n    required GroupLayer? parent,\n    required CameraComponent? camera,\n    required TiledMap map,\n    required Vector2 destTileSize,\n    FilterQuality? filterQuality,\n    Images? images,\n    String? package,\n  }) async {\n    return FlameImageLayer(\n      layer: layer,\n      parent: parent,\n      map: map,\n      destTileSize: destTileSize,\n      filterQuality: filterQuality,\n      image: await (images ?? Flame.images).load(\n        layer.image.source!,\n        package: package,\n      ),\n    );\n  }\n\n  @override\n  void refreshCache() {}\n\n  @override\n  void update(double dt) {}\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/renderable_layers/object_layer.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame_tiled/src/renderable_layers/renderable_layer.dart';\nimport 'package:meta/meta.dart';\nimport 'package:tiled/tiled.dart';\n\n@internal\nclass ObjectLayer extends RenderableLayer<ObjectGroup> {\n  ObjectLayer({\n    required super.layer,\n    required super.parent,\n    required super.map,\n    required super.destTileSize,\n    super.filterQuality,\n  });\n\n  @override\n  void render(Canvas canvas, CameraComponent? camera) {\n    // nothing to do\n  }\n\n  // ignore non-renderable layers when looping over the layers to render\n  @override\n  bool get visible => false;\n\n  static Future<ObjectLayer> load(\n    ObjectGroup layer,\n    TiledMap map,\n    Vector2 destTileSize,\n    FilterQuality? filterQuality,\n  ) async {\n    return ObjectLayer(\n      layer: layer,\n      parent: null,\n      map: map,\n      destTileSize: destTileSize,\n      filterQuality: filterQuality,\n    );\n  }\n\n  @override\n  void handleResize(Vector2 canvasSize) {}\n\n  @override\n  void refreshCache() {}\n\n  @override\n  void update(double dt) {}\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/cache.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame_tiled/src/renderable_layers/group_layer.dart';\nimport 'package:flame_tiled/src/renderable_layers/image_layer.dart';\nimport 'package:flame_tiled/src/renderable_layers/object_layer.dart';\nimport 'package:flame_tiled/src/renderable_layers/tile_layers/tile_layer.dart';\nimport 'package:flame_tiled/src/tile_animation.dart';\nimport 'package:flame_tiled/src/tile_atlas.dart';\nimport 'package:meta/meta.dart';\nimport 'package:tiled/tiled.dart';\n\n@internal\nabstract class RenderableLayer<T extends Layer> {\n  final T layer;\n  final Vector2 destTileSize;\n  final TiledMap map;\n\n  /// The parent [Group] layer (if it exists)\n  final GroupLayer? parent;\n\n  /// The [FilterQuality] that should be used by all the layers.\n  final FilterQuality filterQuality;\n\n  RenderableLayer({\n    required this.layer,\n    required this.parent,\n    required this.map,\n    required this.destTileSize,\n    FilterQuality? filterQuality,\n  }) : filterQuality = filterQuality ?? FilterQuality.none;\n\n  /// [load] is a factory method to create [RenderableLayer] by type of [layer].\n  static Future<RenderableLayer> load({\n    required Layer layer,\n    required GroupLayer? parent,\n    required TiledMap map,\n    required Vector2 destTileSize,\n    required CameraComponent? camera,\n    required Map<Tile, TileFrames> animationFrames,\n    required TiledAtlas atlas,\n    required Paint Function(double opacity) layerPaintFactory,\n    FilterQuality? filterQuality,\n    bool? ignoreFlip,\n    Images? images,\n    String? package,\n  }) async {\n    if (layer is TileLayer) {\n      return FlameTileLayer.load(\n        layer: layer,\n        parent: parent,\n        map: map,\n        destTileSize: destTileSize,\n        animationFrames: animationFrames,\n        atlas: atlas.clone(),\n        ignoreFlip: ignoreFlip,\n        filterQuality: filterQuality,\n        layerPaintFactory: layerPaintFactory,\n      );\n    } else if (layer is ImageLayer) {\n      return FlameImageLayer.load(\n        layer: layer,\n        parent: parent,\n        camera: camera,\n        map: map,\n        destTileSize: destTileSize,\n        filterQuality: filterQuality,\n        images: images,\n        package: package,\n      );\n    } else if (layer is ObjectGroup) {\n      return ObjectLayer.load(\n        layer,\n        map,\n        destTileSize,\n        filterQuality,\n      );\n    } else if (layer is Group) {\n      final groupLayer = layer;\n      return GroupLayer(\n        layer: groupLayer,\n        parent: parent,\n        map: map,\n        destTileSize: destTileSize,\n        filterQuality: filterQuality,\n      );\n    }\n\n    return UnsupportedLayer(\n      layer: layer,\n      parent: parent,\n      map: map,\n      destTileSize: destTileSize,\n    );\n  }\n\n  bool get visible => layer.visible;\n\n  void render(Canvas canvas, CameraComponent? camera);\n\n  void handleResize(Vector2 canvasSize);\n\n  void refreshCache();\n\n  void update(double dt);\n\n  double get scaleX => destTileSize.x / map.tileWidth;\n  double get scaleY => destTileSize.y / map.tileHeight;\n\n  late double offsetX = layer.offsetX * scaleX + (parent?.offsetX ?? 0);\n\n  late double offsetY = layer.offsetY * scaleY + (parent?.offsetY ?? 0);\n\n  double get opacity => layer.opacity * (parent?.opacity ?? 1);\n\n  set opacity(double value) {\n    layer.opacity = value;\n    onOpacityChanged();\n  }\n\n  /// Called after [opacity] is changed.\n  ///\n  /// Override to react to opacity updates (e.g. to rebuild a cached paint).\n  /// When overriding inside a container layer, propagate the call to children\n  /// so that descendant layers with cached paints are also updated.\n  @protected\n  void onOpacityChanged() {}\n\n  late double parallaxX = layer.parallaxX * (parent?.parallaxX ?? 1);\n\n  late double parallaxY = layer.parallaxY * (parent?.parallaxY ?? 1);\n\n  /// Calculates the offset we need to apply to the canvas to compensate for\n  /// parallax positioning and scroll for the layer and the current camera\n  /// position.\n  /// https://doc.mapeditor.org/en/latest/manual/layers/#parallax-scrolling-factor\n  void applyParallaxOffset(Canvas canvas, CameraComponent camera) {\n    final anchor = camera.viewfinder.anchor;\n    final cameraX = camera.viewfinder.position.x;\n    final cameraY = camera.viewfinder.position.y;\n    final viewportCenterX = camera.viewport.size.x * anchor.x;\n    final viewportCenterY = camera.viewport.size.y * anchor.y;\n\n    // Due to how Tiled treats the center of the view as the reference\n    // point for parallax positioning (see Tiled docs), we need to offset the\n    // entire layer\n    var x = (1 - parallaxX) * viewportCenterX;\n    var y = (1 - parallaxY) * viewportCenterY;\n    // Compensate the offset for zoom.\n    x /= camera.viewfinder.zoom;\n    y /= camera.viewfinder.zoom;\n    // Scale to tile space.\n    x /= destTileSize.x;\n    y /= destTileSize.y;\n\n    // Now add the scroll for the current camera position\n    x += cameraX - (cameraX * parallaxX);\n    y += cameraY - (cameraY * parallaxY);\n\n    canvas.translate(x, y);\n  }\n}\n\n@internal\nclass UnsupportedLayer extends RenderableLayer {\n  UnsupportedLayer({\n    required super.layer,\n    required super.parent,\n    required super.map,\n    required super.destTileSize,\n  });\n\n  @override\n  void render(Canvas canvas, CameraComponent? camera) {}\n\n  @override\n  void handleResize(Vector2 canvasSize) {}\n\n  @override\n  void refreshCache() {}\n\n  @override\n  void update(double dt) {}\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/renderable_layers/tile_layers/hexagonal_tile_layer.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/rendering.dart';\nimport 'package:flame_tiled/flame_tiled.dart';\nimport 'package:flame_tiled/src/mutable_rect.dart';\nimport 'package:flame_tiled/src/renderable_layers/tile_layers/tile_layer.dart';\nimport 'package:flame_tiled/src/tile_transform.dart';\nimport 'package:meta/meta.dart';\n\n/// [HexagonalTileLayer] have hexagonal-shaped tiles and also its overall shape\n/// is like a honeycomb.\n@internal\nclass HexagonalTileLayer extends FlameTileLayer {\n  HexagonalTileLayer({\n    required super.layer,\n    required super.parent,\n    required super.map,\n    required super.destTileSize,\n    required super.tiledAtlas,\n    required super.animationFrames,\n    required super.ignoreFlip,\n    required super.layerPaintFactory,\n    super.filterQuality,\n  });\n\n  @override\n  void cacheTiles() {\n    final tileData = layer.tileData!;\n    final halfDestinationTile = destTileSize / 2;\n    final size = destTileSize;\n    final halfMapTile = Vector2(map.tileWidth / 2, map.tileHeight / 2);\n    final batch = tiledAtlas.batch;\n    if (batch == null) {\n      return;\n    }\n\n    var staggerY = 0.0;\n    var staggerX = 0.0;\n    // Hexagonal Pointy Tiles move down by a fractional amount.\n    if (map.staggerAxis == StaggerAxis.y) {\n      staggerY = size.y * 0.75;\n    } else if (map.staggerAxis == StaggerAxis.x) {\n      // Hexagonal Flat Tiles move right by a fractional amount.\n      staggerX = size.x * 0.75;\n    }\n\n    for (var ty = 0; ty < tileData.length; ty++) {\n      final tileRow = tileData[ty];\n\n      // Hexagonal Pointy Tiles shift left and right depending on the row\n      if (map.staggerAxis == StaggerAxis.y) {\n        if ((ty.isOdd && map.staggerIndex == StaggerIndex.odd) ||\n            (ty.isEven && map.staggerIndex == StaggerIndex.even)) {\n          staggerX = halfDestinationTile.x;\n        } else {\n          staggerX = 0.0;\n        }\n      }\n\n      // When staggering in the X axis, we need to hold painting of \"lower\"\n      // tiles (those with staggerY adjustments) otherwise they'll just get\n      // painted over. See the second pass loop after tx.\n      final xSecondPass = <TileTransform>[];\n\n      for (var tx = 0; tx < tileRow.length; tx++) {\n        final tileGid = tileRow[tx];\n        if (tileGid.tile == 0) {\n          continue;\n        }\n\n        final tile = map.tileByGid(tileGid.tile)!;\n        final tileset = map.tilesetByTileGId(tileGid.tile);\n        final img = tile.image ?? tileset.image;\n        if (img == null) {\n          continue;\n        }\n\n        if (!tiledAtlas.contains(img.source)) {\n          return;\n        }\n\n        final spriteOffset = tiledAtlas.offsets[img.source]!;\n        final src = MutableRect.fromRect(\n          tileset\n              .computeDrawRect(tile)\n              .toRect()\n              .translate(spriteOffset.dx, spriteOffset.dy),\n        );\n\n        // Hexagonal Flat tiles shift up and down as we move across the row.\n        if (map.staggerAxis == StaggerAxis.x) {\n          if ((tx.isOdd && map.staggerIndex == StaggerIndex.odd) ||\n              (tx.isEven && map.staggerIndex == StaggerIndex.even)) {\n            staggerY = halfDestinationTile.y;\n          } else {\n            staggerY = 0.0;\n          }\n        }\n\n        final flips = SimpleFlips.fromFlips(tileGid.flips);\n        final scale = size.x / map.tileWidth;\n        final anchorX = src.width - halfMapTile.x;\n        final anchorY = src.height - halfMapTile.y;\n\n        late double offsetX;\n        late double offsetY;\n\n        // halfTile.x: shifts the map half a tile forward rather than\n        //             lining up on at the center.\n        // halfTile.y: shifts the map half a tile down rather than\n        //             lining up on at the center.\n        // StaggerX/Y: Moves the tile forward/down depending on orientation.\n        //  * stagger: Hexagonal tiles move down or right by only a fraction,\n        //             specifically 3/4 the width or height, for packing.\n        if (map.staggerAxis == StaggerAxis.y) {\n          offsetX = tx * size.x + staggerX + halfDestinationTile.x;\n          offsetY = ty * staggerY + halfDestinationTile.y;\n        } else {\n          offsetX = tx * staggerX + halfDestinationTile.x;\n          offsetY = ty * size.y + staggerY + halfDestinationTile.y;\n        }\n\n        offsetX += tileset.tileOffset?.x ?? 0;\n        offsetY += tileset.tileOffset?.y ?? 0;\n\n        final scos = flips.cos * scale;\n        final ssin = flips.sin * scale;\n\n        final transform = transforms[tx][ty] = MutableRSTransform(\n          scos,\n          ssin,\n          offsetX,\n          offsetY,\n          -scos * anchorX + ssin * anchorY,\n          -ssin * anchorX - scos * anchorY,\n        );\n        // A second pass is only needed in the case of staggery.\n        if (map.staggerAxis == StaggerAxis.x && staggerY > 0) {\n          xSecondPass.add(TileTransform(src, transform, flips, batch));\n        } else {\n          batch.addTransform(\n            source: src,\n            transform: transform,\n            flip: shouldFlip(flips),\n          );\n        }\n        if (tile.animation.isNotEmpty) {\n          addAnimation(tile, tileset, src);\n        }\n      }\n\n      for (final tile in xSecondPass) {\n        tile.batch.addTransform(\n          source: tile.source,\n          transform: tile.transform,\n          flip: shouldFlip(tile.flip),\n        );\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/renderable_layers/tile_layers/isometric_tile_layer.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/rendering.dart';\nimport 'package:flame_tiled/flame_tiled.dart';\nimport 'package:flame_tiled/src/mutable_rect.dart';\nimport 'package:flame_tiled/src/renderable_layers/tile_layers/tile_layer.dart';\nimport 'package:meta/meta.dart';\n\n/// [IsometricTileLayer] is the tile layer that has an isometric view.\n/// You can see the details in the [https://en.wikipedia.org/wiki/Isometric_video_game_graphics].\n@internal\nclass IsometricTileLayer extends FlameTileLayer {\n  IsometricTileLayer({\n    required super.layer,\n    required super.parent,\n    required super.map,\n    required super.destTileSize,\n    required super.tiledAtlas,\n    required super.animationFrames,\n    required super.ignoreFlip,\n    required super.layerPaintFactory,\n    super.filterQuality,\n  });\n\n  @override\n  void cacheTiles() {\n    final tileData = layer.tileData!;\n    final halfDestinationTile = destTileSize / 2;\n    final size = destTileSize;\n    final isometricXShift = map.height * halfDestinationTile.x;\n    final isometricYShift = halfDestinationTile.y;\n    final halfMapTile = Vector2(map.tileWidth / 2, map.tileHeight / 2);\n    final batch = tiledAtlas.batch;\n    if (batch == null) {\n      return;\n    }\n\n    for (var ty = 0; ty < tileData.length; ty++) {\n      final tileRow = tileData[ty];\n\n      for (var tx = 0; tx < tileRow.length; tx++) {\n        final tileGid = tileRow[tx];\n        if (tileGid.tile == 0) {\n          continue;\n        }\n\n        final tile = map.tileByGid(tileGid.tile)!;\n        final tileset = map.tilesetByTileGId(tileGid.tile);\n        final img = tile.image ?? tileset.image;\n        if (img == null) {\n          continue;\n        }\n\n        if (!tiledAtlas.contains(img.source)) {\n          return;\n        }\n\n        final spriteOffset = tiledAtlas.offsets[img.source]!;\n        final src = MutableRect.fromRect(\n          tileset\n              .computeDrawRect(tile)\n              .toRect()\n              .translate(spriteOffset.dx, spriteOffset.dy),\n        );\n        final flips = SimpleFlips.fromFlips(tileGid.flips);\n        final scale = size.x / map.tileWidth;\n        final anchorX = src.width - halfMapTile.x;\n        final anchorY = src.height - halfMapTile.y;\n\n        late double offsetX;\n        late double offsetY;\n\n        offsetX = halfDestinationTile.x * (tx - ty) + isometricXShift;\n        offsetY = halfDestinationTile.y * (tx + ty) + isometricYShift;\n\n        offsetX += tileset.tileOffset?.x ?? 0;\n        offsetY += tileset.tileOffset?.y ?? 0;\n\n        final scos = flips.cos * scale;\n        final ssin = flips.sin * scale;\n\n        transforms[tx][ty] = MutableRSTransform(\n          scos,\n          ssin,\n          offsetX,\n          offsetY,\n          -scos * anchorX + ssin * anchorY,\n          -ssin * anchorX - scos * anchorY,\n        );\n\n        batch.addTransform(\n          source: src,\n          transform: transforms[tx][ty],\n          flip: shouldFlip(flips),\n        );\n\n        if (tile.animation.isNotEmpty) {\n          addAnimation(tile, tileset, src);\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/renderable_layers/tile_layers/orthogonal_tile_layer.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/rendering.dart';\nimport 'package:flame_tiled/flame_tiled.dart';\nimport 'package:flame_tiled/src/mutable_rect.dart';\nimport 'package:flame_tiled/src/renderable_layers/tile_layers/tile_layer.dart';\nimport 'package:meta/meta.dart';\n\n/// [OrthogonalTileLayer] is a tile layer that each axis is represented\n/// orthogonally.\n@internal\nclass OrthogonalTileLayer extends FlameTileLayer {\n  OrthogonalTileLayer({\n    required super.layer,\n    required super.parent,\n    required super.map,\n    required super.destTileSize,\n    required super.tiledAtlas,\n    required super.animationFrames,\n    required super.ignoreFlip,\n    required super.layerPaintFactory,\n    super.filterQuality,\n  });\n\n  @override\n  void cacheTiles() {\n    final tileData = layer.tileData!;\n    final size = destTileSize;\n    final halfMapTile = Vector2(map.tileWidth / 2, map.tileHeight / 2);\n    final batch = tiledAtlas.batch;\n    if (batch == null) {\n      return;\n    }\n\n    for (var ty = 0; ty < tileData.length; ty++) {\n      final tileRow = tileData[ty];\n\n      for (var tx = 0; tx < tileRow.length; tx++) {\n        final tileGid = tileRow[tx];\n        if (tileGid.tile == 0) {\n          continue;\n        }\n\n        final tile = map.tileByGid(tileGid.tile)!;\n        final tileset = map.tilesetByTileGId(tileGid.tile);\n        final img = tile.image ?? tileset.image;\n\n        if (img == null) {\n          continue;\n        }\n\n        if (!tiledAtlas.contains(img.source)) {\n          return;\n        }\n\n        final spriteOffset = tiledAtlas.offsets[img.source]!;\n        final src = MutableRect.fromRect(\n          tileset\n              .computeDrawRect(tile)\n              .toRect()\n              .translate(spriteOffset.dx, spriteOffset.dy),\n        );\n\n        final flips = SimpleFlips.fromFlips(tileGid.flips);\n        final scale = size.x / map.tileWidth;\n        final anchorX = src.width - halfMapTile.x;\n        final anchorY = src.height - halfMapTile.y;\n\n        late double offsetX;\n        late double offsetY;\n        offsetX = (tx + 0.5) * size.x;\n        offsetY = (ty + 0.5) * size.y;\n\n        offsetX += tileset.tileOffset?.x ?? 0;\n        offsetY += tileset.tileOffset?.y ?? 0;\n\n        final scos = flips.cos * scale;\n        final ssin = flips.sin * scale;\n\n        transforms[tx][ty] = MutableRSTransform(\n          scos,\n          ssin,\n          offsetX,\n          offsetY,\n          -scos * anchorX + ssin * anchorY,\n          -ssin * anchorX - scos * anchorY,\n        );\n\n        batch.addTransform(\n          source: src,\n          transform: transforms[tx][ty],\n          flip: shouldFlip(flips),\n        );\n\n        if (tile.animation.isNotEmpty) {\n          addAnimation(tile, tileset, src);\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/renderable_layers/tile_layers/staggered_tile_layer.dart",
    "content": "import 'package:flame/extensions.dart';\nimport 'package:flame/rendering.dart';\nimport 'package:flame_tiled/flame_tiled.dart';\nimport 'package:flame_tiled/src/mutable_rect.dart';\nimport 'package:flame_tiled/src/renderable_layers/tile_layers/tile_layer.dart';\nimport 'package:flame_tiled/src/tile_transform.dart';\nimport 'package:meta/meta.dart';\n\n/// [StaggeredTileLayer] is an isometric map using a staggered axis.\n@internal\nclass StaggeredTileLayer extends FlameTileLayer {\n  StaggeredTileLayer({\n    required super.layer,\n    required super.parent,\n    required super.map,\n    required super.destTileSize,\n    required super.tiledAtlas,\n    required super.animationFrames,\n    required super.ignoreFlip,\n    required super.layerPaintFactory,\n    super.filterQuality,\n  });\n\n  @override\n  void cacheTiles() {\n    final tileData = layer.tileData!;\n    final halfDestinationTile = destTileSize / 2;\n    final size = destTileSize;\n    final halfMapTile = Vector2(map.tileWidth / 2, map.tileHeight / 2);\n    final batch = tiledAtlas.batch;\n    if (batch == null) {\n      return;\n    }\n\n    var staggerY = 0.0;\n    var staggerX = 0.0;\n    // Isometric staggered tiles move down by a fractional amount.\n    if (map.staggerAxis == StaggerAxis.y) {\n      staggerY = size.y * 0.5;\n    } else\n    // Isometric staggered tiles move right by a fractional amount.\n    if (map.staggerAxis == StaggerAxis.x) {\n      staggerX = size.x * 0.5;\n    }\n\n    for (var ty = 0; ty < tileData.length; ty++) {\n      final tileRow = tileData[ty];\n\n      // Isometric staggered tiles shift left and right depending on the row\n      if (map.staggerAxis == StaggerAxis.y) {\n        if ((ty.isOdd && map.staggerIndex == StaggerIndex.odd) ||\n            (ty.isEven && map.staggerIndex == StaggerIndex.even)) {\n          staggerX = halfDestinationTile.x;\n        } else {\n          staggerX = 0.0;\n        }\n      }\n\n      // When staggering in the X axis, we need to hold painting of \"lower\"\n      // tiles (those with staggerY adjustments) otherwise they'll just get\n      // painted over. See the second pass loop after tx.\n      final xSecondPass = <TileTransform>[];\n\n      for (var tx = 0; tx < tileRow.length; tx++) {\n        final tileGid = tileRow[tx];\n        if (tileGid.tile == 0) {\n          continue;\n        }\n\n        final tile = map.tileByGid(tileGid.tile)!;\n        final tileset = map.tilesetByTileGId(tileGid.tile);\n        final img = tile.image ?? tileset.image;\n        if (img == null) {\n          continue;\n        }\n\n        if (!tiledAtlas.contains(img.source)) {\n          return;\n        }\n\n        final spriteOffset = tiledAtlas.offsets[img.source]!;\n        final src = MutableRect.fromRect(\n          tileset\n              .computeDrawRect(tile)\n              .toRect()\n              .translate(spriteOffset.dx, spriteOffset.dy),\n        );\n\n        // Tiles shift up and down as we move across the row.\n        if (map.staggerAxis == StaggerAxis.x) {\n          if ((tx.isOdd && map.staggerIndex == StaggerIndex.odd) ||\n              (tx.isEven && map.staggerIndex == StaggerIndex.even)) {\n            staggerY = halfDestinationTile.y;\n          } else {\n            staggerY = 0.0;\n          }\n        }\n\n        final flips = SimpleFlips.fromFlips(tileGid.flips);\n        final scale = size.x / map.tileWidth;\n        final anchorX = src.width - halfMapTile.x;\n        final anchorY = src.height - halfMapTile.y;\n\n        late double offsetX;\n        late double offsetY;\n\n        // halfTile.x: shifts the map half a tile forward rather than\n        //             lining up on at the center.\n        // halfTile.y: shifts the map half a tile down rather than\n        //             lining up on at the center.\n        // StaggerX/Y: Moves the tile forward/down depending on orientation.\n        //  * stagger: Isometric tiles move down or right by only a fraction,\n        //             specifically 1/2 the width or height, for packing.\n        if (map.staggerAxis == StaggerAxis.y) {\n          offsetX = tx * size.x + staggerX + halfDestinationTile.x;\n          offsetY = ty * staggerY + halfDestinationTile.y;\n        } else {\n          offsetX = tx * staggerX + halfDestinationTile.x;\n          offsetY = ty * size.y + staggerY + halfDestinationTile.y;\n        }\n\n        offsetX += tileset.tileOffset?.x ?? 0;\n        offsetY += tileset.tileOffset?.y ?? 0;\n\n        final scos = flips.cos * scale;\n        final ssin = flips.sin * scale;\n\n        final transform = transforms[tx][ty] = MutableRSTransform(\n          scos,\n          ssin,\n          offsetX,\n          offsetY,\n          -scos * anchorX + ssin * anchorY,\n          -ssin * anchorX - scos * anchorY,\n        );\n\n        // A second pass is only needed in the case of staggery.\n        if (map.staggerAxis == StaggerAxis.x && staggerY > 0) {\n          xSecondPass.add(TileTransform(src, transform, flips, batch));\n        } else {\n          batch.addTransform(\n            source: src,\n            transform: transform,\n            flip: shouldFlip(flips),\n          );\n        }\n        if (tile.animation.isNotEmpty) {\n          addAnimation(tile, tileset, src);\n        }\n      }\n\n      for (final tile in xSecondPass) {\n        tile.batch.addTransform(\n          source: tile.source,\n          transform: tile.transform,\n          flip: shouldFlip(tile.flip),\n        );\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/renderable_layers/tile_layers/tile_layer.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/rendering.dart';\nimport 'package:flame_tiled/flame_tiled.dart';\nimport 'package:flame_tiled/src/mutable_rect.dart';\nimport 'package:flame_tiled/src/renderable_layers/group_layer.dart';\nimport 'package:flame_tiled/src/renderable_layers/renderable_layer.dart';\nimport 'package:flame_tiled/src/renderable_layers/tile_layers/hexagonal_tile_layer.dart';\nimport 'package:flame_tiled/src/renderable_layers/tile_layers/isometric_tile_layer.dart';\nimport 'package:flame_tiled/src/renderable_layers/tile_layers/orthogonal_tile_layer.dart';\nimport 'package:flame_tiled/src/renderable_layers/tile_layers/staggered_tile_layer.dart';\nimport 'package:flame_tiled/src/tile_animation.dart';\nimport 'package:flutter/painting.dart';\nimport 'package:meta/meta.dart';\n\n/// {@template flame_tile_layer}\n/// [FlameTileLayer] is the base class of the following classes:\n///\n/// - [OrthogonalTileLayer]\n/// - [StaggeredTileLayer]\n/// - [HexagonalTileLayer]\n/// - [IsometricTileLayer]\n///\n/// [FlameTileLayer] decides its concrete type by [MapOrientation]. So any\n/// subclass of this should implement [cacheTiles] to reflect their map\n/// orientation format.\n///\n/// [FlameTileLayer] stores its source image to [tiledAtlas]\n/// and transform it by [transforms].\n///\n/// The flip is ignored if the [ignoreFlip] is set to true.\n///\n/// {@endtemplate}\n@internal\nabstract class FlameTileLayer extends RenderableLayer<TileLayer> {\n  late Paint _layerPaint;\n  final TiledAtlas tiledAtlas;\n  late List<List<MutableRSTransform?>> transforms;\n  final animations = <TileAnimation>[];\n  final Map<Tile, TileFrames> animationFrames;\n  final bool ignoreFlip;\n  Paint Function(double opacity) layerPaintFactory;\n\n  FlameTileLayer({\n    required super.layer,\n    required super.parent,\n    required super.map,\n    required super.destTileSize,\n    required this.tiledAtlas,\n    required this.animationFrames,\n    required this.ignoreFlip,\n    required this.layerPaintFactory,\n    super.filterQuality,\n  }) {\n    _layerPaint = layerPaintFactory(opacity);\n  }\n\n  @override\n  void onOpacityChanged() {\n    _layerPaint = layerPaintFactory(opacity);\n  }\n\n  /// {@macro flame_tile_layer}\n  static FlameTileLayer load({\n    required TileLayer layer,\n    required GroupLayer? parent,\n    required TiledMap map,\n    required Vector2 destTileSize,\n    required Map<Tile, TileFrames> animationFrames,\n    required TiledAtlas atlas,\n    required Paint Function(double opacity) layerPaintFactory,\n    FilterQuality? filterQuality,\n    bool? ignoreFlip,\n  }) {\n    ignoreFlip ??= false;\n    final mapOrientation = map.orientation;\n    if (mapOrientation == null) {\n      throw StateError('Map orientation should be present');\n    }\n\n    return switch (mapOrientation) {\n      MapOrientation.isometric => IsometricTileLayer(\n        layer: layer,\n        parent: parent,\n        map: map,\n        destTileSize: destTileSize,\n        tiledAtlas: atlas,\n        animationFrames: animationFrames,\n        ignoreFlip: ignoreFlip,\n        filterQuality: filterQuality,\n        layerPaintFactory: layerPaintFactory,\n      ),\n      MapOrientation.staggered => StaggeredTileLayer(\n        layer: layer,\n        parent: parent,\n        map: map,\n        destTileSize: destTileSize,\n        tiledAtlas: atlas,\n        animationFrames: animationFrames,\n        ignoreFlip: ignoreFlip,\n        filterQuality: filterQuality,\n        layerPaintFactory: layerPaintFactory,\n      ),\n      MapOrientation.hexagonal => HexagonalTileLayer(\n        layer: layer,\n        parent: parent,\n        map: map,\n        destTileSize: destTileSize,\n        tiledAtlas: atlas,\n        animationFrames: animationFrames,\n        ignoreFlip: ignoreFlip,\n        filterQuality: filterQuality,\n        layerPaintFactory: layerPaintFactory,\n      ),\n      MapOrientation.orthogonal => OrthogonalTileLayer(\n        layer: layer,\n        parent: parent,\n        map: map,\n        destTileSize: destTileSize,\n        tiledAtlas: atlas,\n        animationFrames: animationFrames,\n        ignoreFlip: ignoreFlip,\n        filterQuality: filterQuality,\n        layerPaintFactory: layerPaintFactory,\n      ),\n    };\n  }\n\n  @override\n  void update(double dt) {\n    for (final animation in animations) {\n      animation.update(dt);\n    }\n  }\n\n  @override\n  void render(Canvas canvas, CameraComponent? camera) {\n    if (tiledAtlas.batch == null) {\n      return;\n    }\n\n    canvas.save();\n    canvas.translate(offsetX, offsetY);\n    if (camera != null) {\n      applyParallaxOffset(canvas, camera);\n    }\n    tiledAtlas.batch!.render(canvas, paint: _layerPaint);\n    canvas.restore();\n  }\n\n  @override\n  void handleResize(Vector2 canvasSize) {}\n\n  @protected\n  void addAnimation(Tile tile, Tileset tileset, MutableRect source) {\n    final frames = animationFrames[tile] ??= () {\n      final frameRectangles = <Rect>[];\n      final durations = <double>[];\n      for (final frame in tile.animation) {\n        final newTile = tileset.tiles[frame.tileId];\n        final image = newTile.image ?? tileset.image;\n        if (image?.source == null || !tiledAtlas.contains(image!.source)) {\n          continue;\n        }\n\n        final spriteOffset = tiledAtlas.offsets[image.source]!;\n        final rect = tileset\n            .computeDrawRect(newTile)\n            .toRect()\n            .translate(spriteOffset.dx, spriteOffset.dy);\n        frameRectangles.add(rect);\n        durations.add(frame.duration / 1000);\n      }\n      return TileFrames(frameRectangles, durations);\n    }();\n    animations.add(TileAnimation(source, frames));\n  }\n\n  @override\n  void refreshCache() {\n    animations.clear();\n    transforms = List.generate(\n      layer.width,\n      (index) => List.filled(layer.height, null),\n    );\n\n    tiledAtlas.batch?.clear();\n\n    cacheTiles();\n  }\n\n  /// We need to know the following information for each tile to render a layer\n  /// correctly.\n  ///\n  /// - source offset\n  /// - rotation\n  /// - translation\n  /// - flip\n  ///\n  /// But gathering these in every tick is a too heavy task for the engine.\n  /// So, [FlameTileLayer] caches these by [cacheTiles] and tiles quickly in\n  /// every frame.\n  @protected\n  void cacheTiles();\n\n  @protected\n  bool shouldFlip(SimpleFlips flips) => !ignoreFlip && flips.flip;\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/renderable_tile_map.dart",
    "content": "import 'dart:async';\n\nimport 'package:collection/collection.dart';\nimport 'package:flame/cache.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/rendering.dart';\nimport 'package:flame_tiled/src/extensions.dart';\nimport 'package:flame_tiled/src/flame_tsx_provider.dart';\nimport 'package:flame_tiled/src/renderable_layers/group_layer.dart';\nimport 'package:flame_tiled/src/renderable_layers/renderable_layer.dart';\nimport 'package:flame_tiled/src/renderable_layers/tile_layers/tile_layer.dart';\nimport 'package:flame_tiled/src/tile_animation.dart';\nimport 'package:flame_tiled/src/tile_atlas.dart';\nimport 'package:flame_tiled/src/tile_stack.dart';\nimport 'package:flutter/painting.dart';\nimport 'package:flutter/services.dart';\nimport 'package:tiled/tiled.dart';\n\nPaint _defaultLayerPaintFactory(double opacity) =>\n    Paint()..color = Color.fromRGBO(255, 255, 255, opacity);\n\n/// {@template _renderable_tiled_map}\n/// This is a wrapper over Tiled's [TiledMap] which can be rendered to a\n/// canvas.\n///\n/// Internally each layer is wrapped with a [RenderableLayer] which handles\n/// rendering and caching for supported layer types:\n///  - [TileLayer] is supported with pre-computed SpriteBatches\n///  - [ImageLayer] is supported with [paintImage]\n///\n/// This also supports the following properties:\n///  - [TiledMap.backgroundColor]\n///  - [Layer.opacity]\n///  - [Layer.offsetX]\n///  - [Layer.offsetY]\n///  - [Layer.parallaxX] (only supported if a [CameraComponent] is supplied)\n///  - [Layer.parallaxY] (only supported if a [CameraComponent] is supplied)\n///\n/// {@endtemplate}\nclass RenderableTiledMap {\n  /// [TiledMap] instance for this map.\n  final TiledMap map;\n\n  /// Layers to be rendered, in the same order as [TiledMap.layers]\n  final List<RenderableLayer> renderableLayers;\n\n  /// The target size for each tile in the tiled map.\n  final Vector2 destTileSize;\n\n  /// Camera used for determining the current viewport for layer rendering.\n  /// Optional, but required for parallax support\n  CameraComponent? camera;\n\n  /// Paint for the map's background color, if there is one\n  late final Paint? _backgroundPaint;\n\n  final Map<Tile, TileFrames> animationFrames;\n\n  /// {@macro _renderable_tiled_map}\n  RenderableTiledMap(\n    this.map,\n    this.renderableLayers,\n    this.destTileSize, {\n    this.camera,\n    this.animationFrames = const {},\n  }) {\n    _refreshCache();\n\n    final backgroundColor = map.backgroundColor?.toColor();\n    if (backgroundColor != null) {\n      _backgroundPaint = Paint();\n      _backgroundPaint!.color = backgroundColor;\n    } else {\n      _backgroundPaint = null;\n    }\n  }\n\n  /// Changes the visibility of the corresponding layer, if different\n  void setLayerVisibility(int layerId, {required bool visible}) {\n    if (map.layers[layerId].visible != visible) {\n      map.layers[layerId].visible = visible;\n      _refreshCache();\n    }\n  }\n\n  /// Gets the visibility of the corresponding layer\n  bool getLayerVisibility(int layerId) {\n    return map.layers[layerId].visible;\n  }\n\n  /// Changes the opacity of the layer at [layerIndex] to [opacity].\n  ///\n  /// [opacity] must be between 0.0 (fully transparent) and 1.0 (fully opaque).\n  void setLayerOpacity(int layerIndex, {required double opacity}) {\n    assert(\n      opacity >= 0.0 && opacity <= 1.0,\n      'opacity must be between 0.0 and 1.0',\n    );\n    final renderableLayer = renderableLayers[layerIndex];\n    renderableLayer.opacity = opacity;\n  }\n\n  /// Gets the opacity of the layer at [layerIndex].\n  double getLayerOpacity(int layerIndex) {\n    return renderableLayers[layerIndex].opacity;\n  }\n\n  /// Changes the Gid of the corresponding layer at the given layerId,\n  /// if different\n  void setTileData({\n    required int layerId,\n    required int x,\n    required int y,\n    required Gid gid,\n  }) {\n    final layer = map.layers.firstWhereOrNull((layer) => layer.id == layerId);\n    if (layer is TileLayer) {\n      final td = layer.tileData;\n      if (td != null) {\n        if (td[y][x].tile != gid.tile ||\n            td[y][x].flips.horizontally != gid.flips.horizontally ||\n            td[y][x].flips.vertically != gid.flips.vertically ||\n            td[y][x].flips.diagonally != gid.flips.diagonally) {\n          td[y][x] = gid;\n          _refreshCache();\n        }\n      }\n    }\n  }\n\n  /// Changes the Gid of the corresponding layer at the given position,\n  /// if different\n  void setTileDataByLayerIndex({\n    required int layerIndex,\n    required int x,\n    required int y,\n    required Gid gid,\n  }) {\n    final layer = map.layers[layerIndex];\n    if (layer is TileLayer) {\n      final td = layer.tileData;\n      if (td != null) {\n        if (td[y][x].tile != gid.tile ||\n            td[y][x].flips.horizontally != gid.flips.horizontally ||\n            td[y][x].flips.vertically != gid.flips.vertically ||\n            td[y][x].flips.diagonally != gid.flips.diagonally) {\n          td[y][x] = gid;\n          _refreshCache();\n        }\n      }\n    }\n  }\n\n  /// Gets the Gid  of the corresponding layer at the given layerId\n  Gid? getTileData({\n    required int layerId,\n    required int x,\n    required int y,\n  }) {\n    final layer = map.layers.firstWhereOrNull((layer) => layer.id == layerId);\n    if (layer is TileLayer) {\n      return layer.tileData?[y][x];\n    }\n    return null;\n  }\n\n  /// Gets the Gid  of the corresponding layer at the given position\n  Gid? getTileDataByLayerIndex({\n    required int layerIndex,\n    required int x,\n    required int y,\n  }) {\n    final layer = map.layers[layerIndex];\n    if (layer is TileLayer) {\n      return layer.tileData?[y][x];\n    }\n    return null;\n  }\n\n  /// Select a group of tiles from the coordinates [x] and [y].\n  ///\n  /// If [all] is set to true, every renderable tile from the map is collected.\n  ///\n  /// If the [named] or [ids] sets are not empty, any layer with matching\n  /// name or id will have their renderable tiles collected. If the matching\n  /// layer is a group layer, all layers in the group will have their tiles\n  /// collected.\n  TileStack tileStack(\n    int x,\n    int y, {\n    Set<String> named = const <String>{},\n    Set<int> ids = const <int>{},\n    bool all = false,\n  }) {\n    return TileStack(\n      _tileStack(\n        renderableLayers,\n        x,\n        y,\n        named: named,\n        ids: ids,\n        all: all,\n      ),\n    );\n  }\n\n  /// Recursive support for [tileStack]\n  List<MutableRSTransform> _tileStack(\n    List<RenderableLayer> layers,\n    int x,\n    int y, {\n    Set<String> named = const <String>{},\n    Set<int> ids = const <int>{},\n    bool all = false,\n  }) {\n    final tiles = <MutableRSTransform>[];\n    for (final layer in layers) {\n      if (layer is GroupLayer) {\n        // if the group matches named or ids; grab every child.\n        // else descend and ask for named children.\n        tiles.addAll(\n          _tileStack(\n            layer.children,\n            x,\n            y,\n            named: named,\n            ids: ids,\n            all:\n                all ||\n                named.contains(layer.layer.name) ||\n                ids.contains(layer.layer.id),\n          ),\n        );\n      } else if (layer is FlameTileLayer) {\n        if (!(all ||\n            named.contains(layer.layer.name) ||\n            ids.contains(layer.layer.id))) {\n          continue;\n        }\n\n        if (layer.transforms[x][y] != null) {\n          tiles.add(layer.transforms[x][y]!);\n        }\n      }\n    }\n    return tiles;\n  }\n\n  /// Parses a file returning a [RenderableTiledMap].\n  ///\n  /// {@template renderable_tile_prefix_path}\n  /// This method looks for files under the path \"assets/tiles/\" by default.\n  /// This can be changed by providing a different path to [prefix].\n  /// {@endtemplate}\n  ///\n  /// {@template renderable_tile_map_factory}\n  /// By default, [FlameTileLayer] renders flipped tiles if they exist.\n  /// You can disable this by setting [ignoreFlip] to `true`.\n  /// {@endtemplate}\n  static Future<RenderableTiledMap> fromFile(\n    String fileName,\n    Vector2 destTileSize, {\n    double? atlasMaxX,\n    double? atlasMaxY,\n    String prefix = 'assets/tiles/',\n    CameraComponent? camera,\n    bool? ignoreFlip,\n    Images? images,\n    AssetBundle? bundle,\n    bool Function(Tileset)? tsxPackingFilter,\n    bool useAtlas = true,\n    Paint Function(double opacity)? layerPaintFactory,\n    double atlasPackingSpacingX = 0,\n    double atlasPackingSpacingY = 0,\n    String? package,\n  }) async {\n    assert(\n      !fileName.contains(RegExp(r'[/\\\\]')),\n      'fileName should not contain path separators, use prefix to specify a '\n      'path.',\n    );\n    final fullPrefix = package == null ? prefix : 'packages/$package/$prefix';\n    final contents = await (bundle ?? Flame.bundle).loadString(\n      '$fullPrefix$fileName',\n    );\n    return fromString(\n      contents,\n      destTileSize,\n      atlasMaxX: atlasMaxX,\n      atlasMaxY: atlasMaxY,\n      prefix: fullPrefix,\n      camera: camera,\n      ignoreFlip: ignoreFlip,\n      images: images,\n      bundle: bundle,\n      tsxPackingFilter: tsxPackingFilter,\n      useAtlas: useAtlas,\n      layerPaintFactory: layerPaintFactory ?? _defaultLayerPaintFactory,\n      atlasPackingSpacingX: atlasPackingSpacingX,\n      atlasPackingSpacingY: atlasPackingSpacingY,\n      package: package,\n    );\n  }\n\n  /// Parses a string returning a [RenderableTiledMap].\n  ///\n  /// {@macro renderable_tile_prefix_path}\n  ///\n  /// {@macro renderable_tile_map_factory}\n  static Future<RenderableTiledMap> fromString(\n    String contents,\n    Vector2 destTileSize, {\n    double? atlasMaxX,\n    double? atlasMaxY,\n    String prefix = 'assets/tiles/',\n    CameraComponent? camera,\n    bool? ignoreFlip,\n    Images? images,\n    AssetBundle? bundle,\n    bool Function(Tileset)? tsxPackingFilter,\n    bool useAtlas = true,\n    Paint Function(double opacity)? layerPaintFactory,\n    double atlasPackingSpacingX = 0,\n    double atlasPackingSpacingY = 0,\n    String? package,\n  }) async {\n    final map = await TiledMap.fromString(\n      contents,\n      (key) => FlameTsxProvider.parse(key, bundle, prefix),\n    );\n    return fromTiledMap(\n      map,\n      destTileSize,\n      atlasMaxX: atlasMaxX,\n      atlasMaxY: atlasMaxY,\n      camera: camera,\n      ignoreFlip: ignoreFlip,\n      images: images,\n      bundle: bundle,\n      tsxPackingFilter: tsxPackingFilter,\n      useAtlas: useAtlas,\n      layerPaintFactory: layerPaintFactory ?? _defaultLayerPaintFactory,\n      atlasPackingSpacingX: atlasPackingSpacingX,\n      atlasPackingSpacingY: atlasPackingSpacingY,\n      package: package,\n    );\n  }\n\n  /// Parses a [TiledMap] returning a [RenderableTiledMap].\n  ///\n  /// {@macro renderable_tile_map_factory}\n  static Future<RenderableTiledMap> fromTiledMap(\n    TiledMap map,\n    Vector2 destTileSize, {\n    double? atlasMaxX,\n    double? atlasMaxY,\n    CameraComponent? camera,\n    bool? ignoreFlip,\n    Images? images,\n    AssetBundle? bundle,\n    bool Function(Tileset)? tsxPackingFilter,\n    bool useAtlas = true,\n    Paint Function(double opacity)? layerPaintFactory,\n    double atlasPackingSpacingX = 0,\n    double atlasPackingSpacingY = 0,\n    String? package,\n  }) async {\n    // We're not going to load animation frames that are never referenced; but\n    // we do supply the common cache for all layers in this map, and maintain\n    // the update cycle for these in one place.\n    final animationFrames = <Tile, TileFrames>{};\n\n    // While this _should_ not be needed - it is possible have tilesets out of\n    // order and Tiled won't complain, but we'll fail.\n    map.tilesets.sort((l, r) => (l.firstGid ?? 0) - (r.firstGid ?? 0));\n\n    final renderableLayers = await _renderableLayers(\n      map.layers,\n      null,\n      map,\n      destTileSize,\n      camera,\n      animationFrames,\n      atlas: await TiledAtlas.fromTiledMap(\n        map,\n        maxX: atlasMaxX,\n        maxY: atlasMaxY,\n        images: images,\n        tsxPackingFilter: tsxPackingFilter,\n        useAtlas: useAtlas,\n        spacingX: atlasPackingSpacingX,\n        spacingY: atlasPackingSpacingY,\n        package: package,\n      ),\n      ignoreFlip: ignoreFlip,\n      images: images,\n      layerPaintFactory: layerPaintFactory ?? _defaultLayerPaintFactory,\n      package: package,\n    );\n\n    return RenderableTiledMap(\n      map,\n      renderableLayers,\n      destTileSize,\n      camera: camera,\n      animationFrames: animationFrames,\n    );\n  }\n\n  static Future<List<RenderableLayer<Layer>>> _renderableLayers(\n    List<Layer> layers,\n    GroupLayer? parent,\n    TiledMap map,\n    Vector2 destTileSize,\n    CameraComponent? camera,\n    Map<Tile, TileFrames> animationFrames, {\n    required TiledAtlas atlas,\n    required Paint Function(double opacity) layerPaintFactory,\n    bool? ignoreFlip,\n    Images? images,\n    String? package,\n  }) {\n    final visibleLayers = layers.where((layer) => layer.visible);\n\n    final layerLoaders = visibleLayers.map((layer) async {\n      final renderableLayer = await RenderableLayer.load(\n        layer: layer,\n        parent: parent,\n        map: map,\n        destTileSize: destTileSize,\n        camera: camera,\n        animationFrames: animationFrames,\n        atlas: atlas,\n        ignoreFlip: ignoreFlip,\n        images: images,\n        layerPaintFactory: layerPaintFactory,\n        package: package,\n      );\n\n      if (layer is Group && renderableLayer is GroupLayer) {\n        renderableLayer.children = await _renderableLayers(\n          layer.layers,\n          renderableLayer,\n          map,\n          destTileSize,\n          camera,\n          animationFrames,\n          atlas: atlas,\n          ignoreFlip: ignoreFlip,\n          images: images,\n          layerPaintFactory: layerPaintFactory,\n          package: package,\n        );\n      }\n\n      return renderableLayer;\n    }).toList();\n\n    return Future.wait(layerLoaders);\n  }\n\n  /// Handle game resize and propagate it to renderable layers\n  void handleResize(Vector2 canvasSize) {\n    for (final layer in renderableLayers) {\n      layer.handleResize(canvasSize);\n    }\n  }\n\n  /// Rebuilds the cache for rendering\n  void _refreshCache() {\n    for (final layer in renderableLayers) {\n      layer.refreshCache();\n    }\n  }\n\n  /// Renders each renderable layer in the same order specified by the Tiled map\n  void render(Canvas c) {\n    if (_backgroundPaint != null) {\n      c.drawPaint(_backgroundPaint);\n    }\n\n    // Paint each layer in reverse order, because the last layers should be\n    // rendered beneath the first layers\n    for (final layer in renderableLayers.where((l) => l.visible)) {\n      layer.render(c, camera);\n    }\n  }\n\n  /// Returns a layer of type [T] with given [name] from all the layers\n  /// of this map. If no such layer is found, null is returned.\n  T? getLayer<T extends Layer>(String name) {\n    try {\n      // layerByName will searches recursively starting with tiled.dart v0.8.5\n      return map.layerByName(name) as T;\n    } on ArgumentError {\n      return null;\n    }\n  }\n\n  void update(double dt) {\n    // First, update animation frames.\n    for (final frame in animationFrames.values) {\n      frame.update(dt);\n    }\n\n    // Then every layer.\n    for (final layer in renderableLayers) {\n      layer.update(dt);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/simple_flips.dart",
    "content": "import 'package:tiled/tiled.dart';\n\n/// {@template _simple_flips}\n/// Tiled represents all flips and rotation using three possible flips:\n/// horizontal, vertical and diagonal.\n/// This class converts that representation to a simpler one, that uses one\n/// angle (with pi/2 steps) and one flip (horizontal). All vertical flips are\n/// represented as horizontal flips + 180º.\n/// Further reference:\n/// https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tile-flipping.\n///\n/// `cos` and `sin` are the cosine and sine of the rotation respectively, and\n/// and are provided for simple calculation with RSTransform.\n/// Further reference:\n/// https://api.flutter.dev/flutter/dart-ui/RSTransform/RSTransform.html\n/// {@endtemplate}\nclass SimpleFlips {\n  /// The angle (in steps of pi/2 rads), clockwise, around the center of the tile.\n  final int angle;\n\n  /// The cosine of the rotation.\n  final int cos;\n\n  /// The sine of the rotation.\n  final int sin;\n\n  /// Whether to flip (across a central vertical axis).\n  final bool flip;\n\n  /// {@macro _simple_flips}\n  SimpleFlips(this.angle, this.cos, this.sin, {required this.flip});\n\n  /// This is the conversion from the truth table that I drew.\n  factory SimpleFlips.fromFlips(Flips flips) {\n    final int angle;\n    final int cos;\n    final int sin;\n    final bool flip;\n\n    if (!flips.diagonally && !flips.vertically && !flips.horizontally) {\n      angle = 0;\n      cos = 1;\n      sin = 0;\n      flip = false;\n    } else if (!flips.diagonally && !flips.vertically && flips.horizontally) {\n      angle = 0;\n      cos = 1;\n      sin = 0;\n      flip = true;\n    } else if (flips.diagonally && !flips.vertically && flips.horizontally) {\n      angle = 1;\n      cos = 0;\n      sin = 1;\n      flip = false;\n    } else if (flips.diagonally && flips.vertically && flips.horizontally) {\n      angle = 1;\n      cos = 0;\n      sin = 1;\n      flip = true;\n    } else if (!flips.diagonally && flips.vertically && flips.horizontally) {\n      angle = 2;\n      cos = -1;\n      sin = 0;\n      flip = false;\n    } else if (!flips.diagonally && flips.vertically && !flips.horizontally) {\n      angle = 2;\n      cos = -1;\n      sin = 0;\n      flip = true;\n    } else if (flips.diagonally && flips.vertically && !flips.horizontally) {\n      angle = 3;\n      cos = 0;\n      sin = -1;\n      flip = false;\n    } else if (flips.diagonally && !flips.vertically && !flips.horizontally) {\n      angle = 3;\n      cos = 0;\n      sin = -1;\n      flip = true;\n    } else {\n      // this should be exhaustive\n      throw 'Invalid combination of booleans: $flips';\n    }\n\n    return SimpleFlips(angle, cos, sin, flip: flip);\n  }\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/tile_animation.dart",
    "content": "import 'dart:ui' show Rect;\n\nimport 'package:flame_tiled/src/mutable_rect.dart';\n\n/// Records a single animation for tile on a layer.\n///\n/// This works because SpriteBatch holds a list of [Rect]. Those rectangles\n/// are usually immutable, but flame_tile uses a mutable rectangle to update\n/// the offsets in the image atlas.\nclass TileAnimation {\n  /// Frames of the animation loop.\n  final TileFrames frames;\n\n  /// Rectangle that gets updated for each new frame in the animation.\n  final MutableRect batchedSource;\n\n  /// Current frame counter.\n  int frame = 0;\n\n  TileAnimation(\n    this.batchedSource,\n    this.frames,\n  );\n\n  void update(double dt) {\n    if (frame != frames.frame) {\n      frame = frames.frame;\n      batchedSource.copy(frames.sources[frame]);\n    }\n  }\n}\n\n/// Records the list of frames for a tile so that it can be reused.\nclass TileFrames {\n  /// Rectangles for each frame in the animation.\n  final List<Rect> sources;\n\n  /// Duration, in seconds, for each frame in the animation.\n  final List<double> durations;\n\n  /// Current frame lifetime.\n  double frameTime = 0.0;\n\n  /// Current frame counter for all frames sharing this animation.\n  int frame = 0;\n\n  TileFrames(this.sources, this.durations);\n\n  void update(double dt) {\n    frameTime += dt;\n\n    // Track really long jank by skipping ahead.\n    while (durations[frame] <= frameTime) {\n      final currentFrameTime = durations[frame];\n      frame = (frame + 1) % durations.length;\n      // We still have time to add to this, even if we're late.\n      frameTime = frameTime - currentFrameTime;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/tile_atlas.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/cache.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/image_composition.dart';\nimport 'package:flame/sprite.dart';\nimport 'package:flame_tiled/src/rectangle_bin_packer.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:tiled/tiled.dart';\n\nbool _defaultTilesetPackingFilter(Tileset _) => true;\n\n/// One image atlas for all Tiled image sets in a map.\n///\n/// Please note that [TiledAtlas] should not be reused without [clone] as it may\n/// have a different [batch] instance.\nclass TiledAtlas {\n  /// Single atlas for all renders.\n  // Retain this as SpriteBatch can dispose of the original image for flips.\n  final Image? atlas;\n\n  /// Map of all source images to their new offset.\n  final Map<String, Offset> offsets;\n\n  /// The single batch operation for this atlas.\n  final SpriteBatch? batch;\n\n  /// Image key for this atlas.\n  final String key;\n\n  /// If SpriteBatch should use atlas or not.\n  final bool useAtlas;\n\n  /// Track one atlas for all images in the Tiled map.\n  ///\n  /// See [fromTiledMap] for asynchronous loading.\n  TiledAtlas._({\n    required this.atlas,\n    required this.offsets,\n    required this.key,\n    this.useAtlas = true,\n  }) : batch = atlas == null\n           ? null\n           : SpriteBatch(\n               atlas,\n               imageKey: key,\n               useAtlas: useAtlas,\n             );\n\n  /// Returns whether or not this atlas contains [source].\n  bool contains(String? source) => offsets.containsKey(source);\n\n  /// Create a new atlas from this object with the intent of getting a new\n  /// [SpriteBatch].\n  TiledAtlas clone() => TiledAtlas._(\n    atlas: atlas?.clone(),\n    offsets: offsets,\n    key: key,\n    useAtlas: useAtlas,\n  );\n\n  /// Maps of tilesets compiled to [TiledAtlas].\n  ///\n  /// This is recommended to be cleared on test setup. Otherwise it\n  /// could lead to unexpected behavior.\n  @visibleForTesting\n  static final atlasMap = <String, TiledAtlas>{};\n\n  @visibleForTesting\n  static String atlasKey(Iterable<TiledImage> images) {\n    if (images.length == 1) {\n      return images.first.source!;\n    }\n\n    final files = ([...images.map((e) => e.source)]..sort()).join(',');\n    return 'atlas{$files}';\n  }\n\n  /// Collect images that we'll use in tiles - exclude image layers.\n  static Set<(String?, TiledImage)> _onlyTileImages(\n    TiledMap map,\n    bool Function(Tileset) filter,\n  ) {\n    final imageSet = <(String?, TiledImage)>{};\n    for (var i = 0; i < map.tilesets.length; ++i) {\n      // Skip tilesets that don't match the filter.\n      if (!filter(map.tilesets[i])) {\n        continue;\n      }\n      final tileset = map.tilesets[i];\n      final image = tileset.image;\n      if (image?.source != null) {\n        imageSet.add((tileset.source, image!));\n      }\n      for (var j = 0; j < map.tilesets[i].tiles.length; ++j) {\n        final tileset = map.tilesets[i];\n        final image = tileset.tiles[j].image;\n        if (image?.source != null) {\n          imageSet.add((tileset.source, image!));\n        }\n      }\n    }\n    return imageSet;\n  }\n\n  /// Loads all the tileset images for the [map] into one [TiledAtlas].\n  static Future<TiledAtlas> fromTiledMap(\n    TiledMap map, {\n    double? maxX,\n    double? maxY,\n    Images? images,\n    bool Function(Tileset)? tsxPackingFilter,\n    bool useAtlas = true,\n    double spacingX = 0,\n    double spacingY = 0,\n    String? package,\n  }) async {\n    final tilesetImageList = _onlyTileImages(\n      map,\n      tsxPackingFilter ?? _defaultTilesetPackingFilter,\n    ).toList();\n\n    final mappedImageList = tilesetImageList.map((entry) {\n      final tiledImage = entry.$2;\n      var tileImageSource = tiledImage.source!;\n      final tilesetSource = entry.$1;\n\n      if (tilesetSource == null) {\n        return (tileImageSource, tiledImage);\n      }\n\n      final tilesetParts = tilesetSource.split('/');\n      final imageParts = tileImageSource.split('/');\n\n      if (tilesetParts.length != imageParts.length) {\n        tileImageSource = [\n          ...tilesetParts.sublist(0, tilesetParts.length - 1),\n          ...imageParts,\n        ].join('/');\n      }\n\n      return (tileImageSource, tiledImage);\n    });\n\n    if (mappedImageList.isEmpty) {\n      // so this map has no tiles... Ok.\n      return TiledAtlas._(\n        atlas: null,\n        offsets: {},\n        key: 'atlas{empty}',\n        useAtlas: useAtlas,\n      );\n    }\n\n    final imagesInstance = images ?? Flame.images;\n\n    final imageList = mappedImageList.map((e) => e.$2).toList();\n\n    final key = atlasKey(imageList);\n    if (atlasMap.containsKey(key)) {\n      return atlasMap[key]!.clone();\n    }\n\n    if (imageList.length == 1) {\n      final mappedEntry = mappedImageList.first;\n      final resolvedImageSource = mappedEntry.$1;\n      final tiledImage = mappedEntry.$2;\n      // The map contains one image, so its either an atlas already, or a\n      // really boring map.\n      final image = (await imagesInstance.load(\n        resolvedImageSource,\n        package: package,\n      )).clone();\n\n      // There could be a special case that a concurrent call to this method\n      // passes the check `if (atlasMap.containsKey(key))` due to the async call\n      // inside this block. So, instance should always be recreated within this\n      // block to prevent unintended reuse.\n      return atlasMap[key] = TiledAtlas._(\n        atlas: image,\n        offsets: {tiledImage.source!: Offset.zero},\n        key: key,\n        useAtlas: useAtlas,\n      );\n    }\n\n    /// Note: Chrome on Android has a maximum texture size of 4096x4096. kIsWeb\n    /// is used to select the smaller texture and might overflow. Consider using\n    /// smaller textures for web targets, or, pack your own atlas.\n    final bin = RectangleBinPacker(\n      maxX ?? (kIsWeb ? 4096 : 8192),\n      maxY ?? (kIsWeb ? 4096 : 8192),\n    );\n    final recorder = PictureRecorder();\n    final canvas = Canvas(recorder);\n\n    final offsetMap = <String, Offset>{};\n\n    var pictureRect = Rect.zero;\n\n    imageList.sort((b, a) {\n      final height = a.height! - b.height!;\n      return height != 0 ? height : a.width! - b.width!;\n    });\n\n    // parallelize the download of images.\n    await Future.wait([\n      ...mappedImageList.map(\n        (entry) => imagesInstance.load(entry.$1, package: package),\n      ),\n    ]);\n\n    final emptyPaint = Paint()\n      ..isAntiAlias = false\n      ..filterQuality = FilterQuality.none;\n    for (final entry in mappedImageList) {\n      final tiledImage = entry.$2;\n      final tileImageSource = entry.$1;\n\n      final image = await imagesInstance.load(\n        tileImageSource,\n        package: package,\n      );\n      final rect = bin.pack(\n        image.width.toDouble() + spacingX,\n        image.height.toDouble() + spacingY,\n      );\n\n      pictureRect = pictureRect.expandToInclude(rect);\n\n      final offset = offsetMap[tiledImage.source!] = Offset(\n        rect.left - spacingX,\n        rect.top - spacingY,\n      );\n\n      canvas.drawImage(image, offset, emptyPaint);\n    }\n    final picture = recorder.endRecording();\n    final image = await picture.toImageSafe(\n      pictureRect.width.toInt(),\n      pictureRect.height.toInt(),\n    );\n    (images ?? Flame.images).add(key, image);\n    return atlasMap[key] = TiledAtlas._(\n      atlas: image,\n      offsets: offsetMap,\n      key: key,\n      useAtlas: useAtlas,\n    );\n  }\n\n  /// Clears images cached in `TiledAtlas`\n  ///\n  /// If you called `Flame.images.clearCache()` you also need to call this\n  /// function to clear disposed images from tiled cache.\n  static void clearCache() {\n    atlasMap.clear();\n  }\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/tile_stack.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/effects.dart';\nimport 'package:flame/rendering.dart';\n\n/// A select group of tiles from RenderableTiledMap that can be animated.\n///\n/// Tiles are nothing more than an x/y coordinate in each layer. TileStack lets\n/// you collect a certain group of tiles out of all the layers, and then\n/// set their positions.  This is typically done by using Flame's effects.\nclass TileStack extends Component implements PositionProvider {\n  final List<MutableRSTransform> _tiles;\n\n  /// The number of tiles in this stack.\n  int get length => _tiles.length;\n\n  TileStack(this._tiles);\n\n  @override\n  Vector2 get position => _tiles.first.position;\n\n  @override\n  set position(Vector2 position) {\n    for (final tile in _tiles) {\n      tile.position = position;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/tile_transform.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/rendering.dart';\nimport 'package:flame/sprite.dart';\nimport 'package:flame_tiled/flame_tiled.dart';\nimport 'package:meta/meta.dart';\n\n/// Caches transforms for staggered maps as the row/col are switched.\n@internal\nclass TileTransform {\n  final Rect source;\n  final MutableRSTransform transform;\n  final SimpleFlips flip;\n  final SpriteBatch batch;\n\n  TileTransform(\n    this.source,\n    this.transform,\n    this.flip,\n    this.batch,\n  );\n}\n"
  },
  {
    "path": "packages/flame_tiled/lib/src/tiled_component.dart",
    "content": "import 'dart:ui';\n\nimport 'package:collection/collection.dart';\nimport 'package:flame/cache.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_tiled/src/renderable_layers/tile_layers/tile_layer.dart';\nimport 'package:flame_tiled/src/renderable_tile_map.dart';\nimport 'package:flame_tiled/src/tile_atlas.dart';\nimport 'package:flutter/services.dart';\nimport 'package:meta/meta.dart';\nimport 'package:tiled/tiled.dart';\n\n/// {@template _tiled_component}\n/// A Flame [Component] to render a Tiled TiledMap.\n///\n/// It uses a preloaded [RenderableTiledMap] to batch rendering calls into\n/// Sprite Batches.\n/// {@endtemplate}\nclass TiledComponent<T extends FlameGame> extends PositionComponent\n    with HasGameReference<T> {\n  /// Map instance of this component.\n  RenderableTiledMap tileMap;\n\n  /// This property **cannot** be reassigned at runtime. To make the\n  /// [PositionComponent] larger or smaller, change its [scale].\n  @override\n  set size(Vector2 size) {\n    // Intentionally left empty.\n  }\n\n  /// This property **cannot** be reassigned at runtime. To make the\n  /// [PositionComponent] larger or smaller, change its [scale].\n  @override\n  set width(double w) {\n    // Intentionally left empty.\n  }\n\n  /// This property **cannot** be reassigned at runtime. To make the\n  /// [PositionComponent] larger or smaller, change its [scale].\n  @override\n  set height(double h) {\n    // Intentionally left empty.\n  }\n\n  /// {@macro _tiled_component}\n  TiledComponent(\n    this.tileMap, {\n    super.position,\n    super.scale,\n    super.angle,\n    super.anchor,\n    super.children,\n    super.priority,\n    super.key,\n  }) : super(\n         size: computeSize(\n           tileMap.map.orientation,\n           tileMap.destTileSize,\n           tileMap.map.tileWidth,\n           tileMap.map.tileHeight,\n           tileMap.map.width,\n           tileMap.map.height,\n           tileMap.map.staggerAxis,\n         ),\n       );\n\n  @override\n  Future<void>? onLoad() async {\n    super.onLoad();\n    // Automatically use the first attached CameraComponent camera if it's not\n    // already set..\n    tileMap.camera ??= game.children.query<CameraComponent>().firstOrNull;\n  }\n\n  @override\n  void update(double dt) {\n    tileMap.update(dt);\n  }\n\n  @override\n  void render(Canvas canvas) {\n    tileMap.render(canvas);\n  }\n\n  @override\n  void onGameResize(Vector2 size) {\n    super.onGameResize(size);\n    tileMap.handleResize(size);\n  }\n\n  /// Loads a [TiledComponent] from a file.\n  ///\n  /// {@macro renderable_tile_prefix_path}\n  ///\n  /// By default, [RenderableTiledMap] renders flipped tiles if they exist.\n  /// You can disable it by passing [ignoreFlip] as `true`.\n  ///\n  /// A custom [atlasMaxX] and [atlasMaxY] can be provided in case you want to\n  /// change the max size of [TiledAtlas] that [TiledComponent] creates\n  /// internally.\n  ///\n  /// TiledComponent uses Flame's `SpriteBatch` to render the map. Which under\n  /// the hood uses `Canvas.drawAtlas` calls to render the tiles. This behavior\n  /// can be changed by setting `useAtlas` to `false`. This will make the map\n  /// be rendered with `Canvas.drawImageRect` calls instead.\n  static Future<TiledComponent> load(\n    String fileName,\n    Vector2 destTileSize, {\n    double? atlasMaxX,\n    double? atlasMaxY,\n    String prefix = 'assets/tiles/',\n    int? priority,\n    bool? ignoreFlip,\n    AssetBundle? bundle,\n    Images? images,\n    bool Function(Tileset)? tsxPackingFilter,\n    bool useAtlas = true,\n    Paint Function(double opacity)? layerPaintFactory,\n    double atlasPackingSpacingX = 0,\n    double atlasPackingSpacingY = 0,\n    ComponentKey? key,\n    String? package,\n  }) async {\n    return TiledComponent(\n      await RenderableTiledMap.fromFile(\n        fileName,\n        destTileSize,\n        atlasMaxX: atlasMaxX,\n        atlasMaxY: atlasMaxY,\n        ignoreFlip: ignoreFlip,\n        prefix: prefix,\n        bundle: bundle,\n        images: images,\n        tsxPackingFilter: tsxPackingFilter,\n        useAtlas: useAtlas,\n        layerPaintFactory: layerPaintFactory,\n        atlasPackingSpacingX: atlasPackingSpacingX,\n        atlasPackingSpacingY: atlasPackingSpacingY,\n        package: package,\n      ),\n      priority: priority,\n      key: key,\n    );\n  }\n\n  @visibleForTesting\n  static Vector2 computeSize(\n    MapOrientation? orientation,\n    Vector2 destTileSize,\n    int tileWidth,\n    int tileHeight,\n    int mapWidth,\n    int mapHeight,\n    StaggerAxis? staggerAxis,\n  ) {\n    if (orientation == null) {\n      return NotifyingVector2.zero();\n    }\n    final xScale = destTileSize.x / tileWidth;\n    final yScale = destTileSize.y / tileHeight;\n\n    final tileScaled = Vector2(\n      tileWidth * xScale,\n      tileHeight * yScale,\n    );\n\n    switch (orientation) {\n      case MapOrientation.staggered:\n        return staggerAxis == StaggerAxis.y\n            ? Vector2(\n                tileScaled.x * mapWidth + tileScaled.x / 2,\n                (mapHeight + 1) * (tileScaled.y / 2),\n              )\n            : Vector2(\n                (mapWidth + 1) * (tileScaled.x / 2),\n                tileScaled.y * mapHeight + tileScaled.y / 2,\n              );\n\n      case MapOrientation.hexagonal:\n        return staggerAxis == StaggerAxis.y\n            ? Vector2(\n                mapWidth * tileScaled.x + tileScaled.x / 2,\n                tileScaled.y + ((mapHeight - 1) * tileScaled.y * 0.75),\n              )\n            : Vector2(\n                tileScaled.x + ((mapWidth - 1) * tileScaled.x * 0.75),\n                (mapHeight * tileScaled.y) + tileScaled.y / 2,\n              );\n\n      case MapOrientation.isometric:\n        final halfTile = tileScaled / 2;\n        final dimensions = mapWidth + mapHeight;\n        return halfTile..scale(dimensions.toDouble());\n\n      case MapOrientation.orthogonal:\n        return Vector2(\n          mapWidth * tileScaled.x,\n          mapHeight * tileScaled.y,\n        );\n    }\n  }\n\n  /// Returns a list of all the Atlases that were created for this component.\n  ///\n  /// This method is useful for debugging purposes as it allows developers to\n  /// check how the tilesets were packed into the atlas.\n  ///\n  /// It returns a record with the Atlas key and its image.\n  List<(String, Image)> atlases() {\n    return tileMap.renderableLayers\n        .whereType<FlameTileLayer>()\n        .where((layer) => layer.tiledAtlas.atlas != null)\n        .map((layer) {\n          final image = layer.tiledAtlas.atlas;\n          final key = layer.tiledAtlas.key;\n          return (key, image!);\n        })\n        .fold<Map<String, (String, Image)>>(\n          {},\n          (previousValue, element) {\n            previousValue.putIfAbsent(element.$1, () => element);\n            return previousValue;\n          },\n        )\n        .values\n        .toList();\n  }\n}\n"
  },
  {
    "path": "packages/flame_tiled/pubspec.yaml",
    "content": "name: flame_tiled\nresolution: workspace\ndescription: Tiled support for the Flame game engine. This uses the tiled package and provides wrappers and components to be used inside Flame.\nversion: 3.1.0\nhomepage: https://github.com/flame-engine/flame/tree/main/packages/flame_tiled\nfunding:\n  - https://opencollective.com/blue-fire\n  - https://github.com/sponsors/bluefireteam\n  - https://patreon.com/bluefireoss\ntopics:\n  - flame\n  - tiled\n  - tilemap\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n  flutter: \">=3.41.0\"\n\ndependencies:\n  collection: ^1.17.1\n  flame: ^1.36.0\n  flutter:\n    sdk: flutter\n  meta: ^1.12.0\n  tiled: ^0.11.0\n  xml: ^6.3.0\n\ndev_dependencies:\n  dartdoc: ^9.0.0\n  flame_lint: ^1.4.3\n  flutter_test:\n    sdk: flutter\n  test: any\n"
  },
  {
    "path": "packages/flame_tiled/test/README.md",
    "content": "The following test assets were retrieved\nfrom [open game art](https://opengameart.org/content/minimalistic-hexagonal-tilesets-both-orientations).\nThey were released as Public domain.\n\n- Tileset_Hexagonal_FlatTop_60x39_60x60.png\n- Tileset_Hexagonal_PointyTop_60x52_60x80.png\n\n\nThe following assets were [downloaded here](https://0x72.itch.io/dungeontileset-ii).\nThey were released as [CC0 1.0 Universal (CC0 1.0) - Public Domain Dedication](https://creativecommons.org/publicdomain/zero/1.0/).\n\n- 0x72_DungeonTilesetII_v1.4.png\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/2_tiles-green_on_red.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.8\" tiledversion=\"1.8.4\" orientation=\"orthogonal\" renderorder=\"right-down\" width=\"2\" height=\"1\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" nextlayerid=\"3\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"green_tile\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"1\" columns=\"1\">\n  <transformations hflip=\"1\" vflip=\"1\" rotate=\"1\" preferuntransformed=\"0\"/>\n  <image source=\"green_sprite.png\" width=\"16\" height=\"16\"/>\n </tileset>\n <tileset firstgid=\"2\" name=\"red_tile-base\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"1\" columns=\"1\">\n  <image source=\"red_sprite.png\" width=\"16\" height=\"16\"/>\n </tileset>\n <layer id=\"2\" name=\"red_tile-base\" width=\"2\" height=\"1\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxjYmBgYAJiAAAgAAU=\n  </data>\n </layer>\n <layer id=\"1\" name=\"green_tile-top\" width=\"2\" height=\"1\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxjZGBYwAAEAAMwAKI=\n  </data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/8_tiles-flips.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.8\" tiledversion=\"1.8.4\" orientation=\"orthogonal\" renderorder=\"right-down\" width=\"4\" height=\"2\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"4_color_sprite\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"1\" columns=\"1\">\n  <image source=\"4_color_sprite.png\" width=\"16\" height=\"16\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"4\" height=\"2\">\n  <data encoding=\"csv\">\n2684354561,3221225473,1073741825,536870913,\n1,1610612737,3758096385,2147483649\n</data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/deleted_layer_map.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.10\" tiledversion=\"1.11.2\" orientation=\"orthogonal\" renderorder=\"right-down\" width=\"10\" height=\"5\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" nextlayerid=\"8\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"4_color_sprite\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"4\" columns=\"2\">\n  <image source=\"4_color_sprite.png\" width=\"32\" height=\"32\"/>\n </tileset>\n <layer id=\"1\" name=\"FirstLayer\" width=\"10\" height=\"5\">\n  <data encoding=\"csv\">\n1,0,0,0,0,0,0,0,0,0,\n0,0,1,0,0,0,0,0,0,0,\n0,0,0,0,0,0,0,0,0,0,\n0,0,0,0,0,0,0,0,0,0,\n0,0,0,0,0,0,0,0,0,0\n</data>\n </layer>\n <objectgroup id=\"2\" name=\"SecondLayer\"/>\n <layer id=\"7\" name=\"ThirdLayer\" width=\"10\" height=\"5\">\n  <data encoding=\"csv\">\n0,0,0,0,0,0,0,0,0,0,\n0,0,0,0,0,0,0,0,0,0,\n0,0,0,0,0,0,0,0,0,0,\n0,0,0,0,0,0,0,0,0,0,\n0,0,0,0,0,0,0,0,0,0\n</data>\n </layer>\n <objectgroup id=\"4\" name=\"FourthLayer\"/>\n <layer id=\"6\" name=\"SixthLayer\" width=\"10\" height=\"5\">\n  <data encoding=\"csv\">\n0,0,0,0,0,0,0,0,0,0,\n0,0,0,0,0,0,0,0,0,0,\n0,0,0,0,0,0,0,0,0,0,\n0,0,0,0,0,1,0,0,0,0,\n0,0,0,0,0,0,0,0,0,0\n</data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/dungeon_animation_hexagonal.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.2\" orientation=\"hexagonal\" renderorder=\"right-down\" width=\"4\" height=\"1\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" hexsidelength=\"0\" staggeraxis=\"y\" staggerindex=\"odd\" nextlayerid=\"5\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"0x72_DungeonTilesetII_v1.4\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"1024\" columns=\"32\">\n  <image source=\"0x72_DungeonTilesetII_v1.4.png\" width=\"512\" height=\"512\"/>\n  <tile id=\"40\">\n   <animation>\n    <frame tileid=\"40\" duration=\"300\"/>\n   </animation>\n  </tile>\n  <tile id=\"100\">\n   <animation>\n    <frame tileid=\"100\" duration=\"180\"/>\n    <frame tileid=\"101\" duration=\"170\"/>\n    <frame tileid=\"102\" duration=\"150\"/>\n   </animation>\n  </tile>\n  <tile id=\"186\">\n   <animation>\n    <frame tileid=\"183\" duration=\"300\"/>\n    <frame tileid=\"184\" duration=\"300\"/>\n    <frame tileid=\"185\" duration=\"300\"/>\n    <frame tileid=\"186\" duration=\"300\"/>\n   </animation>\n  </tile>\n  <tile id=\"353\">\n   <animation>\n    <frame tileid=\"353\" duration=\"176\"/>\n    <frame tileid=\"354\" duration=\"176\"/>\n    <frame tileid=\"355\" duration=\"176\"/>\n    <frame tileid=\"356\" duration=\"176\"/>\n   </animation>\n  </tile>\n  <tile id=\"627\">\n   <animation>\n    <frame tileid=\"627\" duration=\"300\"/>\n    <frame tileid=\"628\" duration=\"300\"/>\n    <frame tileid=\"629\" duration=\"300\"/>\n   </animation>\n  </tile>\n </tileset>\n <layer id=\"1\" name=\"single\" width=\"4\" height=\"1\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxTYmBgUAJiTSA+AsQACCABMg==\n  </data>\n </layer>\n <layer id=\"2\" name=\"spike\" width=\"4\" height=\"1\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxLZWBgSGJkgAMACwMAyQ==\n  </data>\n </layer>\n <group id=\"3\" name=\"group\">\n  <layer id=\"4\" name=\"chest\" width=\"4\" height=\"1\">\n   <data encoding=\"base64\" compression=\"zlib\">\n   eJwrYWJAAQAHbgB3\n  </data>\n  </layer>\n </group>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/dungeon_animation_isometric.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.2\" orientation=\"isometric\" renderorder=\"right-down\" width=\"4\" height=\"1\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" nextlayerid=\"5\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"0x72_DungeonTilesetII_v1.4\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"1024\" columns=\"32\">\n  <image source=\"0x72_DungeonTilesetII_v1.4.png\" width=\"512\" height=\"512\"/>\n  <tile id=\"40\">\n   <animation>\n    <frame tileid=\"40\" duration=\"300\"/>\n   </animation>\n  </tile>\n  <tile id=\"100\">\n   <animation>\n    <frame tileid=\"100\" duration=\"180\"/>\n    <frame tileid=\"101\" duration=\"170\"/>\n    <frame tileid=\"102\" duration=\"150\"/>\n   </animation>\n  </tile>\n  <tile id=\"186\">\n   <animation>\n    <frame tileid=\"183\" duration=\"300\"/>\n    <frame tileid=\"184\" duration=\"300\"/>\n    <frame tileid=\"185\" duration=\"300\"/>\n    <frame tileid=\"186\" duration=\"300\"/>\n   </animation>\n  </tile>\n  <tile id=\"353\">\n   <animation>\n    <frame tileid=\"353\" duration=\"176\"/>\n    <frame tileid=\"354\" duration=\"176\"/>\n    <frame tileid=\"355\" duration=\"176\"/>\n    <frame tileid=\"356\" duration=\"176\"/>\n   </animation>\n  </tile>\n  <tile id=\"627\">\n   <animation>\n    <frame tileid=\"627\" duration=\"300\"/>\n    <frame tileid=\"628\" duration=\"300\"/>\n    <frame tileid=\"629\" duration=\"300\"/>\n   </animation>\n  </tile>\n </tileset>\n <layer id=\"1\" name=\"single\" width=\"4\" height=\"1\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxTYmBgUAJiTSA+AsQACCABMg==\n  </data>\n </layer>\n <layer id=\"2\" name=\"spike\" width=\"4\" height=\"1\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxLZWBgSGJkgAMACwMAyQ==\n  </data>\n </layer>\n <group id=\"3\" name=\"group\">\n  <layer id=\"4\" name=\"chest\" width=\"4\" height=\"1\">\n   <data encoding=\"base64\" compression=\"zlib\">\n   eJwrYWJAAQAHbgB3\n  </data>\n  </layer>\n </group>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/dungeon_animation_orthogonal.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.2\" orientation=\"orthogonal\" renderorder=\"right-down\" width=\"4\" height=\"1\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" nextlayerid=\"5\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"0x72_DungeonTilesetII_v1.4\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"1024\" columns=\"32\">\n  <image source=\"0x72_DungeonTilesetII_v1.4.png\" width=\"512\" height=\"512\"/>\n  <tile id=\"40\">\n   <animation>\n    <frame tileid=\"40\" duration=\"300\"/>\n   </animation>\n  </tile>\n  <tile id=\"100\">\n   <animation>\n    <frame tileid=\"100\" duration=\"180\"/>\n    <frame tileid=\"101\" duration=\"170\"/>\n    <frame tileid=\"102\" duration=\"150\"/>\n   </animation>\n  </tile>\n  <tile id=\"186\">\n   <animation>\n    <frame tileid=\"183\" duration=\"300\"/>\n    <frame tileid=\"184\" duration=\"300\"/>\n    <frame tileid=\"185\" duration=\"300\"/>\n    <frame tileid=\"186\" duration=\"300\"/>\n   </animation>\n  </tile>\n  <tile id=\"353\">\n   <animation>\n    <frame tileid=\"353\" duration=\"176\"/>\n    <frame tileid=\"354\" duration=\"176\"/>\n    <frame tileid=\"355\" duration=\"176\"/>\n    <frame tileid=\"356\" duration=\"176\"/>\n   </animation>\n  </tile>\n  <tile id=\"627\">\n   <animation>\n    <frame tileid=\"627\" duration=\"300\"/>\n    <frame tileid=\"628\" duration=\"300\"/>\n    <frame tileid=\"629\" duration=\"300\"/>\n   </animation>\n  </tile>\n </tileset>\n <layer id=\"1\" name=\"single\" width=\"4\" height=\"1\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxTYmBgUAJiTSA+AsQACCABMg==\n  </data>\n </layer>\n <layer id=\"2\" name=\"spike\" width=\"4\" height=\"1\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxLZWBgSGJkgAMACwMAyQ==\n  </data>\n </layer>\n <group id=\"3\" name=\"group\">\n  <layer id=\"4\" name=\"chest\" width=\"4\" height=\"1\">\n   <data encoding=\"base64\" compression=\"zlib\">\n   eJwrYWJAAQAHbgB3\n  </data>\n  </layer>\n </group>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/dungeon_animation_staggered.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.2\" orientation=\"staggered\" renderorder=\"right-down\" width=\"4\" height=\"1\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" staggeraxis=\"y\" staggerindex=\"odd\" nextlayerid=\"5\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"0x72_DungeonTilesetII_v1.4\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"1024\" columns=\"32\">\n  <image source=\"0x72_DungeonTilesetII_v1.4.png\" width=\"512\" height=\"512\"/>\n  <tile id=\"40\">\n   <animation>\n    <frame tileid=\"40\" duration=\"300\"/>\n   </animation>\n  </tile>\n  <tile id=\"100\">\n   <animation>\n    <frame tileid=\"100\" duration=\"180\"/>\n    <frame tileid=\"101\" duration=\"170\"/>\n    <frame tileid=\"102\" duration=\"150\"/>\n   </animation>\n  </tile>\n  <tile id=\"186\">\n   <animation>\n    <frame tileid=\"183\" duration=\"300\"/>\n    <frame tileid=\"184\" duration=\"300\"/>\n    <frame tileid=\"185\" duration=\"300\"/>\n    <frame tileid=\"186\" duration=\"300\"/>\n   </animation>\n  </tile>\n  <tile id=\"353\">\n   <animation>\n    <frame tileid=\"353\" duration=\"176\"/>\n    <frame tileid=\"354\" duration=\"176\"/>\n    <frame tileid=\"355\" duration=\"176\"/>\n    <frame tileid=\"356\" duration=\"176\"/>\n   </animation>\n  </tile>\n  <tile id=\"627\">\n   <animation>\n    <frame tileid=\"627\" duration=\"300\"/>\n    <frame tileid=\"628\" duration=\"300\"/>\n    <frame tileid=\"629\" duration=\"300\"/>\n   </animation>\n  </tile>\n </tileset>\n <layer id=\"1\" name=\"single\" width=\"4\" height=\"1\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxTYmBgUAJiTSA+AsQACCABMg==\n  </data>\n </layer>\n <layer id=\"2\" name=\"spike\" width=\"4\" height=\"1\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxLZWBgSGJkgAMACwMAyQ==\n  </data>\n </layer>\n <group id=\"3\" name=\"group\">\n  <layer id=\"4\" name=\"chest\" width=\"4\" height=\"1\">\n   <data encoding=\"base64\" compression=\"zlib\">\n   eJwrYWJAAQAHbgB3\n  </data>\n  </layer>\n </group>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/flat_hex_even.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.1\" orientation=\"hexagonal\" renderorder=\"right-down\" width=\"5\" height=\"5\" tilewidth=\"60\" tileheight=\"39\" infinite=\"0\" hexsidelength=\"30\" staggeraxis=\"x\" staggerindex=\"even\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"flat_hex\" tilewidth=\"60\" tileheight=\"60\" tilecount=\"12\" columns=\"4\">\n  <image source=\"Tileset_Hexagonal_FlatTop_60x39_60x60.png\" width=\"240\" height=\"180\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"5\" height=\"5\">\n  <data encoding=\"base64\">\n   AQAAAAYAAAAIAAAACQAAAAkAAIAEAAAACAAAAAYAAAAIAAAABwAAAAUAAAAIAAAABwAAAAsAAIADAABABQAAAAgAAAAHAAAABwAAAAcAAAADAAAACAAAAAoAAAACAAAACgAAAA==\n  </data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/flat_hex_odd.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.1\" orientation=\"hexagonal\" renderorder=\"right-down\" width=\"5\" height=\"5\" tilewidth=\"60\" tileheight=\"39\" infinite=\"0\" hexsidelength=\"30\" staggeraxis=\"x\" staggerindex=\"odd\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"flat_hex\" tilewidth=\"60\" tileheight=\"60\" tilecount=\"12\" columns=\"4\">\n  <image source=\"Tileset_Hexagonal_FlatTop_60x39_60x60.png\" width=\"240\" height=\"180\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"5\" height=\"5\">\n  <data encoding=\"base64\">\n   AQAAAAYAAAAIAAAACQAAAAkAAIAEAAAACAAAAAYAAAAIAAAABwAAAAUAAAAIAAAABwAAAAsAAIADAABABQAAAAgAAAAHAAAABwAAAAcAAAADAAAACAAAAAoAAAACAAAACgAAAA==\n  </data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/image_layer_full_screen.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.10\" tiledversion=\"1.11.2\" orientation=\"orthogonal\" renderorder=\"right-down\"\n    width=\"15\" height=\"15\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" nextlayerid=\"5\"\n    nextobjectid=\"1\">\n    <imagelayer id=\"1\" name=\"BackgroundImage\">\n        <image source=\"Tileset_Hexagonal_PointyTop_60x52_60x80.png\" width=\"240\" height=\"240\" />\n    </imagelayer>\n    <imagelayer id=\"2\" name=\"GreenSprite\" offsetx=\"112\" offsety=\"112\">\n        <image source=\"green_sprite.png\" width=\"16\" height=\"16\" />\n    </imagelayer>\n    <imagelayer id=\"3\" name=\"RedSprite\" offsetx=\"224\" offsety=\"0\">\n        <image source=\"red_sprite.png\" width=\"16\" height=\"16\" />\n    </imagelayer>\n    <imagelayer id=\"4\" name=\"Gear\" offsetx=\"16\" offsety=\"144\">\n        <image source=\"images/gear.png\" width=\"128\" height=\"74\" />\n    </imagelayer>\n</map>"
  },
  {
    "path": "packages/flame_tiled/test/assets/iso_staggered_overlap_x_even.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.1\" orientation=\"staggered\" renderorder=\"right-down\" width=\"4\" height=\"4\" tilewidth=\"128\" tileheight=\"64\" infinite=\"0\" staggeraxis=\"x\" staggerindex=\"even\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"iso_staggered\" tilewidth=\"128\" tileheight=\"256\" tilecount=\"8\" columns=\"4\">\n  <image source=\"dirt_atlas.png\" width=\"512\" height=\"512\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"4\" height=\"4\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxjZGBgYAViNiBmhtLsQMwExBxQORgfRjNB1YHkAAn4AEs=\n  </data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/iso_staggered_overlap_x_odd.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.1\" orientation=\"staggered\" renderorder=\"right-down\" width=\"4\" height=\"4\" tilewidth=\"128\" tileheight=\"64\" infinite=\"0\" staggeraxis=\"x\" staggerindex=\"odd\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"iso_staggered\" tilewidth=\"128\" tileheight=\"256\" tilecount=\"8\" columns=\"4\">\n  <image source=\"dirt_atlas.png\" width=\"512\" height=\"512\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"4\" height=\"4\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxjZGBgYAViNiBmhtLsQMwExBxQORgfRjNB1YHkAAn4AEs=\n  </data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/iso_staggered_overlap_y_even.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.1\" orientation=\"staggered\" renderorder=\"right-down\" width=\"4\" height=\"4\" tilewidth=\"128\" tileheight=\"64\" infinite=\"0\" staggeraxis=\"y\" staggerindex=\"even\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"iso_staggered\" tilewidth=\"128\" tileheight=\"256\" tilecount=\"8\" columns=\"4\">\n  <image source=\"dirt_atlas.png\" width=\"512\" height=\"512\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"4\" height=\"4\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxjZGBgYIViZiBmA2J2IGYCYg6oODsSZoJiNqgcAAo4AE8=\n  </data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/iso_staggered_overlap_y_odd.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.1\" orientation=\"staggered\" renderorder=\"right-down\" width=\"4\" height=\"4\" tilewidth=\"128\" tileheight=\"64\" infinite=\"0\" staggeraxis=\"y\" staggerindex=\"odd\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"iso_staggered\" tilewidth=\"128\" tileheight=\"256\" tilecount=\"8\" columns=\"4\">\n  <image source=\"dirt_atlas.png\" width=\"512\" height=\"512\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"4\" height=\"4\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxjZGBgYIViZiBmA2J2IGYCYg6oODsSZoJiNqgcAAo4AE8=\n  </data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/isometric_plain.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.2\" orientation=\"staggered\" renderorder=\"right-down\" width=\"2\" height=\"5\" tilewidth=\"128\" tileheight=\"74\" infinite=\"0\" staggeraxis=\"y\" staggerindex=\"odd\" nextlayerid=\"3\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" source=\"tiles/isometric_plain_1.tsx\"/>\n <tileset firstgid=\"7\" name=\"isometric_plain_2\" tilewidth=\"128\" tileheight=\"74\" tilecount=\"3\" columns=\"0\">\n  <grid orientation=\"orthogonal\" width=\"1\" height=\"1\"/>\n  <tile id=\"0\">\n   <image width=\"128\" height=\"74\" source=\"images/box2.png\"/>\n  </tile>\n  <tile id=\"1\">\n   <image width=\"128\" height=\"74\" source=\"images/diamond.png\"/>\n  </tile>\n  <tile id=\"2\">\n   <image width=\"128\" height=\"74\" source=\"images/gear.png\"/>\n  </tile>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"2\" height=\"5\">\n  <data encoding=\"csv\">\n1,1,\n1,1,\n2,5,\n2,6,\n3,2\n</data>\n </layer>\n <layer id=\"2\" name=\"Tile Layer 2\" width=\"2\" height=\"5\">\n  <data encoding=\"csv\">\n0,0,\n8,0,\n3221225480,1610612744,\n1073741832,0,\n0,0\n</data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/layers_test.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.5\" tiledversion=\"1.7.2\" orientation=\"orthogonal\" renderorder=\"right-down\" width=\"10\" height=\"6\" tilewidth=\"32\" tileheight=\"32\" infinite=\"0\" nextlayerid=\"5\" nextobjectid=\"10\">\n <layer id=\"1\" name=\"MyTileLayer\" width=\"10\" height=\"6\">\n  <data encoding=\"base64\">\n   AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n  </data>\n </layer>\n <objectgroup id=\"2\" name=\"MyObjectLayer\" offsetx=\"0\" offsety=\"-32\">\n  <object id=\"9\" x=\"64\" y=\"160\" width=\"200\" height=\"40\">\n   <text wrap=\"1\" color=\"#ffffff\">Just a dummy map for \n testing layers</text>\n  </object>\n </objectgroup>\n <imagelayer id=\"3\" name=\"MyImageLayer\">\n  <image source=\"map-level1.png\" width=\"272\" height=\"128\"/>\n </imagelayer>\n <group id=\"4\" name=\"MyGroupLayer\"/>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/map-with-same-level-tsx.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.0\" orientation=\"orthogonal\" renderorder=\"right-down\" width=\"32\" height=\"128\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" backgroundcolor=\"#fdea18\" nextlayerid=\"5\" nextobjectid=\"1\">\n        <tileset firstgid=\"1\" source=\"tiles/samelevel_tileset_1.tsx\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"136\" columns=\"17\">\n  <image source=\"map-level1.png\" width=\"272\" height=\"128\"/>\n  <tile id=\"64\">\n   <properties>\n    <property name=\"type\" value=\"sky\"/>\n   </properties>\n  </tile>\n  <tile id=\"65\">\n   <properties>\n    <property name=\"type\" value=\"xpto\"/>\n   </properties>\n  </tile>\n </tileset>\n <layer id=\"1\" name=\"Ground\" width=\"32\" height=\"128\">\n  <data encoding=\"base64\">\n   QQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAAAAAAAAAAAAAAAAABBAAAAQQAAAEEAAAAAAAAAAAAAAAAAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAAAAAAAAAAAAAAAAAEEAAABBAAAAQQAAAAAAAAAAAAAAAAAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD8AAAA+AAAAQAAAAD8AAAA+AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAAAAAAAAAAAAAAAAAQQAAAEEAAABBAAAAAAAAAAAAAAAAAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPQAAAEEAAABBAAAAPQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAAAAAAAAAAAAAAAAABBAAAAQQAAAEEAAAAAAAAAAAAAAAAAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA9AAAAQQAAAEEAAAA9AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAAAAAAAQQAAAEEAAABBAAAAQQAAAEEAAAAAAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAD0AAABBAAAAQQAAAD0AAABBAAAAQQAAAEEAAAA7AAAAOwAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAAAAAABBAAAAQQAAAEEAAABBAAAAQQAAAAAAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAAAAAAEEAAABBAAAAQQAAAEEAAABBAAAAAAAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPwAAAD8AAAA+AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAD0AAABBAAAAQQAAAD0AAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPQAAAEEAAABBAAAAPQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA9AAAAQQAAAEEAAAA9AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAOwAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD8AAAA/AAAAPwAAAD8AAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD8AAAA/AAAAPwAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADcAAAA4AAAAOQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA3AAAAOAAAADkAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAADsAAAA7AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABBAAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAEEAAABBAAAAQQAAAEEAAABBAAAAOwAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA3AAAAOAAAADkAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAADsAAAA7AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAANwAAADgAAAA5AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA3AAAAOAAAADkAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA3AAAAOAAAADkAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAOwAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABBAAAAPAAAADQAAAA6AAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADcAAAA4AAAAOQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA3AAAAOAAAADkAAABBAAAAOwAAADwAAAA0AAAAOgAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAABcAAAAVAAAAFgAAAEEAAAA8AAAANAAAADoAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAAGAAAAFwAAABdAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD4AAABAAAAAPgAAAEAAAAA/AAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAABBAAAAQQAAAEEAAAA7AAAAOwAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD8AAAA/AAAAPgAAAEAAAAA/AAAAPwAAAD4AAABAAAAAPwAAAD4AAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADcAAAA4AAAAOQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAAA7AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEAAAAA+AAAAPgAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA9AAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAD0AAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADcAAAA4AAAAOQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAOwAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAOwAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAOwAAAEEAAAA7AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAANwAAADgAAAA5AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAANwAAADgAAAA5AAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA3AAAAOAAAADkAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAPAAAADQAAAA6AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABfAAAANQAAADYAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAADwAAAA0AAAAOgAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAPAAAADQAAAA6AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADsAAAA8AAAANAAAADoAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAOwAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAXwAAADUAAAA2AAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAANwAAADgAAAA5AAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAANwAAADgAAAA5AAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAOwAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAANwAAADgAAAA5AAAAPAAAADQAAAA6AAAANwAAADgAAAA5AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABfAAAANQAAADYAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQQAAAF8AAAA1AAAANgAAADsAAABBAAAAQQAAADwAAAA0AAAAOgAAAF8AAAA1AAAANgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAD4AAABAAAAAPgAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAXwAAADUAAAA2AAAAXwAAADUAAAA2AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABfAAAANQAAADYAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABfAAAANQAAADYAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAOwAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAAA3AAAAOAAAADkAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABfAAAANQAAADYAAABfAAAANQAAADYAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABBAAAAQQAAADsAAAA7AAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAF8AAAA1AAAANgAAAF8AAAA1AAAANgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAF8AAAA1AAAANgAAAF8AAAA1AAAANgAAAEEAAABBAAAAOwAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABfAAAANQAAADYAAABfAAAANQAAADYAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAF8AAAA1AAAANgAAAF8AAAA1AAAANgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAXwAAADUAAAA2AAAAPAAAAEAAAAA+AAAAawAAAGwAAABAAAAAPgAAADQAAAA6AAAAPAAAADQAAAA6AAAAXwAAADUAAAA2AAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAABfAAAANQAAADYAAAA8AAAANAAAADoAAAA8AAAAPQAAAGsAAABsAAAAawAAAGwAAAA9AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA7AAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAABAAAAAPgAAAGsAAABsAAAAQAAAAD4AAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADsAAABBAAAAQQAAAAoAAABBAAAAQQAAABcAAAAWAAAAFwAAABYAAAAXAAAAFgAAABcAAAAWAAAAFwAAABYAAAAXAAAAFgAAABcAAAAWAAAAFwAAABYAAAAXAAAAFgAAABcAAAAWAAAAFwAAABYAAAAXAAAAFgAAABcAAAAWAAAAQQAAAEEAAABBAAAACwAAAIEAAACCAAAAGAAAAF0AAAAYAAAAXQAAABgAAABdAAAAGAAAAF0AAAAYAAAAXQAAABgAAABdAAAAGAAAAF0AAAAYAAAAXQAAABgAAABdAAAAGAAAAF0AAAAYAAAAXQAAABgAAABdAAAAGAAAAF0AAABCAAAAggAAAEIAAAAMAAAAQwAAABoAAAAZAAAAGgAAAEQAAABDAAAAGQAAABkAAAAaAAAARAAAAEMAAAAZAAAAGQAAABoAAABEAAAAQwAAABkAAAAZAAAAGgAAAEQAAABDAAAAGQAAABkAAAAaAAAARAAAAEMAAAAZAAAARAAAAEMAAABEAAAAQwAAAA==\n  </data>\n </layer>\n <group id=\"2\" name=\"Background\" opacity=\"0.8\" offsetx=\"-50\" offsety=\"-100\" parallaxx=\"0.4\" parallaxy=\"0.4\">\n  <layer id=\"3\" name=\"Sky tiles\" width=\"32\" height=\"128\" opacity=\"0.9\" offsetx=\"100\" offsety=\"100\">\n   <data encoding=\"base64\">\n   AAAAAAAAAAAAAAAAAAAAAAAAAAA3AAAAOAAAADkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAA0AAAAOgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAADQAAAA6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAANAAAADoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAA0AAAAOgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAADQAAAA6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAANAAAADoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n  </data>\n  </layer>\n </group>\n <imagelayer id=\"4\" name=\"Sky artifact\" opacity=\"0.2\" offsetx=\"400\" offsety=\"0\" repeaty=\"1\">\n  <image source=\"image1.png\" width=\"32\" height=\"32\"/>\n </imagelayer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/map.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.10\" tiledversion=\"1.10.2\" orientation=\"orthogonal\" renderorder=\"right-down\" width=\"32\" height=\"128\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" backgroundcolor=\"#fdea18\" nextlayerid=\"6\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"level1\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"136\" columns=\"17\">\n  <image source=\"map-level1.png\" width=\"272\" height=\"128\"/>\n  <tile id=\"64\">\n   <properties>\n    <property name=\"type\" value=\"sky\"/>\n   </properties>\n  </tile>\n  <tile id=\"65\">\n   <properties>\n    <property name=\"type\" value=\"xpto\"/>\n   </properties>\n  </tile>\n </tileset>\n <layer id=\"1\" name=\"Ground\" width=\"32\" height=\"128\">\n  <data encoding=\"base64\">\n   QQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAAAAAAAAAAAAAAAAABBAAAAQQAAAEEAAAAAAAAAAAAAAAAAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAAAAAAAAAAAAAAAAAEEAAABBAAAAQQAAAAAAAAAAAAAAAAAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD8AAAA+AAAAQAAAAD8AAAA+AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAAAAAAAAAAAAAAAAAQQAAAEEAAABBAAAAAAAAAAAAAAAAAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPQAAAEEAAABBAAAAPQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAAAAAAAAAAAAAAAAABBAAAAQQAAAEEAAAAAAAAAAAAAAAAAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA9AAAAQQAAAEEAAAA9AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAAAAAAAQQAAAEEAAABBAAAAQQAAAEEAAAAAAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAD0AAABBAAAAQQAAAD0AAABBAAAAQQAAAEEAAAA7AAAAOwAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAAAAAABBAAAAQQAAAEEAAABBAAAAQQAAAAAAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAAAAAAEEAAABBAAAAQQAAAEEAAABBAAAAAAAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPwAAAD8AAAA+AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAD0AAABBAAAAQQAAAD0AAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPQAAAEEAAABBAAAAPQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA9AAAAQQAAAEEAAAA9AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAOwAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD8AAAA/AAAAPwAAAD8AAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD8AAAA/AAAAPwAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADcAAAA4AAAAOQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA3AAAAOAAAADkAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAADsAAAA7AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABBAAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAEEAAABBAAAAQQAAAEEAAABBAAAAOwAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA3AAAAOAAAADkAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAADsAAAA7AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAANwAAADgAAAA5AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA3AAAAOAAAADkAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA3AAAAOAAAADkAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAOwAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABBAAAAPAAAADQAAAA6AAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADcAAAA4AAAAOQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA3AAAAOAAAADkAAABBAAAAOwAAADwAAAA0AAAAOgAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAABcAAAAVAAAAFgAAAEEAAAA8AAAANAAAADoAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAAGAAAAFwAAABdAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD4AAABAAAAAPgAAAEAAAAA/AAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAABBAAAAQQAAAEEAAAA7AAAAOwAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD8AAAA/AAAAPgAAAEAAAAA/AAAAPwAAAD4AAABAAAAAPwAAAD4AAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADcAAAA4AAAAOQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAAA7AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEAAAAA+AAAAPgAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA9AAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAD0AAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADcAAAA4AAAAOQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAOwAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAOwAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAOwAAAEEAAAA7AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAANwAAADgAAAA5AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAANwAAADgAAAA5AAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA3AAAAOAAAADkAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAPAAAADQAAAA6AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABfAAAANQAAADYAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAADwAAAA0AAAAOgAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAPAAAADQAAAA6AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADsAAAA8AAAANAAAADoAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAOwAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAXwAAADUAAAA2AAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAANwAAADgAAAA5AAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAANwAAADgAAAA5AAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAOwAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAANwAAADgAAAA5AAAAPAAAADQAAAA6AAAANwAAADgAAAA5AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABfAAAANQAAADYAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQQAAAF8AAAA1AAAANgAAADsAAABBAAAAQQAAADwAAAA0AAAAOgAAAF8AAAA1AAAANgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAD4AAABAAAAAPgAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAXwAAADUAAAA2AAAAXwAAADUAAAA2AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABfAAAANQAAADYAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABfAAAANQAAADYAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAOwAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAAA3AAAAOAAAADkAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABfAAAANQAAADYAAABfAAAANQAAADYAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABBAAAAQQAAADsAAAA7AAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAF8AAAA1AAAANgAAAF8AAAA1AAAANgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAF8AAAA1AAAANgAAAF8AAAA1AAAANgAAAEEAAABBAAAAOwAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABfAAAANQAAADYAAABfAAAANQAAADYAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAF8AAAA1AAAANgAAAF8AAAA1AAAANgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAXwAAADUAAAA2AAAAPAAAAEAAAAA+AAAAawAAAGwAAABAAAAAPgAAADQAAAA6AAAAPAAAADQAAAA6AAAAXwAAADUAAAA2AAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAABfAAAANQAAADYAAAA8AAAANAAAADoAAAA8AAAAPQAAAGsAAABsAAAAawAAAGwAAAA9AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA7AAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAABAAAAAPgAAAGsAAABsAAAAQAAAAD4AAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADsAAABBAAAAQQAAAAoAAABBAAAAQQAAABcAAAAWAAAAFwAAABYAAAAXAAAAFgAAABcAAAAWAAAAFwAAABYAAAAXAAAAFgAAABcAAAAWAAAAFwAAABYAAAAXAAAAFgAAABcAAAAWAAAAFwAAABYAAAAXAAAAFgAAABcAAAAWAAAAQQAAAEEAAABBAAAACwAAAIEAAACCAAAAGAAAAF0AAAAYAAAAXQAAABgAAABdAAAAGAAAAF0AAAAYAAAAXQAAABgAAABdAAAAGAAAAF0AAAAYAAAAXQAAABgAAABdAAAAGAAAAF0AAAAYAAAAXQAAABgAAABdAAAAGAAAAF0AAABCAAAAggAAAEIAAAAMAAAAQwAAABoAAAAZAAAAGgAAAEQAAABDAAAAGQAAABkAAAAaAAAARAAAAEMAAAAZAAAAGQAAABoAAABEAAAAQwAAABkAAAAZAAAAGgAAAEQAAABDAAAAGQAAABkAAAAaAAAARAAAAEMAAAAZAAAARAAAAEMAAABEAAAAQwAAAA==\n  </data>\n </layer>\n <group id=\"2\" name=\"Background\" opacity=\"0.8\" offsetx=\"-50\" offsety=\"-100\" parallaxx=\"0.4\" parallaxy=\"0.4\">\n  <layer id=\"3\" name=\"Sky tiles\" width=\"32\" height=\"128\" opacity=\"0.9\" offsetx=\"100\" offsety=\"100\">\n   <data encoding=\"base64\">\n   AAAAAAAAAAAAAAAAAAAAAAAAAAA3AAAAOAAAADkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAA0AAAAOgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAADQAAAA6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAANAAAADoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAA0AAAAOgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAADQAAAA6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAANAAAADoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n  </data>\n  </layer>\n </group>\n <imagelayer id=\"5\" name=\"Image Layer 2\" offsetx=\"-400\" offsety=\"350\" repeatx=\"1\">\n  <image source=\"image1.png\" width=\"32\" height=\"32\"/>\n </imagelayer>\n <imagelayer id=\"4\" name=\"Sky artifact\" opacity=\"0.2\" offsetx=\"400\" offsety=\"0\" repeaty=\"1\">\n  <image source=\"image1.png\" width=\"32\" height=\"32\"/>\n </imagelayer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/oversized_tiles_hexagonal.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.10\" tiledversion=\"1.10.1\" orientation=\"hexagonal\" renderorder=\"right-down\" width=\"4\" height=\"4\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" hexsidelength=\"8\" staggeraxis=\"y\" staggerindex=\"odd\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"0x72_DungeonTilesetII_v1.4\" tilewidth=\"32\" tileheight=\"32\" tilecount=\"256\" columns=\"16\">\n  <image source=\"0x72_DungeonTilesetII_v1.4.png\" width=\"512\" height=\"512\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"4\" height=\"4\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxjYEAFrlA6lQE7KIbSJUC8AIgBKkgCMg==\n  </data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/oversized_tiles_isometric.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.10\" tiledversion=\"1.10.1\" orientation=\"isometric\" renderorder=\"right-down\" width=\"4\" height=\"4\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"0x72_DungeonTilesetII_v1.4\" tilewidth=\"32\" tileheight=\"32\" tilecount=\"256\" columns=\"16\">\n  <image source=\"0x72_DungeonTilesetII_v1.4.png\" width=\"512\" height=\"512\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"4\" height=\"4\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxjYMAOiqB0EpQOhtKFaOoALSgBmQ==\n  </data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/oversized_tiles_orthogonal.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.10\" tiledversion=\"1.10.1\" orientation=\"orthogonal\" renderorder=\"right-down\" width=\"4\" height=\"4\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"0x72_DungeonTilesetII_v1.4\" tilewidth=\"32\" tileheight=\"32\" tilecount=\"256\" columns=\"16\">\n  <image source=\"0x72_DungeonTilesetII_v1.4.png\" width=\"512\" height=\"512\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"4\" height=\"4\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxjYEAFxVC6hAE7KITSRVAaADKQAcs=\n  </data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/oversized_tiles_staggered.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.10\" tiledversion=\"1.10.1\" orientation=\"staggered\" renderorder=\"right-down\" width=\"4\" height=\"4\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" staggeraxis=\"y\" staggerindex=\"odd\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"0x72_DungeonTilesetII_v1.4\" tilewidth=\"32\" tileheight=\"32\" tilecount=\"256\" columns=\"16\">\n  <image source=\"0x72_DungeonTilesetII_v1.4.png\" width=\"512\" height=\"512\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"4\" height=\"4\">\n  <data encoding=\"base64\" compression=\"zlib\">\n   eJxjYMANDgBxEppYMZQuAeI+IAYANIgCmA==\n  </data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/pointy_hex_even.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.1\" orientation=\"hexagonal\" renderorder=\"right-down\" width=\"5\" height=\"5\" tilewidth=\"60\" tileheight=\"52\" infinite=\"0\" hexsidelength=\"26\" staggeraxis=\"y\" staggerindex=\"even\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"Tileset_Hexagonal_PointyTop_60x52_60x80\" tilewidth=\"60\" tileheight=\"80\" tilecount=\"12\" columns=\"4\">\n  <image source=\"Tileset_Hexagonal_PointyTop_60x52_60x80.png\" width=\"240\" height=\"240\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"5\" height=\"5\">\n  <data encoding=\"csv\">\n6,5,5,5,5,\n5,4,8,12,5,\n5,2147483656,6,2,1073741826,\n5,2147483659,6,6,6,\n5,5,11,5,11\n</data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/pointy_hex_odd.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.1\" orientation=\"hexagonal\" renderorder=\"right-down\" width=\"5\" height=\"5\" tilewidth=\"60\" tileheight=\"52\" infinite=\"0\" hexsidelength=\"26\" staggeraxis=\"y\" staggerindex=\"odd\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"Tileset_Hexagonal_PointyTop_60x52_60x80\" tilewidth=\"60\" tileheight=\"80\" tilecount=\"12\" columns=\"4\">\n  <image source=\"Tileset_Hexagonal_PointyTop_60x52_60x80.png\" width=\"240\" height=\"240\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"5\" height=\"5\">\n  <data encoding=\"csv\">\n6,5,5,5,5,\n5,4,8,12,5,\n5,2147483656,6,2,1073741826,\n5,2147483659,6,6,6,\n5,5,11,5,11\n</data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/single_tile_map_1.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.8\" tiledversion=\"1.8.4\" orientation=\"orthogonal\" renderorder=\"right-down\" width=\"1\" height=\"1\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"4_color_sprite\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"1\" columns=\"1\">\n  <image source=\"4_color_sprite.png\" width=\"16\" height=\"16\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"1\" height=\"1\">\n  <data encoding=\"csv\">\n    1\n  </data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/single_tile_map_2.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.8\" tiledversion=\"1.8.4\" orientation=\"orthogonal\" renderorder=\"right-down\" width=\"1\" height=\"1\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"4_color_sprite\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"1\" columns=\"1\">\n  <image source=\"4_color_sprite.png\" width=\"16\" height=\"16\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"1\" height=\"1\">\n  <data encoding=\"csv\">\n    2147483649\n  </data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/test_isometric.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.1\" orientation=\"isometric\" renderorder=\"right-down\" width=\"5\" height=\"5\" tilewidth=\"128\" tileheight=\"64\" infinite=\"0\" nextlayerid=\"5\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"isometric_spritesheet\" tilewidth=\"128\" tileheight=\"256\" tilecount=\"4\" columns=\"1\">\n  <image source=\"isometric_spritesheet.png\" width=\"128\" height=\"1024\"/>\n </tileset>\n <layer id=\"1\" name=\"ground\" width=\"5\" height=\"5\">\n  <data encoding=\"base64\">\n   AgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAQAAAACAAAAAwAAAAMAAAACAAAAAwAAAAIAAAADAAAAAgAAAAIAAAAEAACAAgAAAAMAAAACAAAAAwAAAAIAAAACAAAAAgAAAA==\n  </data>\n </layer>\n <layer id=\"2\" name=\"item\" class=\"items\" width=\"5\" height=\"5\">\n  <data encoding=\"base64\">\n   AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAACAAAAAAAAAAAAAAAAAAAAAAA==\n  </data>\n </layer>\n <layer id=\"4\" name=\"emty\" width=\"5\" height=\"5\">\n  <data encoding=\"base64\">\n   AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n  </data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/test_shifted.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.1\" orientation=\"isometric\" renderorder=\"right-down\" width=\"5\" height=\"5\" tilewidth=\"128\" tileheight=\"64\" infinite=\"0\" nextlayerid=\"7\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"isometric_spritesheet\" tilewidth=\"128\" tileheight=\"256\" tilecount=\"4\" columns=\"1\">\n  <image source=\"isometric_spritesheet.png\" width=\"128\" height=\"1024\"/>\n </tileset>\n <layer id=\"1\" name=\"ground\" width=\"5\" height=\"5\">\n  <data encoding=\"base64\">\n   AgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAwAAAAMAAAACAAAAAwAAAAIAAAADAAAAAgAAAAIAAAACAAAAAgAAAAMAAAACAAAAAwAAAAIAAAACAAAAAgAAAA==\n  </data>\n </layer>\n <group id=\"5\" name=\"shifted\" offsetx=\"32\" offsety=\"16\">\n  <layer id=\"4\" name=\"shifted\" width=\"5\" height=\"5\">\n   <data encoding=\"base64\">\n   AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n  </data>\n  </layer>\n  <layer id=\"6\" name=\"shifted2\" width=\"5\" height=\"5\" opacity=\"0.68\" offsetx=\"-128\" offsety=\"0\">\n   <data encoding=\"base64\">\n   AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n  </data>\n  </layer>\n </group>\n <layer id=\"2\" name=\"item\" width=\"5\" height=\"5\">\n  <data encoding=\"base64\">\n   AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAACAAAAAAAAAAAAAAAAAAAAAAA==\n  </data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/test_tile_offset_hexagonal.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.10\" tiledversion=\"1.10.2\" orientation=\"hexagonal\" renderorder=\"right-down\" width=\"2\" height=\"2\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" hexsidelength=\"8\" staggeraxis=\"y\" staggerindex=\"odd\" backgroundcolor=\"#000000\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"no_tile_offset\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"4\" columns=\"2\">\n  <image source=\"4_color_sprite.png\" width=\"32\" height=\"32\"/>\n </tileset>\n <tileset firstgid=\"5\" name=\"tile_offset\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"4\" columns=\"2\">\n  <tileoffset x=\"4\" y=\"8\"/>\n  <image source=\"4_color_sprite.png\" width=\"32\" height=\"32\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"2\" height=\"2\">\n  <data encoding=\"csv\">\n5,1,\n1,1\n</data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/test_tile_offset_isometric.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.10\" tiledversion=\"1.10.2\" orientation=\"isometric\" renderorder=\"right-down\" width=\"2\" height=\"2\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" backgroundcolor=\"#000000\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"no_tile_offset\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"4\" columns=\"2\">\n  <image source=\"4_color_sprite.png\" width=\"32\" height=\"32\"/>\n </tileset>\n <tileset firstgid=\"5\" name=\"tile_offset\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"4\" columns=\"2\">\n  <tileoffset x=\"4\" y=\"8\"/>\n  <image source=\"4_color_sprite.png\" width=\"32\" height=\"32\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"2\" height=\"2\">\n  <data encoding=\"csv\">\n5,1,\n1,1\n</data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/test_tile_offset_orthogonal.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.10\" tiledversion=\"1.10.2\" orientation=\"orthogonal\" renderorder=\"right-down\" width=\"2\" height=\"2\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" hexsidelength=\"0\" staggeraxis=\"y\" staggerindex=\"odd\" backgroundcolor=\"#000000\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"no_tile_offset\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"4\" columns=\"2\">\n  <image source=\"4_color_sprite.png\" width=\"32\" height=\"32\"/>\n </tileset>\n <tileset firstgid=\"5\" name=\"tile_offset\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"4\" columns=\"2\">\n  <tileoffset x=\"4\" y=\"8\"/>\n  <image source=\"4_color_sprite.png\" width=\"32\" height=\"32\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"2\" height=\"2\">\n  <data encoding=\"csv\">\n5,1,\n1,1\n</data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/test_tile_offset_staggered.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.10\" tiledversion=\"1.10.2\" orientation=\"staggered\" renderorder=\"right-down\" width=\"2\" height=\"2\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" staggeraxis=\"y\" staggerindex=\"odd\" backgroundcolor=\"#000000\" nextlayerid=\"2\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"no_tile_offset\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"4\" columns=\"2\">\n  <image source=\"4_color_sprite.png\" width=\"32\" height=\"32\"/>\n </tileset>\n <tileset firstgid=\"5\" name=\"tile_offset\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"4\" columns=\"2\">\n  <tileoffset x=\"2\" y=\"40\"/>\n  <image source=\"4_color_sprite.png\" width=\"32\" height=\"32\"/>\n </tileset>\n <layer id=\"1\" name=\"Tile Layer 1\" width=\"2\" height=\"2\">\n  <data encoding=\"csv\">\n5,1,\n1,1\n</data>\n </layer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/tiles/external_tileset_1.tsx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<tileset version=\"1.2\" tiledversion=\"2018.11.29\" name=\"level1\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"136\" columns=\"17\">\n<image source=\"../images/map-level1.png\" width=\"272\" height=\"128\"/>\n  <tile id=\"64\">\n   <properties>\n    <property name=\"type\" value=\"sky\"/>\n   </properties>\n  </tile>\n  <tile id=\"65\">\n   <properties>\n    <property name=\"type\" value=\"xpto\"/>\n   </properties>\n  </tile>\n</tileset>"
  },
  {
    "path": "packages/flame_tiled/test/assets/tiles/isometric_plain_1.tsx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<tileset version=\"1.9\" tiledversion=\"1.9.2\" name=\"isometric_plain_1\" tilewidth=\"128\" tileheight=\"148\" tilecount=\"6\" columns=\"0\">\n <grid orientation=\"orthogonal\" width=\"1\" height=\"1\"/>\n <tile id=\"0\">\n  <image width=\"128\" height=\"74\" source=\"../images/blue.png\"/>\n </tile>\n <tile id=\"1\">\n  <image width=\"128\" height=\"74\" source=\"../images/green.png\"/>\n </tile>\n <tile id=\"2\">\n  <image width=\"128\" height=\"74\" source=\"../images/monkey_pink.png\"/>\n </tile>\n <tile id=\"3\">\n  <image width=\"128\" height=\"74\" source=\"../images/orange.png\"/>\n </tile>\n <tile id=\"4\">\n  <image width=\"128\" height=\"74\" source=\"../images/peach.png\"/>\n </tile>\n <tile id=\"5\">\n  <image width=\"128\" height=\"148\" source=\"../images/purple_rock.png\"/>\n </tile>\n</tileset>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/tiles/samelevel_tileset_1.tsx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<tileset version=\"1.2\" tiledversion=\"2018.11.29\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"136\" columns=\"17\">\n<image source=\"samelevel-map-level1.png\" width=\"272\" height=\"128\"/>\n  <tile id=\"64\">\n   <properties>\n    <property name=\"type\" value=\"sky\"/>\n   </properties>\n  </tile>\n  <tile id=\"65\">\n   <properties>\n    <property name=\"type\" value=\"xpto\"/>\n   </properties>\n  </tile>\n</tileset>\n"
  },
  {
    "path": "packages/flame_tiled/test/assets/tiles_custom_path/external_tileset_custom_path.tsx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<tileset version=\"1.2\" tiledversion=\"2018.11.29\" name=\"level1\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"136\" columns=\"17\">\n<image source=\"../images/map-level1.png\" width=\"272\" height=\"128\"/>\n  <tile id=\"64\">\n   <properties>\n    <property name=\"type\" value=\"sky\"/>\n   </properties>\n  </tile>\n  <tile id=\"65\">\n   <properties>\n    <property name=\"type\" value=\"xpto\"/>\n   </properties>\n  </tile>\n</tileset>"
  },
  {
    "path": "packages/flame_tiled/test/assets/tiles_custom_path/map_custom_path.tmx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<map version=\"1.9\" tiledversion=\"1.9.0\" orientation=\"orthogonal\" renderorder=\"right-down\" width=\"32\" height=\"128\" tilewidth=\"16\" tileheight=\"16\" infinite=\"0\" backgroundcolor=\"#fdea18\" nextlayerid=\"5\" nextobjectid=\"1\">\n <tileset firstgid=\"1\" name=\"level1\" tilewidth=\"16\" tileheight=\"16\" tilecount=\"136\" columns=\"17\">\n  <image source=\"map-level1.png\" width=\"272\" height=\"128\"/>\n  <tile id=\"64\">\n   <properties>\n    <property name=\"type\" value=\"sky\"/>\n   </properties>\n  </tile>\n  <tile id=\"65\">\n   <properties>\n    <property name=\"type\" value=\"xpto\"/>\n   </properties>\n  </tile>\n </tileset>\n <layer id=\"1\" name=\"Ground\" width=\"32\" height=\"128\">\n  <data encoding=\"base64\">\n   QQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAAAAAAAAAAAAAAAAABBAAAAQQAAAEEAAAAAAAAAAAAAAAAAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAAAAAAAAAAAAAAAAAEEAAABBAAAAQQAAAAAAAAAAAAAAAAAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD8AAAA+AAAAQAAAAD8AAAA+AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAAAAAAAAAAAAAAAAAQQAAAEEAAABBAAAAAAAAAAAAAAAAAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPQAAAEEAAABBAAAAPQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAAAAAAAAAAAAAAAAABBAAAAQQAAAEEAAAAAAAAAAAAAAAAAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA9AAAAQQAAAEEAAAA9AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAAAAAAAQQAAAEEAAABBAAAAQQAAAEEAAAAAAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAD0AAABBAAAAQQAAAD0AAABBAAAAQQAAAEEAAAA7AAAAOwAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAAAAAABBAAAAQQAAAEEAAABBAAAAQQAAAAAAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAAAAAAEEAAABBAAAAQQAAAEEAAABBAAAAAAAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPwAAAD8AAAA+AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAD0AAABBAAAAQQAAAD0AAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPQAAAEEAAABBAAAAPQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA9AAAAQQAAAEEAAAA9AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAOwAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD8AAAA/AAAAPwAAAD8AAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD8AAAA/AAAAPwAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADcAAAA4AAAAOQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA3AAAAOAAAADkAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAADsAAAA7AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABBAAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAEEAAABBAAAAQQAAAEEAAABBAAAAOwAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA3AAAAOAAAADkAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAADsAAAA7AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAANwAAADgAAAA5AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA3AAAAOAAAADkAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA3AAAAOAAAADkAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAOwAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABBAAAAPAAAADQAAAA6AAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADcAAAA4AAAAOQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA3AAAAOAAAADkAAABBAAAAOwAAADwAAAA0AAAAOgAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAABcAAAAVAAAAFgAAAEEAAAA8AAAANAAAADoAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAAGAAAAFwAAABdAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD4AAABAAAAAPgAAAEAAAAA/AAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAABBAAAAQQAAAEEAAAA7AAAAOwAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD8AAAA/AAAAPgAAAEAAAAA/AAAAPwAAAD4AAABAAAAAPwAAAD4AAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADcAAAA4AAAAOQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAAA7AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEAAAAA+AAAAPgAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA9AAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAD0AAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADcAAAA4AAAAOQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAOwAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAOwAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAOwAAAEEAAAA7AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAANwAAADgAAAA5AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAANwAAADgAAAA5AAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA3AAAAOAAAADkAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAPAAAADQAAAA6AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABfAAAANQAAADYAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAADwAAAA0AAAAOgAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAPAAAADQAAAA6AAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADsAAAA8AAAANAAAADoAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAOwAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAXwAAADUAAAA2AAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAAA8AAAANAAAADoAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAANwAAADgAAAA5AAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABAAAAAPgAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQAAAAD4AAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAANwAAADgAAAA5AAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAOwAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABfAAAANQAAADYAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAANwAAADgAAAA5AAAAPAAAADQAAAA6AAAANwAAADgAAAA5AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABfAAAANQAAADYAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABAAAAAPgAAAEAAAAA+AAAAQQAAAF8AAAA1AAAANgAAADsAAABBAAAAQQAAADwAAAA0AAAAOgAAAF8AAAA1AAAANgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAF8AAAA1AAAANgAAAEEAAABBAAAAQQAAAD4AAABAAAAAPgAAAEEAAABBAAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAXwAAADUAAAA2AAAAXwAAADUAAAA2AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABfAAAANQAAADYAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABfAAAANQAAADYAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAOwAAAEEAAABBAAAAQQAAAEEAAAA8AAAANAAAADoAAAA3AAAAOAAAADkAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABfAAAANQAAADYAAABfAAAANQAAADYAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABBAAAAQQAAADsAAAA7AAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAF8AAAA1AAAANgAAAF8AAAA1AAAANgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAF8AAAA1AAAANgAAAF8AAAA1AAAANgAAAEEAAABBAAAAOwAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAPAAAADQAAAA6AAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABfAAAANQAAADYAAABfAAAANQAAADYAAABfAAAANQAAADYAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAF8AAAA1AAAANgAAAF8AAAA1AAAANgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAAEEAAABBAAAAQQAAAEEAAABBAAAAQQAAAEEAAABBAAAAPAAAADQAAAA6AAAAXwAAADUAAAA2AAAAPAAAAEAAAAA+AAAAawAAAGwAAABAAAAAPgAAADQAAAA6AAAAPAAAADQAAAA6AAAAXwAAADUAAAA2AAAAXwAAADUAAAA2AAAAQQAAAEEAAABBAAAAQQAAAEEAAAA7AAAAQQAAAEEAAABfAAAANQAAADYAAAA8AAAANAAAADoAAAA8AAAAPQAAAGsAAABsAAAAawAAAGwAAAA9AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA8AAAANAAAADoAAAA7AAAAQQAAAEEAAABBAAAAQQAAADsAAABBAAAAQQAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAABAAAAAPgAAAGsAAABsAAAAQAAAAD4AAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADwAAAA0AAAAOgAAADsAAABBAAAAQQAAAAoAAABBAAAAQQAAABcAAAAWAAAAFwAAABYAAAAXAAAAFgAAABcAAAAWAAAAFwAAABYAAAAXAAAAFgAAABcAAAAWAAAAFwAAABYAAAAXAAAAFgAAABcAAAAWAAAAFwAAABYAAAAXAAAAFgAAABcAAAAWAAAAQQAAAEEAAABBAAAACwAAAIEAAACCAAAAGAAAAF0AAAAYAAAAXQAAABgAAABdAAAAGAAAAF0AAAAYAAAAXQAAABgAAABdAAAAGAAAAF0AAAAYAAAAXQAAABgAAABdAAAAGAAAAF0AAAAYAAAAXQAAABgAAABdAAAAGAAAAF0AAABCAAAAggAAAEIAAAAMAAAAQwAAABoAAAAZAAAAGgAAAEQAAABDAAAAGQAAABkAAAAaAAAARAAAAEMAAAAZAAAAGQAAABoAAABEAAAAQwAAABkAAAAZAAAAGgAAAEQAAABDAAAAGQAAABkAAAAaAAAARAAAAEMAAAAZAAAARAAAAEMAAABEAAAAQwAAAA==\n  </data>\n </layer>\n <group id=\"2\" name=\"Background\" opacity=\"0.8\" offsetx=\"-50\" offsety=\"-100\" parallaxx=\"0.4\" parallaxy=\"0.4\">\n  <layer id=\"3\" name=\"Sky tiles\" width=\"32\" height=\"128\" opacity=\"0.9\" offsetx=\"-50\" offsety=\"-100\">\n   <data encoding=\"base64\">\n   AAAAAAAAAAAAAAAAAAAAAAAAAAA3AAAAOAAAADkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAA0AAAAOgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAADQAAAA6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAANAAAADoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAA0AAAAOgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAADQAAAA6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAANAAAADoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n  </data>\n  </layer>\n </group>\n <imagelayer id=\"4\" name=\"Sky artifact\" opacity=\"0.2\" offsetx=\"400\" offsety=\"0\" repeaty=\"1\">\n  <image source=\"image1.png\" width=\"32\" height=\"32\"/>\n </imagelayer>\n</map>\n"
  },
  {
    "path": "packages/flame_tiled/test/extensions_test.dart",
    "content": "import 'package:flame_tiled/flame_tiled.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('TiledObjectHelpers', () {\n    test('position returns correct values', () {\n      final tiledObject = TiledObject(id: 0);\n\n      expect(tiledObject.position.x, 0);\n      expect(tiledObject.position.y, 0);\n\n      tiledObject.x = 26;\n      tiledObject.y = 83;\n\n      expect(tiledObject.position.x, 26);\n      expect(tiledObject.position.y, 83);\n    });\n\n    test('size returns correct values', () {\n      final tiledObject = TiledObject(id: 0);\n\n      expect(tiledObject.size.x, 0);\n      expect(tiledObject.size.y, 0);\n\n      tiledObject.width = 59;\n      tiledObject.height = 42;\n\n      expect(tiledObject.size.x, 59);\n      expect(tiledObject.size.y, 42);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_tiled/test/image_layer_test.dart",
    "content": "import 'package:flame/cache.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_tiled/flame_tiled.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'test_asset_bundle.dart';\nimport 'test_image_utils.dart';\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  setUp(TiledAtlas.atlasMap.clear);\n\n  group('ImageLayer rendering', () {\n    test(\n      'image layer covers entire map',\n      () async {\n        final bundle = TestAssetBundle(\n          imageNames: [\n            'Tileset_Hexagonal_PointyTop_60x52_60x80.png',\n            'images/gear.png',\n            'green_sprite.png',\n            'red_sprite.png',\n          ],\n          stringNames: ['image_layer_full_screen.tmx'],\n        );\n\n        final component = await TiledComponent.load(\n          'image_layer_full_screen.tmx',\n          Vector2.all(16),\n          bundle: bundle,\n          images: Images(bundle: bundle),\n        );\n\n        // The map is 15x15 tiles at 16x16, so the total size is 240x240\n        expect(component.size, Vector2(240, 240));\n\n        // Initialize game context\n        final game = FlameGame();\n        game.onGameResize(component.size);\n        game.world.add(component);\n        await component.onLoad();\n        await game.ready();\n\n        final pngData = await renderMapToPng(component);\n\n        expect(\n          pngData,\n          matchesGoldenFile('goldens/image_layer_covers_map.png'),\n        );\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/flame_tiled/test/rectangle_bin_packer_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame_tiled/src/rectangle_bin_packer.dart';\n\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('RectangleBinPacker', () {\n    late RectangleBinPacker bin;\n    setUp(() {\n      bin = RectangleBinPacker(1024, 1024);\n    });\n\n    test('asserts oversized requests', () {\n      expect(() => bin.pack(bin.maxX + 1, bin.maxY), throwsAssertionError);\n      expect(() => bin.pack(bin.maxX, bin.maxY + 1), throwsAssertionError);\n      expect(bin.bins, hasLength(1));\n      expect(bin.bins.first.top, 0);\n      expect(bin.bins.first.left, 0);\n      expect(bin.bins.first.width, bin.maxX);\n      expect(bin.bins.first.height, bin.maxY);\n    });\n\n    test('handles full request', () {\n      expect(\n        bin.pack(bin.maxX, bin.maxY),\n        Rect.fromLTWH(\n          0,\n          0,\n          bin.maxX,\n          bin.maxY,\n        ),\n      );\n      expect(bin.bins, hasLength(0));\n    });\n\n    test('bifurcates with one bottom bin when rect is full width', () {\n      expect(\n        bin.pack(bin.maxX, 10),\n        Rect.fromLTWH(\n          0,\n          0,\n          bin.maxX,\n          10,\n        ),\n      );\n      expect(bin.bins, hasLength(1));\n      expect(bin.bins.first.top, 10);\n      expect(bin.bins.first.left, 0);\n      expect(bin.bins.first.width, bin.maxX);\n      expect(bin.bins.first.height, bin.maxY - 10);\n    });\n\n    test('bifurcates with one right bin when rect is full width', () {\n      expect(\n        bin.pack(10, bin.maxY),\n        Rect.fromLTWH(\n          0,\n          0,\n          10,\n          bin.maxY,\n        ),\n      );\n      expect(bin.bins, hasLength(1));\n      expect(bin.bins.first.top, 0);\n      expect(bin.bins.first.left, 10);\n      expect(bin.bins.first.width, bin.maxX - 10);\n      expect(bin.bins.first.height, bin.maxY);\n    });\n\n    test('sanity check', () {\n      final colCount = bin.maxX ~/ 512;\n      final rowCount = bin.maxY ~/ 256;\n      for (var y = 0; y < rowCount; y++) {\n        for (var x = 0; x < colCount; x++) {\n          expect(bin.pack(512, 256), isNot(Rect.zero));\n        }\n      }\n      expect(bin.bins, isEmpty, reason: 'filled up atlas');\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_tiled/test/test_asset_bundle.dart",
    "content": "import 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:flutter/services.dart' show CachingAssetBundle;\n\nclass TestAssetBundle extends CachingAssetBundle {\n  TestAssetBundle({\n    required this.imageNames,\n    required this.stringNames,\n  });\n\n  final List<String> imageNames;\n  final List<String> stringNames;\n\n  @override\n  Future<ByteData> load(String key) async {\n    late String imgName;\n    late String fileName;\n    if (key.contains('..')) {\n      final parts = key.split('/');\n\n      final index = parts.indexOf('..');\n\n      imgName = parts.sublist(index + 1).join('/');\n\n      fileName = key.replaceFirst('assets/images/', 'test/assets/');\n    } else {\n      final pattern = RegExp(r'assets/images/(\\.\\./)*');\n      final split = key.split('/');\n      imgName = split.isNotEmpty ? key.replaceFirst(pattern, '') : key;\n\n      final toLoadName = key.replaceFirst(pattern, '');\n      fileName = 'test/assets/$toLoadName';\n    }\n\n    if (!imageNames.contains(imgName)) {\n      throw StateError(\n        'No $fileName found in the TestAssetBundle. Did you forget to add it?',\n      );\n    }\n    return File(fileName).readAsBytes().then(\n      (bytes) => ByteData.view(Uint8List.fromList(bytes).buffer),\n    );\n  }\n\n  @override\n  Future<String> loadString(String key, {bool cache = true}) {\n    final pattern = RegExp(r'assets/tiles/(\\.\\./)*');\n    final split = key.split('/');\n    final mapName = split.isNotEmpty ? key.replaceFirst(pattern, '') : key;\n\n    final toLoadName = key.replaceFirst(pattern, '');\n    final fileName = 'test/assets/$toLoadName';\n\n    if (!stringNames.contains(mapName)) {\n      throw StateError(\n        'No $fileName found in the TestAssetBundle. Did you forget to add it?',\n      );\n    }\n\n    return File(fileName).readAsString();\n  }\n}\n"
  },
  {
    "path": "packages/flame_tiled/test/test_image_utils.dart",
    "content": "import 'dart:typed_data';\nimport 'dart:ui';\n\nimport 'package:flame/extensions.dart';\nimport 'package:flame_tiled/flame_tiled.dart';\n\nFuture<Uint8List> renderMapToPng(\n  TiledComponent component,\n) async {\n  final canvasRecorder = PictureRecorder();\n  final canvas = Canvas(canvasRecorder);\n  component.tileMap.render(canvas);\n  final picture = canvasRecorder.endRecording();\n\n  final size = component.size;\n  // Map size is now 320 wide, but it has 1 extra tile of height because\n  // its actually double-height tiles.\n  final image = await picture.toImageSafe(size.x.toInt(), size.y.toInt());\n  return imageToPng(image);\n}\n\nFuture<Uint8List> imageToPng(Image image) async =>\n    (await image.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();\n"
  },
  {
    "path": "packages/flame_tiled/test/tile_atlas_test.dart",
    "content": "import 'package:flame/cache.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame_tiled/flame_tiled.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'test_asset_bundle.dart';\nimport 'test_image_utils.dart';\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('TiledAtlas', () {\n    test('atlasKey returns sorted images', () {\n      expect(\n        TiledAtlas.atlasKey([\n          const TiledImage(source: 'foo'),\n          const TiledImage(source: 'bar'),\n          const TiledImage(source: 'baz'),\n        ]),\n        'atlas{bar,baz,foo}',\n      );\n    });\n\n    group('loadImages', () {\n      late AssetBundle bundle;\n\n      setUp(() {\n        TiledAtlas.atlasMap.clear();\n        bundle = TestAssetBundle(\n          imageNames: [\n            'images/blue.png',\n            'images/purple_rock.png',\n            'images/box2.png',\n            'images/diamond.png',\n            'images/gear.png',\n            'images/green.png',\n            'images/monkey_pink.png',\n            'images/orange.png',\n            'images/peach.png',\n          ],\n          stringNames: [\n            'isometric_plain.tmx',\n            'tiles/isometric_plain_1.tsx',\n          ],\n        );\n      });\n\n      test('handles empty map', () async {\n        final atlas = await TiledAtlas.fromTiledMap(\n          TiledMap(height: 1, tileHeight: 1, tileWidth: 1, width: 1),\n          images: Images(bundle: bundle),\n        );\n\n        expect(atlas.atlas, isNull);\n        expect(atlas.offsets, isEmpty);\n        expect(atlas.batch, isNull);\n        expect(atlas.key, 'atlas{empty}');\n        expect(atlas.clone().key, 'atlas{empty}');\n      });\n\n      final simpleMap = TiledMap(\n        height: 1,\n        tileHeight: 1,\n        tileWidth: 1,\n        width: 1,\n        tilesets: [\n          Tileset(\n            tileWidth: 128,\n            tileHeight: 74,\n            tileCount: 1,\n            image: const TiledImage(\n              source: 'images/green.png',\n              width: 128,\n              height: 74,\n            ),\n          ),\n        ],\n      );\n\n      final relativePathSingleImageMap = TiledMap(\n        height: 1,\n        tileHeight: 1,\n        tileWidth: 1,\n        width: 1,\n        tilesets: [\n          Tileset(\n            // Simulate an external TSX tileset referenced by the map.\n            source: 'tiles/isometric_plain_1.tsx',\n            tileWidth: 128,\n            tileHeight: 74,\n            tileCount: 1,\n            // Simulate an image path that is relative to the TSX file.\n            image: const TiledImage(\n              source: '../images/green.png',\n              width: 128,\n              height: 74,\n            ),\n          ),\n        ],\n      );\n\n      test('returns single image atlas for simple map', () async {\n        final images = Images(bundle: bundle);\n        final atlas = await TiledAtlas.fromTiledMap(\n          simpleMap,\n          images: images,\n        );\n\n        expect(atlas.offsets, hasLength(1));\n        expect(atlas.atlas, isNotNull);\n        expect(atlas.atlas!.width, 128);\n        expect(atlas.atlas!.height, 74);\n        expect(atlas.key, 'images/green.png');\n\n        expect(images.containsKey('images/green.png'), isTrue);\n        expect(images.keys, hasLength(1));\n\n        expect(\n          await imageToPng(atlas.atlas!),\n          matchesGoldenFile('goldens/single_atlas.png'),\n        );\n      });\n\n      test(\n        'single image tileset with relative image path loads correctly',\n        () async {\n          final images = Images(bundle: bundle);\n          final atlas = await TiledAtlas.fromTiledMap(\n            relativePathSingleImageMap,\n            images: images,\n          );\n\n          expect(atlas.offsets, hasLength(1));\n          expect(atlas.atlas, isNotNull);\n          expect(atlas.key, '../images/green.png');\n        },\n      );\n\n      test('returns cached atlas', () async {\n        final atlas1 = await TiledAtlas.fromTiledMap(\n          simpleMap,\n          images: Images(bundle: bundle),\n        );\n        final atlas2 = await TiledAtlas.fromTiledMap(\n          simpleMap,\n          images: Images(bundle: bundle),\n        );\n\n        expect(atlas1, isNot(same(atlas2)));\n        expect(atlas1.key, atlas2.key);\n        expect(atlas2.atlas!.isCloneOf(atlas2.atlas!), isTrue);\n      });\n\n      test('packs complex maps with multiple images', () async {\n        final component = await TiledComponent.load(\n          'isometric_plain.tmx',\n          Vector2(128, 74),\n          bundle: bundle,\n          images: Images(bundle: bundle),\n        );\n\n        final atlas = TiledAtlas.atlasMap.values.first;\n        expect(\n          await imageToPng(atlas.atlas!),\n          matchesGoldenFile('goldens/larger_atlas.png'),\n        );\n        expect(\n          renderMapToPng(component),\n          matchesGoldenFile('goldens/larger_atlas_component.png'),\n        );\n      });\n\n      test(\n        'packs complex maps with multiple images using a custom spacing',\n        () async {\n          final component = await TiledComponent.load(\n            'isometric_plain.tmx',\n            Vector2(128, 74),\n            bundle: bundle,\n            images: Images(bundle: bundle),\n            atlasPackingSpacingX: 2,\n            atlasPackingSpacingY: 2,\n          );\n\n          final atlas = TiledAtlas.atlasMap.values.first;\n          expect(\n            await imageToPng(atlas.atlas!),\n            matchesGoldenFile('goldens/larger_atlas_with_spacing.png'),\n          );\n          expect(\n            renderMapToPng(component),\n            matchesGoldenFile(\n              'goldens/larger_atlas_component_with_spacing.png',\n            ),\n          );\n        },\n      );\n\n      test('can ignore tilesets in the packing', () async {\n        await TiledComponent.load(\n          'isometric_plain.tmx',\n          Vector2(128, 74),\n          bundle: bundle,\n          images: Images(bundle: bundle),\n          tsxPackingFilter: (tileset) => tileset.name != 'isometric_plain_2',\n        );\n\n        final atlas = TiledAtlas.atlasMap.values.first;\n        expect(\n          await imageToPng(atlas.atlas!),\n          matchesGoldenFile('goldens/larger_atlas_with_skipped_tileset.png'),\n        );\n      });\n\n      test('clearing cache', () async {\n        await TiledAtlas.fromTiledMap(\n          simpleMap,\n          images: Images(bundle: bundle),\n        );\n\n        expect(TiledAtlas.atlasMap.isNotEmpty, true);\n\n        TiledAtlas.clearCache();\n\n        expect(TiledAtlas.atlasMap.isEmpty, true);\n      });\n    });\n\n    group('Single tileset map', () {\n      late AssetBundle bundle;\n\n      setUp(() {\n        TiledAtlas.atlasMap.clear();\n        bundle = TestAssetBundle(\n          imageNames: [\n            '4_color_sprite.png',\n          ],\n          stringNames: [\n            'single_tile_map_1.tmx',\n            'single_tile_map_2.tmx',\n          ],\n        );\n      });\n\n      test(\n        '''Two maps with a same tileset but different tile alignment should be rendered differently''',\n        () async {\n          final components = await Future.wait([\n            TiledComponent.load(\n              'single_tile_map_1.tmx',\n              Vector2(16, 16),\n              bundle: bundle,\n              images: Images(bundle: bundle),\n            ),\n            TiledComponent.load(\n              'single_tile_map_2.tmx',\n              Vector2(16, 16),\n              bundle: bundle,\n              images: Images(bundle: bundle),\n            ),\n          ]);\n\n          final atlas = TiledAtlas.atlasMap.values.first;\n          final imageRendered_1 = renderMapToPng(components[0]);\n          final imageRendered_2 = renderMapToPng(components[1]);\n\n          expect(TiledAtlas.atlasMap.length, 1);\n          expect(\n            await imageToPng(atlas.atlas!),\n            matchesGoldenFile('goldens/single_tile_atlas.png'),\n          );\n          expect(imageRendered_1, isNot(same(imageRendered_2)));\n          expect(\n            imageRendered_1,\n            matchesGoldenFile('goldens/single_tile_map_1.png'),\n          );\n          expect(\n            imageRendered_2,\n            matchesGoldenFile('goldens/single_tile_map_2.png'),\n          );\n        },\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_tiled/test/tiled_component_sizes_test.dart",
    "content": "import 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_tiled/flame_tiled.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('TiledComponent.computeSize', () {\n    test('orthogonal', () {\n      expect(\n        TiledComponent.computeSize(\n          MapOrientation.orthogonal,\n          Vector2(16, 16),\n          16,\n          16,\n          5,\n          5,\n          null,\n        ),\n        Vector2(80, 80),\n        reason: 'full sized tiles',\n      );\n\n      expect(\n        TiledComponent.computeSize(\n          MapOrientation.orthogonal,\n          Vector2(8, 8),\n          16,\n          16,\n          5,\n          4,\n          null,\n        ),\n        Vector2(40, 32),\n        reason: 'half sized tiles',\n      );\n      expect(\n        TiledComponent.computeSize(\n          MapOrientation.orthogonal,\n          Vector2(8, 32),\n          16,\n          16,\n          4,\n          5,\n          null,\n        ),\n        Vector2(32, 160),\n        reason: 'odd sized tiles',\n      );\n    });\n\n    test('isometric', () {\n      expect(\n        TiledComponent.computeSize(\n          MapOrientation.isometric,\n          Vector2(16, 16),\n          16,\n          16,\n          5,\n          4,\n          null,\n        ),\n        Vector2(72, 72),\n        reason: 'full sized tiles',\n      );\n\n      expect(\n        TiledComponent.computeSize(\n          MapOrientation.isometric,\n          Vector2(8, 8),\n          16,\n          16,\n          4,\n          5,\n          null,\n        ),\n        Vector2(36, 36),\n        reason: 'full sized tiles',\n      );\n\n      expect(\n        TiledComponent.computeSize(\n          MapOrientation.isometric,\n          Vector2(8, 32),\n          16,\n          16,\n          7,\n          5,\n          null,\n        ),\n        Vector2(48, 192),\n        reason: 'odd sized tiles',\n      );\n    });\n\n    test('isometric', () {\n      expect(\n        TiledComponent.computeSize(\n          MapOrientation.isometric,\n          Vector2(16, 16),\n          16,\n          16,\n          5,\n          4,\n          null,\n        ),\n        Vector2(72, 72),\n        reason: 'full sized tiles',\n      );\n\n      expect(\n        TiledComponent.computeSize(\n          MapOrientation.isometric,\n          Vector2(8, 8),\n          16,\n          16,\n          4,\n          5,\n          null,\n        ),\n        Vector2(36, 36),\n        reason: 'full sized tiles',\n      );\n\n      expect(\n        TiledComponent.computeSize(\n          MapOrientation.isometric,\n          Vector2(8, 32),\n          16,\n          16,\n          7,\n          5,\n          null,\n        ),\n        Vector2(48, 192),\n        reason: 'odd sized tiles',\n      );\n    });\n\n    test('hexagonal', () {\n      expect(\n        TiledComponent.computeSize(\n          MapOrientation.hexagonal,\n          Vector2(16, 16),\n          16,\n          16,\n          4,\n          5,\n          StaggerAxis.x,\n        ),\n        Vector2(52, 88),\n        reason: 'full sized tiles, stagger x',\n      );\n\n      expect(\n        TiledComponent.computeSize(\n          MapOrientation.hexagonal,\n          Vector2(16, 16),\n          16,\n          16,\n          4,\n          5,\n          StaggerAxis.y,\n        ),\n        Vector2(72, 64),\n        reason: 'full sized tiles, stagger y',\n      );\n\n      expect(\n        TiledComponent.computeSize(\n          MapOrientation.hexagonal,\n          Vector2(8, 8),\n          16,\n          16,\n          4,\n          5,\n          StaggerAxis.x,\n        ),\n        Vector2(26, 44),\n        reason: 'half sized tiles, stagger x',\n      );\n\n      expect(\n        TiledComponent.computeSize(\n          MapOrientation.hexagonal,\n          Vector2(8, 8),\n          16,\n          16,\n          4,\n          5,\n          StaggerAxis.y,\n        ),\n        Vector2(36, 32),\n        reason: 'full sized tiles, stagger y',\n      );\n    });\n\n    test('staggered', () {\n      expect(\n        TiledComponent.computeSize(\n          MapOrientation.staggered,\n          Vector2(16, 16),\n          16,\n          16,\n          4,\n          5,\n          StaggerAxis.x,\n        ),\n        Vector2(40, 88),\n        reason: 'full sized tiles, stagger x',\n      );\n\n      expect(\n        TiledComponent.computeSize(\n          MapOrientation.staggered,\n          Vector2(16, 16),\n          16,\n          16,\n          4,\n          5,\n          StaggerAxis.y,\n        ),\n        Vector2(72, 48),\n        reason: 'full sized tiles, stagger y',\n      );\n\n      expect(\n        TiledComponent.computeSize(\n          MapOrientation.staggered,\n          Vector2(8, 8),\n          16,\n          16,\n          4,\n          5,\n          StaggerAxis.x,\n        ),\n        Vector2(20, 44),\n        reason: 'half sized tiles, stagger x',\n      );\n\n      expect(\n        TiledComponent.computeSize(\n          MapOrientation.staggered,\n          Vector2(8, 8),\n          16,\n          16,\n          4,\n          5,\n          StaggerAxis.y,\n        ),\n        Vector2(36, 24),\n        reason: 'half sized tiles, stagger y',\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "packages/flame_tiled/test/tiled_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flame/cache.dart';\nimport 'package:flame/components.dart';\nimport 'package:flame/extensions.dart';\nimport 'package:flame/flame.dart';\nimport 'package:flame/game.dart';\nimport 'package:flame_tiled/flame_tiled.dart';\nimport 'package:flame_tiled/src/renderable_layers/group_layer.dart';\nimport 'package:flame_tiled/src/renderable_layers/tile_layers/tile_layer.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'test_asset_bundle.dart';\nimport 'test_image_utils.dart';\n\nvoid main() {\n  /// This represents the byte count of one pixel.\n  ///\n  /// Usually, Color is represented as [Uint8List] and Uint8 has the ability to\n  /// store 0 - 255(8 bit = 1 byte) per index. And it can be interpreted\n  /// as [Color] by using 4 indexes of [Uint8List] into one.\n  /// Examples:\n  ///   RGBA [255, 0, 0 255] => red,\n  ///   RGBA [255, 255, 0 255] => Yellow.\n  const pixel = 4;\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  setUp(TiledAtlas.atlasMap.clear);\n  group('TiledComponent', () {\n    late TiledComponent tiled;\n    setUp(() async {\n      Flame.bundle = TestAssetBundle(\n        imageNames: ['map-level1.png', 'image1.png'],\n        stringNames: ['map.tmx', 'tiles_custom_path/map_custom_path.tmx'],\n      );\n      tiled = await TiledComponent.load(\n        'map.tmx',\n        Vector2.all(16),\n        key: ComponentKey.named('test'),\n      );\n    });\n\n    test('correct loads the file', () {\n      expect(tiled.tileMap.renderableLayers.length, equals(4));\n    });\n\n    test('component atlases returns the loaded atlases', () {\n      final atlases = tiled.atlases();\n      expect(atlases, hasLength(1));\n      expect(atlases.first.$1, equals('map-level1.png'));\n    });\n\n    test('correct loads the file, with different prefix', () async {\n      tiled = await TiledComponent.load(\n        'map_custom_path.tmx',\n        Vector2.all(16),\n        prefix: 'assets/tiles/tiles_custom_path/',\n      );\n\n      expect(tiled.tileMap.renderableLayers.length, equals(3));\n    });\n\n    test('throws assertion error if fileName contains a path', () async {\n      expectLater(\n        TiledComponent.load(\n          'path/to/map.tmx',\n          Vector2.all(16),\n        ),\n        throwsAssertionError,\n      );\n    });\n\n    test('assigns key', () async {\n      expect(tiled.key, equals(ComponentKey.named('test')));\n    });\n\n    group('is positionable', () {\n      test('size, width, and height are readable - not writable', () {\n        expect(tiled.size, Vector2(512.0, 2048.0));\n        expect(tiled.width, 512);\n        expect(tiled.height, 2048);\n\n        tiled.size = Vector2(256, 1024);\n        expect(tiled.size, Vector2(512.0, 2048.0));\n        tiled.width = 2;\n        expect(tiled.size, Vector2(512.0, 2048.0));\n        tiled.height = 2;\n        expect(tiled.size, Vector2(512.0, 2048.0));\n      });\n\n      test('from constructor', () {\n        final map = TiledComponent(\n          tiled.tileMap,\n          position: Vector2(10, 20),\n          anchor: Anchor.bottomCenter,\n          children: [tiled],\n          angle: 1.4,\n          priority: 2,\n          scale: Vector2(1.5, 2.0),\n        );\n\n        expect(tiled.parent, map);\n        expect(map.anchor, Anchor.bottomCenter);\n        expect(map.angle, 1.4);\n        expect(map.priority, 2);\n        expect(map.position, Vector2(10, 20));\n        expect(map.scale, Vector2(1.5, 2.0));\n      });\n    });\n  });\n\n  test('correctly loads external tileset', () async {\n    // Flame.bundle is a global static. Updating these in tests can lead to\n    // odd errors if you're trying to debug.\n    Flame.bundle = TestAssetBundle(\n      imageNames: ['map-level1.png', 'image1.png'],\n      stringNames: ['map.tmx', 'tiles/external_tileset_1.tsx'],\n    );\n\n    final tsxProvider = await FlameTsxProvider.parse(\n      'tiles/external_tileset_1.tsx',\n      Flame.bundle,\n    );\n\n    expect(tsxProvider.getCachedSource() != null, true);\n    final source = tsxProvider.getCachedSource()!;\n    expect(source.getStringOrNull('name'), 'level1');\n    expect(source.getSingleChildOrNull('image'), isNotNull);\n    expect(\n      source.getSingleChildOrNull('image')!.getStringOrNull('width'),\n      '272',\n    );\n\n    expect(\n      tsxProvider.filename == 'tiles/external_tileset_1.tsx',\n      true,\n    );\n  });\n\n  test('correctly loads external tileset with custom path', () async {\n    // Flame.bundle is a global static. Updating these in tests can lead to\n    // odd errors if you're trying to debug.\n    Flame.bundle = TestAssetBundle(\n      imageNames: ['map-level1.png', 'image1.png'],\n      stringNames: [\n        'map.tmx',\n        'tiles_custom_path/external_tileset_custom_path.tsx',\n      ],\n    );\n\n    // TestAssetBundle strips assets/tiles/ from the prefix.\n    final tsxProvider = await FlameTsxProvider.parse(\n      'external_tileset_custom_path.tsx',\n      Flame.bundle,\n      'assets/tiles/tiles_custom_path/',\n    );\n\n    expect(tsxProvider.getCachedSource() != null, true);\n    final source = tsxProvider.getCachedSource()!;\n    expect(source.getStringOrNull('name'), 'level1');\n    expect(source.getSingleChildOrNull('image'), isNotNull);\n    expect(\n      source.getSingleChildOrNull('image')!.getStringOrNull('width'),\n      '272',\n    );\n\n    expect(\n      tsxProvider.filename == 'external_tileset_custom_path.tsx',\n      true,\n    );\n  });\n\n  group('Layered tiles render correctly with layered sprite batch', () {\n    late Uint8List canvasPixelData;\n    late RenderableTiledMap overlapMap;\n    setUp(() async {\n      final bundle = TestAssetBundle(\n        imageNames: [\n          'green_sprite.png',\n          'red_sprite.png',\n        ],\n        stringNames: ['2_tiles-green_on_red.tmx'],\n      );\n      overlapMap = await RenderableTiledMap.fromFile(\n        '2_tiles-green_on_red.tmx',\n        Vector2.all(16),\n        bundle: bundle,\n        images: Images(bundle: bundle),\n      );\n      final canvasRecorder = PictureRecorder();\n      final canvas = Canvas(canvasRecorder);\n      overlapMap.render(canvas);\n      final picture = canvasRecorder.endRecording();\n\n      final image = await picture.toImageSafe(32, 16);\n      final bytes = await image.toByteData();\n      canvasPixelData = bytes!.buffer.asUint8List();\n    });\n\n    test(\n      'Correctly loads batches list',\n      () => expect(overlapMap.renderableLayers.length == 2, true),\n    );\n\n    test(\n      'Canvas pixel dimensions match',\n      () => expect(\n        canvasPixelData.length == 16 * 32 * pixel,\n        true,\n      ),\n    );\n\n    test('Base test - right tile pixel is red', () {\n      expect(\n        canvasPixelData[16 * pixel] == 255 &&\n            canvasPixelData[(16 * pixel) + 1] == 0 &&\n            canvasPixelData[(16 * pixel) + 2] == 0 &&\n            canvasPixelData[(16 * pixel) + 3] == 255,\n        true,\n      );\n      final rightTilePixels = <int>[];\n      for (var i = 16 * pixel; i < 16 * 32 * pixel; i += 32 * pixel) {\n        rightTilePixels.addAll(canvasPixelData.getRange(i, i + (16 * pixel)));\n      }\n\n      var allRed = true;\n      for (var i = 0; i < rightTilePixels.length; i += pixel) {\n        allRed &=\n            rightTilePixels[i] == 255 &&\n            rightTilePixels[i + 1] == 0 &&\n            rightTilePixels[i + 2] == 0 &&\n            rightTilePixels[i + 3] == 255;\n      }\n      expect(allRed, true);\n    });\n\n    test('Left tile pixel is green', () {\n      expect(\n        canvasPixelData[15 * pixel] == 0 &&\n            canvasPixelData[(15 * pixel) + 1] == 255 &&\n            canvasPixelData[(15 * pixel) + 2] == 0 &&\n            canvasPixelData[(15 * pixel) + 3] == 255,\n        true,\n      );\n\n      final leftTilePixels = <int>[];\n      for (var i = 0; i < 15 * 32 * pixel; i += 32 * pixel) {\n        leftTilePixels.addAll(canvasPixelData.getRange(i, i + (16 * pixel)));\n      }\n\n      var allGreen = true;\n      for (var i = 0; i < leftTilePixels.length; i += pixel) {\n        allGreen &=\n            leftTilePixels[i] == 0 &&\n            leftTilePixels[i + 1] == 255 &&\n            leftTilePixels[i + 2] == 0 &&\n            leftTilePixels[i + 3] == 255;\n      }\n      expect(allGreen, true);\n    });\n  });\n\n  group('Flipped and rotated tiles render correctly with sprite batch:', () {\n    late Uint8List pixelsBeforeFlipApplied;\n    late Uint8List pixelsAfterFlipApplied;\n    late RenderableTiledMap overlapMap;\n\n    Future<Uint8List> renderMap() async {\n      final canvasRecorder = PictureRecorder();\n      final canvas = Canvas(canvasRecorder);\n      overlapMap.render(canvas);\n      final picture = canvasRecorder.endRecording();\n\n      final image = await picture.toImageSafe(64, 32);\n      final bytes = await image.toByteData();\n      return bytes!.buffer.asUint8List();\n    }\n\n    setUp(() async {\n      final bundle = TestAssetBundle(\n        imageNames: [\n          '4_color_sprite.png',\n        ],\n        stringNames: ['8_tiles-flips.tmx'],\n      );\n      overlapMap = await RenderableTiledMap.fromFile(\n        '8_tiles-flips.tmx',\n        Vector2.all(16),\n        bundle: bundle,\n        images: Images(bundle: bundle),\n      );\n\n      pixelsBeforeFlipApplied = await renderMap();\n      await Flame.images.ready();\n      pixelsAfterFlipApplied = await renderMap();\n    });\n\n    test('[useAtlas = true] Green tile pixels are in correct spots', () {\n      const oneColorRect = 8;\n      final leftTilePixels = <int>[];\n      for (\n        var i = 65 * oneColorRect * pixel;\n        i < ((64 * 23) + (oneColorRect * 3)) * pixel;\n        i += 64 * pixel\n      ) {\n        leftTilePixels.addAll(\n          pixelsAfterFlipApplied.getRange(i, i + (16 * pixel)),\n        );\n      }\n\n      var allGreen = true;\n      for (var i = 0; i < leftTilePixels.length; i += pixel) {\n        allGreen &=\n            leftTilePixels[i] == 0 &&\n            leftTilePixels[i + 1] == 255 &&\n            leftTilePixels[i + 2] == 0 &&\n            leftTilePixels[i + 3] == 255;\n      }\n      expect(allGreen, true);\n\n      final rightTilePixels = <int>[];\n      for (\n        var i = 69 * 8 * pixel;\n        i < ((64 * 23) + (8 * 7)) * pixel;\n        i += 64 * pixel\n      ) {\n        rightTilePixels.addAll(\n          pixelsAfterFlipApplied.getRange(i, i + (16 * pixel)),\n        );\n      }\n\n      for (var i = 0; i < rightTilePixels.length; i += pixel) {\n        allGreen &=\n            rightTilePixels[i] == 0 &&\n            rightTilePixels[i + 1] == 255 &&\n            rightTilePixels[i + 2] == 0 &&\n            rightTilePixels[i + 3] == 255;\n      }\n      expect(allGreen, true);\n    });\n\n    test('[useAtlas = false] Green tile pixels are in correct spots', () {\n      final leftTilePixels = <int>[];\n      for (\n        var i = 65 * 8 * pixel;\n        i < ((64 * 23) + (8 * 3)) * pixel;\n        i += 64 * pixel\n      ) {\n        leftTilePixels.addAll(\n          pixelsBeforeFlipApplied.getRange(i, i + (16 * pixel)),\n        );\n      }\n\n      var allGreen = true;\n      for (var i = 0; i < leftTilePixels.length; i += pixel) {\n        allGreen &=\n            leftTilePixels[i] == 0 &&\n            leftTilePixels[i + 1] == 255 &&\n            leftTilePixels[i + 2] == 0 &&\n            leftTilePixels[i + 3] == 255;\n      }\n      expect(allGreen, true);\n\n      final rightTilePixels = <int>[];\n      for (\n        var i = 69 * 8 * pixel;\n        i < ((64 * 23) + (8 * 7)) * pixel;\n        i += 64 * pixel\n      ) {\n        rightTilePixels.addAll(\n          pixelsBeforeFlipApplied.getRange(i, i + (16 * pixel)),\n        );\n      }\n\n      for (var i = 0; i < rightTilePixels.length; i += pixel) {\n        allGreen &=\n            rightTilePixels[i] == 0 &&\n            rightTilePixels[i + 1] == 255 &&\n            rightTilePixels[i + 2] == 0 &&\n            rightTilePixels[i + 3] == 255;\n      }\n      expect(allGreen, true);\n    });\n  });\n\n  group('ignoring flip makes different texture and rendering result', () {\n    Image? texture;\n    Uint8List? rendered;\n\n    Future<void> prepareForGolden({required bool ignoreFlip}) async {\n      final bundle = TestAssetBundle(\n        imageNames: [\n          '4_color_sprite.png',\n        ],\n        stringNames: ['8_tiles-flips.tmx'],\n      );\n      final tiledComponent = TiledComponent(\n        await RenderableTiledMap.fromFile(\n          '8_tiles-flips.tmx',\n          Vector2.all(16),\n          ignoreFlip: ignoreFlip,\n          bundle: bundle,\n          images: Images(bundle: bundle),\n        ),\n      );\n\n      await Flame.images.ready();\n\n      texture = (tiledComponent.tileMap.renderableLayers[0] as FlameTileLayer)\n          .tiledAtlas\n          .batch\n          ?.atlas;\n\n      rendered = await renderMapToPng(tiledComponent);\n    }\n\n    test('flip works with [ignoreFlip = false]', () async {\n      await prepareForGolden(ignoreFlip: false);\n      expect(texture, matchesGoldenFile('goldens/texture_with_flip.png'));\n      expect(rendered, matchesGoldenFile('goldens/rendered_with_flip.png'));\n    });\n\n    test('flip ignored with [ignoreFlip = true]', () async {\n      await prepareForGolden(ignoreFlip: true);\n      expect(\n        texture,\n        matchesGoldenFile('goldens/texture_with_flip_ignored.png'),\n      );\n      expect(\n        rendered,\n        matchesGoldenFile('goldens/rendered_with_flip_ignored.png'),\n      );\n    });\n  });\n\n  group('Test getLayer:', () {\n    late RenderableTiledMap renderableTiledMap;\n    setUp(() async {\n      Flame.bundle = TestAssetBundle(\n        imageNames: ['map-level1.png'],\n        stringNames: ['layers_test.tmx'],\n      );\n      renderableTiledMap = await RenderableTiledMap.fromFile(\n        'layers_test.tmx',\n        Vector2.all(32),\n        bundle: Flame.bundle,\n      );\n    });\n\n    test('Get Tile Layer', () {\n      expect(\n        renderableTiledMap.getLayer<TileLayer>('MyTileLayer'),\n        isNotNull,\n      );\n    });\n\n    test('Get Object Layer', () {\n      expect(\n        renderableTiledMap.getLayer<ObjectGroup>('MyObjectLayer'),\n        isNotNull,\n      );\n    });\n\n    test('Get Image Layer', () {\n      expect(\n        renderableTiledMap.getLayer<ImageLayer>('MyImageLayer'),\n        isNotNull,\n      );\n    });\n\n    test('Get Group Layer', () {\n      expect(\n        renderableTiledMap.getLayer<Group>('MyGroupLayer'),\n        isNotNull,\n      );\n    });\n\n    test('Get no layer', () {\n      expect(\n        renderableTiledMap.getLayer<TileLayer>('Nonexistent layer'),\n        isNull,\n      );\n    });\n  });\n\n  group('orthogonal with groups, offsets, opacity and parallax', () {\n    late TiledComponent component;\n    final mapSizePx = Vector2(32 * 16, 128 * 16);\n\n    setUp(() async {\n      Flame.bundle = TestAssetBundle(\n        imageNames: [\n          'image1.png',\n          'map-level1.png',\n        ],\n        stringNames: ['map.tmx'],\n      );\n      component = await TiledComponent.load(\n        'map.tmx',\n        Vector2(16, 16),\n        bundle: Flame.bundle,\n      );\n\n      // Need to initialize a game and call `onLoad` and `onGameResize` to\n      // get the camera and canvas sizes all initialized\n      final game = FlameGame();\n      game.onGameResize(mapSizePx);\n      final camera = game.camera;\n      game.world.add(component);\n      camera.viewfinder.position = Vector2(150, 20);\n      camera.viewport.size = mapSizePx.clone();\n      game.onGameResize(mapSizePx);\n      component.onGameResize(mapSizePx);\n      await component.onLoad();\n      await game.ready();\n    });\n\n    test('component size', () {\n      expect(component.tileMap.destTileSize, Vector2(16, 16));\n      expect(component.size, mapSizePx);\n    });\n\n    test(\n      'renders',\n      () async {\n        final pngData = await renderMapToPng(component);\n\n        expect(pngData, matchesGoldenFile('goldens/orthogonal.png'));\n      },\n    );\n  });\n\n  group('isometric', () {\n    late TiledComponent component;\n\n    setUp(() async {\n      final bundle = TestAssetBundle(\n        imageNames: [\n          'isometric_spritesheet.png',\n        ],\n        stringNames: ['test_isometric.tmx'],\n      );\n      component = await TiledComponent.load(\n        'test_isometric.tmx',\n        Vector2(256 / 4, 128 / 4),\n        bundle: bundle,\n        images: Images(bundle: bundle),\n      );\n    });\n\n    test('component size', () {\n      expect(component.tileMap.destTileSize, Vector2(64, 32));\n      expect(component.size, Vector2(320, 160));\n    });\n\n    test('renders', () async {\n      // Map size is now 320 wide, but it has 1 extra tile of height because\n      // its actually double-height tiles.\n      final pngData = await renderMapToPng(component);\n\n      expect(pngData, matchesGoldenFile('goldens/isometric.png'));\n    });\n  });\n\n  group('hexagonal', () {\n    late TiledComponent component;\n\n    Future<TiledComponent> setupMap(\n      String tmxFile,\n      String imageFile,\n      Vector2 destTileSize,\n    ) async {\n      final bundle = TestAssetBundle(\n        imageNames: [\n          imageFile,\n        ],\n        stringNames: [tmxFile],\n      );\n      return component = await TiledComponent.load(\n        tmxFile,\n        destTileSize,\n        bundle: bundle,\n        images: Images(bundle: bundle),\n      );\n    }\n\n    test('flat + even staggered', () async {\n      await setupMap(\n        'flat_hex_even.tmx',\n        'Tileset_Hexagonal_FlatTop_60x39_60x60.png',\n        Vector2(60, 39),\n      );\n\n      expect(component.size, Vector2(240, 214.5));\n\n      final pngData = await renderMapToPng(component);\n\n      expect(pngData, matchesGoldenFile('goldens/flat_hex_even.png'));\n    });\n\n    test('flat + odd staggered', () async {\n      await setupMap(\n        'flat_hex_odd.tmx',\n        'Tileset_Hexagonal_FlatTop_60x39_60x60.png',\n        Vector2(60, 39),\n      );\n\n      expect(component.size, Vector2(240, 214.5));\n\n      final pngData = await renderMapToPng(component);\n\n      expect(pngData, matchesGoldenFile('goldens/flat_hex_odd.png'));\n    });\n\n    test('pointy + even staggered', () async {\n      await setupMap(\n        'pointy_hex_even.tmx',\n        'Tileset_Hexagonal_PointyTop_60x52_60x80.png',\n        Vector2(60, 52),\n      );\n\n      expect(component.size, Vector2(330, 208));\n\n      final pngData = await renderMapToPng(component);\n\n      expect(pngData, matchesGoldenFile('goldens/pointy_hex_even.png'));\n    });\n\n    test('pointy + odd staggered', () async {\n      await setupMap(\n        'pointy_hex_odd.tmx',\n        'Tileset_Hexagonal_PointyTop_60x52_60x80.png',\n        Vector2(60, 52),\n      );\n\n      expect(component.size, Vector2(330, 208));\n\n      final pngData = await renderMapToPng(component);\n\n      expect(pngData, matchesGoldenFile('goldens/pointy_hex_odd.png'));\n    });\n  });\n\n  group('tile offset', () {\n    late TiledComponent component;\n\n    Future<TiledComponent> setupMap(\n      String tmxFile,\n      String imageFile,\n      Vector2 destTileSize,\n    ) async {\n      final bundle = TestAssetBundle(\n        imageNames: [\n          imageFile,\n        ],\n        stringNames: [tmxFile],\n      );\n      return component = await TiledComponent.load(\n        tmxFile,\n        destTileSize,\n        bundle: bundle,\n        images: Images(bundle: bundle),\n      );\n    }\n\n    test('tile offset hexagonal', () async {\n      await setupMap(\n        // flame tiled currently does not support hexagon side length property,\n        // to use export from Tiled, tweak that value\n        'test_tile_offset_hexagonal.tmx',\n        '4_color_sprite.png',\n        Vector2(16, 16),\n      );\n\n      expect(component.size, Vector2(40, 28));\n\n      final pngData = await renderMapToPng(component);\n\n      expect(\n        pngData,\n        matchesGoldenFile('goldens/test_tile_offset_hexagonal.png'),\n      );\n    });\n\n    test('tile offset isometric', () async {\n      await setupMap(\n        'test_tile_offset_isometric.tmx',\n        '4_color_sprite.png',\n        Vector2(16, 16),\n      );\n\n      expect(component.size, Vector2(32, 32));\n\n      final pngData = await renderMapToPng(component);\n\n      expect(\n        pngData,\n        matchesGoldenFile('goldens/test_tile_offset_isometric.png'),\n      );\n    });\n\n    test('tile offset orthogonal', () async {\n      await setupMap(\n        'test_tile_offset_orthogonal.tmx',\n        '4_color_sprite.png',\n        Vector2(16, 16),\n      );\n\n      expect(component.size, Vector2(32, 32));\n\n      final pngData = await renderMapToPng(component);\n\n      expect(\n        pngData,\n        matchesGoldenFile('goldens/test_tile_offset_orthogonal.png'),\n      );\n    });\n\n    test('tile offset staggered', () async {\n      await setupMap(\n        'test_tile_offset_staggered.tmx',\n        '4_color_sprite.png',\n        Vector2(16, 16),\n      );\n\n      expect(component.size, Vector2(40, 24));\n\n      final pngData = await renderMapToPng(component);\n\n      expect(\n        pngData,\n        matchesGoldenFile('goldens/test_tile_offset_staggered.png'),\n      );\n    });\n  });\n\n  group('isometric staggered', () {\n    late TiledComponent component;\n\n    Future<TiledComponent> setupMap(\n      String tmxFile,\n      String imageFile,\n      Vector2 destTileSize,\n    ) async {\n      final bundle = TestAssetBundle(\n        imageNames: [\n          imageFile,\n        ],\n        stringNames: [tmxFile],\n      );\n      return component = await TiledComponent.load(\n        tmxFile,\n        destTileSize,\n        bundle: bundle,\n        images: Images(bundle: bundle),\n      );\n    }\n\n    test('x + odd', () async {\n      await setupMap(\n        'iso_staggered_overlap_x_odd.tmx',\n        'dirt_atlas.png',\n        Vector2(128, 64),\n      );\n\n      expect(component.size, Vector2(320, 288));\n\n      final pngData = await renderMapToPng(component);\n\n      expect(\n        pngData,\n        matchesGoldenFile('goldens/iso_staggered_overlap_x_odd.png'),\n      );\n    });\n\n    test('x + even + half sized', () async {\n      await setupMap(\n        'iso_staggered_overlap_x_even.tmx',\n        'dirt_atlas.png',\n        Vector2(128 / 2, 64 / 2),\n      );\n\n      expect(component.size, Vector2(320 / 2, 288 / 2));\n\n      final pngData = await renderMapToPng(component);\n\n      expect(\n        pngData,\n        matchesGoldenFile('goldens/iso_staggered_overlap_x_even.png'),\n      );\n    });\n\n    test('y + odd + half', () async {\n      await setupMap(\n        'iso_staggered_overlap_y_odd.tmx',\n        'dirt_atlas.png',\n        Vector2(128 / 2, 64 / 2),\n      );\n\n      expect(component.size, Vector2(576 / 2, 160 / 2));\n\n      final pngData = await renderMapToPng(component);\n\n      expect(\n        pngData,\n        matchesGoldenFile('goldens/iso_staggered_overlap_y_odd.png'),\n      );\n    });\n\n    test('y + even', () async {\n      await setupMap(\n        'iso_staggered_overlap_y_even.tmx',\n        'dirt_atlas.png',\n        Vector2(128, 64),\n      );\n\n      expect(component.size, Vector2(576, 160));\n\n      final pngData = await renderMapToPng(component);\n\n      expect(\n        pngData,\n        matchesGoldenFile('goldens/iso_staggered_overlap_y_even.png'),\n      );\n    });\n  });\n\n  group('shifted and scaled', () {\n    late TiledComponent component;\n    final size = Vector2(256, 128);\n\n    Future<void> setupMap(\n      Vector2 destTileSize,\n    ) async {\n      final bundle = TestAssetBundle(\n        imageNames: [\n          'isometric_spritesheet.png',\n        ],\n        stringNames: ['test_shifted.tmx'],\n      );\n      component = await TiledComponent.load(\n        'test_shifted.tmx',\n        destTileSize,\n        bundle: bundle,\n        images: Images(bundle: bundle),\n      );\n    }\n\n    test('regular', () async {\n      await setupMap(size);\n      final pngData = await renderMapToPng(component);\n\n      expect(\n        pngData,\n        matchesGoldenFile('goldens/shifted_scaled_regular.png'),\n      );\n    });\n\n    test('smaller', () async {\n      final smallSize = size / 3;\n      await setupMap(smallSize);\n      final pngData = await renderMapToPng(component);\n\n      expect(\n        pngData,\n        matchesGoldenFile('goldens/shifted_scaled_smaller.png'),\n      );\n    });\n\n    test('larger', () async {\n      final largeSize = size * 2;\n      await setupMap(largeSize);\n      final pngData = await renderMapToPng(component);\n\n      expect(\n        pngData,\n        matchesGoldenFile('goldens/shifted_scaled_larger.png'),\n      );\n    });\n  });\n\n  group('TileStack', () {\n    late TiledComponent component;\n    final size = Vector2(256 / 2, 128 / 2);\n\n    setUp(() async {\n      final bundle = TestAssetBundle(\n        imageNames: [\n          'isometric_spritesheet.png',\n        ],\n        stringNames: ['test_isometric.tmx'],\n      );\n      component = await TiledComponent.load(\n        'test_isometric.tmx',\n        size,\n        bundle: bundle,\n        images: Images(bundle: bundle),\n      );\n    });\n    test('from all layers', () {\n      var stack = component.tileMap.tileStack(0, 0, all: true);\n      expect(stack.length, 2);\n\n      stack = component.tileMap.tileStack(1, 0, all: true);\n      expect(stack.length, 1);\n    });\n\n    test('from some layers', () {\n      var stack = component.tileMap.tileStack(0, 0, named: {'empty'});\n      expect(stack.length, 0);\n\n      stack = component.tileMap.tileStack(0, 0, named: {'item'});\n      expect(stack.length, 1);\n\n      stack = component.tileMap.tileStack(0, 0, ids: {1});\n      expect(stack.length, 1);\n\n      stack = component.tileMap.tileStack(0, 0, ids: {1, 2});\n      expect(stack.length, 2);\n    });\n\n    test('can be positioned together', () async {\n      final stack = component.tileMap.tileStack(0, 0, all: true);\n      stack.position = stack.position + Vector2.all(20);\n\n      final pngData = await renderMapToPng(component);\n      expect(\n        pngData,\n        matchesGoldenFile('goldens/tile_stack_all_move.png'),\n      );\n    });\n\n    test('can be positioned singularly', () async {\n      final stack = component.tileMap.tileStack(0, 0, named: {'item'});\n      stack.position = stack.position + Vector2(-20, 20);\n\n      final pngData = await renderMapToPng(component);\n      expect(\n        pngData,\n        matchesGoldenFile('goldens/tile_stack_single_move.png'),\n      );\n    });\n  });\n\n  group('animated tiles', () {\n    late TiledComponent component;\n    late RenderableTiledMap map;\n    final size = Vector2(16, 16);\n\n    for (final mapType in [\n      'orthogonal',\n      'isometric',\n      'hexagonal',\n      'staggered',\n    ]) {\n      group(mapType, () {\n        setUp(() async {\n          final bundle = TestAssetBundle(\n            imageNames: [\n              '0x72_DungeonTilesetII_v1.4.png',\n            ],\n            stringNames: ['dungeon_animation_$mapType.tmx'],\n          );\n          component = await TiledComponent.load(\n            'dungeon_animation_$mapType.tmx',\n            size,\n            bundle: bundle,\n            images: Images(bundle: bundle),\n          );\n          map = component.tileMap;\n        });\n\n        test('handle single frame animations ($mapType)', () {\n          expect(map.renderableLayers.first, isInstanceOf<FlameTileLayer>());\n          final layer = map.renderableLayers.first as FlameTileLayer;\n          expect(\n            layer.animations,\n            hasLength(1),\n            reason: 'layer has only one animation',\n          );\n          expect(\n            layer.animationFrames,\n            hasLength(4),\n            reason: 'layer only caches frames in use',\n          );\n          expect(layer.animations.first.frames.sources, hasLength(1));\n        });\n\n        test('handle single frame animations ($mapType)', () {\n          expect(\n            map.renderableLayers[1],\n            isInstanceOf<FlameTileLayer>(),\n          );\n          final layer = map.renderableLayers[1] as FlameTileLayer;\n          expect(\n            layer.animations,\n            hasLength(2),\n            reason: 'two animations on this layer',\n          );\n          expect(\n            layer.animationFrames,\n            hasLength(4),\n            reason: 'layer only caches frames in use',\n          );\n\n          final waterAnimation = layer.animations.first;\n          final spikeAnimation = layer.animations.last;\n          expect(waterAnimation.frames.durations, [0.18, 0.17, 0.15]);\n          expect(spikeAnimation.frames.durations, [0.176, 0.176, 0.176, 0.176]);\n\n          map.update(0.177);\n          expect(waterAnimation.frame, 0);\n          expect(waterAnimation.frames.frameTime, 0.177);\n          expect(\n            waterAnimation.batchedSource.toRect(),\n            waterAnimation.frames.sources[0],\n          );\n\n          expect(spikeAnimation.frame, 1);\n          expect(spikeAnimation.frames.frameTime, moreOrLessEquals(0.001));\n          expect(\n            spikeAnimation.batchedSource.toRect(),\n            spikeAnimation.frames.sources[1],\n          );\n\n          map.update(0.003);\n          expect(waterAnimation.frame, 1);\n          expect(waterAnimation.frames.frameTime, moreOrLessEquals(0.0));\n          expect(spikeAnimation.frame, 1);\n          expect(spikeAnimation.frames.frameTime, moreOrLessEquals(0.004));\n\n          map.update(0.17 + 0.15);\n          expect(waterAnimation.frame, 0, reason: 'wraps around');\n          expect(\n            waterAnimation.batchedSource.toRect(),\n            waterAnimation.frames.sources[0],\n          );\n        });\n\n        /// This will not produce a pretty map for non-orthogonal, but that's\n        /// OK, we're looking for parsing and handling of animations.\n        test('renders ($mapType)', () async {\n          var pngData = await renderMapToPng(component);\n          await expectLater(\n            pngData,\n            matchesGoldenFile('goldens/dungeon_animation_${mapType}_0.png'),\n          );\n\n          component.update(0.18);\n          pngData = await renderMapToPng(component);\n          await expectLater(\n            pngData,\n            matchesGoldenFile('goldens/dungeon_animation_${mapType}_1.png'),\n          );\n\n          component.update(0.18);\n          pngData = await renderMapToPng(component);\n          await expectLater(\n            pngData,\n            matchesGoldenFile('goldens/dungeon_animation_${mapType}_2.png'),\n          );\n\n          component.update(0.18);\n          pngData = await renderMapToPng(component);\n          await expectLater(\n            pngData,\n            matchesGoldenFile('goldens/dungeon_animation_${mapType}_3.png'),\n          );\n        });\n      });\n    }\n  });\n\n  group('oversized tiles', () {\n    late TiledComponent component;\n    final size = Vector2(16, 16);\n\n    for (final mapType in [\n      'orthogonal',\n      'isometric',\n      'hexagonal',\n      'staggered',\n    ]) {\n      group(mapType, () {\n        setUp(() async {\n          final bundle = TestAssetBundle(\n            imageNames: [\n              '0x72_DungeonTilesetII_v1.4.png',\n            ],\n            stringNames: ['oversized_tiles_$mapType.tmx'],\n          );\n          component = await TiledComponent.load(\n            'oversized_tiles_$mapType.tmx',\n            size,\n            bundle: bundle,\n            images: Images(bundle: bundle),\n          );\n        });\n\n        test('renders ($mapType)', () async {\n          final pngData = await renderMapToPng(component);\n          await expectLater(\n            pngData,\n            matchesGoldenFile('goldens/oversized_tiles_$mapType.png'),\n          );\n        });\n      });\n    }\n  });\n\n  group('RenderableTiledMap.TileData', () {\n    late RenderableTiledMap renderableTiledMap;\n\n    setUp(() async {\n      final bundle = TestAssetBundle(\n        imageNames: ['4_color_sprite.png'],\n        stringNames: ['deleted_layer_map.tmx'],\n      );\n      renderableTiledMap = await RenderableTiledMap.fromFile(\n        'deleted_layer_map.tmx',\n        Vector2.all(16),\n        bundle: bundle,\n        images: Images(bundle: bundle),\n      );\n    });\n\n    test('same TileData is found by layerId and layerIndex', () {\n      final tileData1 = renderableTiledMap.getTileData(layerId: 6, x: 5, y: 3);\n      final tileData2 = renderableTiledMap.getTileDataByLayerIndex(\n        layerIndex: 4,\n        x: 5,\n        y: 3,\n      );\n      expect(tileData1, isNotNull);\n      expect(tileData2, isNotNull);\n      expect(tileData1, equals(tileData2));\n    });\n\n    test('returns null for non-existent layer', () {\n      expect(renderableTiledMap.getTileData(layerId: 3, x: 1, y: 1), isNull);\n      expect(renderableTiledMap.getTileData(layerId: 5, x: 1, y: 1), isNull);\n    });\n  });\n\n  group('RenderableTiledMap.LayerOpacity', () {\n    late RenderableTiledMap renderableTiledMap;\n\n    setUp(() async {\n      Flame.bundle = TestAssetBundle(\n        imageNames: ['map-level1.png'],\n        stringNames: ['layers_test.tmx'],\n      );\n      renderableTiledMap = await RenderableTiledMap.fromFile(\n        'layers_test.tmx',\n        Vector2.all(32),\n        bundle: Flame.bundle,\n      );\n    });\n\n    test('getLayerOpacity returns 1.0 by default', () {\n      expect(renderableTiledMap.getLayerOpacity(0), equals(1.0));\n    });\n\n    test('setLayerOpacity changes the layer opacity', () {\n      renderableTiledMap.setLayerOpacity(0, opacity: 0.5);\n      expect(renderableTiledMap.getLayerOpacity(0), equals(0.5));\n    });\n\n    test('setLayerOpacity sets opacity to 0', () {\n      renderableTiledMap.setLayerOpacity(0, opacity: 0.0);\n      expect(renderableTiledMap.getLayerOpacity(0), equals(0.0));\n    });\n\n    test('setLayerOpacity sets opacity to 1', () {\n      renderableTiledMap.setLayerOpacity(0, opacity: 0.25);\n      renderableTiledMap.setLayerOpacity(0, opacity: 1.0);\n      expect(renderableTiledMap.getLayerOpacity(0), equals(1.0));\n    });\n\n    test('setLayerOpacity asserts on out-of-range value', () {\n      expect(\n        () => renderableTiledMap.setLayerOpacity(0, opacity: 1.5),\n        throwsA(isA<AssertionError>()),\n      );\n      expect(\n        () => renderableTiledMap.setLayerOpacity(0, opacity: -0.1),\n        throwsA(isA<AssertionError>()),\n      );\n    });\n  });\n\n  group('RenderableTiledMap.LayerOpacity nested groups', () {\n    // map.tmx layer structure (renderableLayers indices):\n    //   0: FlameTileLayer  \"Ground\"          (opacity 1.0)\n    //   1: GroupLayer      \"Background\"      (opacity 0.8)\n    //      └─ FlameTileLayer \"Sky tiles\"     (own opacity 0.9, effective 0.72)\n    //   2: FlameImageLayer \"Image Layer 2\"   (opacity 1.0)\n    //   3: FlameImageLayer \"Sky artifact\"    (opacity 0.2)\n    late RenderableTiledMap renderableTiledMap;\n\n    setUp(() async {\n      Flame.bundle = TestAssetBundle(\n        imageNames: ['image1.png', 'map-level1.png'],\n        stringNames: ['map.tmx'],\n      );\n      renderableTiledMap = await RenderableTiledMap.fromFile(\n        'map.tmx',\n        Vector2.all(16),\n        bundle: Flame.bundle,\n      );\n    });\n\n    test('child effective opacity is parent * own', () {\n      // GroupLayer opacity=0.8, child TileLayer own opacity=0.9 → 0.72\n      final group = renderableTiledMap.renderableLayers[1] as GroupLayer;\n      final child = group.children.first as FlameTileLayer;\n      expect(child.opacity, closeTo(0.72, 1e-6));\n    });\n\n    test('setting GroupLayer opacity updates child effective opacity', () {\n      // Change group from 0.8→0.5; child own opacity stays 0.9→effective 0.45\n      renderableTiledMap.setLayerOpacity(1, opacity: 0.5);\n      final group = renderableTiledMap.renderableLayers[1] as GroupLayer;\n      expect(group.opacity, equals(0.5));\n      final child = group.children.first as FlameTileLayer;\n      expect(child.opacity, closeTo(0.45, 1e-6));\n    });\n\n    test('setting GroupLayer opacity to 0 makes child fully transparent', () {\n      renderableTiledMap.setLayerOpacity(1, opacity: 0.0);\n      final group = renderableTiledMap.renderableLayers[1] as GroupLayer;\n      final child = group.children.first as FlameTileLayer;\n      expect(child.opacity, equals(0.0));\n    });\n\n    test('sibling layers are not affected by group opacity change', () {\n      renderableTiledMap.setLayerOpacity(1, opacity: 0.1);\n      // \"Ground\" at index 0 has no parent, so remains 1.0\n      expect(renderableTiledMap.getLayerOpacity(0), equals(1.0));\n      // \"Sky artifact\" at index 3 is independent\n      expect(renderableTiledMap.getLayerOpacity(3), equals(0.2));\n    });\n  });\n}\n"
  },
  {
    "path": "pubspec.yaml",
    "content": "name: _\nrepository: https://github.com/flame-engine/flame\nworkspace:\n  - doc/flame/examples\n  - doc/tutorials/klondike/app\n  - doc/tutorials/space_shooter/app\n  - doc/tutorials/platformer/app\n  - packages/flame_behaviors\n  - packages/flame_behaviors/example\n  - packages/flame_behavior_tree\n  - packages/flame_behavior_tree/behavior_tree\n  - packages/flame_behavior_tree/example\n  - packages/flame_test\n  - packages/flame_test/example\n  - packages/flame_tiled\n  - packages/flame_tiled/example\n  - packages/flame_oxygen\n  - packages/flame_oxygen/example\n  - packages/flame_isolate\n  - packages/flame_isolate/example\n  - packages/flame_texturepacker\n  - packages/flame_texturepacker/example\n  - packages/flame_lint\n  - packages/flame_sprite_fusion\n  - packages/flame_sprite_fusion/example\n  - packages/flame_steering_behaviors\n  - packages/flame_steering_behaviors/example\n  - packages/flame_devtools\n  - packages/flame_fire_atlas\n  - packages/flame_fire_atlas/example\n  - packages/flame_audio\n  - packages/flame_audio/example\n  - packages/flame_studio\n  - packages/flame_spine\n  - packages/flame_spine/example\n  - packages/flame_splash_screen\n  - packages/flame_splash_screen/example\n  - packages/flame_bloc\n  - packages/flame_bloc/example\n  - packages/flame_kenney_xml\n  - packages/flame_kenney_xml/example\n  - packages/flame\n  - packages/flame/example\n  - packages/flame_lottie\n  - packages/flame_lottie/example\n  - packages/flame_markdown\n  - packages/flame_markdown/example\n  - packages/flame_console\n  - packages/flame_console/example\n  - packages/flame_rive\n  - packages/flame_rive/example\n  - packages/flame_forge2d\n  - packages/flame_forge2d/example\n  - packages/flame_noise\n  - packages/flame_riverpod\n  - packages/flame_riverpod/example\n  - packages/flame_svg\n  - packages/flame_svg/example\n  - packages/flame_network_assets\n  - packages/flame_network_assets/example\n  - packages/flame_3d\n  - packages/flame_3d/example\n  - packages/flame_jenny\n  - packages/flame_jenny/jenny\n  - examples/games/trex\n  - examples/games/padracing\n  - examples/games/rogue_shooter\n  - examples/games/crystal_ball\n  - examples\n\nenvironment:\n  sdk: \">=3.11.0 <4.0.0\"\n\ndev_dependencies:\n  melos: ^7.4.0\n\nmelos:\n  command:\n    version:\n      # Only allow versioning to happen on main branch.\n      branch: main\n      # Generates a link to a prefilled GitHub release creation page.\n      releaseUrl: true\n      includeCommitId: true\n      linkToCommits: true\n\n    bootstrap:\n      environment:\n        sdk: \">=3.11.0 <4.0.0\"\n        flutter: \">=3.41.0\"\n      dependencies:\n        meta: ^1.12.0\n        vector_math: ^2.1.4\n      dev_dependencies:\n        dartdoc: ^9.0.0\n        mocktail: ^1.0.4\n        test: any\n\n    publish:\n      hooks:\n        pre: melos devtools-build\n\n  scripts:\n    lint:all:\n      steps:\n        - analyze\n        - format\n      description: Run all static analysis checks.\n\n    analyze:\n      run: melos exec dart analyze .\n      description: Run `dart analyze` for all packages.\n\n    format-check:\n      run: melos exec dart format . --set-exit-if-changed\n      description: Run `dart format` checks for all packages.\n\n    markdown-check:\n      run: markdownlint . --ignore-path .markdownlintignore --config .markdownlint.yaml\n      description: Runs the markdown linting check.\n\n    markdown-fix:\n      run: markdownlint . --fix --ignore-path .markdownlintignore --config .markdownlint.yaml\n      description: Fixes the markdown linting errors.\n\n    dartdoc:\n      run: melos exec flutter pub run dartdoc\n      description: Run dartdoc checks for all packages.\n\n    doc-setup:\n      run: ./scripts/doc-setup.sh\n      description: Prepares the environment for documentation development.\n\n    doc-build:\n      run: cd \"$MELOS_ROOT_PATH/doc/_sphinx\" && make html\n      description: Create the sphinx html docs.\n\n    doc-serve:\n      run: cd \"$MELOS_ROOT_PATH/doc/_sphinx\" && make livehtml\n      description: Recompiles the docs every time there is a change in them and opens your browser.\n\n    doc-kill:\n      run: cd \"$MELOS_ROOT_PATH/doc/_sphinx\" && python3 kill-server.py\n      description: Kills any TCP processes running on port 8000.\n\n    doc-clean:\n      run: cd \"$MELOS_ROOT_PATH/doc/_sphinx\" && make clean\n      description: Removes all Sphinx's cached generated files.\n\n    doc-linkcheck:\n      run: cd \"$MELOS_ROOT_PATH/doc/_sphinx\" && make linkcheck\n      description: Checks whether there are any broken links in the docs.\n\n    test:select:\n      run: melos exec -c 1 -- flutter test\n      packageFilters:\n        dirExists: test\n      description: Run `flutter test` for selected packages.\n\n    test:\n      run: melos run test:select --no-select\n      description: Run all Flutter tests in this project.\n\n    test:seeded:\n      run: melos exec -c 1 -- flutter test --dart-define=RANDOM_SEED=$RANDOM_SEED\n      packageFilters:\n        dirExists: test\n      description: Run `flutter test` for selected packages with a seed for random tests. Set the RANDOM_SEED environment variable to a specific value to reproduce a test run.\n\n    coverage:\n      steps:\n        - melos exec -- flutter test --coverage\n        - melos exec -- genhtml coverage/lcov.info --output-directory=coverage/\n      packageFilters:\n        dirExists: test\n      description: Generate coverage for the selected package.\n\n    upgrade: melos exec flutter pub upgrade\n\n    update-goldens:\n      run: melos exec -- flutter test --update-goldens\n      packageFilters:\n        dirExists: test\n      description: Re-generate all golden test files\n\n    devtools-build:\n      run: melos exec -- dart run devtools_extensions build_and_copy --source=. --dest=../flame/extension/devtools\n      packageFilters:\n        scope: flame_devtools\n      description: Builds the devtools and copies the build directory to the Flame package.\n\n    devtools-simulator:\n      run: melos exec -- flutter run -d chrome --dart-define=use_simulated_environment=true\n      packageFilters:\n        scope: flame_devtools\n      description: Runs the devtools in the simulated mode.\n"
  },
  {
    "path": "scripts/cspell-run.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\ncspell \"$@\" --no-progress -c .github/cspell.json \"**/*.{md,dart}\""
  },
  {
    "path": "scripts/cspell-verify.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\nfix=$([[ \"$*\" == *--fix* ]] && echo true || echo false)\n\nfunction sort_check_fn() {\n    sort -f -c\n}\n\nfunction sort_run_fn() {\n    sort -f\n}\n\nfunction sort_dictionary() {\n    local file=\"$1\"\n    local tmp_file=$(mktemp)\n\n    head -n 1 \"$file\" > \"$tmp_file\"\n    tail -n +2 \"$file\" | sort_run_fn >> \"$tmp_file\"\n    mv \"$tmp_file\" \"$file\"\n}\n\nfunction delete_unused() {\n    local file=\"$1\"\n    local word=\"$2\"\n\n    perl -i -ne \"print unless /^\\s*${word}\\s*([# ].*)?$/i\" \"$file\"\n}\n\nfunction lowercase() {\n    tr 'A-Z' 'a-z'\n}\n\nword_list=\"word_list.tmp\"\n\ndictionary_dir=\".github/.cspell\"\ntmp_dir=\".cspell.tmp\"\n\nmv \"$dictionary_dir\" \"$tmp_dir\"\nmkdir \"$dictionary_dir\"\nfor file in \"$tmp_dir\"/*; do\n    if [[ -f \"$file\" ]]; then\n        touch \"$dictionary_dir/$(basename \"$file\")\"\n    fi\ndone\n\n./scripts/cspell-run.sh --dot --unique --words-only | lowercase | sort -f > $word_list  || exit 1\nrm -r \"$dictionary_dir\"\nmv \"$tmp_dir\" \"$dictionary_dir\"\n\nerror=0\nfor file in .github/.cspell/*.txt; do\n    echo \"Processing dictionary '$file'...\"\n\n    violation=$(awk '!/^\\s*(#|$)/' \"$file\" | sort_check_fn 2>&1 || true)\n    if [ -n \"$violation\" ]; then\n        # Extract only the line content after the last ': '\n        violation_line=$(echo \"$violation\" | sed 's/.*: //')\n        echo \"Error: The dictionary '$file' is not in alphabetical order. First violation: '$violation_line'\" >&2\n        error=1\n        if $fix; then\n            echo \"Fixing the dictionary '$file'\"\n            sort_dictionary \"$file\"\n        fi\n    fi\n\n    while IFS= read -r line; do\n        # split the line by # to remove comments\n        word=$(echo \"$line\" | cut -d '#' -f 1 | xargs | lowercase) # xargs trims whitespace\n\n        # check if the word exists in the project\n        if [[ -n \"$word\" ]] && ! grep -wxF \"$word\" \"$word_list\" >/dev/null; then\n            echo \"Error: The word '$word' in the dictionary '$file' is not needed.\" >&2\n            error=1\n            if $fix; then\n                echo \"Fixing the dictionary '$file' with excess word $word\"\n                delete_unused \"$file\" \"$word\"\n            fi\n        fi\n    done < \"$file\"\ndone\n\nrm $word_list\nexit $error"
  },
  {
    "path": "scripts/doc-setup.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\necho Checking python version:\npython3 --version && python3 -c \"import sys; sys.exit(0 if sys.version_info >= (3,8) else 2)\" || (echo \"Error: Python 3.8+ is required\" && exit 1)\necho Installing required python modules:\npython3 -m pip install -r \"doc/_sphinx/requirements.txt\"\necho Installing dartdoc_json:\ndart pub global activate dartdoc_json\necho Done.\n"
  }
]